# frozen_string_literal: true

module Contracts
  module Validators
    DEFAULT_VALIDATOR_STRATEGIES = {
      # e.g. lambda {true}
      Proc => lambda { |contract| contract },

      # e.g. [Num, String]
      # TODO: account for these errors too
      Array => lambda do |contract|
        lambda do |arg|
          return false unless arg.is_a?(Array) && arg.length == contract.length

          arg.zip(contract).all? do |_arg, _contract|
            Contract.valid?(_arg, _contract)
          end
        end
      end,

      # e.g. { :a => Num, :b => String }
      Hash => lambda do |contract|
        lambda do |arg|
          return false unless arg.is_a?(Hash)

          contract.keys.all? do |k|
            Contract.valid?(arg[k], contract[k])
          end
        end
      end,

      Range => lambda do |contract|
        lambda do |arg|
          contract.include?(arg)
        end
      end,

      Regexp => lambda do |contract|
        lambda do |arg|
          arg =~ contract
        end
      end,

      Contracts::Args => lambda do |contract|
        lambda do |arg|
          Contract.valid?(arg, contract.contract)
        end
      end,

      Contracts::Func => lambda do |_|
        lambda do |arg|
          arg.is_a?(Method) || arg.is_a?(Proc)
        end
      end,

      :valid => lambda do |contract|
        lambda { |arg| contract.valid?(arg) }
      end,

      :class => lambda do |contract|
        lambda { |arg| arg.is_a?(contract) }
      end,

      :default => lambda do |contract|
        lambda { |arg| contract == arg }
      end,
    }.freeze

    # Allows to override validator with custom one.
    # Example:
    #   Contract.override_validator(Array) do |contract|
    #     lambda do |arg|
    #       # .. implementation for Array contract ..
    #     end
    #   end
    #
    #   Contract.override_validator(:class) do |contract|
    #     lambda do |arg|
    #       arg.is_a?(contract) || arg.is_a?(RSpec::Mocks::Double)
    #     end
    #   end
    def override_validator(name, &block)
      validator_strategies[name] = block
    end

    # This is a little weird. For each contract
    # we pre-make a proc to validate it so we
    # don't have to go through this decision tree every time.
    # Seems silly but it saves us a bunch of time (4.3sec vs 5.2sec)
    def make_validator!(contract)
      klass = contract.class
      key = if validator_strategies.key?(klass)
              klass
            else
              if contract.respond_to? :valid?
                :valid
              elsif [Class, Module].include?(klass)
                :class
              else
                :default
              end
            end

      validator_strategies[key].call(contract)
    end

    def make_validator(contract)
      contract_id = Support.contract_id(contract)

      if memoized_validators.key?(contract_id)
        return memoized_validators[contract_id]
      end

      memoized_validators[contract_id] = make_validator!(contract)
    end

    # @private
    def reset_validators
      clean_memoized_validators
      restore_validators
    end

    # @private
    def validator_strategies
      @_validator_strategies ||= restore_validators
    end

    # @private
    def restore_validators
      @_validator_strategies = DEFAULT_VALIDATOR_STRATEGIES.dup
    end

    # @private
    def memoized_validators
      @_memoized_validators ||= clean_memoized_validators
    end

    # @private
    def clean_memoized_validators
      @_memoized_validators = {}
    end
  end
end
