# frozen_string_literal: true

require "appmap/util"

module AppMap
  class Hook
    class << self
      def method_hash_key(cls, method)
        [cls, method.name].hash
      rescue TypeError => e
        warn "Error building hash key for #{cls}, #{method}: #{e}"
      end
    end

    SIGNATURES = {}
    LOOKUP_SIGNATURE = lambda do |id|
      method = super(id)

      hash_key = Hook.method_hash_key(method.owner, method)
      return method unless hash_key

      signature = SIGNATURES[hash_key]
      if signature
        method.singleton_class.module_eval do
          define_method(:parameters) do
            signature
          end
        end
      end

      method
    end

    RUBY_MAJOR_VERSION, RUBY_MINOR_VERSION, _ = RUBY_VERSION.split(".").map(&:to_i)

    # Single hooked method.
    # Call #activate to override the original.
    class Method
      attr_reader :hook_package, :hook_class, :hook_method, :record_around, :parameters, :arity

      HOOK_DISABLE_KEY = "AppMap::Hook.disable"

      def initialize(hook_package, hook_class, hook_method, record_around: false)
        @hook_package = hook_package
        @hook_class = hook_class
        @hook_method = hook_method
        @record_around = record_around
        @parameters = hook_method.parameters
        @arity = hook_method.arity
      end

      def activate
        if HookLog.enabled?
          msg = if method_display_name
            "#{method_display_name}"
          else
            "#{hook_method.name} (class resolution deferred)"
          end
          HookLog.log "Hooking #{msg} at line #{(hook_method.source_location || []).join(":")}"
        end

        hook_method_parameters = hook_method.parameters.dup.freeze
        hash_key = Hook.method_hash_key(hook_class, hook_method)
        SIGNATURES[hash_key] = hook_method_parameters if hash_key

        # irb(main):001:0> Kernel.public_instance_method(:system)
        # (irb):1:in `public_instance_method': method `system' for module `Kernel' is  private (NameError)
        if hook_class == Kernel
          hook_class.define_method_with_arity(hook_method.name, hook_method.arity, hook_method_def)
        else
          cls = defining_class(hook_class)
          if cls
            cls.define_method_with_arity(hook_method.name, hook_method.arity, hook_method_def)
          end
        end
      end

      protected

      def defining_class(hook_class)
        cls = if RUBY_MAJOR_VERSION == 2 && RUBY_MINOR_VERSION <= 5
          hook_class
            .ancestors
            .select { |cls| cls.method_defined?(hook_method.name) }
            .find { |cls| cls.instance_method(hook_method.name).owner == cls }
        else
          hook_class.ancestors.find { |cls| cls.method_defined?(hook_method.name, false) }
        end

        return cls if cls

        warn "#{hook_method.name} not found on #{hook_class}" if Hook::LOG
      end

      def trace?
        return false unless AppMap.tracing_enabled?(thread: Thread.current)
        return false if Thread.current[HOOK_DISABLE_KEY]
        return false if hook_package&.shallow? && AppMap.tracing.last_package_for_current_thread == hook_package

        true
      end

      def method_display_name
        return @method_display_name if @method_display_name

        return @method_display_name = [defined_class, "#", hook_method.name].join if defined_class

        "#{hook_method.name} (class resolution deferred)"
      end

      def defined_class
        @defined_class ||= Hook.qualify_method_name(hook_method)&.first
      end

      def after_hook(_receiver, call_event, elapsed_before, elapsed, after_start_time, return_value, exception)
        return_event = handle_return(call_event.id, elapsed, return_value, exception)
        return_event.elapsed_instrumentation = elapsed_before + (AppMap::Util.gettime - after_start_time)
        AppMap.tracing.record_event(return_event) if return_event
      end

      def with_disabled_hook
        # Don't record functions, such as to_s and inspect, that might be called
        # by the fn. Otherwise there can be a stack overflow.
        Thread.current[HOOK_DISABLE_KEY] = true
        yield
      ensure
        Thread.current[HOOK_DISABLE_KEY] = false
      end
    end
  end

  module ObjectMethods
    define_method(:method, AppMap::Hook::LOOKUP_SIGNATURE)
    define_method(:public_method, AppMap::Hook::LOOKUP_SIGNATURE)
    define_method(:singleton_method, AppMap::Hook::LOOKUP_SIGNATURE)
  end

  module ModuleMethods
    define_method(:instance_method, AppMap::Hook::LOOKUP_SIGNATURE)
    define_method(:public_instance_method, AppMap::Hook::LOOKUP_SIGNATURE)
  end
end

unless ENV["APPMAP_NO_PATCH_OBJECT"] == "true"
  class Object
    prepend AppMap::ObjectMethods
  end
end

unless ENV["APPMAP_NO_PATCH_MODULE"] == "true"
  class Module
    prepend AppMap::ModuleMethods
  end
end

if RUBY_VERSION < "3"
  require "appmap/hook/method/ruby2"
else
  require "appmap/hook/method/ruby3"
end
