# frozen_string_literal: true

module Dry
  module Types
    # Nominal types define a primitive class and do not apply any constructors or constraints
    #
    # Use these types for annotations and the base for building more complex types on top of them.
    #
    # @api public
    class Nominal
      include Type
      include Options
      include Meta
      include Builder
      include Printable
      include ::Dry::Equalizer(:primitive, :options, :meta, inspect: false, immutable: true)

      # @return [Class]
      attr_reader :primitive

      # @return [String, nil]
      attr_reader :namespace

      # @param [Class] primitive
      #
      # @return [Type]
      #
      # @api private
      def self.[](primitive)
        if primitive == ::Array
          Types::Array
        elsif primitive == ::Hash
          Types::Hash
        else
          self
        end
      end

      ALWAYS = proc { true }

      # @param [Type,Class] primitive
      # @param [Hash] options
      #
      # @api private
      def initialize(primitive, **options)
        super
        @primitive = primitive
        @namespace = options[:namespace]
        freeze
      end

      # @return [String]
      #
      # @api public
      def name = primitive.name

      # @return [false]
      #
      # @api public
      def default? = false

      # @return [false]
      #
      # @api public
      def constrained? = false

      # @return [false]
      #
      # @api public
      def optional? = false

      # @param [BasicObject] input
      #
      # @return [BasicObject]
      #
      # @api private
      def call_unsafe(input) = input

      # @param [BasicObject] input
      #
      # @return [BasicObject]
      #
      # @api private
      def call_safe(input, &) = input

      # @param [Object] input
      #
      # @yieldparam [Failure] failure
      # @yieldreturn [Result]
      #
      # @return [Result,Logic::Result] when a block is not provided
      # @return [nil] otherwise
      #
      # @api public
      def try(input, &) = success(input)

      # @param (see Dry::Types::Success#initialize)
      #
      # @return [Result::Success]
      #
      # @api public
      def success(input) = Result::Success.new(input)

      # @param (see Failure#initialize)
      #
      # @return [Result::Failure]
      #
      # @api public
      def failure(input, error)
        raise ::ArgumentError, "error must be a CoercionError" unless error.is_a?(CoercionError)

        Result::Failure.new(input, error)
      end

      # Checks whether value is of a #primitive class
      #
      # @param [Object] value
      #
      # @return [Boolean]
      #
      # @api public
      def primitive?(value) = value.is_a?(primitive)

      # @api private
      def coerce(input, &)
        if primitive?(input)
          input
        elsif block_given?
          yield
        else
          raise CoercionError, "#{input.inspect} must be an instance of #{primitive}"
        end
      end

      # @api private
      def try_coerce(input)
        result = success(input)

        coerce(input) do
          result = failure(
            input,
            CoercionError.new("#{input.inspect} must be an instance of #{primitive}")
          )
        end

        if block_given?
          yield(result)
        else
          result
        end
      end

      # Return AST representation of a type nominal
      #
      # @return [Array]
      #
      # @api public
      def to_ast(meta: true)
        [:nominal, [primitive, namespace_ast, meta_ast(meta)]]
      end

      # Return self. Nominal types are lax by definition
      #
      # @return [Nominal]
      #
      # @api public
      def lax = self

      # Wrap the type with a proc
      #
      # @return [Proc]
      #
      # @api public
      def to_proc = ALWAYS

      private

      # @api private
      def namespace_ast
        if @namespace
          {namespace: @namespace}
        else
          EMPTY_HASH
        end
      end

      # @api private
      def meta_ast(meta)
        if meta
          self.meta
        else
          EMPTY_HASH
        end
      end
    end

    extend ::Dry::Core::Deprecations[:"dry-types"]
    Definition = Nominal
    deprecate_constant(:Definition, message: "Nominal")
  end
end
