module RBS
  class ConstantTable
    attr_reader :definition_builder
    attr_reader :constant_scopes_cache

    def env
      definition_builder.env
    end

    def resolver
      @resolver ||= TypeNameResolver.from_env(env)
    end

    def initialize(builder:)
      @definition_builder = builder
      @constant_scopes_cache = {}
    end

    def absolute_type(type, context:)
      type.map_type_name do |type_name, location|
        absolute_type_name(type_name, context: context, location: location)
      end
    end

    def absolute_type_name(type_name, context:, location:)
      resolver.resolve(type_name, context: context) or
        raise NoTypeFoundError.new(type_name: type_name, location: location)
    end

    def name_to_constant(name)
      case
      when entry = env.constant_decls[name]
        type = absolute_type(entry.decl.type, context: entry.context)
        Constant.new(name: name, type: type, entry: entry)
      when entry = env.class_decls[name]
        type = Types::ClassSingleton.new(name: name, location: nil)
        Constant.new(name: name, type: type, entry: entry)
      end
    end

    def split_name(name)
      name.namespace.path + [name.name]
    end

    def resolve_constant_reference(name, context:)
      raise "Context cannot be empty: Specify `[Namespace.root]`" if context.empty?

      head, *tail = split_name(name)

      head_constant = case
                      when name.absolute?
                        name_to_constant(TypeName.new(name: head, namespace: Namespace.root))
                      when context == [Namespace.root]
                        name_to_constant(TypeName.new(name: head, namespace: Namespace.root))
                      else
                        resolve_constant_reference_context(head, context: context) ||
                          resolve_constant_reference_inherit(head, scopes: constant_scopes(context.first.to_type_name))
                      end

      if head_constant
        tail.inject(head_constant) do |constant, name|
          resolve_constant_reference_inherit name,
                                             scopes: constant_scopes(constant.name),
                                             no_object: constant.name != BuiltinNames::Object.name
        end
      end
    end

    def resolve_constant_reference_context(name, context:)
      head, *tail = context

      if head
        if head.path.last == name
          name_to_constant(head.to_type_name)
        else
          name_to_constant(TypeName.new(name: name, namespace: head)) ||
            resolve_constant_reference_context(name, context: tail)
        end
      end
    end

    def resolve_constant_reference_inherit(name, scopes:, no_object: false)
      scopes.each do |context|
        if context.path == [:Object]
          unless no_object
            constant = name_to_constant(TypeName.new(name: name, namespace: context)) ||
              name_to_constant(TypeName.new(name: name, namespace: Namespace.root))
          end
        else
          constant = name_to_constant(TypeName.new(name: name, namespace: context))
        end

        return constant if constant
      end

      nil
    end

    def constant_scopes(name)
      constant_scopes_cache[name] ||= constant_scopes0(name, scopes: [])
    end

    def constant_scopes_module(name, scopes:)
      entry = env.class_decls[name]
      namespace = name.to_namespace

      entry.decls.each do |d|
        d.decl.members.each do |member|
          case member
          when AST::Members::Include
            if member.name.class?
              constant_scopes_module absolute_type_name(member.name, context: d.context, location: member.location),
                                     scopes: scopes
            end
          end
        end
      end

      scopes.unshift namespace
    end

    def constant_scopes0(name, scopes: [])
      entry = env.class_decls[name]
      namespace = name.to_namespace

      case entry
      when Environment::ClassEntry
        unless name == BuiltinNames::BasicObject.name
          super_name = entry.primary.decl.super_class&.yield_self do |super_class|
            absolute_type_name(super_class.name, context: entry.primary.context, location: entry.primary.decl.location)
          end || BuiltinNames::Object.name

          constant_scopes0 super_name, scopes: scopes
        end

        entry.decls.each do |d|
          d.decl.members.each do |member|
            case member
            when AST::Members::Include
              if member.name.class?
                constant_scopes_module absolute_type_name(member.name, context: d.context, location: member.location),
                                       scopes: scopes
              end
            end
          end
        end

        scopes.unshift namespace

      when Environment::ModuleEntry
        constant_scopes0 BuiltinNames::Module.name, scopes: scopes
        constant_scopes_module name, scopes: scopes

      else
        raise "Unexpected declaration: #{name} (#{entry.class})"
      end

      scopes
    end
  end
end
