# frozen_string_literal: true

module Dry
  module Types
    # Constructor types apply a function to the input that is supposed to return
    # a new value. Coercion is a common use case for constructor types.
    #
    # @api public
    class Constructor < Nominal
      include ::Dry::Equalizer(:type, :options, inspect: false, immutable: true)

      # @return [#call]
      attr_reader :fn

      # @return [Type]
      attr_reader :type

      undef :constrained?, :meta, :optional?, :primitive, :primitive?, :default?, :name, :namespace

      # @param [Builder, Object] input
      # @param [Hash] options
      # @param [#call, nil] block
      #
      # @api public
      def self.new(input, fn: Undefined, **options, &block)
        type = input.is_a?(Builder) ? input : Nominal.new(input)
        super(type, **options, fn: Function[Undefined.default(fn, block)])
      end

      # @param [Builder, Object] input
      # @param [Hash] options
      # @param [#call, nil] block
      #
      # @api public
      def self.[](type, fn:, **options)
        function = Function[fn]

        if function.wrapper?
          wrapper_type.new(type, fn: function, **options)
        else
          new(type, fn: function, **options)
        end
      end

      # @api private
      def self.wrapper_type
        @wrapper_type ||=
          if self < Wrapper
            self
          else
            const_set(:Wrapping, ::Class.new(self).include(Wrapper))
          end
      end

      # Instantiate a new constructor type instance
      #
      # @param [Type] type
      # @option [Function] fn
      # @param [Hash] options
      #
      # @api private
      def initialize(type, fn: nil, **options)
        @type = type
        @fn = fn

        super(type, **options, fn: fn)
      end

      # @return [Object]
      #
      # @api private
      def call_safe(input, &block)
        # block.call is slower in JRuby than yield, but in JRuby 10.0.2.0 there is a bug
        # with default block arguments, so block.call is chosen for correctness.
        # This was introduced in PR https://github.com/dry-rb/dry-types/pull/493, might be reverted
        # in the bug is fixed in JRuby.
        coerced = fn.(input) { |output = input| return block.call(output) }
        type.call_safe(coerced) { |output = coerced| block.call(output) }
      end

      # @return [Object]
      #
      # @api private
      def call_unsafe(input) = type.call_unsafe(fn.(input))

      # @param [Object] input
      # @param [#call,nil] block
      #
      # @return [Logic::Result, Types::Result]
      # @return [Object] if block given and try fails
      #
      # @api public
      def try(input, &)
        value = fn.(input)
      rescue CoercionError => exception
        failure = failure(input, exception)
        block_given? ? yield(failure) : failure
      else
        type.try(value, &)
      end

      # Build a new constructor by appending a block to the coercion function
      #
      # @param [#call, nil] new_fn
      # @param [Hash] options
      # @param [#call, nil] block
      #
      # @return [Constructor]
      #
      # @api public
      def constructor(new_fn = nil, **options, &block)
        next_fn = Function[new_fn || block]

        if next_fn.wrapper?
          self.class.wrapper_type.new(with(**options), fn: next_fn)
        else
          with(**options, fn: fn >> next_fn)
        end
      end
      alias_method :append, :constructor
      alias_method :>>, :constructor

      # @return [Class]
      #
      # @api private
      def constrained_type = Constrained::Coercible

      # @see Nominal#to_ast
      #
      # @api public
      def to_ast(meta: true)
        [:constructor, [type.to_ast(meta: meta), fn.to_ast]]
      end

      # Build a new constructor by prepending a block to the coercion function
      #
      # @param [#call, nil] new_fn
      # @param [Hash] options
      # @param [#call, nil] block
      #
      # @return [Constructor]
      #
      # @api public
      def prepend(new_fn = nil, **options, &block)
        with(**options, fn: fn << (new_fn || block))
      end
      alias_method :<<, :prepend

      # Build a lax type
      #
      # @return [Lax]
      # @api public
      def lax = Lax.new(constructor_type[type.lax, **options])

      # Wrap the type with a proc
      #
      # @return [Proc]
      #
      # @api public
      def to_proc = proc { self.(_1) }

      private

      # @param [Symbol] meth
      # @param [Boolean] include_private
      # @return [Boolean]
      #
      # @api private
      def respond_to_missing?(meth, include_private = false)
        super || type.respond_to?(meth)
      end

      # Delegates missing methods to {#type}
      #
      # @param [Symbol] method
      # @param [Array] args
      # @param [#call, nil] block
      #
      # @api private
      def method_missing(method, ...)
        if type.respond_to?(method)
          response = type.public_send(method, ...)

          if response.is_a?(Type) && response.instance_of?(type.class)
            response.constructor_type[response, **options]
          else
            response
          end
        else
          super
        end
      end
    end
  end
end
