# frozen_string_literal: true

require_relative '../cli/silencer'
Reek::CLI::Silencer.without_warnings { require 'parser' }

module Reek
  module AST
    # Base class for AST nodes extended with utility methods. Contains some
    # methods to ease the transition from Sexp to AST::Node.
    #
    class Node < ::Parser::AST::Node
      # Struct representing the rules for the search performed by each_node method.
      NodeLookupRule = Struct.new(:target_types, :ignoring)

      private_constant :NodeLookupRule

      def initialize(type, children = [], options = {})
        @comments = options.fetch(:comments, [])
        super
      end

      def full_comment
        comments.map(&:text).join("\n")
      end

      def leading_comment
        line = location.line
        comment_lines = comments.select do |comment|
          comment.location.line < line
        end
        comment_lines.map(&:text).join("\n")
      end

      def line
        loc && loc.line
      end

      def name
        to_s
      end

      #
      # Carries out a depth-first traversal of this syntax tree, yielding every
      # Sexp of the searched for type or types. The traversal stops at any node
      # whose type is listed in `ignoring`.
      #
      # If a type is searched for *and* listed in ignoring, it will be yielded
      # but traversal will not continue to its children.
      #
      # If the root's type is ignored, traversal does not stop, unless the root
      # is of a target type.
      #
      # Takes a block as well.
      #
      # @param target_types [Symbol, Array<Symbol>] the type or types to look
      #     for
      # @param ignoring [Array<Symbol>] types to ignore
      # @param blk block to execute for every hit. Gets passed in the
      #     matching element itself.
      #
      # @example
      #   node.each_node(:send, [:mlhs]) do |call_node| .... end
      #   node.each_node(:lvar).any? { _1.var_name == 'something' }
      #   node.each_node([:block]).flat_map do |elem| ... end
      #
      # Returns an array with all matching nodes.
      def each_node(target_types, ignoring = [], &blk)
        return enum_for(:each_node, target_types, ignoring) unless blk

        lookup_rule = NodeLookupRule.new(Array(target_types), ignoring)
        look_for(lookup_rule, &blk)
      end

      def contains_nested_node?(target_type)
        each_node(target_type).any?
      end

      # @quality :reek:DuplicateMethodCall { max_calls: 2 } is ok for lines.first
      # @quality :reek:FeatureEnvy
      def format_to_ruby
        if location
          lines = location.expression.source.split("\n").map(&:strip)
          case lines.length
          when 1 then lines.first
          when 2 then lines.join('; ')
          else [lines.first, lines.last].join(' ... ')
          end
        else
          to_s
        end
      end

      # Provide length for statement counting. A sexp counts as one statement.
      def length
        1
      end

      # Most nodes represent only one statement (although they can have nested
      # statements). The special type :begin exists primarily to contain more
      # statements.
      #
      # @return Array of unique outer-level statements contained in this node
      def statements
        [self]
      end

      def source
        loc.expression.source_buffer.name
      end

      # Method will be overridden by the code in the IfNode, CaseNode, and LogicOperatorBase sexp extensions.
      def condition; end

      protected

      # See ".each_node" for documentation.
      def look_for(lookup_rule, &blk)
        if lookup_rule.target_types.include?(type)
          yield self
          return if lookup_rule.ignoring.include?(type)
        end
        each_sexp do |elem|
          elem.look_for_recurse(lookup_rule, &blk)
        end
      end

      # See ".each_node" for documentation.
      def look_for_recurse(lookup_rule, &blk)
        yield self if lookup_rule.target_types.include?(type)
        return if lookup_rule.ignoring.include?(type)

        each_sexp do |elem|
          elem.look_for_recurse(lookup_rule, &blk)
        end
      end

      private

      attr_reader :comments

      def each_sexp
        children.each { |elem| yield elem if elem.is_a? ::Parser::AST::Node }
      end
    end
  end
end
