# frozen_string_literal: true

require "rom/types"

module Hanami
  module Model
    # Types definitions
    #
    # @since 0.7.0
    module Types
      include ROM::Types

      # @since 0.7.0
      # @api private
      def self.included(mod)
        mod.extend(ClassMethods)
      end

      # Class level interface
      #
      # @since 0.7.0
      module ClassMethods
        # Define an entity of the given type
        #
        # @param type [Hanami::Entity] an entity
        #
        # @since 1.1.0
        #
        # @example
        #   require "hanami/model"
        #
        #   class Account < Hanami::Entity
        #     attributes do
        #       # ...
        #       attribute :owner, Types::Entity(User)
        #     end
        #   end
        #
        #   account = Account.new(owner: User.new(name: "Luca"))
        #   account.owner.class # => User
        #   account.owner.name  # => "Luca"
        #
        #   account = Account.new(owner: { name: "MG" })
        #   account.owner.class # => User
        #   account.owner.name  # => "MG"
        def Entity(type)
          type = Schema::CoercibleType.new(type) unless type.is_a?(Dry::Types::Definition)
          type
        end

        # Define an array of given type
        #
        # @param type [Object] an object
        #
        # @since 0.7.0
        #
        # @example
        #   require "hanami/model"
        #
        #   class Account < Hanami::Entity
        #     attributes do
        #       # ...
        #       attribute :users, Types::Collection(User)
        #     end
        #   end
        #
        #   account = Account.new(users: [User.new(name: "Luca")])
        #   user    = account.users.first
        #   user.class # => User
        #   user.name  # => "Luca"
        #
        #   account = Account.new(users: [{ name: "MG" }])
        #   user    = account.users.first
        #   user.class # => User
        #   user.name  # => "MG"
        def Collection(type)
          type = Schema::CoercibleType.new(type) unless type.is_a?(Dry::Types::Definition)
          Types::Array.member(type)
        end
      end

      # Types for schema definitions
      #
      # @since 0.7.0
      module Schema
        # Coercer for objects within custom schema definition
        #
        # @since 0.7.0
        # @api private
        class CoercibleType < Dry::Types::Definition
          # Coerce given value into the wrapped object type
          #
          # @param value [Object] the value
          #
          # @return [Object] the coerced value of `object` type
          #
          # @raise [TypeError] if value can't be coerced
          #
          # @since 0.7.0
          # @api private
          def call(value)
            return if value.nil?

            if valid?(value)
              coerce(value)
            else
              raise TypeError.new("#{value.inspect} must be coercible into #{object}")
            end
          end

          # Check if value can be coerced
          #
          # It is true if value is an instance of `object` type or if value
          # responds to `#to_hash`.
          #
          # @param value [Object] the value
          #
          # @return [TrueClass,FalseClass] the result of the check
          #
          # @since 0.7.0
          # @api private
          def valid?(value)
            value.is_a?(object) ||
              value.respond_to?(:to_hash)
          end

          # Coerce given value into an instance of `object` type
          #
          # @param value [Object] the value
          #
          # @return [Object] the coerced value of `object` type
          def coerce(value)
            case value
            when object
              value
            else
              object.new(value.to_hash)
            end
          end

          # @since 0.7.0
          # @api private
          def object
            result = primitive
            return result unless result.respond_to?(:primitive)

            result.primitive
          end
        end
      end
    end
  end
end
