# frozen_string_literal: true

module Unparser
  # Preprocessor to normalize AST generated by parser
  class Preprocessor
    include Adamantium::Flat, NodeHelpers, AbstractType, Concord.new(:node, :parent_type), Procto.call(:result)

    # Return preprocessor result
    #
    # @return [Parser::AST::Node]
    #
    # @api private
    #
    abstract_method :result

    EMPTY = Parser::AST::Node.new(:empty)

    # Run preprocessor for node
    #
    # @param [Parser::AST::Node, nil] node
    #
    # @return [Parser::AST::Node, nil]
    #
    # @api private
    #
    def self.run(node, parent_type = nil)
      return EMPTY if node.nil?

      REGISTRY.fetch(node.type, [Noop]).reduce(node) do |current, processor|
        processor.call(current, parent_type)
      end
    end

    REGISTRY = Hash.new { |hash, key| hash[key] = [] }

    # Register preprocessor
    #
    # @param [Symbol] type
    #
    # @return [undefined]
    #
    # @api private
    #
    def self.register(type)
      REGISTRY[type] << self
    end
    private_class_method :register

  private

    # Visit node
    #
    # @param [Parser::AST::Node] child
    #
    # @return [undefined]
    #
    # @api private
    #
    def visit(child)
      self.class.run(child, node.type)
    end

    # Return children
    #
    # @return [Array<Parser::AST::Node>]
    #
    # @api private
    #
    def children
      node.children
    end

    # Return visited children
    #
    # @return [Array<Parser::Ast::Node>]
    #
    # @api private
    #
    def visited_children
      children.map do |node|
        if node.is_a?(Parser::AST::Node)
          visit(node)
        else
          node
        end
      end
    end

    # Noop preprocessor that just passes node through.
    class Noop < self

      register :int
      register :str

      # Return preprocessor result
      #
      # @return [Parser::AST::Node]
      #
      # @api private
      #
      def result
        node.updated(nil, visited_children)
      end

    end # Noop

    # Preprocessor transforming numeric nodes with infinity as value to round trippable equivalent.
    class Infinity < self

      register :float
      register :int

      NEG_INFINITY = -(Float::INFINITY - 1)

      # Return preprocessor result
      #
      # @return [Parser::AST::Node]
      #
      # @api private
      #
      def result
        value = node.children.first
        case value
        when Float::INFINITY
          s(:const, s(:const, nil, :Float), :INFINITY)
        when NEG_INFINITY
          s(:send, s(:const, s(:const, nil, :Float), :INFINITY), :-@)
        else
          node
        end
      end
    end

    # Preprocessor for begin nodes. Removes begin nodes with one child.
    #
    # This reduces the amount of complex logic needed inside unparser to emit "nice" syntax with minimal
    # tokens.
    #
    class Begin < self

      register :begin

      # Return preprocessor result
      #
      # @return [Parser::AST::Node]
      #
      # @api private
      #
      def result
        if children.one? && !parent_type.equal?(:regexp)
          visit(children.first)
        else
          Noop.call(node, parent_type)
        end
      end

    end # Begin
  end # Preprocessor
end # Unparser
