# frozen_string_literal: true

module Solargraph
  module Pin
    # A DelegatedMethod is a more complicated version of a MethodAlias that
    # allows aliasing a method from a different closure (class/module etc).
    class DelegatedMethod < Pin::Method
      # A DelegatedMethod can be constructed with either a :resolved_method
      # pin, or a :receiver_chain. When a :receiver_chain is supplied, it
      # will be used to *dynamically* resolve a receiver type within the
      # given closure/scope, and the delegated method will then be resolved
      # to a method pin on that type.
      #
      # @param method [Method, nil] an already resolved method pin.
      # @param receiver [Source::Chain, nil] the source code used to resolve the receiver for this delegated method.
      # @param name [String]
      # @param receiver_method_name [String] the method name that will be called on the receiver (defaults to :name).
      def initialize(method: nil, receiver: nil, name: method&.name, receiver_method_name: name, **splat)
        raise ArgumentError, 'either :method or :receiver is required' if (method && receiver) || (!method && !receiver)
        super(name: name, **splat)

        @receiver_chain = receiver
        @resolved_method = method
        @receiver_method_name = receiver_method_name
      end

      def inner_desc
        "#{name} => #{@receiver_chain}##{@receiver_method_name}"
      end

      def location
        return super if super

        @resolved_method&.send(:location)
      end


      def type_location
        return super if super

        @resolved_method&.send(:type_location)
      end

      %i[comments parameters return_type signatures].each do |method|
        define_method(method) do
          # @sg-ignore Need to set context correctly in define_method blocks
          @resolved_method ? @resolved_method.send(method) : super()
        end
      end

      %i[typify realize infer probe].each do |method|
        # @param api_map [ApiMap]
        define_method(method) do |api_map|
          # @sg-ignore Unresolved call to resolve_method
          resolve_method(api_map)
          # @sg-ignore Need to set context correctly in define_method blocks
          @resolved_method ? @resolved_method.send(method, api_map) : super(api_map)
        end
      end

      # @param api_map [ApiMap]
      def resolvable?(api_map)
        resolve_method(api_map)
        !!@resolved_method
      end

      private

      # Resolves the receiver chain and method name to a method pin, resetting any previously resolution.
      #
      # @param api_map [ApiMap]
      # @return [Pin::Method, nil]
      def resolve_method api_map
        return if @resolved_method

        resolver = @receiver_chain.define(api_map, self, []).first

        unless resolver
          Solargraph.logger.warn \
            "Delegated receiver for #{path} was resolved to nil from `#{print_chain(@receiver_chain)}'"
          return
        end

        receiver_type = resolver.return_type

        return if receiver_type.undefined?

        receiver_path, method_scope =
          if @receiver_chain.constant?
            # HACK: the `return_type` of a constant is Class<Whatever>, but looking up a method expects
            # the arguments `"Whatever"` and `scope: :class`.
            [receiver_type.to_s.sub(/^Class<(.+)>$/, '\1'), :class]
          else
            [receiver_type.to_s, :instance]
          end

        method_stack = api_map.get_method_stack(receiver_path, @receiver_method_name, scope: method_scope)
        @resolved_method = method_stack.first
      end

      # helper to print a source chain as code, probably not 100% correct.
      #
      # @param chain [Source::Chain]
      # @return [String]
      def print_chain(chain)
        out = +''
        chain.links.each_with_index do |link, index|
          if index > 0
            if Source::Chain::Constant
              out << '::' unless link.word.start_with?('::')
            else
              out << '.'
            end
          end
          out << link.word
        end
        out
      end
    end
  end
end
