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

    # Validates whether or not a field is unique against the documents in the
    # database.
    #
    # @example Define the uniqueness validator.
    #
    #   class Person
    #     include Mongoid::Document
    #     field :title
    #
    #     validates_uniqueness_of :title
    #   end
    class UniquenessValidator < ActiveModel::EachValidator
      attr_reader :klass

      # Unfortunately, we have to tie Uniqueness validators to a class.
      #
      # @example Setup the validator.
      #   UniquenessValidator.new.setup(Person)
      #
      # @param [ Class ] klass The class getting validated.
      #
      # @since 1.0.0
      def setup(klass)
        @klass = klass
      end

      # Validate the document for uniqueness violations.
      #
      # @example Validate the document.
      #   validate_each(person, :title, "Sir")
      #
      # @param [ Document ] document The document to validate.
      # @param [ Symbol ] attribute The field to validate on.
      # @param [ Object ] value The value of the field.
      #
      # @return [ Errors ] The errors.
      #
      # @since 1.0.0
      def validate_each(document, attribute, value)
        if document.embedded?
          return if skip_validation?(document)
          relation = document._parent.send(document.metadata.name)
          criteria = relation.where(criterion(document, attribute, value))
          criteria = scope(criteria, document, attribute)
          if document.primary_key == Array.wrap(attribute)
            document.errors.add(attribute, :taken, options.except(:case_sensitive, :scope).merge(:value => value)) if criteria.count > 1
          else
            document.errors.add(attribute, :taken, options.except(:case_sensitive, :scope).merge(:value => value)) if criteria.exists?
          end
        else
          criteria = klass.where(criterion(document, attribute, value))
          criteria = scope(criteria, document, attribute)
          document.errors.add(attribute, :taken, options.except(:case_sensitive, :scope).merge(:value => value)) if criteria.exists?
        end
      end

      protected

      # Should the uniqueness validation be case sensitive?
      #
      # @example Is the validation case sensitive?
      #   validator.case_sensitive?
      #
      # @return [ true, false ] If the validation is case sensitive.
      #
      # @since 2.3.0
      def case_sensitive?
        !(options[:case_sensitive] == false)
      end

      # Get the default criteria for checking uniqueness.
      #
      # @example Get the criteria.
      #   validator.criterion(person, :title, "Sir")
      #
      # @param [ Document ] document The document to validate.
      # @param [ Symbol ] attribute The name of the attribute.
      # @param [ Object ] value The value of the object.
      #
      # @return [ Criteria ] The uniqueness criteria.
      #
      # @since 2.3.0
      def criterion(document, attribute, value)
        { attribute => filter(value) }.tap do |selector|
          if document.persisted? ||
            (document.embedded? && (document.primary_key != Array.wrap(attribute)))
            selector.merge!(:_id => { "$ne" => document.id })
          end
        end
      end

      # Filter the value based on whether the check is case sensitive or not.
      #
      # @example Filter the value.
      #   validator.filter("testing")
      #
      # @param [ Object ] value The value to filter.
      #
      # @return [ Object, Regexp ] The value, filtered or not.
      #
      # @since 2.3.0
      def filter(value)
        !case_sensitive? && value ? /^#{Regexp.escape(value.to_s)}$/i : value
      end

      # Scope the criteria to the scope options provided.
      #
      # @example Scope the criteria.
      #   validator.scope(criteria, document)
      #
      # @param [ Criteria ] criteria The criteria to scope.
      # @param [ Document ] document The document being validated.
      #
      # @return [ Criteria ] The scoped criteria.
      #
      # @since 2.3.0
      def scope(criteria, document, attribute)
        unless document.primary_key == Array.wrap(attribute)
          Array.wrap(options[:scope]).each do |item|
            criteria = criteria.where(item => document.attributes[item.to_s])
          end
        end
        criteria
      end

      # Should validation be skipped?
      #
      # @example Should the validation be skipped?
      #   validator.skip_validation?(doc)
      #
      # @param [ Document ] document The embedded document.
      #
      # @return [ true, false ] If the validation should be skipped.
      #
      # @since 2.3.0
      def skip_validation?(document)
        !document._parent || document.embedded_one?
      end
    end
  end
end
