# typed: strict
# frozen_string_literal: true

module RubyLsp
  module Listeners
    class Completion
      include Requests::Support::Common

      KEYWORDS = [
        "alias",
        "and",
        "begin",
        "BEGIN",
        "break",
        "case",
        "class",
        "def",
        "defined?",
        "do",
        "else",
        "elsif",
        "end",
        "END",
        "ensure",
        "false",
        "for",
        "if",
        "in",
        "module",
        "next",
        "nil",
        "not",
        "or",
        "redo",
        "rescue",
        "retry",
        "return",
        "self",
        "super",
        "then",
        "true",
        "undef",
        "unless",
        "until",
        "when",
        "while",
        "yield",
        "__ENCODING__",
        "__FILE__",
        "__LINE__",
      ].freeze

      #: (ResponseBuilders::CollectionResponseBuilder[Interface::CompletionItem] response_builder, GlobalState global_state, NodeContext node_context, SorbetLevel sorbet_level, Prism::Dispatcher dispatcher, URI::Generic uri, String? trigger_character) -> void
      def initialize( # rubocop:disable Metrics/ParameterLists
        response_builder,
        global_state,
        node_context,
        sorbet_level,
        dispatcher,
        uri,
        trigger_character
      )
        @response_builder = response_builder
        @global_state = global_state
        @index = global_state.index #: RubyIndexer::Index
        @type_inferrer = global_state.type_inferrer #: TypeInferrer
        @node_context = node_context
        @sorbet_level = sorbet_level
        @uri = uri
        @trigger_character = trigger_character

        dispatcher.register(
          self,
          :on_constant_path_node_enter,
          :on_constant_read_node_enter,
          :on_call_node_enter,
          :on_global_variable_and_write_node_enter,
          :on_global_variable_operator_write_node_enter,
          :on_global_variable_or_write_node_enter,
          :on_global_variable_read_node_enter,
          :on_global_variable_target_node_enter,
          :on_global_variable_write_node_enter,
          :on_instance_variable_read_node_enter,
          :on_instance_variable_write_node_enter,
          :on_instance_variable_and_write_node_enter,
          :on_instance_variable_operator_write_node_enter,
          :on_instance_variable_or_write_node_enter,
          :on_instance_variable_target_node_enter,
          :on_class_variable_and_write_node_enter,
          :on_class_variable_operator_write_node_enter,
          :on_class_variable_or_write_node_enter,
          :on_class_variable_read_node_enter,
          :on_class_variable_target_node_enter,
          :on_class_variable_write_node_enter,
        )
      end

      # Handle completion on regular constant references (e.g. `Bar`)
      #: (Prism::ConstantReadNode node) -> void
      def on_constant_read_node_enter(node)
        # The only scenario where Sorbet doesn't provide constant completion is on ignored files. Even if the file has
        # no sigil, Sorbet will still provide completion for constants
        return unless @sorbet_level.ignore?

        name = RubyIndexer::Index.constant_name(node)
        return if name.nil?

        range = range_from_location(node.location)
        candidates = @index.constant_completion_candidates(name, @node_context.nesting)
        candidates.each do |entries|
          complete_name = entries.first #: as !nil
            .name
          @response_builder << build_entry_completion(
            complete_name,
            name,
            range,
            entries,
            top_level?(complete_name),
          )
        end
      end

      # Handle completion on namespaced constant references (e.g. `Foo::Bar`)
      #: (Prism::ConstantPathNode node) -> void
      def on_constant_path_node_enter(node)
        # The only scenario where Sorbet doesn't provide constant completion is on ignored files. Even if the file has
        # no sigil, Sorbet will still provide completion for constants
        return unless @sorbet_level.ignore?

        name = begin
          node.full_name
        rescue Prism::ConstantPathNode::MissingNodesInConstantPathError
          node.slice
        rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError
          nil
        end
        return if name.nil?

        constant_path_completion(name, range_from_location(node.location))
      end

      #: (Prism::CallNode node) -> void
      def on_call_node_enter(node)
        # The only scenario where Sorbet doesn't provide constant completion is on ignored files. Even if the file has
        # no sigil, Sorbet will still provide completion for constants
        if @sorbet_level.ignore?
          receiver = node.receiver

          # When writing `Foo::`, the AST assigns a method call node (because you can use that syntax to invoke
          # singleton methods). However, in addition to providing method completion, we also need to show possible
          # constant completions
          if (receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)) &&
              node.call_operator == "::"

            name = RubyIndexer::Index.constant_name(receiver)

            if name
              start_loc = node.location
              end_loc = node.call_operator_loc #: as !nil

              constant_path_completion(
                "#{name}::",
                Interface::Range.new(
                  start: Interface::Position.new(line: start_loc.start_line - 1, character: start_loc.start_column),
                  end: Interface::Position.new(line: end_loc.end_line - 1, character: end_loc.end_column),
                ),
              )
              return
            end
          end
        end

        name = node.message
        return unless name

        case name
        when "require"
          complete_require(node)
        when "require_relative"
          complete_require_relative(node)
        else
          complete_methods(node, name)
        end
      end

      #: (Prism::GlobalVariableAndWriteNode node) -> void
      def on_global_variable_and_write_node_enter(node)
        handle_global_variable_completion(node.name.to_s, node.name_loc)
      end

      #: (Prism::GlobalVariableOperatorWriteNode node) -> void
      def on_global_variable_operator_write_node_enter(node)
        handle_global_variable_completion(node.name.to_s, node.name_loc)
      end

      #: (Prism::GlobalVariableOrWriteNode node) -> void
      def on_global_variable_or_write_node_enter(node)
        handle_global_variable_completion(node.name.to_s, node.name_loc)
      end

      #: (Prism::GlobalVariableReadNode node) -> void
      def on_global_variable_read_node_enter(node)
        handle_global_variable_completion(node.name.to_s, node.location)
      end

      #: (Prism::GlobalVariableTargetNode node) -> void
      def on_global_variable_target_node_enter(node)
        handle_global_variable_completion(node.name.to_s, node.location)
      end

      #: (Prism::GlobalVariableWriteNode node) -> void
      def on_global_variable_write_node_enter(node)
        handle_global_variable_completion(node.name.to_s, node.name_loc)
      end

      #: (Prism::InstanceVariableReadNode node) -> void
      def on_instance_variable_read_node_enter(node)
        handle_instance_variable_completion(node.name.to_s, node.location)
      end

      #: (Prism::InstanceVariableWriteNode node) -> void
      def on_instance_variable_write_node_enter(node)
        handle_instance_variable_completion(node.name.to_s, node.name_loc)
      end

      #: (Prism::InstanceVariableAndWriteNode node) -> void
      def on_instance_variable_and_write_node_enter(node)
        handle_instance_variable_completion(node.name.to_s, node.name_loc)
      end

      #: (Prism::InstanceVariableOperatorWriteNode node) -> void
      def on_instance_variable_operator_write_node_enter(node)
        handle_instance_variable_completion(node.name.to_s, node.name_loc)
      end

      #: (Prism::InstanceVariableOrWriteNode node) -> void
      def on_instance_variable_or_write_node_enter(node)
        handle_instance_variable_completion(node.name.to_s, node.name_loc)
      end

      #: (Prism::InstanceVariableTargetNode node) -> void
      def on_instance_variable_target_node_enter(node)
        handle_instance_variable_completion(node.name.to_s, node.location)
      end

      #: (Prism::ClassVariableAndWriteNode node) -> void
      def on_class_variable_and_write_node_enter(node)
        handle_class_variable_completion(node.name.to_s, node.name_loc)
      end

      #: (Prism::ClassVariableOperatorWriteNode node) -> void
      def on_class_variable_operator_write_node_enter(node)
        handle_class_variable_completion(node.name.to_s, node.name_loc)
      end

      #: (Prism::ClassVariableOrWriteNode node) -> void
      def on_class_variable_or_write_node_enter(node)
        handle_class_variable_completion(node.name.to_s, node.name_loc)
      end

      #: (Prism::ClassVariableTargetNode node) -> void
      def on_class_variable_target_node_enter(node)
        handle_class_variable_completion(node.name.to_s, node.location)
      end

      #: (Prism::ClassVariableReadNode node) -> void
      def on_class_variable_read_node_enter(node)
        handle_class_variable_completion(node.name.to_s, node.location)
      end

      #: (Prism::ClassVariableWriteNode node) -> void
      def on_class_variable_write_node_enter(node)
        handle_class_variable_completion(node.name.to_s, node.name_loc)
      end

      private

      #: (String name, Interface::Range range) -> void
      def constant_path_completion(name, range)
        top_level_reference = if name.start_with?("::")
          name = name.delete_prefix("::")
          true
        else
          false
        end

        # If we're trying to provide completion for an aliased namespace, we need to first discover it's real name in
        # order to find which possible constants match the desired search
        aliased_namespace = if name.end_with?("::")
          name.delete_suffix("::")
        else
          *namespace, incomplete_name = name.split("::")
          namespace.join("::")
        end

        nesting = @node_context.nesting
        namespace_entries = @index.resolve(aliased_namespace, nesting)
        return unless namespace_entries

        namespace_name = namespace_entries.first #: as !nil
          .name
        real_namespace = @index.follow_aliased_namespace(namespace_name)

        candidates = @index.constant_completion_candidates(
          "#{real_namespace}::#{incomplete_name}",
          top_level_reference ? [] : nesting,
        )
        candidates.each do |entries|
          # The only time we may have a private constant reference from outside of the namespace is if we're dealing
          # with ConstantPath and the entry name doesn't start with the current nesting
          first_entry = entries.first #: as !nil
          next if first_entry.private? && !first_entry.name.start_with?("#{nesting}::")

          entry_name = first_entry.name
          full_name = if aliased_namespace != real_namespace
            constant_name = entry_name.delete_prefix("#{real_namespace}::")
            aliased_namespace.empty? ? constant_name : "#{aliased_namespace}::#{constant_name}"
          elsif !entry_name.start_with?(aliased_namespace)
            *_, short_name = entry_name.split("::")
            "#{aliased_namespace}::#{short_name}"
          else
            entry_name
          end

          @response_builder << build_entry_completion(
            full_name,
            name,
            range,
            entries,
            top_level_reference || top_level?(first_entry.name),
          )
        end
      end

      #: (String name, Prism::Location location) -> void
      def handle_global_variable_completion(name, location)
        candidates = @index.prefix_search(name)

        return if candidates.none?

        range = range_from_location(location)

        candidates.flatten.uniq(&:name).each do |entry|
          entry_name = entry.name

          @response_builder << Interface::CompletionItem.new(
            label: entry_name,
            filter_text: entry_name,
            label_details: Interface::CompletionItemLabelDetails.new(
              description: entry.file_name,
            ),
            text_edit: Interface::TextEdit.new(range: range, new_text: entry_name),
            kind: Constant::CompletionItemKind::VARIABLE,
          )
        end
      end

      #: (String name, Prism::Location location) -> void
      def handle_class_variable_completion(name, location)
        type = @type_inferrer.infer_receiver_type(@node_context)
        return unless type

        range = range_from_location(location)

        @index.class_variable_completion_candidates(name, type.name).each do |entry|
          variable_name = entry.name

          label_details = Interface::CompletionItemLabelDetails.new(
            description: entry.file_name,
          )

          @response_builder << Interface::CompletionItem.new(
            label: variable_name,
            label_details: label_details,
            text_edit: Interface::TextEdit.new(
              range: range,
              new_text: variable_name,
            ),
            kind: Constant::CompletionItemKind::FIELD,
            data: {
              owner_name: entry.owner&.name,
            },
          )
        end
      rescue RubyIndexer::Index::NonExistingNamespaceError
        # If by any chance we haven't indexed the owner, then there's no way to find the right declaration
      end

      #: (String name, Prism::Location location) -> void
      def handle_instance_variable_completion(name, location)
        # Sorbet enforces that all instance variables be declared on typed strict or higher, which means it will be able
        # to provide all features for them
        return if @sorbet_level.strict?

        type = @type_inferrer.infer_receiver_type(@node_context)
        return unless type

        range = range_from_location(location)
        @index.instance_variable_completion_candidates(name, type.name).each do |entry|
          variable_name = entry.name

          label_details = Interface::CompletionItemLabelDetails.new(
            description: entry.file_name,
          )

          @response_builder << Interface::CompletionItem.new(
            label: variable_name,
            label_details: label_details,
            text_edit: Interface::TextEdit.new(
              range: range,
              new_text: variable_name,
            ),
            kind: Constant::CompletionItemKind::FIELD,
            data: {
              owner_name: entry.owner&.name,
            },
          )
        end
      rescue RubyIndexer::Index::NonExistingNamespaceError
        # If by any chance we haven't indexed the owner, then there's no way to find the right declaration
      end

      #: (Prism::CallNode node) -> void
      def complete_require(node)
        arguments_node = node.arguments
        return unless arguments_node

        path_node_to_complete = arguments_node.arguments.first

        return unless path_node_to_complete.is_a?(Prism::StringNode)

        matched_uris = @index.search_require_paths(path_node_to_complete.content)

        matched_uris.map!(&:require_path).sort!.each do |path|
          @response_builder << build_completion(
            path, #: as !nil
            path_node_to_complete,
          )
        end
      end

      #: (Prism::CallNode node) -> void
      def complete_require_relative(node)
        arguments_node = node.arguments
        return unless arguments_node

        path_node_to_complete = arguments_node.arguments.first
        return unless path_node_to_complete.is_a?(Prism::StringNode)

        # If the file is unsaved (e.g.: untitled:Untitled-1), we can't provide relative completion as we don't know
        # where the user intends to save it
        full_path = @uri.to_standardized_path
        return unless full_path

        origin_dir = Pathname.new(full_path).dirname
        content = path_node_to_complete.content
        # if the path is not a directory, glob all possible next characters
        # for example ../somethi| (where | is the cursor position)
        # should find files for ../somethi*/
        path_query = if content.end_with?("/") || content.empty?
          "#{content}**/*.rb"
        else
          "{#{content}*/**/*.rb,**/#{content}*.rb}"
        end

        Dir.glob(path_query, File::FNM_PATHNAME | File::FNM_EXTGLOB, base: origin_dir).sort!.each do |path|
          @response_builder << build_completion(
            path.delete_suffix(".rb"),
            path_node_to_complete,
          )
        end
      rescue Errno::EPERM
        # If the user writes a relative require pointing to a path that the editor has no permissions to read, then glob
        # might fail with EPERM
      end

      #: (Prism::CallNode node, String name) -> void
      def complete_methods(node, name)
        # If the node has a receiver, then we don't need to provide local nor keyword completions. Sorbet can provide
        # local and keyword completion for any file with a Sorbet level of true or higher
        if !@sorbet_level.true_or_higher? && !node.receiver
          add_local_completions(node, name)
          add_keyword_completions(node, name)
        end

        # Sorbet can provide completion for methods invoked on self on typed true or higher files
        return if @sorbet_level.true_or_higher? && self_receiver?(node)

        type = @type_inferrer.infer_receiver_type(@node_context)
        return unless type

        # When the trigger character is a dot, Prism matches the name of the call node to whatever is next in the source
        # code, leading to us searching for the wrong name. What we want to do instead is show every available method
        # when dot is pressed
        method_name = @trigger_character == "." ? nil : name

        range = if method_name
          range_from_location(
            node.message_loc, #: as !nil
          )
        else
          loc = node.call_operator_loc

          if loc
            Interface::Range.new(
              start: Interface::Position.new(line: loc.start_line - 1, character: loc.start_column + 1),
              end: Interface::Position.new(line: loc.start_line - 1, character: loc.start_column + 1),
            )
          end
        end

        return unless range

        guessed_type = type.is_a?(TypeInferrer::GuessedType) && type.name
        external_references = @node_context.fully_qualified_name != type.name

        @index.method_completion_candidates(method_name, type.name).each do |entry|
          next if entry.visibility != :public && external_references

          entry_name = entry.name
          owner_name = entry.owner&.name
          new_text = entry_name

          if entry_name.end_with?("=")
            method_name = entry_name.delete_suffix("=")

            # For writer methods, format as assignment and prefix "self." when no receiver is specified
            new_text = node.receiver.nil? ? "self.#{method_name} = " : "#{method_name} = "
          end

          label_details = Interface::CompletionItemLabelDetails.new(
            description: entry.file_name,
            detail: entry.decorated_parameters,
          )
          @response_builder << Interface::CompletionItem.new(
            label: entry_name,
            filter_text: entry_name,
            label_details: label_details,
            text_edit: Interface::TextEdit.new(range: range, new_text: new_text),
            kind: Constant::CompletionItemKind::METHOD,
            data: {
              owner_name: owner_name,
              guessed_type: guessed_type,
            },
          )
        end
      rescue RubyIndexer::Index::NonExistingNamespaceError
        # We have not indexed this namespace, so we can't provide any completions
      end

      #: (Prism::CallNode node, String name) -> void
      def add_local_completions(node, name)
        range = range_from_location(
          node.message_loc, #: as !nil
        )

        @node_context.locals_for_scope.each do |local|
          local_name = local.to_s
          next unless local_name.start_with?(name)

          @response_builder << Interface::CompletionItem.new(
            label: local_name,
            filter_text: local_name,
            text_edit: Interface::TextEdit.new(range: range, new_text: local_name),
            kind: Constant::CompletionItemKind::VARIABLE,
            data: {
              skip_resolve: true,
            },
          )
        end
      end

      #: (Prism::CallNode node, String name) -> void
      def add_keyword_completions(node, name)
        range = range_from_location(
          node.message_loc, #: as !nil
        )

        KEYWORDS.each do |keyword|
          next unless keyword.start_with?(name)

          @response_builder << Interface::CompletionItem.new(
            label: keyword,
            text_edit: Interface::TextEdit.new(range: range, new_text: keyword),
            kind: Constant::CompletionItemKind::KEYWORD,
            data: {
              keyword: true,
            },
          )
        end
      end

      #: (String label, Prism::StringNode node) -> Interface::CompletionItem
      def build_completion(label, node)
        # We should use the content location as we only replace the content and not the delimiters of the string
        loc = node.content_loc

        Interface::CompletionItem.new(
          label: label,
          text_edit: Interface::TextEdit.new(
            range: range_from_location(loc),
            new_text: label,
          ),
          kind: Constant::CompletionItemKind::FILE,
        )
      end

      #: (String real_name, String incomplete_name, Interface::Range range, Array[RubyIndexer::Entry] entries, bool top_level) -> Interface::CompletionItem
      def build_entry_completion(real_name, incomplete_name, range, entries, top_level)
        first_entry = entries.first #: as !nil
        kind = case first_entry
        when RubyIndexer::Entry::Class
          Constant::CompletionItemKind::CLASS
        when RubyIndexer::Entry::Module
          Constant::CompletionItemKind::MODULE
        when RubyIndexer::Entry::Constant
          Constant::CompletionItemKind::CONSTANT
        else
          Constant::CompletionItemKind::REFERENCE
        end

        insertion_text = real_name.dup
        filter_text = real_name.dup

        # If we have two entries with the same name inside the current namespace and the user selects the top level
        # option, we have to ensure it's prefixed with `::` or else we're completing the wrong constant. For example:
        # If we have the index with ["Foo::Bar", "Bar"], and we're providing suggestions for `B` inside a `Foo` module,
        # then selecting the `Foo::Bar` option needs to complete to `Bar` and selecting the top level `Bar` option needs
        # to complete to `::Bar`.
        if top_level
          insertion_text.prepend("::")
          filter_text.prepend("::")
        end

        # If the user is searching for a constant inside the current namespace, then we prefer completing the short name
        # of that constant. E.g.:
        #
        # module Foo
        #  class Bar
        #  end
        #
        #  Foo::B # --> completion inserts `Bar` instead of `Foo::Bar`
        # end
        nesting = @node_context.nesting
        unless @node_context.fully_qualified_name.start_with?(incomplete_name)
          nesting.each do |namespace|
            prefix = "#{namespace}::"
            shortened_name = insertion_text.delete_prefix(prefix)

            # If a different entry exists for the shortened name, then there's a conflict and we should not shorten it
            conflict_name = "#{@node_context.fully_qualified_name}::#{shortened_name}"
            break if real_name != conflict_name && @index[conflict_name]

            insertion_text = shortened_name

            # If the user is typing a fully qualified name `Foo::Bar::Baz`, then we should not use the short name (e.g.:
            # `Baz`) as filtering. So we only shorten the filter text if the user is not including the namespaces in
            # their typing
            filter_text.delete_prefix!(prefix) unless incomplete_name.start_with?(prefix)
          end
        end

        # When using a top level constant reference (e.g.: `::Bar`), the editor includes the `::` as part of the filter.
        # For these top level references, we need to include the `::` as part of the filter text or else it won't match
        # the right entries in the index

        label_details = Interface::CompletionItemLabelDetails.new(
          description: entries.map(&:file_name).join(","),
        )

        Interface::CompletionItem.new(
          label: real_name,
          label_details: label_details,
          filter_text: filter_text,
          text_edit: Interface::TextEdit.new(
            range: range,
            new_text: insertion_text,
          ),
          kind: kind,
        )
      end

      # Check if there are any conflicting names for `entry_name`, which would require us to use a top level reference.
      # For example:
      #
      # ```ruby
      # class Bar; end
      #
      # module Foo
      #   class Bar; end
      #
      #   # in this case, the completion for `Bar` conflicts with `Foo::Bar`, so we can't suggest `Bar` as the
      #   # completion, but instead need to suggest `::Bar`
      #   B
      # end
      # ```
      #: (String entry_name) -> bool
      def top_level?(entry_name)
        nesting = @node_context.nesting
        nesting.length.downto(0) do |i|
          prefix = nesting[0...i] #: as !nil
            .join("::")
          full_name = prefix.empty? ? entry_name : "#{prefix}::#{entry_name}"
          next if full_name == entry_name

          return true if @index[full_name]
        end

        false
      end
    end
  end
end
