<?php

/**
 * Part of the Joomla Framework Router 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\Router\Command;

use Joomla\Console\Command\AbstractCommand;
use Joomla\Router\RouterInterface;
use Symfony\Component\Console\Helper\TableCell;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

/**
 * Command listing information about the application's router.
 *
 * @since  2.0.0
 */
class DebugRouterCommand extends AbstractCommand
{
    /**
     * The default command name
     *
     * @var    string
     * @since  2.0.0
     */
    protected static $defaultName = 'debug:router';

    /**
     * The application router.
     *
     * @var    RouterInterface
     * @since  2.0.0
     */
    private $router;

    /**
     * Instantiate the command.
     *
     * @param   RouterInterface  $router  The application router.
     *
     * @since   2.0.0
     */
    public function __construct(RouterInterface $router)
    {
        $this->router = $router;

        parent::__construct();
    }

    /**
     * Configure the command.
     *
     * @return  void
     *
     * @since   2.0.0
     */
    protected function configure(): void
    {
        $this->setDescription("Displays information about the application's routes");
        $this->addOption('show-controllers', null, InputOption::VALUE_NONE, 'Show the controller for a route in the overview');
        $this->setHelp(
            <<<'EOF'
The <info>%command.name%</info> command lists all of the application's routes:

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

To show the controllers that handle each route, use the <info>--show-controllers</info> option:

  <info>php %command.full_name% --show-controllers</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);

        $showControllers = $input->getOption('show-controllers');

        $io->title(sprintf('%s Router Information', $this->getApplication()->getName()));

        if (empty($this->router->getRoutes())) {
            $io->warning('The router has no routes.');

            return 0;
        }

        $tableHeaders = [
            'Methods',
            'Pattern',
            'Rules',
        ];

        $tableRows = [];

        if ($showControllers) {
            $tableHeaders[] = 'Controller';
        }

        foreach ($this->router->getRoutes() as $route) {
            $row   = [];
            $row[] = $route->getMethods() ? implode('|', $route->getMethods()) : 'ANY';
            $row[] = $route->getPattern();

            $rules = $route->getRules();

            if (empty($rules)) {
                $row[] = 'N/A';
            } else {
                ksort($rules);

                $rulesAsString = '';

                foreach ($rules as $key => $value) {
                    $rulesAsString .= sprintf("%s: %s\n", $key, $this->formatValue($value));
                }

                $row[] = new TableCell(rtrim($rulesAsString), ['rowspan' => count($rules)]);
            }

            if ($showControllers) {
                $row[] = $this->formatCallable($route->getController());
            }

            $tableRows[] = $row;
        }

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

        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 ($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.');
    }

    /**
     * Formats a value as string.
     *
     * @param   mixed  $value  A value to format
     *
     * @return  string
     *
     * @since   2.0.0
     * @note    This method is based on \Symfony\Bundle\FrameworkBundle\Console\Descriptor\Descriptor::formatValue()
     */
    private function formatValue($value): string
    {
        if (\is_object($value)) {
            return sprintf('object(%s)', \get_class($value));
        }

        if (\is_string($value)) {
            return $value;
        }

        return preg_replace("/\n\s*/s", '', var_export($value, true));
    }
}
