# encoding: utf-8
module Mongoid #:nodoc:
  module Criterion #:nodoc:
    module Optional

      # Adds fields to be sorted in ascending order. Will add them in the order
      # they were passed into the method.
      #
      # Example:
      #
      # <tt>criteria.ascending(:title, :dob)</tt>
      def ascending(*fields)
        @options[:sort] = [] unless @options[:sort] || fields.first.nil?
        fields.flatten.each { |field| @options[:sort] << [ field, :asc ] }
        self
      end

      alias :asc :ascending

      # Tells the criteria that the cursor that gets returned needs to be
      # cached. This is so multiple iterations don't hit the database multiple
      # times, however this is not advisable when working with large data sets
      # as the entire results will get stored in memory.
      #
      # Example:
      #
      # <tt>criteria.cache</tt>
      def cache
        @options.merge!(:cache => true); self
      end

      # Will return true if the cache option has been set.
      #
      # Example:
      #
      # <tt>criteria.cached?</tt>
      def cached?
        @options[:cache] == true
      end

      # If the document is using BSON::ObjectIDs the convert the argument to
      # either an object id or an array of them if the supplied argument is an
      # Array. Otherwise just return.
      #
      # Options:
      #  args: A +String+ or an +Array+ convert to +BSON::ObjectID+
      #  cast: A +Boolean+ define if we can or not cast to BSON::ObjectID.
      #        If false, we use the default type of args
      #
      # Example:
      #
      # <tt>Mongoid.cast_ids!("4ab2bc4b8ad548971900005c", true)</tt>
      # <tt>Mongoid.cast_ids!(["4ab2bc4b8ad548971900005c"])</tt>
      #
      # Returns:
      #
      # If using object ids:
      #   An +Array+ of +BSON::ObjectID+ of each element if params is an +Array+
      #   A +BSON::ObjectID+ from params if params is +String+
      # Otherwise:
      #   <tt>args</tt>
      def cast_ids!(args, cast = true)
        return args if !using_object_ids? || args.is_a?(BSON::ObjectID) || !cast
        if args.is_a?(String)
          BSON::ObjectID(args)
        else
          args.map{ |a|
            a.is_a?(BSON::ObjectID) ? a : BSON::ObjectID(a)
          }
        end
      end

      # Adds fields to be sorted in descending order. Will add them in the order
      # they were passed into the method.
      #
      # Example:
      #
      # <tt>criteria.descending(:title, :dob)</tt>
      def descending(*fields)
        @options[:sort] = [] unless @options[:sort] || fields.first.nil?
        fields.flatten.each { |field| @options[:sort] << [ field, :desc ] }
        self
      end

      alias :desc :descending

      # Flags the criteria to execute against a read-only slave in the pool
      # instead of master.
      #
      # Example:
      #
      # <tt>criteria.enslave</tt>
      def enslave
        @options.merge!(:enslave => true); self
      end

      # Will return true if the criteria is enslaved.
      #
      # Example:
      #
      # <tt>criteria.enslaved?</tt>
      def enslaved?
        @options[:enslave] == true
      end

      # Adds a criterion to the +Criteria+ that specifies additional options
      # to be passed to the Ruby driver, in the exact format for the driver.
      #
      # Options:
      #
      # extras: A +Hash+ that gets set to the driver options.
      #
      # Example:
      #
      # <tt>criteria.extras(:limit => 20, :skip => 40)</tt>
      #
      # Returns: <tt>self</tt>
      def extras(extras)
        @options.merge!(extras); filter_options; self
      end

      # Adds a criterion to the +Criteria+ that specifies an id that must be matched.
      #
      # Options:
      #
      # object_id: A single id or an array of ids in +String+ or <tt>BSON::ObjectID</tt> format
      #
      # Example:
      #
      # <tt>criteria.id("4ab2bc4b8ad548971900005c")</tt>
      # <tt>criteria.id(["4ab2bc4b8ad548971900005c", "4c454e7ebf4b98032d000001"])</tt>
      #
      # Returns: <tt>self</tt>
      def id(*ids)
        ids.flatten!
        if ids.size > 1
          self.in(:_id => cast_ids!(ids, self.klass.primary_key.nil?))
        else
          @selector[:_id] = cast_ids!(ids.first, self.klass.primary_key.nil?)
        end
        self
      end

      # Adds a criterion to the +Criteria+ that specifies the maximum number of
      # results to return. This is mostly used in conjunction with <tt>skip()</tt>
      # to handle paginated results.
      #
      # Options:
      #
      # value: An +Integer+ specifying the max number of results. Defaults to 20.
      #
      # Example:
      #
      # <tt>criteria.limit(100)</tt>
      #
      # Returns: <tt>self</tt>
      def limit(value = 20)
        @options[:limit] = value; self
      end

      # Returns the offset option. If a per_page option is in the list then it
      # will replace it with a skip parameter and return the same value. Defaults
      # to 20 if nothing was provided.
      def offset(*args)
        args.size > 0 ? skip(args.first) : @options[:skip]
      end

      # Adds a criterion to the +Criteria+ that specifies the sort order of
      # the returned documents in the database. Similar to a SQL "ORDER BY".
      #
      # Options:
      #
      # params: An +Array+ of [field, direction] sorting pairs.
      #
      # Example:
      #
      # <tt>criteria.order_by([[:field1, :asc], [:field2, :desc]])</tt>
      #
      # Returns: <tt>self</tt>
      def order_by(*args)
        @options[:sort] = [] unless @options[:sort] || args.first.nil?
        arguments = args.first
        case arguments
        when Hash then arguments.each { |field, direction| @options[:sort] << [ field, direction ] }
        when Array then @options[:sort].concat(arguments)
        when Complex
          args.flatten.each { |complex| @options[:sort] << [ complex.key, complex.operator.to_sym ] }
        end; self
      end

      # Adds a criterion to the +Criteria+ that specifies how many results to skip
      # when returning Documents. This is mostly used in conjunction with
      # <tt>limit()</tt> to handle paginated results, and is similar to the
      # traditional "offset" parameter.
      #
      # Options:
      #
      # value: An +Integer+ specifying the number of results to skip. Defaults to 0.
      #
      # Example:
      #
      # <tt>criteria.skip(20)</tt>
      #
      # Returns: <tt>self</tt>
      def skip(value = 0)
        @options[:skip] = value; self
      end

      # Adds a criterion to the +Criteria+ that specifies a type or an Array of
      # type that must be matched.
      #
      # Options:
      #
      # types : An +Array+ of types of a +String+ representing the Type of you search
      #
      # Example:
      #
      # <tt>criteria.type('Browser')</tt>
      # <tt>criteria.type(['Firefox', 'Browser'])</tt>
      #
      # Returns: <tt>self</tt>
      def type(types)
        types = [types] unless types.is_a?(Array)
        self.in(:_type => types)
      end
    end
  end
end