# typed: strict
# frozen_string_literal: true

module RubyIndexer
  class DeclarationListener
    OBJECT_NESTING = ["Object"].freeze #: Array[String]
    BASIC_OBJECT_NESTING = ["BasicObject"].freeze #: Array[String]

    #: Array[String]
    attr_reader :indexing_errors

    #: (Index index, Prism::Dispatcher dispatcher, Prism::ParseLexResult | Prism::ParseResult parse_result, URI::Generic uri, ?collect_comments: bool) -> void
    def initialize(index, dispatcher, parse_result, uri, collect_comments: false)
      @index = index
      @uri = uri
      @enhancements = Enhancement.all(self) #: Array[Enhancement]
      @visibility_stack = [VisibilityScope.public_scope] #: Array[VisibilityScope]
      @comments_by_line = parse_result.comments.to_h do |c|
        [c.location.start_line, c]
      end #: Hash[Integer, Prism::Comment]
      @inside_def = false #: bool
      @code_units_cache = parse_result
        .code_units_cache(@index.configuration.encoding) #: (^(Integer arg0) -> Integer | Prism::CodeUnitsCache)

      @source_lines = parse_result.source.lines #: Array[String]

      # The nesting stack we're currently inside. Used to determine the fully qualified name of constants, but only
      # stored by unresolved aliases which need the original nesting to be lazily resolved
      @stack = [] #: Array[String]

      # A stack of namespace entries that represent where we currently are. Used to properly assign methods to an owner
      @owner_stack = [] #: Array[Entry::Namespace]
      @indexing_errors = [] #: Array[String]
      @collect_comments = collect_comments

      dispatcher.register(
        self,
        :on_class_node_enter,
        :on_class_node_leave,
        :on_module_node_enter,
        :on_module_node_leave,
        :on_singleton_class_node_enter,
        :on_singleton_class_node_leave,
        :on_def_node_enter,
        :on_def_node_leave,
        :on_call_node_enter,
        :on_call_node_leave,
        :on_multi_write_node_enter,
        :on_constant_path_write_node_enter,
        :on_constant_path_or_write_node_enter,
        :on_constant_path_operator_write_node_enter,
        :on_constant_path_and_write_node_enter,
        :on_constant_write_node_enter,
        :on_constant_or_write_node_enter,
        :on_constant_and_write_node_enter,
        :on_constant_operator_write_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_target_node_enter,
        :on_global_variable_write_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_alias_method_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_target_node_enter,
        :on_class_variable_write_node_enter,
      )
    end

    #: (Prism::ClassNode node) -> void
    def on_class_node_enter(node)
      constant_path = node.constant_path
      superclass = node.superclass
      nesting = Index.actual_nesting(@stack, constant_path.slice)

      parent_class = case superclass
      when Prism::ConstantReadNode, Prism::ConstantPathNode
        superclass.slice
      else
        case nesting
        when OBJECT_NESTING
          # When Object is reopened, its parent class should still be the top-level BasicObject
          "::BasicObject"
        when BASIC_OBJECT_NESTING
          # When BasicObject is reopened, its parent class should still be nil
          nil
        else
          # Otherwise, the parent class should be the top-level Object
          "::Object"
        end
      end

      add_class(
        nesting,
        node.location,
        constant_path.location,
        parent_class_name: parent_class,
        comments: collect_comments(node),
      )
    end

    #: (Prism::ClassNode node) -> void
    def on_class_node_leave(node)
      pop_namespace_stack
    end

    #: (Prism::ModuleNode node) -> void
    def on_module_node_enter(node)
      constant_path = node.constant_path
      add_module(constant_path.slice, node.location, constant_path.location, comments: collect_comments(node))
    end

    #: (Prism::ModuleNode node) -> void
    def on_module_node_leave(node)
      pop_namespace_stack
    end

    #: (Prism::SingletonClassNode node) -> void
    def on_singleton_class_node_enter(node)
      @visibility_stack.push(VisibilityScope.public_scope)

      current_owner = @owner_stack.last

      if current_owner
        expression = node.expression
        name = (expression.is_a?(Prism::SelfNode) ? "<Class:#{last_name_in_stack}>" : "<Class:#{expression.slice}>")
        real_nesting = Index.actual_nesting(@stack, name)

        existing_entries = @index[real_nesting.join("::")] #: as Array[Entry::SingletonClass]?

        if existing_entries
          entry = existing_entries.first #: as !nil
          entry.update_singleton_information(
            Location.from_prism_location(node.location, @code_units_cache),
            Location.from_prism_location(expression.location, @code_units_cache),
            collect_comments(node),
          )
        else
          entry = Entry::SingletonClass.new(
            real_nesting,
            @uri,
            Location.from_prism_location(node.location, @code_units_cache),
            Location.from_prism_location(expression.location, @code_units_cache),
            collect_comments(node),
            nil,
          )
          @index.add(entry, skip_prefix_tree: true)
        end

        @owner_stack << entry
        @stack << name
      end
    end

    #: (Prism::SingletonClassNode node) -> void
    def on_singleton_class_node_leave(node)
      pop_namespace_stack
    end

    #: (Prism::MultiWriteNode node) -> void
    def on_multi_write_node_enter(node)
      value = node.value
      values = value.is_a?(Prism::ArrayNode) && value.opening_loc ? value.elements : []

      [*node.lefts, *node.rest, *node.rights].each_with_index do |target, i|
        current_value = values[i]
        # The moment we find a splat on the right hand side of the assignment, we can no longer figure out which value
        # gets assigned to what
        values.clear if current_value.is_a?(Prism::SplatNode)

        case target
        when Prism::ConstantTargetNode
          add_constant(target, fully_qualify_name(target.name.to_s), current_value)
        when Prism::ConstantPathTargetNode
          add_constant(target, fully_qualify_name(target.slice), current_value)
        end
      end
    end

    #: (Prism::ConstantPathWriteNode node) -> void
    def on_constant_path_write_node_enter(node)
      # ignore variable constants like `var::FOO` or `self.class::FOO`
      target = node.target
      return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)

      name = fully_qualify_name(target.location.slice)
      add_constant(node, name)
    end

    #: (Prism::ConstantPathOrWriteNode node) -> void
    def on_constant_path_or_write_node_enter(node)
      # ignore variable constants like `var::FOO` or `self.class::FOO`
      target = node.target
      return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)

      name = fully_qualify_name(target.location.slice)
      add_constant(node, name)
    end

    #: (Prism::ConstantPathOperatorWriteNode node) -> void
    def on_constant_path_operator_write_node_enter(node)
      # ignore variable constants like `var::FOO` or `self.class::FOO`
      target = node.target
      return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)

      name = fully_qualify_name(target.location.slice)
      add_constant(node, name)
    end

    #: (Prism::ConstantPathAndWriteNode node) -> void
    def on_constant_path_and_write_node_enter(node)
      # ignore variable constants like `var::FOO` or `self.class::FOO`
      target = node.target
      return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode)

      name = fully_qualify_name(target.location.slice)
      add_constant(node, name)
    end

    #: (Prism::ConstantWriteNode node) -> void
    def on_constant_write_node_enter(node)
      name = fully_qualify_name(node.name.to_s)
      add_constant(node, name)
    end

    #: (Prism::ConstantOrWriteNode node) -> void
    def on_constant_or_write_node_enter(node)
      name = fully_qualify_name(node.name.to_s)
      add_constant(node, name)
    end

    #: (Prism::ConstantAndWriteNode node) -> void
    def on_constant_and_write_node_enter(node)
      name = fully_qualify_name(node.name.to_s)
      add_constant(node, name)
    end

    #: (Prism::ConstantOperatorWriteNode node) -> void
    def on_constant_operator_write_node_enter(node)
      name = fully_qualify_name(node.name.to_s)
      add_constant(node, name)
    end

    #: (Prism::CallNode node) -> void
    def on_call_node_enter(node)
      message = node.name

      case message
      when :private_constant
        handle_private_constant(node)
      when :attr_reader
        handle_attribute(node, reader: true, writer: false)
      when :attr_writer
        handle_attribute(node, reader: false, writer: true)
      when :attr_accessor
        handle_attribute(node, reader: true, writer: true)
      when :attr
        has_writer = node.arguments&.arguments&.last&.is_a?(Prism::TrueNode) || false
        handle_attribute(node, reader: true, writer: has_writer)
      when :alias_method
        handle_alias_method(node)
      when :include, :prepend, :extend
        handle_module_operation(node, message)
      when :public
        handle_visibility_change(node, :public)
      when :protected
        handle_visibility_change(node, :protected)
      when :private
        handle_visibility_change(node, :private)
      when :module_function
        handle_module_function(node)
      when :private_class_method
        handle_private_class_method(node)
      end

      @enhancements.each do |enhancement|
        enhancement.on_call_node_enter(node)
      rescue StandardError => e
        @indexing_errors << <<~MSG
          Indexing error in #{@uri} with '#{enhancement.class.name}' on call node enter enhancement: #{e.message}
        MSG
      end
    end

    #: (Prism::CallNode node) -> void
    def on_call_node_leave(node)
      message = node.name
      case message
      when :public, :protected, :private, :private_class_method
        # We want to restore the visibility stack when we leave a method definition with a visibility modifier
        # e.g. `private def foo; end`
        if node.arguments&.arguments&.first&.is_a?(Prism::DefNode)
          @visibility_stack.pop
        end
      end

      @enhancements.each do |enhancement|
        enhancement.on_call_node_leave(node)
      rescue StandardError => e
        @indexing_errors << <<~MSG
          Indexing error in #{@uri} with '#{enhancement.class.name}' on call node leave enhancement: #{e.message}
        MSG
      end
    end

    #: (Prism::DefNode node) -> void
    def on_def_node_enter(node)
      owner = @owner_stack.last
      return unless owner

      @inside_def = true
      method_name = node.name.to_s
      comments = collect_comments(node)
      scope = current_visibility_scope

      case node.receiver
      when nil
        location = Location.from_prism_location(node.location, @code_units_cache)
        name_location = Location.from_prism_location(node.name_loc, @code_units_cache)
        signatures = [Entry::Signature.new(list_params(node.parameters))]

        @index.add(Entry::Method.new(
          method_name,
          @uri,
          location,
          name_location,
          comments,
          signatures,
          scope.visibility,
          owner,
        ))

        if scope.module_func
          singleton = @index.existing_or_new_singleton_class(owner.name)

          @index.add(Entry::Method.new(
            method_name,
            @uri,
            location,
            name_location,
            comments,
            signatures,
            :public,
            singleton,
          ))
        end
      when Prism::SelfNode
        singleton = @index.existing_or_new_singleton_class(owner.name)

        @index.add(Entry::Method.new(
          method_name,
          @uri,
          Location.from_prism_location(node.location, @code_units_cache),
          Location.from_prism_location(node.name_loc, @code_units_cache),
          comments,
          [Entry::Signature.new(list_params(node.parameters))],
          scope.visibility,
          singleton,
        ))

        @owner_stack << singleton
      end
    end

    #: (Prism::DefNode node) -> void
    def on_def_node_leave(node)
      @inside_def = false

      if node.receiver.is_a?(Prism::SelfNode)
        @owner_stack.pop
      end
    end

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

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

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

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

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

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

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

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

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

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

    #: (Prism::AliasMethodNode node) -> void
    def on_alias_method_node_enter(node)
      method_name = node.new_name.slice
      comments = collect_comments(node)
      @index.add(
        Entry::UnresolvedMethodAlias.new(
          method_name,
          node.old_name.slice,
          @owner_stack.last,
          @uri,
          Location.from_prism_location(node.new_name.location, @code_units_cache),
          comments,
        ),
      )
    end

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

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

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

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

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

    #: (String name, Prism::Location node_location, Array[Entry::Signature] signatures, ?visibility: Symbol, ?comments: String?) -> void
    def add_method(name, node_location, signatures, visibility: :public, comments: nil)
      location = Location.from_prism_location(node_location, @code_units_cache)

      @index.add(Entry::Method.new(
        name,
        @uri,
        location,
        location,
        comments,
        signatures,
        visibility,
        @owner_stack.last,
      ))
    end

    #: (String name, Prism::Location full_location, Prism::Location name_location, ?comments: String?) -> void
    def add_module(name, full_location, name_location, comments: nil)
      location = Location.from_prism_location(full_location, @code_units_cache)
      name_loc = Location.from_prism_location(name_location, @code_units_cache)

      entry = Entry::Module.new(
        Index.actual_nesting(@stack, name),
        @uri,
        location,
        name_loc,
        comments,
      )

      advance_namespace_stack(name, entry)
    end

    #: ((String | Array[String]) name_or_nesting, Prism::Location full_location, Prism::Location name_location, ?parent_class_name: String?, ?comments: String?) -> void
    def add_class(name_or_nesting, full_location, name_location, parent_class_name: nil, comments: nil)
      nesting = name_or_nesting.is_a?(Array) ? name_or_nesting : Index.actual_nesting(@stack, name_or_nesting)
      entry = Entry::Class.new(
        nesting,
        @uri,
        Location.from_prism_location(full_location, @code_units_cache),
        Location.from_prism_location(name_location, @code_units_cache),
        comments,
        parent_class_name,
      )

      advance_namespace_stack(
        nesting.last, #: as !nil
        entry,
      )
    end

    #: { (Index index, Entry::Namespace base) -> void } -> void
    def register_included_hook(&block)
      owner = @owner_stack.last
      return unless owner

      @index.register_included_hook(owner.name) do |index, base|
        block.call(index, base)
      end
    end

    #: -> void
    def pop_namespace_stack
      @stack.pop
      @owner_stack.pop
      @visibility_stack.pop
    end

    #: -> Entry::Namespace?
    def current_owner
      @owner_stack.last
    end

    private

    #: ((Prism::GlobalVariableAndWriteNode | Prism::GlobalVariableOperatorWriteNode | Prism::GlobalVariableOrWriteNode | Prism::GlobalVariableTargetNode | Prism::GlobalVariableWriteNode) node, Prism::Location loc) -> void
    def handle_global_variable(node, loc)
      name = node.name.to_s
      comments = collect_comments(node)

      @index.add(Entry::GlobalVariable.new(
        name,
        @uri,
        Location.from_prism_location(loc, @code_units_cache),
        comments,
      ))
    end

    #: ((Prism::ClassVariableAndWriteNode | Prism::ClassVariableOperatorWriteNode | Prism::ClassVariableOrWriteNode | Prism::ClassVariableTargetNode | Prism::ClassVariableWriteNode) node, Prism::Location loc) -> void
    def handle_class_variable(node, loc)
      name = node.name.to_s
      # Ignore incomplete class variable names, which aren't valid Ruby syntax.
      # This could occur if the code is in an incomplete or temporary state.
      return if name == "@@"

      comments = collect_comments(node)

      owner = @owner_stack.last

      # set the class variable's owner to the attached context when defined within a singleton scope.
      if owner.is_a?(Entry::SingletonClass)
        owner = @owner_stack.reverse.find { |entry| !entry.name.include?("<Class:") }
      end

      @index.add(Entry::ClassVariable.new(
        name,
        @uri,
        Location.from_prism_location(loc, @code_units_cache),
        comments,
        owner,
      ))
    end

    #: ((Prism::InstanceVariableAndWriteNode | Prism::InstanceVariableOperatorWriteNode | Prism::InstanceVariableOrWriteNode | Prism::InstanceVariableTargetNode | Prism::InstanceVariableWriteNode) node, Prism::Location loc) -> void
    def handle_instance_variable(node, loc)
      name = node.name.to_s
      return if name == "@"

      # When instance variables are declared inside the class body, they turn into class instance variables rather than
      # regular instance variables
      owner = @owner_stack.last

      if owner && !@inside_def
        owner = @index.existing_or_new_singleton_class(owner.name)
      end

      @index.add(Entry::InstanceVariable.new(
        name,
        @uri,
        Location.from_prism_location(loc, @code_units_cache),
        collect_comments(node),
        owner,
      ))
    end

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

      first_argument = arguments.first

      name = case first_argument
      when Prism::StringNode
        first_argument.content
      when Prism::SymbolNode
        first_argument.value
      end

      return unless name

      receiver = node.receiver
      name = "#{receiver.slice}::#{name}" if receiver

      # The private_constant method does not resolve the constant name. It always points to a constant that needs to
      # exist in the current namespace
      entries = @index[fully_qualify_name(name)]
      entries&.each { |entry| entry.visibility = :private }
    end

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

      new_name, old_name = arguments
      return unless new_name && old_name

      new_name_value = case new_name
      when Prism::StringNode
        new_name.content
      when Prism::SymbolNode
        new_name.value
      end

      return unless new_name_value

      old_name_value = case old_name
      when Prism::StringNode
        old_name.content
      when Prism::SymbolNode
        old_name.value
      end

      return unless old_name_value

      comments = collect_comments(node)
      @index.add(
        Entry::UnresolvedMethodAlias.new(
          new_name_value,
          old_name_value,
          @owner_stack.last,
          @uri,
          Location.from_prism_location(new_name.location, @code_units_cache),
          comments,
        ),
      )
    end

    #: ((Prism::ConstantWriteNode | Prism::ConstantOrWriteNode | Prism::ConstantAndWriteNode | Prism::ConstantOperatorWriteNode | Prism::ConstantPathWriteNode | Prism::ConstantPathOrWriteNode | Prism::ConstantPathOperatorWriteNode | Prism::ConstantPathAndWriteNode | Prism::ConstantTargetNode | Prism::ConstantPathTargetNode) node, String name, ?Prism::Node? value) -> void
    def add_constant(node, name, value = nil)
      value = node.value unless node.is_a?(Prism::ConstantTargetNode) || node.is_a?(Prism::ConstantPathTargetNode)
      comments = collect_comments(node)

      @index.add(
        case value
        when Prism::ConstantReadNode, Prism::ConstantPathNode
          Entry::UnresolvedConstantAlias.new(
            value.slice,
            @stack.dup,
            name,
            @uri,
            Location.from_prism_location(node.location, @code_units_cache),
            comments,
          )
        when Prism::ConstantWriteNode, Prism::ConstantAndWriteNode, Prism::ConstantOrWriteNode,
        Prism::ConstantOperatorWriteNode

          # If the right hand side is another constant assignment, we need to visit it because that constant has to be
          # indexed too
          Entry::UnresolvedConstantAlias.new(
            value.name.to_s,
            @stack.dup,
            name,
            @uri,
            Location.from_prism_location(node.location, @code_units_cache),
            comments,
          )
        when Prism::ConstantPathWriteNode, Prism::ConstantPathOrWriteNode, Prism::ConstantPathOperatorWriteNode,
        Prism::ConstantPathAndWriteNode

          Entry::UnresolvedConstantAlias.new(
            value.target.slice,
            @stack.dup,
            name,
            @uri,
            Location.from_prism_location(node.location, @code_units_cache),
            comments,
          )
        else
          Entry::Constant.new(
            name,
            @uri,
            Location.from_prism_location(node.location, @code_units_cache),
            comments,
          )
        end,
      )
    end

    #: (Prism::Node node) -> String?
    def collect_comments(node)
      return unless @collect_comments

      comments = +""

      start_line = node.location.start_line - 1
      start_line -= 1 unless comment_exists_at?(start_line)
      start_line.downto(1) do |line|
        comment = @comments_by_line[line]
        break unless comment

        # a trailing comment from a previous line is not a comment for this node
        break if comment.trailing?

        comment_content = comment.location.slice

        # invalid encodings would raise an "invalid byte sequence" exception
        if !comment_content.valid_encoding? || comment_content.match?(@index.configuration.magic_comment_regex)
          next
        end

        comment_content.delete_prefix!("#")
        comment_content.delete_prefix!(" ")
        comments.prepend("#{comment_content}\n")
      end

      comments.chomp!
      comments
    end

    #: (Integer line) -> bool
    def comment_exists_at?(line)
      @comments_by_line.key?(line) || !@source_lines[line - 1].to_s.strip.empty?
    end

    #: (String name) -> String
    def fully_qualify_name(name)
      if @stack.empty? || name.start_with?("::")
        name
      else
        "#{@stack.join("::")}::#{name}"
      end.delete_prefix("::")
    end

    #: (Prism::CallNode node, reader: bool, writer: bool) -> void
    def handle_attribute(node, reader:, writer:)
      arguments = node.arguments&.arguments
      return unless arguments

      receiver = node.receiver
      return unless receiver.nil? || receiver.is_a?(Prism::SelfNode)

      comments = collect_comments(node)
      scope = current_visibility_scope

      arguments.each do |argument|
        name, loc = case argument
        when Prism::SymbolNode
          [argument.value, argument.value_loc]
        when Prism::StringNode
          [argument.content, argument.content_loc]
        end

        next unless name && loc

        if reader
          @index.add(Entry::Accessor.new(
            name,
            @uri,
            Location.from_prism_location(loc, @code_units_cache),
            comments,
            scope.visibility,
            @owner_stack.last,
          ))
        end

        next unless writer

        @index.add(Entry::Accessor.new(
          "#{name}=",
          @uri,
          Location.from_prism_location(loc, @code_units_cache),
          comments,
          scope.visibility,
          @owner_stack.last,
        ))
      end
    end

    #: (Prism::CallNode node, Symbol operation) -> void
    def handle_module_operation(node, operation)
      return if @inside_def

      owner = @owner_stack.last
      return unless owner

      arguments = node.arguments&.arguments
      return unless arguments

      arguments.each do |node|
        next unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode) ||
          (node.is_a?(Prism::SelfNode) && operation == :extend)

        if node.is_a?(Prism::SelfNode)
          singleton = @index.existing_or_new_singleton_class(owner.name)
          singleton.mixin_operations << Entry::Include.new(owner.name)
        else
          case operation
          when :include
            owner.mixin_operations << Entry::Include.new(node.full_name)
          when :prepend
            owner.mixin_operations << Entry::Prepend.new(node.full_name)
          when :extend
            singleton = @index.existing_or_new_singleton_class(owner.name)
            singleton.mixin_operations << Entry::Include.new(node.full_name)
          end
        end
      rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError,
             Prism::ConstantPathNode::MissingNodesInConstantPathError
        # Do nothing
      end
    end

    #: (Prism::CallNode node) -> void
    def handle_module_function(node)
      # Invoking `module_function` in a class raises
      owner = @owner_stack.last
      return unless owner.is_a?(Entry::Module)

      arguments_node = node.arguments

      # If `module_function` is invoked without arguments, all methods defined after it become singleton methods and the
      # visibility for instance methods changes to private
      unless arguments_node
        @visibility_stack.push(VisibilityScope.module_function_scope)
        return
      end

      owner_name = owner.name

      arguments_node.arguments.each do |argument|
        method_name = case argument
        when Prism::StringNode
          argument.content
        when Prism::SymbolNode
          argument.value
        end
        next unless method_name

        entries = @index.resolve_method(method_name, owner_name)
        next unless entries

        entries.each do |entry|
          entry_owner_name = entry.owner&.name
          next unless entry_owner_name

          entry.visibility = :private

          singleton = @index.existing_or_new_singleton_class(entry_owner_name)
          location = Location.from_prism_location(argument.location, @code_units_cache)
          @index.add(Entry::Method.new(
            method_name,
            @uri,
            location,
            location,
            collect_comments(node)&.concat(entry.comments),
            entry.signatures,
            :public,
            singleton,
          ))
        end
      end
    end

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

      # If we're passing a method definition directly to `private_class_method`, push a new private scope. That will be
      # applied when the indexer finds the method definition and then popped on `call_node_leave`
      if arguments.first.is_a?(Prism::DefNode)
        @visibility_stack.push(VisibilityScope.new(visibility: :private))
        return
      end

      owner_name = @owner_stack.last&.name
      return unless owner_name

      # private_class_method accepts strings, symbols or arrays of strings and symbols as arguments. Here we build a
      # single list of all of the method names that have to be made private
      arrays, others = arguments.partition do |argument|
        argument.is_a?(Prism::ArrayNode)
      end #: as [Array[Prism::ArrayNode], Array[Prism::Node]]
      arrays.each { |array| others.concat(array.elements) }

      names = others.filter_map do |argument|
        case argument
        when Prism::StringNode
          argument.unescaped
        when Prism::SymbolNode
          argument.value
        end
      end

      names.each do |name|
        entries = @index.resolve_method(name, @index.existing_or_new_singleton_class(owner_name).name)
        next unless entries

        entries.each { |entry| entry.visibility = :private }
      end
    end

    #: -> VisibilityScope
    def current_visibility_scope
      @visibility_stack.last #: as !nil
    end

    #: (Prism::ParametersNode? parameters_node) -> Array[Entry::Parameter]
    def list_params(parameters_node)
      return [] unless parameters_node

      parameters = []

      parameters_node.requireds.each do |required|
        name = parameter_name(required)
        next unless name

        parameters << Entry::RequiredParameter.new(name: name)
      end

      parameters_node.optionals.each do |optional|
        name = parameter_name(optional)
        next unless name

        parameters << Entry::OptionalParameter.new(name: name)
      end

      rest = parameters_node.rest

      if rest.is_a?(Prism::RestParameterNode)
        rest_name = rest.name || Entry::RestParameter::DEFAULT_NAME
        parameters << Entry::RestParameter.new(name: rest_name)
      end

      parameters_node.keywords.each do |keyword|
        name = parameter_name(keyword)
        next unless name

        case keyword
        when Prism::RequiredKeywordParameterNode
          parameters << Entry::KeywordParameter.new(name: name)
        when Prism::OptionalKeywordParameterNode
          parameters << Entry::OptionalKeywordParameter.new(name: name)
        end
      end

      keyword_rest = parameters_node.keyword_rest

      case keyword_rest
      when Prism::KeywordRestParameterNode
        keyword_rest_name = parameter_name(keyword_rest) || Entry::KeywordRestParameter::DEFAULT_NAME
        parameters << Entry::KeywordRestParameter.new(name: keyword_rest_name)
      when Prism::ForwardingParameterNode
        parameters << Entry::ForwardingParameter.new
      end

      parameters_node.posts.each do |post|
        name = parameter_name(post)
        next unless name

        parameters << Entry::RequiredParameter.new(name: name)
      end

      block = parameters_node.block
      parameters << Entry::BlockParameter.new(name: block.name || Entry::BlockParameter::DEFAULT_NAME) if block

      parameters
    end

    #: (Prism::Node? node) -> Symbol?
    def parameter_name(node)
      case node
      when Prism::RequiredParameterNode, Prism::OptionalParameterNode,
        Prism::RequiredKeywordParameterNode, Prism::OptionalKeywordParameterNode,
        Prism::RestParameterNode, Prism::KeywordRestParameterNode
        node.name
      when Prism::MultiTargetNode
        names = node.lefts.map { |parameter_node| parameter_name(parameter_node) }

        rest = node.rest
        if rest.is_a?(Prism::SplatNode)
          name = rest.expression&.slice
          names << (rest.operator == "*" ? "*#{name}".to_sym : name&.to_sym)
        end

        names << nil if rest.is_a?(Prism::ImplicitRestNode)

        names.concat(node.rights.map { |parameter_node| parameter_name(parameter_node) })

        names_with_commas = names.join(", ")
        :"(#{names_with_commas})"
      end
    end

    #: (String short_name, Entry::Namespace entry) -> void
    def advance_namespace_stack(short_name, entry)
      @visibility_stack.push(VisibilityScope.public_scope)
      @owner_stack << entry
      @index.add(entry)
      @stack << short_name
    end

    # Returns the last name in the stack not as we found it, but in terms of declared constants. For example, if the
    # last entry in the stack is a compact namespace like `Foo::Bar`, then the last name is `Bar`
    #: -> String?
    def last_name_in_stack
      name = @stack.last
      return unless name

      name.split("::").last
    end

    #: (Prism::CallNode, Symbol) -> void
    def handle_visibility_change(node, visibility)
      owner  = @owner_stack.last
      return unless owner

      owner_name = owner.name
      method_names = string_or_symbol_argument_values(node)

      if method_names.empty?
        @visibility_stack.push(VisibilityScope.new(visibility: visibility))
        return
      end

      method_names.each do |method_name|
        entries = @index.resolve_method(method_name, owner_name)
        next unless entries

        entries.each do |entry|
          entry.visibility = visibility
        end
      end
    end

    #: (Prism::CallNode) -> Array[String]
    def string_or_symbol_argument_values(node)
      arguments = node.arguments&.arguments
      return [] unless arguments

      arguments.filter_map do |argument|
        case argument
        when Prism::StringNode
          argument.content
        when Prism::SymbolNode
          argument.value
        end
      end
    end
  end
end
