module DataMapper
  class Property
    # for options_with_message
    accept_options :message, :messages, :set, :validates, :auto_validation, :format
  end

  module Validations
    module AutoValidations
      @disable_auto_validations = false

      # adds message for validator
      def options_with_message(base_options, property, validator_name)
        options = base_options.clone
        opts    = property.options

        if opts.key?(:messages)
          options[:message] = opts[:messages][validator_name]
        elsif opts.key?(:message)
          options[:message] = opts[:message]
        end

        options
      end

      attr_reader :disable_auto_validations

      # disables generation of validations for
      # duration of given block
      def without_auto_validations(&block)
        @disable_auto_validations = true
        block.call
        @disable_auto_validations = false
      end

      ##
      # Auto-generate validations for a given property. This will only occur
      # if the option :auto_validation is either true or left undefined.
      #
      #   Triggers that generate validator creation
      #
      #   :required => true
      #       Setting the option :required to true causes a
      #       validates_presence_of validator to be automatically created on
      #       the property
      #
      #   :length => 20
      #       Setting the option :length causes a validates_length_of
      #       validator to be automatically created on the property. If the
      #       value is a Integer the validation will set :maximum => value if
      #       the value is a Range the validation will set :within => value
      #
      #   :format => :predefined / lambda / Proc
      #       Setting the :format option causes a validates_format_of
      #       validator to be automatically created on the property
      #
      #   :set => ["foo", "bar", "baz"]
      #       Setting the :set option causes a validates_within
      #       validator to be automatically created on the property
      #
      #   Integer type
      #       Using a Integer type causes a validates_numericality_of
      #       validator to be created for the property.  integer_only
      #       is set to true
      #
      #   BigDecimal or Float type
      #       Using a Integer type causes a validates_numericality_of
      #       validator to be created for the property.  integer_only
      #       is set to false, and precision/scale match the property
      #
      #
      #   Messages
      #
      #   :messages => {..}
      #       Setting :messages hash replaces standard error messages
      #       with custom ones. For instance:
      #       :messages => {:presence => "Field is required",
      #                     :format => "Field has invalid format"}
      #       Hash keys are: :presence, :format, :length, :is_unique,
      #                      :is_number, :is_primitive
      #
      #   :message => "Some message"
      #       It is just shortcut if only one validation option is set
      #
      def auto_generate_validations(property)
        return if disabled_auto_validations? || skip_auto_validation_for?(property)

        # all auto-validations (aside from presence) should skip
        # validation when the value is nil
        opts = { :allow_nil => true }

        if property.options.key?(:validates)
          opts[:context] = property.options[:validates]
        end

        infer_presence_validation_for(property, opts.dup)
        infer_length_validation_for(property, opts.dup)
        infer_format_validation_for(property, opts.dup)
        infer_uniqueness_validation_for(property, opts.dup)
        infer_within_validation_for(property, opts.dup)
        infer_type_validation_for(property, opts.dup)
      end # auto_generate_validations

      # Checks whether auto validations are currently
      # disabled (see +disable_auto_validations+ method
      # that takes a block)
      #
      # @return [TrueClass, FalseClass]
      #   true if auto validation is currently disabled
      #
      def disabled_auto_validations?
        @disable_auto_validations || false
      end

      alias_method :auto_validations_disabled?, :disabled_auto_validations?

      # Checks whether or not property should be auto validated.
      # It is the case for properties with :auto_validation option
      # given and it's value evaluates to true
      #
      # @return [TrueClass, FalseClass]
      #   true for properties with :auto_validation option that has positive value
      def skip_auto_validation_for?(property)
        property.options.key?(:auto_validation) && !property.options[:auto_validation]
      end

      def infer_presence_validation_for(property, options)
        return if skip_presence_validation?(property)

        validates_presence_of property.name, options_with_message(options, property, :presence)
      end

      def infer_length_validation_for(property, options)
        return unless [ DataMapper::Property::String, DataMapper::Property::Text ].include?(property.class)

        case length = property.options.fetch(:length, DataMapper::Property::String::DEFAULT_LENGTH)
          when Range then options[:within]  = length
          else            options[:maximum] = length
        end

        validates_length_of property.name, options_with_message(options, property, :length)
      end

      def infer_format_validation_for(property, options)
        return unless property.options.key?(:format)

        options[:with] = property.options[:format]

        validates_format_of property.name, options_with_message(options, property, :format)
      end

      def infer_uniqueness_validation_for(property, options)
        return unless property.options.key?(:unique)

        case value = property.options[:unique]
          when Array, Symbol
            options[:scope] = Array(value)

            validates_uniqueness_of property.name, options_with_message(options, property, :is_unique)
          when TrueClass
            validates_uniqueness_of property.name, options_with_message(options, property, :is_unique)
        end
      end

      def infer_within_validation_for(property, options)
        return unless property.options.key?(:set)

        options[:set] = property.options[:set]

        validates_within property.name, options_with_message(options, property, :within)
      end

      def infer_type_validation_for(property, options)
        return if property.custom?

        if property.kind_of?(Property::Numeric)
          options[:gte] = property.min if property.min
          options[:lte] = property.max if property.max
        end

        if Integer == property.primitive
          options[:integer_only] = true

          validates_numericality_of property.name, options_with_message(options, property, :is_number)
        elsif BigDecimal == property.primitive || Float == property.primitive
          options[:precision] = property.precision
          options[:scale]     = property.scale

          validates_numericality_of property.name, options_with_message(options, property, :is_number)
        else
          # We only need this in the case we don't already
          # have a numeric validator, because otherwise
          # it will cause duplicate validation errors
          validates_primitive_type_of property.name, options_with_message(options, property, :is_primitive)
        end
      end

      private

      def skip_presence_validation?(property)
        property.allow_blank? || property.serial?
      end
    end # module AutoValidations
  end # module Validations
end # module DataMapper
