module Prism
  # The dispatcher class fires events for nodes that are found while walking an
  # AST to all registered listeners. It's useful for performing different types
  # of analysis on the AST while only having to walk the tree once.
  #
  # To use the dispatcher, you would first instantiate it and register listeners
  # for the events you're interested in:
  #
  #     class OctalListener
  #       def on_integer_node_enter(node)
  #         if node.octal? && !node.slice.start_with?("0o")
  #           warn("Octal integers should be written with the 0o prefix")
  #         end
  #       end
  #     end
  #
  #     listener = OctalListener.new
  #     dispatcher = Prism::Dispatcher.new
  #     dispatcher.register(listener, :on_integer_node_enter)
  #
  # Then, you can walk any number of trees and dispatch events to the listeners:
  #
  #     result = Prism.parse("001 + 002 + 003")
  #     dispatcher.dispatch(result.value)
  #
  # Optionally, you can also use `#dispatch_once` to dispatch enter and leave
  # events for a single node without recursing further down the tree. This can
  # be useful in circumstances where you want to reuse the listeners you already
  # have registers but want to stop walking the tree at a certain point.
  #
  #     integer = result.value.statements.body.first.receiver.receiver
  #     dispatcher.dispatch_once(integer)
  #
  class Dispatcher < Visitor
    # attr_reader listeners: Hash[Symbol, Array[Listener]]
    attr_reader :listeners

    # Initialize a new dispatcher.
    def initialize
      @listeners = {}
    end

    # Register a listener for one or more events.
    #
    # def register: (Listener, *Symbol) -> void
    def register(listener, *events)
      register_events(listener, events)
    end

    # Register all public methods of a listener that match the pattern
    # `on_<node_name>_(enter|leave)`.
    #
    # def register_public_methods: (Listener) -> void
    def register_public_methods(listener)
      register_events(listener, listener.public_methods(false).grep(/\Aon_.+_(?:enter|leave)\z/))
    end

    # Register a listener for the given events.
    private def register_events(listener, events)
      events.each { |event| (listeners[event] ||= []) << listener }
    end

    # Walks `root` dispatching events to all registered listeners.
    #
    # def dispatch: (Node) -> void
    alias dispatch visit

    # Dispatches a single event for `node` to all registered listeners.
    #
    # def dispatch_once: (Node) -> void
    def dispatch_once(node)
      node.accept(DispatchOnce.new(listeners))
    end
    <%- nodes.each do |node| -%>

    # Dispatch enter and leave events for <%= node.name %> nodes and continue
    # walking the tree.
    def visit_<%= node.human %>(node)
      listeners[:on_<%= node.human %>_enter]&.each { |listener| listener.on_<%= node.human %>_enter(node) }
      super
      listeners[:on_<%= node.human %>_leave]&.each { |listener| listener.on_<%= node.human %>_leave(node) }
    end
    <%- end -%>

    class DispatchOnce < Visitor # :nodoc:
      attr_reader :listeners

      def initialize(listeners)
        @listeners = listeners
      end
      <%- nodes.each do |node| -%>

      # Dispatch enter and leave events for <%= node.name %> nodes.
      def visit_<%= node.human %>(node)
        listeners[:on_<%= node.human %>_enter]&.each { |listener| listener.on_<%= node.human %>_enter(node) }
        listeners[:on_<%= node.human %>_leave]&.each { |listener| listener.on_<%= node.human %>_leave(node) }
      end
      <%- end -%>
    end

    private_constant :DispatchOnce
  end
end
