# frozen_string_literal: true

require_relative 'array_coercer'
require_relative 'set_coercer'
require_relative 'primitive_coercer'

module Grape
  module Validations
    module Types
      # Chooses the best coercer for the given type. For example, if the type
      # is Integer, it will return a coercer which will be able to coerce a value
      # to the integer.
      #
      # There are a few very special coercers which might be returned.
      #
      # +Grape::Types::MultipleTypeCoercer+ is a coercer which is returned when
      # the given type implies values in an array with different types.
      # For example, +[Integer, String]+ allows integer and string values in
      # an array.
      #
      # +Grape::Types::CustomTypeCoercer+ is a coercer which is returned when
      # a method is specified by a user with +coerce_with+ option or the user
      # specifies a custom type which implements requirments of
      # +Grape::Types::CustomTypeCoercer+.
      #
      # +Grape::Types::CustomTypeCollectionCoercer+ is a very similar to the
      # previous one, but it expects an array or set of values having a custom
      # type implemented by the user.
      #
      # There is also a group of custom types implemented by Grape, check
      # +Grape::Validations::Types::SPECIAL+ to get the full list.
      #
      # @param type [Class] the type to which input strings
      #   should be coerced
      # @param method [Class,#call] the coercion method to use
      # @return [Object] object to be used
      #   for coercion and type validation
      def self.build_coercer(type, method: nil, strict: false)
        cache_instance(type, method, strict) do
          create_coercer_instance(type, method, strict)
        end
      end

      def self.create_coercer_instance(type, method, strict)
        # Use a special coercer for multiply-typed parameters.
        if Types.multiple?(type)
          MultipleTypeCoercer.new(type, method)

        # Use a special coercer for custom types and coercion methods.
        elsif method || Types.custom?(type)
          CustomTypeCoercer.new(type, method)

        # Special coercer for collections of types that implement a parse method.
        # CustomTypeCoercer (above) already handles such types when an explicit coercion
        # method is supplied.
        elsif Types.collection_of_custom?(type)
          Types::CustomTypeCollectionCoercer.new(
            type.first, type.is_a?(Set)
          )
        elsif Types.special?(type)
          Types::SPECIAL[type].new
        elsif type.is_a?(Array)
          ArrayCoercer.new type, strict
        elsif type.is_a?(Set)
          SetCoercer.new type, strict
        else
          PrimitiveCoercer.new type, strict
        end
      end

      def self.cache_instance(type, method, strict, &_block)
        key = cache_key(type, method, strict)

        return @__cache[key] if @__cache.key?(key)

        instance = yield

        @__cache_write_lock.synchronize do
          @__cache[key] = instance
        end

        instance
      end

      def self.cache_key(type, method, strict)
        [type, method, strict].each_with_object(+'_') do |val, memo|
          next if val.nil?

          memo << '_' << val.to_s
        end
      end

      instance_variable_set(:@__cache,            {})
      instance_variable_set(:@__cache_write_lock, Mutex.new)
    end
  end
end
