<?php
/**
 * Part of the Joomla Framework Event Package
 *
 * @copyright  Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
 * @license    GNU General Public License version 2 or later; see LICENSE
 */

namespace Joomla\Event\Command;

use Joomla\Console\Command\AbstractCommand;
use Joomla\Event\DispatcherAwareInterface;
use Joomla\Event\DispatcherAwareTrait;
use Joomla\Event\DispatcherInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

/**
 * Command listing information about the application's event dispatcher.
 *
 * @since  2.0.0
 */
class DebugEventDispatcherCommand extends AbstractCommand implements DispatcherAwareInterface
{
	use DispatcherAwareTrait;

	/**
	 * The default command name
	 *
	 * @var    string
	 * @since  2.0.0
	 */
	protected static $defaultName = 'debug:event-dispatcher';

	/**
	 * Instantiate the command.
	 *
	 * @param   DispatcherInterface  $dispatcher  The application event dispatcher.
	 *
	 * @since   2.0.0
	 */
	public function __construct(DispatcherInterface $dispatcher)
	{
		$this->setDispatcher($dispatcher);

		parent::__construct();
	}

	/**
	 * Configure the command.
	 *
	 * @return  void
	 *
	 * @since   2.0.0
	 */
	protected function configure(): void
	{
		$this->setDescription("Displays information about the application's event dispatcher");
		$this->addArgument('event', InputArgument::OPTIONAL, 'Show the listeners for a specific event');
		$this->setHelp(<<<'EOF'
The <info>%command.name%</info> command lists all of the registered event handlers in an application's event dispatcher:

  <info>php %command.full_name%</info>

To get specific listeners for an event, specify its name:

  <info>php %command.full_name% application.before_execute</info>
EOF
		);
	}

	/**
	 * Internal function to execute the command.
	 *
	 * @param   InputInterface   $input   The input to inject into the command.
	 * @param   OutputInterface  $output  The output to inject into the command.
	 *
	 * @return  integer  The command exit code
	 *
	 * @since   2.0.0
	 */
	protected function doExecute(InputInterface $input, OutputInterface $output): int
	{
		$io = new SymfonyStyle($input, $output);

		if ($event = $input->getArgument('event'))
		{
			$listeners = $this->dispatcher->getListeners($event);

			if (empty($listeners))
			{
				$io->warning(sprintf('The event "%s" does not have any registered listeners.', $event));

				return 0;
			}

			$io->title(sprintf('%s Registered Listeners for "%s" Event', $this->getApplication()->getName(), $event));

			$this->renderEventListenerTable($listeners, $io);

			return 0;
		}

		$listeners = $this->dispatcher->getListeners();

		if (empty($listeners))
		{
			$io->comment('There are no listeners registered to the event dispatcher.');

			return 0;
		}

		$io->title(sprintf('%s Registered Listeners Grouped By Event', $this->getApplication()->getName()));

		ksort($listeners);

		foreach ($listeners as $subscribedEvent => $eventListeners)
		{
			$io->section(sprintf('"%s" event', $subscribedEvent));

			$this->renderEventListenerTable($eventListeners, $io);
		}

		return 0;
	}

	/**
	 * Formats a callable resource to be displayed in the console output
	 *
	 * @param   callable  $callable  A callable resource to format
	 *
	 * @return  string
	 *
	 * @since   2.0.0
	 * @throws  \ReflectionException
	 * @note    This method is based on \Symfony\Bundle\FrameworkBundle\Console\Descriptor\TextDescriptor::formatCallable()
	 */
	private function formatCallable($callable): string
	{
		if (\is_array($callable))
		{
			if (\is_object($callable[0]))
			{
				return sprintf('%s::%s()', \get_class($callable[0]), $callable[1]);
			}

			return sprintf('%s::%s()', $callable[0], $callable[1]);
		}

		if (\is_string($callable))
		{
			return sprintf('%s()', $callable);
		}

		if ($callable instanceof \Closure)
		{
			$r = new \ReflectionFunction($callable);

			if (strpos($r->name, '{closure}') !== false)
			{
				return 'Closure()';
			}

			if (null !== $class = $r->getClosureScopeClass())
			{
				return sprintf('%s::%s()', $class->name, $r->name);
			}

			return $r->name . '()';
		}

		if (method_exists($callable, '__invoke'))
		{
			return sprintf('%s::__invoke()', \get_class($callable));
		}

		throw new \InvalidArgumentException('Callable is not describable.');
	}

	/**
	 * Renders the table of listeners for an event
	 *
	 * @param   array         $eventListeners  The listeners for an event
	 * @param   SymfonyStyle  $io              The I/O helper
	 *
	 * @return  void
	 *
	 * @since   2.0.0
	 */
	private function renderEventListenerTable(array $eventListeners, SymfonyStyle $io): void
	{
		$tableHeaders = ['Order', 'Callable'];
		$tableRows    = [];

		foreach ($eventListeners as $order => $listener)
		{
			$tableRows[] = [
				sprintf('#%d', $order + 1),
				$this->formatCallable($listener),
			];
		}

		$io->table($tableHeaders, $tableRows);
	}
}
