# typed: true
# frozen_string_literal: true

require "tapioca/sorbet_ext/name_patch"

module T
  module Generic
    # This module intercepts calls to generic type instantiations and type variable definitions.
    # Tapioca stores the data from those calls in a `GenericTypeRegistry` which can then be used
    # to look up the original call details when we are trying to do code generation.
    #
    # We are interested in the data of the `[]`, `type_member` and `type_template` calls which
    # are all needed to generate good generic information at runtime.
    module TypeStoragePatch
      def [](*types)
        # `T::Generic#[]` just returns `self`, so let's call and store it.
        constant = super
        # `register_type` method builds and returns an instantiated clone of the generic type
        # so, we just return that from this method as well.
        Tapioca::GenericTypeRegistry.register_type(constant, types)
      end

      def type_member(variance = :invariant, fixed: nil, lower: T.untyped, upper: BasicObject)
        # `T::Generic#type_member` just instantiates a `T::Type::TypeMember` instance and returns it.
        # We use that when registering the type member and then later return it from this method.
        Tapioca::TypeVariableModule.new(
          T.cast(self, Module),
          Tapioca::TypeVariableModule::Type::Member,
          variance,
          fixed,
          lower,
          upper
        ).tap do |type_variable|
          Tapioca::GenericTypeRegistry.register_type_variable(self, type_variable)
        end
      end

      def type_template(variance = :invariant, fixed: nil, lower: T.untyped, upper: BasicObject)
        # `T::Generic#type_template` just instantiates a `T::Type::TypeTemplate` instance and returns it.
        # We use that when registering the type template and then later return it from this method.
        Tapioca::TypeVariableModule.new(
          T.cast(self, Module),
          Tapioca::TypeVariableModule::Type::Template,
          variance,
          fixed,
          lower,
          upper
        ).tap do |type_variable|
          Tapioca::GenericTypeRegistry.register_type_variable(self, type_variable)
        end
      end
    end

    prepend TypeStoragePatch
  end

  module Types
    class Simple
      module GenericPatch
        def valid?(obj)
          # Since `Tapioca::TypeVariable` is a `Module`, it will be wrapped by a
          # `Simple` type. We want to always make type variable types valid, so we
          # need to explicitly check that `raw_type` is a `Tapioca::TypeVariable`
          # and return `true`
          if defined?(Tapioca::TypeVariableModule) && Tapioca::TypeVariableModule === @raw_type
            return true
          end

          obj.is_a?(@raw_type)
        end

        # This method intercepts calls to the `name` method for simple types, so that
        # it can ask the name to the type if the type is generic, since, by this point,
        # we've created a clone of that type with the `name` method returning the
        # appropriate name for that specific concrete type.
        def name
          if T::Generic === @raw_type || Tapioca::TypeVariableModule === @raw_type
            # for types that are generic or are type variables, use the name
            # returned by the "name" method of this instance
            @name ||= T.unsafe(@raw_type).name.freeze
          else
            # otherwise, fallback to the normal name lookup
            super
          end
        end
      end

      prepend GenericPatch
    end
  end
end

module Tapioca
  # This is subclassing from `Module` so that instances of this type will be modules.
  # The reason why we want that is because that means those instances will automatically
  # get bound to the constant names they are assigned to by Ruby. As a result, we don't
  # need to do any matching of constants to type variables to bind their names, Ruby will
  # do that automatically for us and we get the `name` method for free from `Module`.
  class TypeVariableModule < Module
    extend T::Sig

    class Type < T::Enum
      enums do
        Member = new("type_member")
        Template = new("type_template")
      end
    end

    sig do
      params(context: Module, type: Type, variance: Symbol, fixed: T.untyped, lower: T.untyped, upper: T.untyped).void
    end
    def initialize(context, type, variance, fixed, lower, upper) # rubocop:disable Metrics/ParameterLists
      @context = context
      @type = type
      @variance = variance
      @fixed = fixed
      @lower = lower
      @upper = upper
      super()
    end

    sig { returns(T.nilable(String)) }
    def name
      constant_name = super

      # This is a hack to work around modules under anonymous modules not having
      # names in 2.6 and 2.7: https://bugs.ruby-lang.org/issues/14895
      #
      # This happens when a type variable is declared under `class << self`, for
      # example.
      #
      # The workaround is to give the parent context a name, at which point, our
      # module gets bound to a name under that name, as well.
      unless constant_name
        constant_name = with_bound_name_pre_3_0 { super }
      end

      constant_name&.split("::")&.last
    end

    sig { returns(String) }
    def serialize
      parts = []
      parts << ":#{@variance}" unless @variance == :invariant
      parts << "fixed: #{@fixed}" if @fixed
      parts << "lower: #{@lower}" unless @lower == T.untyped
      parts << "upper: #{@upper}" unless @upper == BasicObject

      parameters = parts.join(", ")

      serialized = @type.serialize.dup
      serialized << "(#{parameters})" unless parameters.empty?
      serialized
    end

    private

    sig do
      type_parameters(:Result)
        .params(block: T.proc.returns(T.type_parameter(:Result)))
        .returns(T.type_parameter(:Result))
    end
    def with_bound_name_pre_3_0(&block)
      require "securerandom"
      temp_name = "TYPE_VARIABLE_TRACKING_#{SecureRandom.hex}"
      self.class.const_set(temp_name, @context)
      block.call
    ensure
      self.class.send(:remove_const, temp_name) if temp_name
    end
  end
end