# frozen_string_literal: true

module Solargraph
  module Parser
    module ParserGem
      module NodeProcessors
        class SendNode < Parser::NodeProcessor::Base
          include ParserGem::NodeMethods

          def process
            # @sg-ignore Variable type could not be inferred for method_name
            # @type [Symbol]
            method_name = node.children[1]
            # :nocov:
            unless method_name.instance_of?(Symbol)
              Solargraph.assert_or_log(:parser_method_name, "Expected method name to be a Symbol, got #{method_name.class} for node #{node.inspect}")
              return process_children
            end
            # :nocov:
            if node.children[0].nil?
              if [:private, :public, :protected].include?(method_name)
                process_visibility
              elsif method_name == :module_function
                process_module_function
              elsif [:attr_reader, :attr_writer, :attr_accessor].include?(method_name)
                process_attribute
              elsif method_name == :include
                process_include
              elsif method_name == :extend
                process_extend
              elsif method_name == :prepend
                process_prepend
              elsif method_name == :require
                process_require
              elsif method_name == :autoload
                process_autoload
              elsif method_name == :private_constant
                process_private_constant
              # @sg-ignore
              elsif method_name == :alias_method && node.children[2] && node.children[2] && node.children[2].type == :sym && node.children[3] && node.children[3].type == :sym
                process_alias_method
              # @sg-ignore
              elsif method_name == :private_class_method && node.children[2].is_a?(AST::Node)
                # Processing a private class can potentially handle children on its own
                return if process_private_class_method
              end
            # @sg-ignore
            elsif method_name == :require && node.children[0].to_s == '(const nil :Bundler)'
              pins.push Pin::Reference::Require.new(Solargraph::Location.new(region.filename, Solargraph::Range.from_to(0, 0, 0, 0)), 'bundler/require', source: :parser)
            end
            process_children
          end

          private

          # @return [void]
          def process_visibility
            if (node.children.length > 2)
              node.children[2..-1].each do |child|
                # @sg-ignore Variable type could not be inferred for method_name
                # @type [Symbol]
                visibility = node.children[1]
                # :nocov:
                unless visibility.instance_of?(Symbol)
                  Solargraph.assert_or_log(:parser_visibility, "Expected visibility name to be a Symbol, got #{visibility.class} for node #{node.inspect}")
                  return process_children
                end
                # :nocov:
                if child.is_a?(::Parser::AST::Node) && (child.type == :sym || child.type == :str)
                  name = child.children[0].to_s
                  matches = pins.select{ |pin| pin.is_a?(Pin::Method) && pin.name == name && pin.namespace == region.closure.full_context.namespace && pin.context.scope == (region.scope || :instance)}
                  matches.each do |pin|
                    # @todo Smelly instance variable access
                    pin.instance_variable_set(:@visibility, visibility)
                  end
                else
                  process_children region.update(visibility: visibility)
                end
              end
            else
              # @todo Smelly instance variable access
              region.instance_variable_set(:@visibility, node.children[1])
            end
          end

          # @return [void]
          def process_attribute
            node.children[2..-1].each do |a|
              loc = get_node_location(node)
              clos = region.closure
              cmnt = comments_for(node)
              if node.children[1] == :attr_reader || node.children[1] == :attr_accessor
                pins.push Solargraph::Pin::Method.new(
                  location: loc,
                  closure: clos,
                  name: a.children[0].to_s,
                  comments: cmnt,
                  scope: region.scope || :instance,
                  visibility: region.visibility,
                  attribute: true,
                  source: :parser
                )
              end
              if node.children[1] == :attr_writer || node.children[1] == :attr_accessor
                method_pin = Solargraph::Pin::Method.new(
                  location: loc,
                  closure: clos,
                  name: "#{a.children[0]}=",
                  comments: cmnt,
                  scope: region.scope || :instance,
                  visibility: region.visibility,
                  attribute: true,
                  source: :parser
                )
                pins.push method_pin
                method_pin.parameters.push Pin::Parameter.new(name: 'value', decl: :arg, closure: pins.last, source: :parser)
                if method_pin.return_type.defined?
                  pins.last.docstring.add_tag YARD::Tags::Tag.new(:param, '', pins.last.return_type.items.map(&:rooted_tags), 'value')
                end
              end
            end
          end

          # @return [void]
          def process_include
            if node.children[2].is_a?(AST::Node) && node.children[2].type == :const
              cp = region.closure
              node.children[2..-1].each do |i|
                type = region.scope == :class ? Pin::Reference::Extend : Pin::Reference::Include
                pins.push type.new(
                  location: get_node_location(i),
                  closure: cp,
                  name: unpack_name(i),
                  source: :parser
                )
              end
            end
          end

          # @return [void]
          def process_prepend
            if node.children[2].is_a?(AST::Node) && node.children[2].type == :const
              cp = region.closure
              node.children[2..-1].each do |i|
                pins.push Pin::Reference::Prepend.new(
                  location: get_node_location(i),
                  closure: cp,
                  name: unpack_name(i),
                  source: :parser
                )
              end
            end
          end

          # @return [void]
          def process_extend
            node.children[2..-1].each do |i|
              loc = get_node_location(node)
              if i.type == :self
                pins.push Pin::Reference::Extend.new(
                  location: loc,
                  closure: region.closure,
                  name: region.closure.full_context.namespace,
                  source: :parser
                )
              else
                pins.push Pin::Reference::Extend.new(
                  location: loc,
                  closure: region.closure,
                  name: unpack_name(i),
                  source: :parser
                )
              end
            end
          end

          # @return [void]
          def process_require
            if node.children[2].is_a?(AST::Node) && node.children[2].type == :str
              path = node.children[2].children[0].to_s
              pins.push Pin::Reference::Require.new(get_node_location(node), path, source: :parser)
            end
          end

          # @return [void]
          def process_autoload
            if node.children[3].is_a?(AST::Node) && node.children[3].type == :str
              path = node.children[3].children[0].to_s
              pins.push Pin::Reference::Require.new(get_node_location(node), path, source: :parser)
            end
          end

          # @return [void]
          def process_module_function
            if node.children[2].nil?
              # @todo Smelly instance variable access
              region.instance_variable_set(:@visibility, :module_function)
            elsif node.children[2].type == :sym || node.children[2].type == :str
              node.children[2..-1].each do |x|
                cn = x.children[0].to_s
                # @type [Pin::Method, nil]
                ref = pins.find { |p| p.is_a?(Pin::Method) && p.namespace == region.closure.full_context.namespace && p.name == cn }
                unless ref.nil?
                  pins.delete ref
                  mm = Solargraph::Pin::Method.new(
                    location: ref.location,
                    closure: ref.closure,
                    name: ref.name,
                    parameters: ref.parameters,
                    comments: ref.comments,
                    scope: :class,
                    visibility: :public,
                    node: ref.node,
                    source: :parser
                  )
                  cm = Solargraph::Pin::Method.new(
                    location: ref.location,
                    closure: ref.closure,
                    name: ref.name,
                    parameters: ref.parameters,
                    comments: ref.comments,
                    scope: :instance,
                    visibility: :private,
                    node: ref.node,
                    source: :parser)
                  pins.push mm, cm
                  pins.select{|pin| pin.is_a?(Pin::InstanceVariable) && pin.closure.path == ref.path}.each do |ivar|
                    pins.delete ivar
                    pins.push Solargraph::Pin::InstanceVariable.new(
                      location: ivar.location,
                      closure: cm,
                      name: ivar.name,
                      comments: ivar.comments,
                      assignment: ivar.assignment,
                      source: :parser
                    )
                    pins.push Solargraph::Pin::InstanceVariable.new(
                      location: ivar.location,
                      closure: mm,
                      name: ivar.name,
                      comments: ivar.comments,
                      assignment: ivar.assignment,
                      source: :parser
                    )
                  end
                end
              end
            elsif node.children[2].type == :def
              NodeProcessor.process node.children[2], region.update(visibility: :module_function), pins, locals
            end
          end

          # @return [void]
          def process_private_constant
            if node.children[2] && (node.children[2].type == :sym || node.children[2].type == :str)
              cn = node.children[2].children[0].to_s
              ref = pins.select{|p| [Solargraph::Pin::Namespace, Solargraph::Pin::Constant].include?(p.class) && p.namespace == region.closure.full_context.namespace && p.name == cn}.first
              # HACK: Smelly instance variable access
              ref.instance_variable_set(:@visibility, :private) unless ref.nil?
            end
          end

          # @return [void]
          def process_alias_method
            loc = get_node_location(node)
            pins.push Solargraph::Pin::MethodAlias.new(
              location: get_node_location(node),
              closure: region.closure,
              name: node.children[2].children[0].to_s,
              original: node.children[3].children[0].to_s,
              scope: region.scope || :instance,
              source: :parser
            )
          end

          # @return [Boolean]
          def process_private_class_method
            if node.children[2].type == :sym || node.children[2].type == :str
              ref = pins.select { |p| p.is_a?(Pin::Method) && p.namespace == region.closure.full_context.namespace && p.name == node.children[2].children[0].to_s }.first
              # HACK: Smelly instance variable access
              ref.instance_variable_set(:@visibility, :private) unless ref.nil?
              false
            else
              process_children region.update(scope: :class, visibility: :private)
              true
            end
          end
        end
      end
    end
  end
end
