# typed: strict
# frozen_string_literal: true

module RubyLsp
  module Requests
    # The
    # [references](https://microsoft.github.io/language-server-protocol/specification#textDocument_references)
    # request finds all references for the selected symbol.
    class References < Request
      include Support::Common

      #: (GlobalState global_state, Store store, (RubyDocument | ERBDocument) document, Hash[Symbol, untyped] params) -> void
      def initialize(global_state, store, document, params)
        super()
        @global_state = global_state
        @store = store
        @document = document
        @params = params
        @locations = [] #: Array[Interface::Location]
      end

      # @override
      #: -> Array[Interface::Location]
      def perform
        position = @params[:position]
        char_position, _ = @document.find_index_by_position(position)

        node_context = RubyDocument.locate(
          @document.ast,
          char_position,
          node_types: [
            Prism::ConstantReadNode,
            Prism::ConstantPathNode,
            Prism::ConstantPathTargetNode,
            Prism::InstanceVariableAndWriteNode,
            Prism::InstanceVariableOperatorWriteNode,
            Prism::InstanceVariableOrWriteNode,
            Prism::InstanceVariableReadNode,
            Prism::InstanceVariableTargetNode,
            Prism::InstanceVariableWriteNode,
            Prism::CallNode,
            Prism::DefNode,
          ],
          code_units_cache: @document.code_units_cache,
        )
        target = node_context.node
        parent = node_context.parent
        return @locations if !target || target.is_a?(Prism::ProgramNode)

        if target.is_a?(Prism::ConstantReadNode) && parent.is_a?(Prism::ConstantPathNode)
          target = determine_target(
            target,
            parent,
            position,
          )
        end

        target = target #: as Prism::ConstantReadNode | Prism::ConstantPathNode | Prism::ConstantPathTargetNode | Prism::InstanceVariableAndWriteNode | Prism::InstanceVariableOperatorWriteNode | Prism::InstanceVariableOrWriteNode | Prism::InstanceVariableReadNode | Prism::InstanceVariableTargetNode | Prism::InstanceVariableWriteNode | Prism::CallNode | Prism::DefNode,

        reference_target = create_reference_target(target, node_context)
        return @locations unless reference_target

        Dir.glob(File.join(@global_state.workspace_path, "**/*.rb")).each do |path|
          uri = URI::Generic.from_path(path: path)
          # If the document is being managed by the client, then we should use whatever is present in the store instead
          # of reading from disk
          next if @store.key?(uri)

          parse_result = Prism.parse_lex_file(path)
          collect_references(reference_target, parse_result, uri)
        rescue Errno::EISDIR, Errno::ENOENT
          # If `path` is a directory, just ignore it and continue. If the file doesn't exist, then we also ignore it.
        end

        @store.each do |_uri, document|
          collect_references(reference_target, document.parse_result, document.uri)
        end

        @locations
      end

      private

      #: ((Prism::ConstantReadNode | Prism::ConstantPathNode | Prism::ConstantPathTargetNode | Prism::InstanceVariableAndWriteNode | Prism::InstanceVariableOperatorWriteNode | Prism::InstanceVariableOrWriteNode | Prism::InstanceVariableReadNode | Prism::InstanceVariableTargetNode | Prism::InstanceVariableWriteNode | Prism::CallNode | Prism::DefNode) target_node, NodeContext node_context) -> RubyIndexer::ReferenceFinder::Target?
      def create_reference_target(target_node, node_context)
        case target_node
        when Prism::ConstantReadNode, Prism::ConstantPathNode, Prism::ConstantPathTargetNode
          name = RubyIndexer::Index.constant_name(target_node)
          return unless name

          entries = @global_state.index.resolve(name, node_context.nesting)
          return unless entries

          fully_qualified_name = entries.first #: as !nil
            .name
          RubyIndexer::ReferenceFinder::ConstTarget.new(fully_qualified_name)
        when
          Prism::InstanceVariableAndWriteNode,
          Prism::InstanceVariableOperatorWriteNode,
          Prism::InstanceVariableOrWriteNode,
          Prism::InstanceVariableReadNode,
          Prism::InstanceVariableTargetNode,
          Prism::InstanceVariableWriteNode
          receiver_type = @global_state.type_inferrer.infer_receiver_type(node_context)
          return unless receiver_type

          ancestors = @global_state.index.linearized_ancestors_of(receiver_type.name)
          RubyIndexer::ReferenceFinder::InstanceVariableTarget.new(target_node.name.to_s, ancestors)
        when Prism::CallNode, Prism::DefNode
          RubyIndexer::ReferenceFinder::MethodTarget.new(target_node.name.to_s)
        end
      end

      #: (RubyIndexer::ReferenceFinder::Target target, Prism::LexResult parse_result, URI::Generic uri) -> void
      def collect_references(target, parse_result, uri)
        dispatcher = Prism::Dispatcher.new
        finder = RubyIndexer::ReferenceFinder.new(
          target,
          @global_state.index,
          dispatcher,
          uri,
          include_declarations: @params.dig(:context, :includeDeclaration) || true,
        )
        dispatcher.visit(parse_result.value.first)

        finder.references.each do |reference|
          @locations << Interface::Location.new(
            uri: uri.to_s,
            range: range_from_location(reference.location),
          )
        end
      end
    end
  end
end
