module Sunspot
  module DSL
    #
    # Provides an API for areas of the query DSL that operate on specific
    # fields. This functionality is provided by the query DSL and the dynamic
    # query DSL.
    #
    class FieldQuery < Scope
      def initialize(search, query, setup) #:nodoc:
        @search, @query = search, query
        super(query.scope, setup)
      end

      # Specify the order that results should be returned in. This method can
      # be called multiple times; precedence will be in the order given.
      #
      # ==== Parameters
      #
      # field_name<Symbol>:: the field to use for ordering
      # direction<Symbol>:: :asc or :desc (default :asc)
      #
      def order_by(field_name, direction = nil)
        sort =
          if special = Sunspot::Query::Sort.special(field_name)
            special.new(direction)
          else
            Sunspot::Query::Sort::FieldSort.new(
              @setup.field(field_name), direction
            )
          end
        @query.add_sort(sort)
      end

      #
      # Specify that the results should be ordered based on their
      # distance from a given point.
      #
      # ==== Parameters
      #
      # field_name<Symbol>::
      #   the field that stores the location (declared as `latlon`)
      # lat<Numeric>::
      #   the reference latitude
      # lon<Numeric>::
      #   the reference longitude
      # direction<Symbol>::
      #   :asc or :desc (default :asc)
      #
      def order_by_geodist(field_name, lat, lon, direction = nil)
        @query.add_sort(
          Sunspot::Query::Sort::GeodistSort.new(@setup.field(field_name), lat, lon, direction)
        )
      end

      #
      # DEPRECATED Use <code>order_by(:random)</code>
      #
      def order_by_random
        order_by(:random)
      end

      # Specify a field for result grouping. Grouping groups documents
      # with a common field value, return only the top document per
      # group.
      #
      # More information in the Solr documentation:
      # <http://wiki.apache.org/solr/FieldCollapsing>
      #
      # ==== Parameters
      #
      # field_names...<Symbol>:: the fields to use for grouping
      def group(*field_names, &block)
        group = Sunspot::Query::Group.new()

        field_names.each do |field_name|
          field = @setup.field(field_name)
          group.add_field(field)
        end

        if block
          dsl = Group.new(@setup, group)
          Sunspot::Util.instance_eval_or_call(dsl, &block)
        end

        @query.add_group(group)
        @search.add_group(group)
      end

      #
      # Request a facet on the search query. A facet is a feature of Solr that
      # determines the number of documents that match the existing search *and*
      # an additional criterion. This allows you to build powerful drill-down
      # interfaces for search, at each step presenting the searcher with a set
      # of refinements that are known to return results.
      #
      # In Sunspot, each facet returns zero or more rows, each of which
      # represents a particular criterion conjoined with the actual query being
      # performed. For _field_ _facets_, each row represents a particular value
      # for a given field. For _query_ _facets_, each row represents an
      # arbitrary scope; the facet itself is just a means of logically grouping
      # the scopes.
      #
      # === Examples
      #
      # ==== Field Facets
      #
      # A field facet is specified by passing one or more Symbol arguments to
      # this method:
      #
      #   Sunspot.search(Post) do
      #     with(:blog_id, 1)
      #     facet(:category_id)
      #   end
      #
      # The facet specified above will have a row for each category_id that is
      # present in a document which also has a blog_id of 1.
      #
      # ==== Multiselect Facets
      #
      # In certain circumstances, it is beneficial to exclude certain query
      # scopes from a facet; the most common example is multi-select faceting,
      # where the user has selected a certain value, but the facet should still
      # show all options that would be available if they had not:
      #
      #   Sunspot.search(Post) do
      #     with(:blog_id, 1)
      #     category_filter = with(:category_id, 2)
      #     facet(:category_id, :exclude => category_filter)
      #   end
      #
      # Although the results of the above search will be restricted to those
      # with a category_id of 2, the category_id facet will operate as if a
      # category had not been selected, allowing the user to select additional
      # categories (which will presumably be ORed together).
      #
      # It possible to exclude multiple filters by passing an array:
      #
      #   Sunspot.search(Post) do
      #     with(:blog_id, 1)
      #     category_filter = with(:category_id, 2)
      #     author_filter = with(:author_id, 3)
      #     facet(:category_id,
      #           :exclude => [category_filter, author_filter].compact)
      #   end
      #
      # You should consider using +.compact+ to ensure that the array does not
      # contain any nil values.
      #
      # <strong>As far as I can tell, Solr only supports multi-select with
      # field facets; if +:exclude+ is passed to a query facet, this method will
      # raise an error. Also, the +:only+ and +:extra+ options use query
      # faceting under the hood, so these can't be used with +:extra+ either.
      # </strong>
      #
      # ==== Query Facets
      #
      # A query facet is a collection of arbitrary scopes, each of which
      # represents a row. This is specified by passing a block into the #facet
      # method; the block then contains one or more +row+ blocks, each of which
      # creates a query facet row. The +row+ blocks follow the usual Sunspot
      # scope DSL.
      #
      # For example, a query facet can be used to facet over a set of ranges:
      #
      #   Sunspot.search(Post) do
      #     facet(:average_rating) do
      #       row(1.0..2.0) do
      #         with(:average_rating, 1.0..2.0)
      #       end
      #       row(2.0..3.0) do
      #         with(:average_rating, 2.0..3.0)
      #       end
      #       row(3.0..4.0) do
      #         with(:average_rating, 3.0..4.0)
      #       end
      #       row(4.0..5.0) do
      #         with(:average_rating, 4.0..5.0)
      #       end
      #     end
      #   end
      #
      # Note that the arguments to the +facet+ and +row+ methods simply provide
      # labels for the facet and its rows, so that they can be retrieved and
      # identified from the Search object. They are not passed to Solr and no
      # semantic meaning is attached to them. The label for +facet+ should be
      # a symbol; the label for +row+ can be whatever you'd like.
      #
      # ==== Range Facets
      #
      # One can use the Range Faceting feature on any date field or any numeric
      # field that supports range queries. This is particularly useful for the
      # cases in the past where one might stitch together a series of range
      # queries (as facet by query) for things like prices, etc.
      #
      # For example faceting over average ratings can be done as follows:
      #
      #   Sunspot.search(Post) do
      #     facet :average_rating, :range => 1..5, :range_interval => 1
      #   end
      #
      # ==== Parameters
      #
      # field_names...<Symbol>:: fields for which to return field facets
      #
      # ==== Options
      #
      # :sort<Symbol>::
      #   Either :count (values matching the most terms first) or :index (lexical)
      # :limit<Integer>::
      #   The maximum number of facet rows to return
      # :offset<Integer>::
      #   The offset from which to start returning facet rows
      # :minimum_count<Integer>::
      #   The minimum count a facet row must have to be returned
      # :zeros<Boolean>::
      #   Return facet rows for which there are no matches (equivalent to
      #   :minimum_count => 0). Default is false.
      # :exclude<Object,Array>::
      #   Exclude one or more filters when performing the faceting (see
      #   Multiselect Faceting above). The object given for this argument should
      #   be the return value(s) of a scoping method (+with+, +any_of+,
      #   +all_of+, etc.). <strong>Only can be used for field facets that do not
      #   use the +:extra+ or +:only+ options.</strong>
      # :name<Symbol>::
      #   Give a custom name to a field facet. The main use case for this option
      #   is for requesting the same field facet multiple times, using different
      #   filter exclusions (see Multiselect Faceting above). If you pass this
      #   option, it is also the argument that should be passed to Search#facet
      #   when retrieving the facet result.
      # :only<Array>::
      #   Only return facet rows for the given values. Useful if you are only
      #   interested in faceting on a subset of values for a given field.
      #   <strong>Only applies to field facets.</strong>
      # :extra<Symbol,Array>::
      #   One or more of :any and :none. :any returns a facet row with a count
      #   of all matching documents that have some value for this field. :none
      #   returns a facet row with a count of all matching documents that have
      #   no value for this field. The facet row(s) corresponding to the extras
      #   have a value of the symbol passed. <strong>Only applies to field
      #   facets.</strong>
      #
      def facet(*field_names, &block)
        options = Sunspot::Util.extract_options_from(field_names)

        if block
          if field_names.length != 1
            raise(
              ArgumentError,
              "wrong number of arguments (#{field_names.length} for 1)"
            )
          end
          search_facet = @search.add_query_facet(field_names.first, options)
          Sunspot::Util.instance_eval_or_call(
            QueryFacet.new(@query, @setup, search_facet, options),
            &block
          )
        elsif options[:only]
          if options.has_key?(:exclude)
            raise(
              ArgumentError,
              "can't use :exclude with :only (see documentation)"
            )
          end
          field_names.each do |field_name|
            field = @setup.field(field_name)
            search_facet = @search.add_field_facet(field, options)
            Util.Array(options[:only]).each do |value|
              facet = Sunspot::Query::QueryFacet.new
              facet.add_positive_restriction(field, Sunspot::Query::Restriction::EqualTo, value)
              @query.add_query_facet(facet)
              search_facet.add_row(value, facet.to_boolean_phrase)
            end
          end
        else
          field_names.each do |field_name|
            search_facet = nil
            field = @setup.field(field_name)
            facet =
              if options[:time_range]
                unless field.type.is_a?(Sunspot::Type::TimeType)
                  raise(
                    ArgumentError,
                    ':time_range can only be specified for Date or Time fields'
                  )
                end
                search_facet = @search.add_date_facet(field, options)
                Sunspot::Query::DateFieldFacet.new(field, options)
              elsif options[:range]
                unless [Sunspot::Type::TimeType, Sunspot::Type::FloatType, Sunspot::Type::IntegerType ].inject(false){|res,type| res || field.type.is_a?(type)}
                  raise(
                    ArgumentError,
                    ':range can only be specified for date or numeric fields'
                  )
                end
                search_facet = @search.add_range_facet(field, options)
                Sunspot::Query::RangeFacet.new(field, options)
              else
                search_facet = @search.add_field_facet(field, options)
                Sunspot::Query::FieldFacet.new(field, options)
              end
            @query.add_field_facet(facet)
            Util.Array(options[:extra]).each do |extra|
              if options.has_key?(:exclude)
                raise(
                  ArgumentError,
                  "can't use :exclude with :extra (see documentation)"
                )
              end
              extra_facet = Sunspot::Query::QueryFacet.new
              case extra
              when :any
                extra_facet.add_negated_restriction(
                  field,
                  Sunspot::Query::Restriction::EqualTo,
                  nil
                )
              when :none
                extra_facet.add_positive_restriction(
                  field,
                  Sunspot::Query::Restriction::EqualTo,
                  nil
                )
              else
                raise(
                  ArgumentError,
                  "Allowed values for :extra are :any and :none"
                )
              end
              search_facet.add_row(extra, extra_facet.to_boolean_phrase)
              @query.add_query_facet(extra_facet)
            end
          end
        end
      end

      def stats(*field_names, &block)
        options = Sunspot::Util.extract_options_from(field_names)

        field_names.each do |field_name|
          field = @setup.field(field_name)
          query_stats = @query.add_stats(
            Sunspot::Query::FieldStats.new(field, options)
          )
          search_stats = @search.add_field_stats(field)

          Sunspot::Util.instance_eval_or_call(
            FieldStats.new(query_stats, @setup, search_stats),
            &block) if block
        end
      end

      def dynamic(base_name, &block)
        dynamic_field_factory = @setup.dynamic_field_factory(base_name)
        Sunspot::Util.instance_eval_or_call(
          FieldQuery.new(@search, @query, dynamic_field_factory),
          &block
        )
      end
      #
      # Specify that results should be ordered based on a
      # FunctionQuery - http://wiki.apache.org/solr/FunctionQuery
      # Solr 3.1 and up
      #
      #  For example, to order by field1 + (field2*field3):
      #
      #    order_by_function :sum, :field1, [:product, :field2, :field3], :desc
      #
      # ==== Parameters
      # function_name<Symbol>::
      #   the function to run
      # arguments::
      #   the arguments for this function.
      #   - Symbol for a field or function name
      #   - Array for a nested function
      #   - String for a literal constant
      # direction<Symbol>::
      #   :asc or :desc
      def order_by_function(*args)
        @query.add_sort(
          Sunspot::Query::Sort::FunctionSort.new(@setup,args)
        )
      end
    end
  end
end