<?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);
    }
}
