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

    # The selector is a hash-like object that has special behaviour for merging
    # mongoid criteria selectors.
    class Selector < Hash

      attr_reader :fields, :klass

      # Create the new selector.
      #
      # @example Create the selector.
      #   Selector.new(Person)
      #
      # @param [ Class ] klass The class the selector is for.
      #
      # @since 1.0.0
      def initialize(klass)
        @fields, @klass = klass.fields.except("_id", "_type"), klass
      end

      # Set the value for the supplied key, attempting to typecast the value.
      #
      # @example Set the value for the key.
      #   selector["$ne"] = { :name => "Zorg" }
      #
      # @param [ String, Symbol ] key The hash key.
      # @param [ Object ] value The value to set.
      #
      # @since 2.0.0
      def []=(key, value)
        super(key, try_to_typecast(key, value))
      end

      # Merge the selector with another hash.
      #
      # @example Merge the objects.
      #   selector.merge!({ :key => "value" })
      #
      # @param [ Hash, Selector ] other The object to merge with.
      #
      # @return [ Selector ] The merged selector.
      #
      # @since 1.0.0
      def merge!(other)
        tap do |selector|
          other.each_pair do |key, value|
            selector[key] = value
          end
        end
      end
      alias :update :merge!

      if RUBY_VERSION < '1.9'

        # Generate pretty inspection for old ruby versions.
        #
        # @example Inspect the selector.
        #   selector.inspect
        #
        # @return [ String ] The inspected selector.
        def inspect
          ret = self.keys.inject([]) do |ret, key|
            ret << "#{key.inspect}=>#{self[key].inspect}"
          end
          "{#{ret.sort.join(', ')}}"
        end
      end

      private

      # If the key is defined as a field, then attempt to typecast it.
      #
      # @example Try to cast.
      #   selector.try_to_typecast(:id, 1)
      #
      # @param [ String, Symbol ] key The field name.
      # @param [ Object ] value The value.
      #
      # @return [ Object ] The typecasted value.
      #
      # @since 1.0.0
      def try_to_typecast(key, value)
        access = key.to_s
        return value unless fields.has_key?(access)
        field = fields[access]
        typecast_value_for(field, value)
      end

      # Get the typecast value for the defined field.
      #
      # @example Get the typecast value.
      #   selector.typecast_value_for(:name, "Corbin")
      #
      # @param [ Field ] field The defined field.
      # @param [ Object ] value The value to cast.
      #
      # @return [ Object ] The cast value.
      #
      # @since 1.0.0
      def typecast_value_for(field, value)
        return field.set(value) if field.type === value
        case value
        when Hash
          value = value.dup
          value.each_pair do |k, v|
            value[k] = typecast_hash_value(field, k, v)
          end
        when Array
          value.map { |v| typecast_value_for(field, v) }
        when Regexp
          value
        else
          field.type == Array ? value.class.set(value) : field.set(value)
        end
      end

      # Typecast the value for booleans and integers in hashes.
      #
      # @example Typecast the hash values.
      #   selector.typecast_hash_value(field, "$exists", "true")
      #
      # @param [ Field ] field The defined field.
      # @param [ String ] key The modifier key.
      # @param [ Object ] value The value to cast.
      #
      # @return [ Object ] The cast value.
      #
      # @since 1.0.0
      def typecast_hash_value(field, key, value)
        case key
        when "$exists"
          Boolean.set(value)
        when "$size"
          Integer.set(value)
        else
          typecast_value_for(field, value)
        end
      end
    end
  end
end
