# frozen_string_literal: true
# :markup: markdown
#--
# rbs_inline: enabled

module Prism
  # Finds the Prism AST node corresponding to a given Method, UnboundMethod,
  # Proc, or Thread::Backtrace::Location. On CRuby, uses node_id from the
  # instruction sequence for an exact match. On other implementations, falls
  # back to best-effort matching by source location line number.
  #
  # This module is autoloaded so that programs that don't use Prism.find don't
  # pay for its definition.
  module NodeFind # :nodoc:
    # Find the node for the given callable or backtrace location.
    #--
    #: (Method | UnboundMethod | Proc | Thread::Backtrace::Location callable) -> Node?
    def self.find(callable)
      case callable
      when Proc
        if defined?(::RubyVM)
          RubyVMCallableFind.new.find(callable)
        elsif callable.lambda?
          LineLambdaFind.new.find(callable)
        else
          LineProcFind.new.find(callable)
        end
      when Method, UnboundMethod
        if defined?(::RubyVM)
          RubyVMCallableFind.new.find(callable)
        else
          LineMethodFind.new.find(callable)
        end
      when Thread::Backtrace::Location
        if defined?(::RubyVM)
          RubyVMBacktraceLocationFind.new.find(callable)
        else
          LineBacktraceLocationFind.new.find(callable)
        end
      else
        raise ArgumentError, "Expected a Method, UnboundMethod, Proc, or Thread::Backtrace::Location, got #{callable.class}"
      end
    end

    # Base class that handles parsing a file.
    class Find
      private

      # Parse the given file path, returning a ParseResult or nil.
      #--
      #: (String? file) -> ParseResult?
      def parse_file(file)
        return unless file && File.readable?(file)
        result = Prism.parse_file(file)
        result if result.success?
      end
    end

    # Finds the AST node for a Method, UnboundMethod, or Proc using the node_id
    # from the instruction sequence.
    class RubyVMCallableFind < Find
      # Find the node for the given callable using the ISeq node_id.
      #--
      #: (Method | UnboundMethod | Proc callable) -> Node?
      def find(callable)
        return unless (source_location = callable.source_location)
        return unless (result = parse_file(source_location[0]))
        return unless (iseq = RubyVM::InstructionSequence.of(callable))

        header = iseq.to_a[4]
        return unless header[:parser] == :prism

        result.value.find { |node| node.node_id == header[:node_id] }
      end
    end

    # Finds the AST node for a Thread::Backtrace::Location using the node_id
    # from the backtrace location.
    class RubyVMBacktraceLocationFind < Find
      # Find the node for the given backtrace location using node_id.
      #--
      #: (Thread::Backtrace::Location location) -> Node?
      def find(location)
        file = location.absolute_path || location.path
        return unless (result = parse_file(file))
        return unless RubyVM::AbstractSyntaxTree.respond_to?(:node_id_for_backtrace_location)

        node_id = RubyVM::AbstractSyntaxTree.node_id_for_backtrace_location(location)

        result.value.find { |node| node.node_id == node_id }
      end
    end

    # Finds the AST node for a Method or UnboundMethod using best-effort line
    # matching. Used on non-CRuby implementations.
    class LineMethodFind < Find
      # Find the node for the given method by matching on name and line.
      #--
      #: (Method | UnboundMethod callable) -> Node?
      def find(callable)
        return unless (source_location = callable.source_location)
        return unless (result = parse_file(source_location[0]))

        name = callable.name
        start_line = source_location[1]

        result.value.find do |node|
          case node
          when DefNode
            node.name == name && node.location.start_line == start_line
          when CallNode
            node.block.is_a?(BlockNode) && node.location.start_line == start_line
          else
            false
          end
        end
      end
    end

    # Finds the AST node for a lambda using best-effort line matching. Used
    # on non-CRuby implementations.
    class LineLambdaFind < Find
      # Find the node for the given lambda by matching on line.
      #--
      #: (Proc callable) -> Node?
      def find(callable)
        return unless (source_location = callable.source_location)
        return unless (result = parse_file(source_location[0]))

        start_line = source_location[1]

        result.value.find do |node|
          case node
          when LambdaNode
            node.location.start_line == start_line
          when CallNode
            node.block.is_a?(BlockNode) && node.location.start_line == start_line
          else
            false
          end
        end
      end
    end

    # Finds the AST node for a non-lambda Proc using best-effort line
    # matching. Used on non-CRuby implementations.
    class LineProcFind < Find
      # Find the node for the given proc by matching on line.
      #--
      #: (Proc callable) -> Node?
      def find(callable)
        return unless (source_location = callable.source_location)
        return unless (result = parse_file(source_location[0]))

        start_line = source_location[1]

        result.value.find do |node|
          case node
          when ForNode
            node.location.start_line == start_line
          when CallNode
            node.block.is_a?(BlockNode) && node.location.start_line == start_line
          else
            false
          end
        end
      end
    end

    # Finds the AST node for a Thread::Backtrace::Location using best-effort
    # line matching. Used on non-CRuby implementations.
    class LineBacktraceLocationFind < Find
      # Find the node for the given backtrace location by matching on line.
      #--
      #: (Thread::Backtrace::Location location) -> Node?
      def find(location)
        file = location.absolute_path || location.path
        return unless (result = parse_file(file))

        start_line = location.lineno
        result.value.find { |node| node.location.start_line == start_line }
      end
    end
  end
end
