# frozen_string_literal: true

class Pry
  # This class is responsible for taking a string (identifying a
  # command/class/method/etc) and returning the relevant type of object.
  # For example, if the user looks up "show-source" then  a
  # `Pry::Command` will be returned. Alternatively, if the user passes in "Pry#repl" then
  # a `Pry::Method` object will be returned.
  #
  # The `CodeObject.lookup` method is responsible for 1. figuring out what kind of
  # object the user wants (applying precedence rules in doing so -- i.e methods
  # get precedence over commands with the same name) and 2. Returning
  # the appropriate object. If the user fails to provide a string
  # identifier for the object (i.e they pass in `nil` or "") then the
  # object looked up will be the 'current method' or 'current class'
  # associated with the Binding.
  #
  # TODO: This class is a clusterfuck. We need a much more robust
  # concept of what a "Code Object" really is. Currently
  # commands/classes/candidates/methods and so on just share a very
  # ill-defined interface.
  class CodeObject
    module Helpers
      # we need this helper as some Pry::Method objects can wrap Procs
      # @return [Boolean]
      def real_method_object?
        is_a?(::Method) || is_a?(::UnboundMethod)
      end

      def c_method?
        real_method_object? && source_type == :c
      end

      def module_with_yard_docs?
        is_a?(WrappedModule) && yard_docs?
      end

      def command?
        is_a?(Module) && self <= Pry::Command
      end

      # @return [Boolean] `true` if this module was defined by means of the C API,
      #   `false` if it's a Ruby module.
      # @note If a module defined by C was extended with a lot of methods written
      #   in Ruby, this method would fail.
      def c_module?
        return unless is_a?(WrappedModule)

        method_locations = wrapped.methods(false).map do |m|
          wrapped.method(m).source_location
        end

        method_locations.concat(
          wrapped.instance_methods(false).map do |m|
            wrapped.instance_method(m).source_location
          end
        )

        c_methods = method_locations.grep(nil).count
        ruby_methods = method_locations.count - c_methods

        c_methods > ruby_methods
      end
    end

    include Pry::Helpers::CommandHelpers

    class << self
      def lookup(str, pry_instance, options = {})
        co = new(str, pry_instance, options)

        co.default_lookup || co.method_or_class_lookup ||
          co.command_lookup || co.empty_lookup
      end
    end

    attr_accessor :str
    attr_accessor :target
    attr_accessor :pry_instance
    attr_accessor :super_level

    def initialize(str, pry_instance, options = {})
      options = {
        super: 0
      }.merge!(options)

      @str = str
      @pry_instance = pry_instance
      @target = pry_instance.current_context
      @super_level = options[:super]
    end

    # TODO: just make it so find_command_by_match_or_listing doesn't raise?
    def command_lookup
      pry_instance.commands.find_command_by_match_or_listing(str)
    rescue StandardError
      nil
    end

    # when no parameter is given (i.e CodeObject.lookup(nil)), then we
    # lookup the 'current object' from the binding.
    def empty_lookup
      return nil if str && !str.empty?

      obj = if internal_binding?(target)
              mod = target_self.is_a?(Module) ? target_self : target_self.class
              Pry::WrappedModule(mod)
            else
              Pry::Method.from_binding(target)
            end

      # respect the super level (i.e user might have specified a
      # --super flag to show-source)
      lookup_super(obj, super_level)
    end

    # lookup variables and constants and `self` that are not modules
    def default_lookup
      # we skip instance methods as we want those to fall through to
      # method_or_class_lookup()
      if safe_to_evaluate?(str) && !looks_like_an_instance_method?(str)
        obj = target.eval(str)

        # restrict to only objects we KNOW for sure support the full API
        # Do NOT support just any object that responds to source_location
        if sourcable_object?(obj)
          Pry::Method(obj)
        elsif !obj.is_a?(Module)
          Pry::WrappedModule(obj.class)
        end
      end
    rescue Pry::RescuableException
      nil
    end

    def method_or_class_lookup
      obj =
        case str
        when /\S+\(\)\z/
          Pry::Method.from_str(str.sub(/\(\)\z/, ''), target) ||
          Pry::WrappedModule.from_str(str, target)
        else
          Pry::WrappedModule.from_str(str, target) ||
          Pry::Method.from_str(str, target)
        end

      lookup_super(obj, super_level)
    end

    private

    def sourcable_object?(obj)
      [::Proc, ::Method, ::UnboundMethod].any? { |o| obj.is_a?(o) }
    end

    # Returns true if `str` looks like a method, i.e Klass#method
    # We need to consider this case because method lookups should fall
    # through to the `method_or_class_lookup()` method but a
    # defined?() on a "Klass#method` string will see the `#` as a
    # comment and only evaluate the `Klass` part.
    # @param [String] str
    # @return [Boolean] Whether the string looks like an instance method.
    def looks_like_an_instance_method?(str)
      str =~ /\S#\S/
    end

    # We use this method to decide whether code is safe to eval. Method's are
    # generally not, but everything else is.
    # TODO: is just checking != "method" enough??
    # TODO: see duplication of this method in Pry::WrappedModule
    # @param [String] str The string to lookup
    # @return [Boolean]
    def safe_to_evaluate?(str)
      return true if str.strip == "self"
      return false if str =~ /%/

      kind = target.eval("defined?(#{str})")
      kind =~ /variable|constant/
    end

    def target_self
      target.eval('self')
    end

    # grab the nth (`super_level`) super of `obj
    # @param [Object] obj
    # @param [Fixnum] super_level How far up the super chain to ascend.
    def lookup_super(obj, super_level)
      return unless obj

      sup = obj.super(super_level)
      raise Pry::CommandError, "No superclass found for #{obj.wrapped}" unless sup

      sup
    end
  end
end
