# frozen_string_literal: true

require 'method_source'

class Pry
  class << self
    # If the given object is a `Pry::Method`, return it unaltered. If it's
    # anything else, return it wrapped in a `Pry::Method` instance.
    def Method(obj)
      if obj.is_a? Pry::Method
        obj
      else
        Pry::Method.new(obj)
      end
    end
  end

  # This class wraps the normal `Method` and `UnboundMethod` classes
  # to provide extra functionality useful to Pry.
  class Method # rubocop:disable Metrics/ClassLength
    extend Helpers::BaseHelpers
    extend Forwardable

    include Helpers::BaseHelpers
    include Helpers::DocumentationHelpers
    include CodeObject::Helpers

    class << self
      # Given a string representing a method name and optionally a binding to
      # search in, find and return the requested method wrapped in a
      # `Pry::Method` instance.
      #
      # @param [String] name The name of the method to retrieve.
      # @param [Binding] target The context in which to search for the method.
      # @param [Hash] options
      # @option options [Boolean] :instance Look for an instance method if
      #   `name` doesn't contain any context.
      # @option options [Boolean] :methods Look for a bound/singleton method if
      #  `name` doesn't contain any context.
      # @return [Pry::Method, nil] A `Pry::Method` instance containing the
      #   requested method, or `nil` if name is `nil` or no method could be
      #   located matching the parameters.
      def from_str(name, target = TOPLEVEL_BINDING, options = {})
        if name.nil?
          nil
        elsif name.to_s =~ /(.+)\#(\S+)\Z/
          context = Regexp.last_match(1)
          meth_name = Regexp.last_match(2)
          from_module(target.eval(context), meth_name, target)
        elsif name.to_s =~ /(.+)(\[\])\Z/
          context = Regexp.last_match(1)
          meth_name = Regexp.last_match(2)
          from_obj(target.eval(context), meth_name, target)
        elsif name.to_s =~ /(.+)(\.|::)(\S+)\Z/
          context = Regexp.last_match(1)
          meth_name = Regexp.last_match(3)
          from_obj(target.eval(context), meth_name, target)
        elsif options[:instance]
          from_module(target.eval("self"), name, target)
        elsif options[:methods]
          from_obj(target.eval("self"), name, target)
        else
          from_str(name, target, instance: true) ||
            from_str(name, target, methods: true)
        end
      rescue Pry::RescuableException
        nil
      end

      # Given a `Binding`, try to extract the `::Method` it originated from and
      # use it to instantiate a `Pry::Method`. Return `nil` if this isn't
      # possible.
      #
      # @param [Binding] binding
      # @return [Pry::Method, nil]
      #
      def from_binding(binding)
        meth_name = binding.eval('::Kernel.__method__')
        if [:__script__, nil].include?(meth_name)
          nil
        else
          method =
            begin
              if Object === binding.eval('self') # rubocop:disable Style/CaseEquality
                new(
                  Kernel.instance_method(:method)
                    .bind(binding.eval("self"))
                    .call(meth_name)
                )
              else
                str = 'class << self; self; end' \
                      '.instance_method(::Kernel.__method__).bind(self)'
                new(binding.eval(str))
              end
            rescue NameError, NoMethodError # rubocop:disable Lint/ShadowedException
              Disowned.new(binding.eval('self'), meth_name.to_s)
            end

          if WeirdMethodLocator.weird_method?(method, binding)
            WeirdMethodLocator.new(method, binding).find_method || method
          else
            method
          end
        end
      end

      # In order to support 2.0 Refinements we need to look up methods
      # inside the relevant Binding.
      # @param [Object] obj The owner/receiver of the method.
      # @param [Symbol] method_name The name of the method.
      # @param [Symbol] method_type The type of method: :method or :instance_method
      # @param [Binding] target The binding where the method is looked up.
      # @return [Method, UnboundMethod] The 'refined' method object.
      def lookup_method_via_binding(
        obj, method_name, method_type, target = TOPLEVEL_BINDING
      )
        Pry.current[:obj] = obj
        Pry.current[:name] = method_name
        receiver = obj.is_a?(Module) ? "Module" : "Kernel"
        target.eval(
          "::#{receiver}.instance_method(:#{method_type})" \
          ".bind(Pry.current[:obj]).call(Pry.current[:name])"
        )
      ensure
        Pry.current[:obj] = Pry.current[:name] = nil
      end

      # Given a `Class` or `Module` and the name of a method, try to
      # instantiate a `Pry::Method` containing the instance method of
      # that name. Return `nil` if no such method exists.
      #
      # @param [Class, Module] klass
      # @param [String] name
      # @param [Binding] target The binding where the method is looked up.
      # @return [Pry::Method, nil]
      def from_class(klass, name, target = TOPLEVEL_BINDING)
        new(lookup_method_via_binding(klass, name, :instance_method, target))
      rescue StandardError
        nil
      end
      alias from_module from_class

      # Given an object and the name of a method, try to instantiate
      # a `Pry::Method` containing the method of that name bound to
      # that object. Return `nil` if no such method exists.
      #
      # @param [Object] obj
      # @param [String] name
      # @param [Binding] target The binding where the method is looked up.
      # @return [Pry::Method, nil]
      def from_obj(obj, name, target = TOPLEVEL_BINDING)
        new(lookup_method_via_binding(obj, name, :method, target))
      rescue StandardError
        nil
      end

      # Get all of the instance methods of a `Class` or `Module`
      # @param [Class,Module] klass
      # @param [Boolean] include_super Whether to include methods from ancestors.
      # @return [Array[Pry::Method]]
      def all_from_class(klass, include_super = true)
        %w[public protected private].flat_map do |visibility|
          safe_send(
            klass, :"#{visibility}_instance_methods", include_super
          ).map do |method_name|
            new(
              safe_send(klass, :instance_method, method_name),
              visibility: visibility.to_sym
            )
          end
        end
      end

      #
      # Get all of the methods on an `Object`
      #
      # @param [Object] obj
      #
      # @param [Boolean] include_super
      #   indicates whether or not to include methods from ancestors.
      #
      # @return [Array[Pry::Method]]
      #
      def all_from_obj(obj, include_super = true)
        all_from_class(singleton_class_of(obj), include_super)
      end

      # Get every `Class` and `Module`, in order, that will be checked when looking
      # for an instance method to call on this object.
      # @param [Object] obj
      # @return [Array[Class, Module]]
      def resolution_order(obj)
        if Class === obj # rubocop:disable Style/CaseEquality
          singleton_class_resolution_order(obj) + instance_resolution_order(Class)
        else
          klass = begin
                    singleton_class_of(obj)
                  rescue StandardError
                    obj.class
                  end
          instance_resolution_order(klass)
        end
      end

      # Get every `Class` and `Module`, in order, that will be checked when looking
      # for methods on instances of the given `Class` or `Module`.
      # This does not treat singleton classes of classes specially.
      # @param [Class, Module] klass
      # @return [Array[Class, Module]]
      def instance_resolution_order(klass)
        # include klass in case it is a singleton class,
        ([klass] + Pry::Method.safe_send(klass, :ancestors)).uniq
      end

      def method_definition?(name, definition_line)
        singleton_method_definition?(name, definition_line) ||
          instance_method_definition?(name, definition_line)
      end

      def singleton_method_definition?(name, definition_line)
        regexp =
          /^define_singleton_method\(?\s*[:\"\']#{Regexp.escape(name)}|
           ^def\s*self\.#{Regexp.escape(name)}/x
        regexp =~ definition_line.strip
      end

      def instance_method_definition?(name, definition_line)
        regexp =
          /^define_method\(?\s*[:\"\']#{Regexp.escape(name)}|
           ^def\s*#{Regexp.escape(name)}/x
        regexp =~ definition_line.strip
      end

      # Get the singleton classes of superclasses that could define methods on
      # the given class object, and any modules they include.
      # If a module is included at multiple points in the ancestry, only
      # the lowest copy will be returned.
      def singleton_class_resolution_order(klass)
        ancestors = Pry::Method.safe_send(klass, :ancestors)
        resolution_order = ancestors.grep(Class).flat_map do |anc|
          [singleton_class_of(anc), *singleton_class_of(anc).included_modules]
        end

        resolution_order.reverse.uniq.reverse - Class.included_modules
      end

      def singleton_class_of(obj)
        class << obj; self; end
      rescue TypeError # can't define singleton. Fixnum, Symbol, Float, ...
        obj.class
      end
    end

    # Workaround for https://github.com/pry/pry/pull/2086
    def_delegators :@method, :owner, :parameters, :receiver

    # A new instance of `Pry::Method` wrapping the given `::Method`,
    # `UnboundMethod`, or `Proc`.
    #
    # @param [::Method, UnboundMethod, Proc] method
    # @param [Hash] known_info Can be used to pre-cache expensive to compute stuff.
    # @return [Pry::Method]
    def initialize(method, known_info = {})
      @method = method
      @visibility = known_info[:visibility]
    end

    # Get the name of the method as a String, regardless of the underlying
    # Method#name type.
    #
    # @return [String]
    def name
      @method.name.to_s
    end

    # Get the owner of the method as a Pry::Module
    # @return [Pry::Module]
    def wrapped_owner
      @wrapped_owner ||= Pry::WrappedModule.new(owner)
    end

    # Get underlying object wrapped by this Pry::Method instance
    # @return [Method, UnboundMethod, Proc]
    def wrapped
      @method
    end

    # Is the method undefined? (aka `Disowned`)
    # @return [Boolean] false
    def undefined?
      false
    end

    # Get the name of the method including the class on which it was defined.
    # @example
    #   method(:puts).method_name
    #   => "Kernel.puts"
    # @return [String]
    def name_with_owner
      "#{wrapped_owner.method_prefix}#{name}"
    end

    # @return [String, nil] The source code of the method, or `nil` if it's unavailable.
    def source
      @source ||= case source_type
                  when :c
                    c_source
                  when :ruby
                    ruby_source
                  end
    end

    # Update the live copy of the method's source.
    def redefine(source)
      Patcher.new(self).patch_in_ram source
      Pry::Method(owner.instance_method(name))
    end

    # Can we get the source code for this method?
    # @return [Boolean]
    def source?
      !!source
    rescue MethodSource::SourceNotFoundError
      false
    end

    # @return [String, nil] The documentation for the method, or `nil` if it's
    #   unavailable.
    def doc
      @doc ||=
        case source_type
        when :c
          info = pry_doc_info
          info.docstring if info
        when :ruby
          get_comment_content(comment)
        end
    end

    # @return [Symbol] The source type of the method. The options are
    #   `:ruby` for Ruby methods or `:c` for methods written in C.
    def source_type
      source_location.nil? ? :c : :ruby
    end

    # @return [String, nil] The name of the file the method is defined in, or
    #   `nil` if the filename is unavailable.
    def source_file
      if source_location.nil?
        if source_type == :c
          info = pry_doc_info
          info.file if info
        end
      else
        source_location.first
      end
    end

    # @return [Fixnum, nil] The line of code in `source_file` which begins
    #   the method's definition, or `nil` if that information is unavailable.
    def source_line
      source_location.nil? ? nil : source_location.last
    end

    # @return [Range, nil] The range of lines in `source_file` which contain
    #    the method's definition, or `nil` if that information is unavailable.
    def source_range
      source_location.nil? ? nil : (source_line)..(source_line + source.lines.count - 1)
    end

    # @return [Symbol] The visibility of the method. May be `:public`,
    #   `:protected`, or `:private`.
    def visibility
      @visibility ||=
        if owner.public_instance_methods.any? { |m| m.to_s == name }
          :public
        elsif owner.protected_instance_methods.any? { |m| m.to_s == name }
          :protected
        elsif owner.private_instance_methods.any? { |m| m.to_s == name }
          :private
        else
          :none
        end
    end

    # @return [String] A representation of the method's signature, including its
    #   name and parameters. Optional and "rest" parameters are marked with `*`
    #   and block parameters with `&`. Keyword arguments are shown with `:`
    #   If the parameter names are unavailable, they're given numbered names instead.
    #   Paraphrased from `awesome_print` gem.
    def signature
      if respond_to?(:parameters)
        args = parameters.inject([]) do |args_array, (arg_type, name)|
          name ||= (arg_type == :block ? 'block' : "arg#{args_array.size + 1}")
          args_array.push(
            case arg_type
            when :req    then name.to_s
            when :opt    then "#{name}=?"
            when :rest   then "*#{name}"
            when :block  then "&#{name}"
            when :key    then "#{name}:?"
            when :keyreq then "#{name}:"
            else '?'
            end
          )
        end
      else
        args = (1..arity.abs).map { |i| "arg#{i}" }
        args[-1] = "*#{args[-1]}" if arity < 0
      end

      "#{name}(#{args.join(', ')})"
    end

    # @return [Pry::Method, nil] The wrapped method that is called when you
    #   use "super" in the body of this method.
    def super(times = 1)
      if @method.is_a?(UnboundMethod)
        sup = super_using_ancestors(Pry::Method.instance_resolution_order(owner), times)
      else
        sup = super_using_ancestors(Pry::Method.resolution_order(receiver), times)
        sup &&= sup.bind(receiver)
      end
      Pry::Method.new(sup) if sup
    end

    # @return [String, nil] The original name the method was defined under,
    #   before any aliasing, or `nil` if it can't be determined.
    def original_name
      return nil if source_type != :ruby

      method_name_from_first_line(source.lines.first)
    end

    # @return [Boolean] Was the method defined outside a source file?
    def dynamically_defined?
      !!(source_file && source_file =~ /(\(.*\))|<.*>/)
    end

    # @return [Boolean] Whether the method is unbound.
    def unbound_method?
      is_a?(::UnboundMethod)
    end

    # @return [Boolean] Whether the method is bound.
    def bound_method?
      is_a?(::Method)
    end

    # @return [Boolean] Whether the method is a singleton method.
    def singleton_method?
      wrapped_owner.singleton_class?
    end

    # @return [Boolean] Was the method defined within the Pry REPL?
    def pry_method?
      source_file == Pry.eval_path
    end

    # @return [Array<String>] All known aliases for the method.
    def aliases
      owner = @method.owner
      # Avoid using `to_sym` on {Method#name}, which returns a `String`, because
      # it won't be garbage collected.
      name = @method.name

      all_methods_to_compare = owner.instance_methods | owner.private_instance_methods
      alias_list = all_methods_to_compare.combination(2).select do |pair|
        pair.include?(name) &&
          owner.instance_method(pair.first) == owner.instance_method(pair.last)
      end.flatten
      alias_list.delete(name)

      alias_list.map(&:to_s)
    end

    # @return [Boolean] Is the method definitely an alias?
    def alias?
      name != original_name
    end

    # @return [Boolean]
    def ==(other)
      return other == @method if other.is_a?(Pry::Method)

      @method == other
    end

    # @param [Class] klass
    # @return [Boolean]
    def is_a?(klass)
      (klass == Pry::Method) || @method.is_a?(klass)
    end
    alias kind_of? is_a?

    # @param [String, Symbol] method_name
    # @return [Boolean]
    def respond_to?(method_name, include_all = false)
      super || @method.respond_to?(method_name, include_all)
    end

    # Delegate any unknown calls to the wrapped method.
    def method_missing(method_name, *args, &block)
      if @method.respond_to?(method_name)
        @method.__send__(method_name, *args, &block)
      else
        super
      end
    end

    def respond_to_missing?(method_name, include_private = false)
      @method.respond_to?(method_name) || super
    end

    def comment
      Pry::Code.from_file(source_file).comment_describing(source_line)
    end

    private

    # @return [YARD::CodeObjects::MethodObject]
    # @raise [CommandError] when the method can't be found or `pry-doc` isn't installed.
    def pry_doc_info
      if defined?(PryDoc)
        Pry::MethodInfo.info_for(@method) ||
          raise(
            CommandError,
            "Cannot locate this method: #{name}. (source_location returns nil)"
          )
      else
        fail_msg = "Cannot locate this method: #{name}."
        if Helpers::Platform.mri?
          fail_msg += " Run `gem install pry-doc` to install" \
                      " Ruby Core documentation," \
                      " and `require 'pry-doc'` to load it.\n"
        end
        raise CommandError, fail_msg
      end
    end

    # @param [Class, Module] ancestors The ancestors to investigate
    # @return [Method] The unwrapped super-method
    def super_using_ancestors(ancestors, times = 1)
      next_owner = owner
      times.times do
        i = ancestors.index(next_owner) + 1
        while ancestors[i] &&
              !(ancestors[i].method_defined?(name) ||
                ancestors[i].private_method_defined?(name))
          i += 1
        end
        (next_owner = ancestors[i]) || (return nil)
      end

      begin
        safe_send(next_owner, :instance_method, name)
      rescue StandardError
        nil
      end
    end

    # @param [String] first_ln The first line of a method definition.
    # @return [String, nil]
    def method_name_from_first_line(first_ln)
      return nil if first_ln.strip !~ /^def /

      tokens = SyntaxHighlighter.tokenize(first_ln)
      tokens = tokens.tokens.each_slice(2) if tokens.respond_to?(:tokens)
      tokens.each_cons(2) do |t1, t2|
        if t2.last == :method || t2.last == :ident && t1 == [".", :operator]
          return t2.first
        end
      end

      nil
    end

    def c_source
      info = pry_doc_info
      strip_comments_from_c_code(info.source) if info && info.source
    end

    def ruby_source
      # Clone of `MethodSource.source_helper` that knows to use our
      # hacked version of `source_location` for our input buffer for methods
      # defined in `(pry)`.
      file, line = *source_location
      unless file
        raise SourceNotFoundError, "Could not locate source for #{name_with_owner}!"
      end

      begin
        code = Pry::Code.from_file(file).expression_at(line)
      rescue SyntaxError => e
        raise MethodSource::SourceNotFoundError, e.message
      end
      strip_leading_whitespace(code)
    end
  end
end
