module ActiveModel
  module Validations
    class LessOrGreaterThanValidator < ActiveModel::EachValidator
      def check_validity!
        unless options[:attr] or options[:value]
          # TODO: If options[:attr], check that it is an actual column or virtual attribute
          raise ArgumentError, "must supply :attr or :value option"
        end
        unless allowed_operators.include?(operator)
          # TODO: If options[:attr], check that it is an actual column or virtual attribute
          raise ArgumentError, ":operator must be one of #{allowed_operators.join(', ')} but was #{operator}"
        end
      end

    protected
      def default_operator
        raise NotImplementedError, 'must be defined in subclass'
      end

      def allowed_operators
        [:<, :<=, :>, :>=, :==]
      end

      def operator
        options[:operator] || default_operator
      end

      # Defaults to the standard mathematical name for these operators, but you're free to override with
      # something that makes more sense ('later than' might make more sense for dates, for example)
      def operator_text
        options[:operator_text] ||
        {
          :<  => 'less than',
          :<= => 'less than or equal to',
          :>  => 'greater than',
          :>= => 'greater than or equal to',
          :== => 'equal to',
        }[operator]
      end

      # The value we're comparing against.
      # If we're comparing to another attribute, options[:attr] must be a symbol.
      # Otherwise, options[:value] may be any comparable value (number, date).
      def other_value
        if comparing_to_attr?
          @record.send options[:attr]
        else
          options[:value]
        end
      end

      def comparing_to_attr?
        !!options[:attr]
      end

      def human_attr_name
        return unless options[:attr]
        if defined?(ActiveRecord::Base)
          ActiveRecord::Base.human_attribute_name(options[:attr])
        else
          options[:attr]
        end
      end

      def message_key
        comparing_to_attr? ? :less_or_greater_than_attr : :less_or_greater_than
      end

    public

      def validate_each(record, attribute, value)
        @record = record
        return if value.blank? or other_value.blank?
        unless value.send(operator, other_value)
          record.errors.add(attribute, message_key,
                            options.merge(
                              value:         value,
                              operator:      operator,
                              operator_text: operator_text,
                              attr_name:     human_attr_name,
                              other_value:   other_value,
                            )
          )
        end
      end
    end
  end
end