# A wrapper for slingshot  elastic-search adapter for Mongoid
module Mebla
  # Handles all searching functions and chains search to define filters, sorting or facets.
  #
  # This example searches posts by tags, sorts and filters the results::
  #  
  # criteria = Post.search.terms(:tags, ['ruby', 'rails']).ascending(:publish_date).only(:author => ['cousine'])
  #
  # This will search the index for posts tagged 'ruby' or 'rails', arrange the results ascendingly according
  # to their publish dates, and filter the results by the author named 'cousine'.
  #
  # The search won't be executed unless we try accessing the results collection::
  #
  #  results = criteria.hits
  #
  # Or directly iterate the collection::
  #
  #  criteria.each do |result|
  #    ...
  #  end
  #
  # Mebla supports multiple methods of searching
  # 
  # You can either search by direct Lucene query::
  #  
  #  Mebla.search("query")
  #
  # Or by term::
  #
  #  Mebla.search.term(:field, "term")
  #
  # Or by terms::
  #
  #  Mebla.search.terms(:field, ["term 1", "term 2", ...])  
  class Search
    include Enumerable
    attr_reader :slingshot_search, :results    
    
    # Creates a new Search object
    # @param [String] query_string optional search query
    # @param [String, Symbol, Array] type_names a string, symbol or array representing the models to be searcheds
    def initialize(query_string = "", type_names = [])
      # Convert type names from string or symbol to array
      type_names = case true
        when type_names.is_a?(Symbol), type_names.is_a?(String)
          [type_names]      
        when type_names.is_a?(Array) && !type_names.empty?
          type_names.collect{|name| name.to_s}
        else
          []
        end
        
        @slingshot_search = Slingshot::Search::Search.new(Mebla.context.slingshot_index_name, {})
        # Add a type filter to return only certain types
        unless type_names.empty?
          only(:_type => type_names)
        end
        
        unless query_string.blank?
          query(query_string)
        end
    end
    
    # Creates a terms search criteria
    # @param [String, Symbol] field the field to search
    # @param [Array] values the terms to match
    # @param [Hash] options to refine the search
    # @return [Mebla::Search]
    #
    # Match Posts tagged with either 'ruby' or 'rails'::
    #
    #  Post.search.terms(:tags, ['ruby', 'rails'], :minimum_match => 1)
    def terms(field, values, options = {})      
      @slingshot_search = @slingshot_search.query
      @slingshot_search.instance_variable_get(:@query).terms(field, values, options)
      self
    end
    
    # Creates a term search criteria
    # @param [String, Symbol] field the field to search
    # @param [String] value term to match
    # @return [Mebla::Search]
    def term(field, value)
      @slingshot_search = @slingshot_search.query
      @slingshot_search.instance_variable_get(:@query).term(field, value)
      self
    end
    
    # Creates a Lucene query string search criteria
    # @param [String] query_string search query
    # @param [Hash] options to refine the search
    #
    # Match Posts with "Test Lucene query" as title::
    #
    #  Post.search.query("Test Lucene query", :default_field => "title")
    # 
    # You can also instead::
    #
    #  Post.search.query("title: Test Lucene query")
    #
    # Or to search all fields::
    #
    #  Post.search.query("Test Lucene query")
    #
    # @note For more information check {http://lucene.apache.org/java/2_4_0/queryparsersyntax.html  Lucene's query syntax}
    def query(query_string, options = {})
      @slingshot_search = @slingshot_search.query
      @slingshot_search.instance_variable_get(:@query).string(query_string, options)
      self
    end
    
    # Sorts results ascendingly
    # @param [String, Symbol] field to sort by
    # @return [Mebla::Search]
    def ascending(field)
      @slingshot_search = @slingshot_search.sort
      @slingshot_search.instance_variable_get(:@sort).send(field.to_sym, 'asc')
      self
    end
    
    # Sorts results descendingly
    # @param [String, Symbol] field to sort by
    # @return [Mebla::Search]
    def descending(field)
      @slingshot_search = @slingshot_search.sort
      @slingshot_search.instance_variable_get(:@sort).send(field.to_sym, 'desc')
      self
    end
    
    # Creates a new facet for the search
    # @param [String] name of the facet
    # @param [String, Symbol] field to create a facet for
    # @param [Hash] options
    # @return [Mebla::Search]
    #
    # Defining a global facet named "tags"::
    #
    #  Post.search("*").facet("tags", :tag, :global => true)
    # 
    # @note check {http://www.elasticsearch.org/guide/reference/api/search/facets/ elasticsearch's facet reference} for more information
    def facet(name, field, options={})
      # Get the hash
      facet_hash = @slingshot_search.instance_variable_get(:@facets)
      # Create a new Facet
      facet_obj = Slingshot::Search::Facet.new(name, options)
      facet_obj.terms(field)
      # Initialize the hash if its nil
      if facet_hash.nil?
        @slingshot_search.instance_variable_set(:@facets, {})
      end
      # Add the facet to the hash
      @slingshot_search.instance_variable_get(:@facets).update facet_obj.to_hash        
      self
    end
    
    # Filters the results according to the criteria
    # @param [*Hash] fields hash for each filter
    # @return [Mebla::Search]
    # 
    # Get all indexed Posts and filter them by tags and authors::
    #
    #  Post.search("*").only(:tag => ["ruby", "rails"], :author => ["cousine"])
    def only(*fields)
      return if fields.empty?
      fields.each do |field|
        @slingshot_search = @slingshot_search.filter(:terms, field)
      end
      self
    end
    
    # Sets the maximum number of hits per query, defaults to 10
    # @param [Integer] value
    # @return [Mebla::Search]
    def size(value)
      @slingshot_search = @slingshot_search.size(value)
      self
    end
    
    # Sets the starting offset for the query
    # @param [Integer] value
    # @return [Mebla::Search]
    def from(value)
      @slingshot_search = @slingshot_search.from(value)
      self
    end
    
    # Performs the search and returns the results
    # @return [Mebla::ResultSet]
    def hits
      return @results if @results
      # Log search query
      Mebla.log("Searching:\n#{@slingshot_search.to_json.to_s}", :debug)
      response = @slingshot_search.perform.json
      Mebla.log("Response:\n#{response.to_json.to_s}", :info)
      @results = Mebla::ResultSet.new(response)
      # Log results statistics
      Mebla.log("Searched for:\n#{@slingshot_search.to_json.to_s}\ngot #{@results.total} documents in #{@results.time} ms", :debug)
      @results
    end
    
    # Returns the internal results list
    # @return [Array]
    def entries
      hits.entries
    end
    
    # Retrieves the total number of hits
    # @return [Integer]
    def total
      hits.total
    end
    
    # Retrieves the time taken to perform the search in ms
    # @return [Float]
    def time
      hits.time
    end
    
    # Retrieves the facets
    # @return [Hash]
    #
    # Reading a facet named 'tags'::
    #
    #  facets = Post.search("*").facet("tags", :tag)
    #  facets["terms"].each do |term|
    #    puts "#{term['term']} - #{term['count']}"
    #  end
    def facets
      hits.facets
    end
    
    # Iterates over the results collection
    def each(&block)
      hits.each(&block)
    end
    
    alias_method :asc, :ascending
    alias_method :desc, :descending
  end
end