module Indexed
  
  class Categories
    
    attr_reader :categories, :category_hash, :ignore_unassigned_tokens
    
    each_delegate :load_from_cache,
                  :to => :categories
    
    # A list of indexed categories.
    #
    # Options:
    #  * ignore_unassigned_tokens: Ignore the given token if it cannot be matched to a category.
    #                              The default behaviour is that if a token does not match to
    #                              any category, the query will not return anything (since a
    #                              single token cannot be matched). If you set this option to
    #                              true, any token that cannot be matched to a category will be
    #                              simply ignored.
    #                              Use this if only a few matched words are important, like for
    #                              example of the query "Jonathan Myers 86455 Las Cucarachas"
    #                              you only want to match the zipcode, to have the search engine
    #                              display advertisements on the side for the zipcode.
    #                              Nifty! :)
    #
    def initialize options = {}
      clear
      
      @ignore_unassigned_tokens = options[:ignore_unassigned_tokens] || false
    end
    
    # Clears both the array of categories and the hash of categories.
    #
    def clear
      @categories    = []
      @category_hash = {}
    end
    
    # Add the given category to the list of categories.
    #
    def << category
      categories << category
      # Note: [category] is an optimization, since I need an array
      #       of categories.
      #       It's faster to just package it in an array on loading
      #       Picky than doing it over and over with each query.
      #
      category_hash[category.name] = [category]
    end
    
    # Return all possible combinations for the given token.
    #
    # This checks if it needs to also search through similar
    # tokens, if for example, the token is one with ~.
    # If yes, it puts together all solutions.
    #
    def possible_combinations_for token
      token.similar? ? similar_possible_for(token) : possible_for(token)
    end
    # Gets all similar tokens and puts together the possible combinations
    # for each found similar token.
    # 
    def similar_possible_for token
      # Get as many similar tokens as necessary
      #
      tokens = similar_tokens_for token
      # possible combinations
      #
      inject_possible_for tokens
    end
    def similar_tokens_for token
      text = token.text
      categories.inject([]) do |result, category|
        next_token = token
        # Note: We could also break off here if not all the available
        #       similars are needed.
        #       Wait for a concrete case that needs this before taking
        #       action.
        #
        while next_token = next_token.next_similar_token(category)
          result << next_token if next_token && next_token.text != text
        end
        result
      end
    end
    def inject_possible_for tokens
      tokens.inject([]) do |result, token|
        possible = possible_categories token
        result + possible_for(token, possible)
      end
    end
    
    # Returns possible Combinations for the token.
    #
    # Note: The preselected_categories param is an optimization.
    #
    # Note: Returns [] if no categories matched (will produce no result).
    #       Returns nil if this token needs to be removed from the query.
    #       (Also none of the categories matched, but the ignore unassigned
    #       tokens option is true)
    #
    def possible_for token, preselected_categories = nil
      possible = (preselected_categories || possible_categories(token)).map { |category| category.combination_for(token) }
      possible.compact!
      # This is an optimization to mark tokens that are ignored.
      #
      return if ignore_unassigned_tokens && possible.empty?
      possible # wrap in combinations
    end
    # This returns the possible categories for this token.
    # If the user has already preselected a category for this token,
    # like "artist:moby", if not just return all for the given token,
    # since all are possible.
    #
    # Note: Once I thought this was called too often. But it is not (18.01.2011).
    #
    def possible_categories token
      user_defined_categories(token) || categories
    end
    # This returns the array of categories if the user has defined
    # an existing category.
    #
    # Note: Returns nil if the user did not define one
    #       or if he/she has defined a non-existing one. 
    #
    def user_defined_categories token
      category_hash[token.user_defined_category_name]
    end
    
  end
  
end