# typed: strict
# frozen_string_literal: true

module RubyLsp
  module Listeners
    class SemanticHighlighting
      include Requests::Support::Common
      extend T::Sig

      SPECIAL_RUBY_METHODS = T.let(
        [
          Module.instance_methods(false),
          Kernel.instance_methods(false),
          Kernel.methods(false),
          Bundler::Dsl.instance_methods(false),
          Module.private_instance_methods(false),
        ].flatten.map(&:to_s),
        T::Array[String],
      )

      sig do
        params(
          dispatcher: Prism::Dispatcher,
          response_builder: ResponseBuilders::SemanticHighlighting,
          range: T.nilable(T::Range[Integer]),
        ).void
      end
      def initialize(dispatcher, response_builder, range: nil)
        @response_builder = response_builder
        @range = range
        @special_methods = T.let(nil, T.nilable(T::Array[String]))
        @current_scope = T.let(ParameterScope.new, ParameterScope)
        @inside_regex_capture = T.let(false, T::Boolean)
        @inside_implicit_node = T.let(false, T::Boolean)

        dispatcher.register(
          self,
          :on_call_node_enter,
          :on_class_node_enter,
          :on_def_node_enter,
          :on_def_node_leave,
          :on_block_node_enter,
          :on_block_node_leave,
          :on_self_node_enter,
          :on_module_node_enter,
          :on_local_variable_write_node_enter,
          :on_local_variable_read_node_enter,
          :on_block_parameter_node_enter,
          :on_required_keyword_parameter_node_enter,
          :on_optional_keyword_parameter_node_enter,
          :on_keyword_rest_parameter_node_enter,
          :on_optional_parameter_node_enter,
          :on_required_parameter_node_enter,
          :on_rest_parameter_node_enter,
          :on_constant_read_node_enter,
          :on_constant_write_node_enter,
          :on_constant_and_write_node_enter,
          :on_constant_operator_write_node_enter,
          :on_constant_or_write_node_enter,
          :on_constant_target_node_enter,
          :on_constant_path_node_enter,
          :on_local_variable_and_write_node_enter,
          :on_local_variable_operator_write_node_enter,
          :on_local_variable_or_write_node_enter,
          :on_local_variable_target_node_enter,
          :on_block_local_variable_node_enter,
          :on_match_write_node_enter,
          :on_match_write_node_leave,
          :on_implicit_node_enter,
          :on_implicit_node_leave,
        )
      end

      sig { params(node: Prism::CallNode).void }
      def on_call_node_enter(node)
        return if @inside_implicit_node
        return unless visible?(node, @range)

        message = node.message
        return unless message

        # We can't push a semantic token for [] and []= because the argument inside the brackets is a part of
        # the message_loc
        return if message.start_with?("[") && (message.end_with?("]") || message.end_with?("]="))
        return if message == "=~"
        return if special_method?(message)

        type = Requests::Support::Sorbet.annotation?(node) ? :type : :method
        @response_builder.add_token(T.must(node.message_loc), type)
      end

      sig { params(node: Prism::MatchWriteNode).void }
      def on_match_write_node_enter(node)
        call = node.call

        if call.message == "=~"
          @inside_regex_capture = true
          process_regexp_locals(call)
        end
      end

      sig { params(node: Prism::MatchWriteNode).void }
      def on_match_write_node_leave(node)
        @inside_regex_capture = true if node.call.message == "=~"
      end

      sig { params(node: Prism::ConstantReadNode).void }
      def on_constant_read_node_enter(node)
        return if @inside_implicit_node
        return unless visible?(node, @range)

        @response_builder.add_token(node.location, :namespace)
      end

      sig { params(node: Prism::ConstantWriteNode).void }
      def on_constant_write_node_enter(node)
        return unless visible?(node, @range)

        @response_builder.add_token(node.name_loc, :namespace)
      end

      sig { params(node: Prism::ConstantAndWriteNode).void }
      def on_constant_and_write_node_enter(node)
        return unless visible?(node, @range)

        @response_builder.add_token(node.name_loc, :namespace)
      end

      sig { params(node: Prism::ConstantOperatorWriteNode).void }
      def on_constant_operator_write_node_enter(node)
        return unless visible?(node, @range)

        @response_builder.add_token(node.name_loc, :namespace)
      end

      sig { params(node: Prism::ConstantOrWriteNode).void }
      def on_constant_or_write_node_enter(node)
        return unless visible?(node, @range)

        @response_builder.add_token(node.name_loc, :namespace)
      end

      sig { params(node: Prism::ConstantTargetNode).void }
      def on_constant_target_node_enter(node)
        return unless visible?(node, @range)

        @response_builder.add_token(node.location, :namespace)
      end

      sig { params(node: Prism::DefNode).void }
      def on_def_node_enter(node)
        @current_scope = ParameterScope.new(@current_scope)
        return unless visible?(node, @range)

        @response_builder.add_token(node.name_loc, :method, [:declaration])
      end

      sig { params(node: Prism::DefNode).void }
      def on_def_node_leave(node)
        @current_scope = T.must(@current_scope.parent)
      end

      sig { params(node: Prism::BlockNode).void }
      def on_block_node_enter(node)
        @current_scope = ParameterScope.new(@current_scope)
      end

      sig { params(node: Prism::BlockNode).void }
      def on_block_node_leave(node)
        @current_scope = T.must(@current_scope.parent)
      end

      sig { params(node: Prism::BlockLocalVariableNode).void }
      def on_block_local_variable_node_enter(node)
        @response_builder.add_token(node.location, :variable)
      end

      sig { params(node: Prism::BlockParameterNode).void }
      def on_block_parameter_node_enter(node)
        name = node.name
        @current_scope << name.to_sym if name
      end

      sig { params(node: Prism::RequiredKeywordParameterNode).void }
      def on_required_keyword_parameter_node_enter(node)
        @current_scope << node.name
        return unless visible?(node, @range)

        location = node.name_loc
        @response_builder.add_token(location.copy(length: location.length - 1), :parameter)
      end

      sig { params(node: Prism::OptionalKeywordParameterNode).void }
      def on_optional_keyword_parameter_node_enter(node)
        @current_scope << node.name
        return unless visible?(node, @range)

        location = node.name_loc
        @response_builder.add_token(location.copy(length: location.length - 1), :parameter)
      end

      sig { params(node: Prism::KeywordRestParameterNode).void }
      def on_keyword_rest_parameter_node_enter(node)
        name = node.name

        if name
          @current_scope << name.to_sym

          @response_builder.add_token(T.must(node.name_loc), :parameter) if visible?(node, @range)
        end
      end

      sig { params(node: Prism::OptionalParameterNode).void }
      def on_optional_parameter_node_enter(node)
        @current_scope << node.name
        return unless visible?(node, @range)

        @response_builder.add_token(node.name_loc, :parameter)
      end

      sig { params(node: Prism::RequiredParameterNode).void }
      def on_required_parameter_node_enter(node)
        @current_scope << node.name
        return unless visible?(node, @range)

        @response_builder.add_token(node.location, :parameter)
      end

      sig { params(node: Prism::RestParameterNode).void }
      def on_rest_parameter_node_enter(node)
        name = node.name

        if name
          @current_scope << name.to_sym

          @response_builder.add_token(T.must(node.name_loc), :parameter) if visible?(node, @range)
        end
      end

      sig { params(node: Prism::SelfNode).void }
      def on_self_node_enter(node)
        return unless visible?(node, @range)

        @response_builder.add_token(node.location, :variable, [:default_library])
      end

      sig { params(node: Prism::LocalVariableWriteNode).void }
      def on_local_variable_write_node_enter(node)
        return unless visible?(node, @range)

        @response_builder.add_token(node.name_loc, @current_scope.type_for(node.name))
      end

      sig { params(node: Prism::LocalVariableReadNode).void }
      def on_local_variable_read_node_enter(node)
        return if @inside_implicit_node
        return unless visible?(node, @range)

        # Numbered parameters
        if /_\d+/.match?(node.name)
          @response_builder.add_token(node.location, :parameter)
          return
        end

        @response_builder.add_token(node.location, @current_scope.type_for(node.name))
      end

      sig { params(node: Prism::LocalVariableAndWriteNode).void }
      def on_local_variable_and_write_node_enter(node)
        return unless visible?(node, @range)

        @response_builder.add_token(node.name_loc, @current_scope.type_for(node.name))
      end

      sig { params(node: Prism::LocalVariableOperatorWriteNode).void }
      def on_local_variable_operator_write_node_enter(node)
        return unless visible?(node, @range)

        @response_builder.add_token(node.name_loc, @current_scope.type_for(node.name))
      end

      sig { params(node: Prism::LocalVariableOrWriteNode).void }
      def on_local_variable_or_write_node_enter(node)
        return unless visible?(node, @range)

        @response_builder.add_token(node.name_loc, @current_scope.type_for(node.name))
      end

      sig { params(node: Prism::LocalVariableTargetNode).void }
      def on_local_variable_target_node_enter(node)
        # If we're inside a regex capture, Prism will add LocalVariableTarget nodes for each captured variable.
        # Unfortunately, if the regex contains a backslash, the location will be incorrect and we'll end up highlighting
        # the entire regex as a local variable. We process these captures in process_regexp_locals instead and then
        # prevent pushing local variable target tokens. See https://github.com/ruby/prism/issues/1912
        return if @inside_regex_capture

        return unless visible?(node, @range)

        @response_builder.add_token(node.location, @current_scope.type_for(node.name))
      end

      sig { params(node: Prism::ClassNode).void }
      def on_class_node_enter(node)
        return unless visible?(node, @range)

        constant_path = node.constant_path

        if constant_path.is_a?(Prism::ConstantReadNode)
          @response_builder.add_token(constant_path.location, :class, [:declaration])
        else
          each_constant_path_part(constant_path) do |part|
            loc = case part
            when Prism::ConstantPathNode
              part.name_loc
            when Prism::ConstantReadNode
              part.location
            end
            next unless loc

            @response_builder.add_token(loc, :class, [:declaration])
          end
        end

        superclass = node.superclass

        if superclass.is_a?(Prism::ConstantReadNode)
          @response_builder.add_token(superclass.location, :class)
        elsif superclass
          each_constant_path_part(superclass) do |part|
            loc = case part
            when Prism::ConstantPathNode
              part.name_loc
            when Prism::ConstantReadNode
              part.location
            end
            next unless loc

            @response_builder.add_token(loc, :class)
          end
        end
      end

      sig { params(node: Prism::ModuleNode).void }
      def on_module_node_enter(node)
        return unless visible?(node, @range)

        constant_path = node.constant_path

        if constant_path.is_a?(Prism::ConstantReadNode)
          @response_builder.add_token(constant_path.location, :namespace, [:declaration])
        else
          each_constant_path_part(constant_path) do |part|
            loc = case part
            when Prism::ConstantPathNode
              part.name_loc
            when Prism::ConstantReadNode
              part.location
            end
            next unless loc

            @response_builder.add_token(loc, :namespace, [:declaration])
          end
        end
      end

      sig { params(node: Prism::ImplicitNode).void }
      def on_implicit_node_enter(node)
        return unless visible?(node, @range)

        @inside_implicit_node = true
      end

      sig { params(node: Prism::ImplicitNode).void }
      def on_implicit_node_leave(node)
        @inside_implicit_node = false
      end

      sig { params(node: Prism::ConstantPathNode).void }
      def on_constant_path_node_enter(node)
        return if @inside_implicit_node
        return unless visible?(node, @range)

        @response_builder.add_token(node.name_loc, :namespace)
      end

      private

      # Textmate provides highlighting for a subset of these special Ruby-specific methods.  We want to utilize that
      # highlighting, so we avoid making a semantic token for it.
      sig { params(method_name: String).returns(T::Boolean) }
      def special_method?(method_name)
        SPECIAL_RUBY_METHODS.include?(method_name)
      end

      sig { params(node: Prism::CallNode).void }
      def process_regexp_locals(node)
        receiver = node.receiver

        # The regexp needs to be the receiver of =~ for local variable capture
        return unless receiver.is_a?(Prism::RegularExpressionNode)

        content = receiver.content
        loc = receiver.content_loc

        # For each capture name we find in the regexp, look for a local in the current_scope
        Regexp.new(content, Regexp::FIXEDENCODING).names.each do |name|
          # The +3 is to compensate for the "(?<" part of the capture name
          capture_name_offset = T.must(content.index("(?<#{name}>")) + 3
          local_var_loc = loc.copy(start_offset: loc.start_offset + capture_name_offset, length: name.length)

          @response_builder.add_token(local_var_loc, @current_scope.type_for(name))
        end
      end
    end
  end
end