<?php
/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
namespace Drupal\Console\Core\Descriptor;

use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Descriptor\Descriptor;
use Symfony\Component\Console\Descriptor\ApplicationDescription;

/**
 * Text descriptor.
 *
 * @author Jean-François Simon <contact@jfsimon.fr>
 *
 * @internal
 */
class TextDescriptor extends Descriptor
{
    /**
     * {@inheritdoc}
     */
    protected function describeInputArgument(InputArgument $argument, array $options = [])
    {
        if (null !== $argument->getDefault() && (!is_array($argument->getDefault()) || count($argument->getDefault()))) {
            $default = sprintf('<comment> [default: %s]</comment>', $this->formatDefaultValue($argument->getDefault()));
        } else {
            $default = '';
        }
        $totalWidth = isset($options['total_width']) ? $options['total_width'] : strlen($argument->getName());
        $spacingWidth = $totalWidth - strlen($argument->getName()) + 2;
        $this->writeText(
            sprintf(
                '  <info>%s</info>%s%s%s',
                $argument->getName(),
                str_repeat(' ', $spacingWidth),
                // + 17 = 2 spaces + <info> + </info> + 2 spaces
                preg_replace(
                    '/\s*\R\s*/',
                    PHP_EOL.str_repeat(' ', $totalWidth + 17),
                    $options['translator']->trans($argument->getDescription())
                ),
                $default
            ), $options
        );
    }
    /**
     * {@inheritdoc}
     */
    protected function describeInputOption(InputOption $option, array $options = [])
    {
        if ($option->acceptValue() && null !== $option->getDefault() && (!is_array($option->getDefault()) || count($option->getDefault()))) {
            $default = sprintf('<comment> [default: %s]</comment>', $this->formatDefaultValue($option->getDefault()));
        } else {
            $default = '';
        }
        $value = '';
        if ($option->acceptValue()) {
            $value = '='.strtoupper($option->getName());
            if ($option->isValueOptional()) {
                $value = '['.$value.']';
            }
        }
        $totalWidth = isset($options['total_width']) ? $options['total_width'] : $this->calculateTotalWidthForOptions([$option]);
        $synopsis = sprintf(
            '%s%s',
            $option->getShortcut() ? sprintf('-%s, ', $option->getShortcut()) : '    ',
            sprintf('--%s%s', $option->getName(), $value)
        );
        $spacingWidth = $totalWidth - strlen($synopsis) + 2;
        $this->writeText(
            sprintf(
                '  <info>%s</info>%s%s%s%s',
                $synopsis,
                str_repeat(' ', $spacingWidth),
                // + 17 = 2 spaces + <info> + </info> + 2 spaces
                preg_replace(
                    '/\s*\R\s*/',
                    "\n".str_repeat(' ', $totalWidth + 17),
                    $options['translator']->trans($option->getDescription())
                ),
                $default,
                $option->isArray() ? '<comment> (multiple values allowed)</comment>' : ''
            ), $options
        );
    }
    /**
     * {@inheritdoc}
     */
    protected function describeInputDefinition(InputDefinition $definition, array $options = [])
    {
        $command_name = null;
        if (array_key_exists('command', $options)) {
            $command_name = $options['command']->getName();
        }

        $totalWidth = $this->calculateTotalWidthForOptions($definition->getOptions());
        foreach ($definition->getArguments() as $argument) {
            $totalWidth = max($totalWidth, strlen($argument->getName()));
        }

        if ($definition->getArguments()) {
            $this->writeText($options['translator']->trans('commands.list.messages.arguments'), $options);
            $this->writeText("\n");
            foreach ($definition->getArguments() as $argument) {
                $this->describeInputArgument($argument, array_merge($options, ['total_width' => $totalWidth]));
                $this->writeText("\n");
            }
        }
        if ($definition->getArguments() && $definition->getOptions()) {
            $this->writeText("\n");
        }
        if ($definition->getOptions()) {
            $laterOptions = [];
            $this->writeText($options['translator']->trans('commands.list.messages.options'), $options);

            $exitOptionName = 'help';
            if ($command_name === $exitOptionName) {
                $exitOptionName = '';
            }

            foreach ($definition->getOptions() as $option) {
                if ($option->getName() === $exitOptionName) {
                    break;
                }

                if (strlen($option->getShortcut()) > 1) {
                    $laterOptions[] = $option;
                    continue;
                }
                $this->writeText("\n");
                $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth]));
            }
            foreach ($laterOptions as $option) {
                $this->writeText("\n");
                $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth]));
            }
        }
    }
    /**
     * {@inheritdoc}
     */
    protected function describeCommand(Command $command, array $options = [])
    {
        $namespace = substr(
            $command->getName(),
            0,
            (strpos($command->getName(), ':')?:0)
        );
        $commandData = $command->getApplication()->getData();
        $examples = [];
        if (array_key_exists($namespace, $commandData['commands'])) {
            $commands = $commandData['commands'][$namespace];
            foreach ($commands as $item) {
                if ($item['name'] == $command->getName()) {
                    $examples = $item['examples'];
                    break;
                }
            }
        }

        $command->getSynopsis(true);
        $command->getSynopsis(false);
        $command->mergeApplicationDefinition(false);

        $this->writeText($command->trans('commands.list.messages.usage'), $options);
        foreach (array_merge([$command->getSynopsis(true)], $command->getAliases(), $command->getUsages()) as $key => $usage) {
            if ($key > 0) {
                $this->writeText("\n");
            }
            $this->writeText('  '.$usage, $options);
        }

        $this->writeText("\n");
        $definition = $command->getNativeDefinition();
        if ($definition->getOptions() || $definition->getArguments()) {
            $this->writeText("\n");
            $options =
                array_merge(
                    $options,
                    [ 'command' => $command ]
                );
            $this->describeInputDefinition($definition, $options);
            $this->writeText("\n");
        }

        if ($examples) {
            $this->writeText("\n");
            $this->writeText("<comment>Examples:</comment>", $options);
            foreach ($examples as $key => $example) {
                $this->writeText("\n");
                if ($key != 0) {
                    $this->writeText("\n");
                }
                $this->writeText('  <info>'.$example['description'].'</info>');
                $this->writeText("\n");
                $this->writeText('  '.$example['execution']);
            }
        }

        if ($help = $command->getProcessedHelp()) {
            $this->writeText("\n");
            $this->writeText($command->trans('commands.list.messages.help'), $options);
            $this->writeText("\n");
            $this->writeText(' '.str_replace("\n", "\n ", $help), $options);
            $this->writeText("\n");
        }
    }
    /**
     * {@inheritdoc}
     */
    protected function describeApplication(Application $application, array $options = [])
    {
        $describedNamespace = isset($options['namespace']) ? $options['namespace'] : null;
        $description = new ApplicationDescription($application, $describedNamespace);
        if (isset($options['raw_text']) && $options['raw_text']) {
            $width = $this->getColumnWidth($description->getCommands());
            foreach ($description->getCommands() as $command) {
                $this->writeText(sprintf("%-${width}s %s", $command->getName(), $command->getDescription()), $options);
                $this->writeText("\n");
            }
        } else {
            if ('' != $help = $application->getHelp()) {
                $this->writeText("$help\n\n", $options);
            }
            if (empty($describedNamespace)) {
                $this->writeText(
                    $application->trans('commands.list.messages.usage'),
                    $options
                );
                $this->writeText(
                    $application->trans(
                        'commands.list.messages.usage-details'
                    ),
                    $options
                );
                $options['application'] = $application;

                $this->describeInputDefinition(
                    new InputDefinition(
                        $application->getDefinition()->getOptions()
                    ),
                    $options
                );
                $this->writeText("\n");
                $this->writeText("\n");
            }

            $width = $this->getColumnWidth($description->getCommands()) + 4;
            if ($describedNamespace) {
                $this->writeText(sprintf($application->trans('commands.list.messages.comment'), $describedNamespace), $options);
            } else {
                $this->writeText($application->trans('commands.list.messages.available-commands'), $options);
            }

            $singleCommands = [
                'about',
                'chain',
                'check',
                'composerize',
                'exec',
                'help',
                'init',
                'list',
                'shell',
                'server',
                'snippet'
            ];

            // add commands by namespace
            foreach ($description->getNamespaces() as $namespace) {
                if (!$describedNamespace && ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) {
                    $this->writeText("\n");
                    $this->writeText(' <comment>'.$namespace['id'].'</comment>', $options);
                }
                foreach ($namespace['commands'] as $name) {
                    if (ApplicationDescription::GLOBAL_NAMESPACE == $namespace['id']) {
                        if (!in_array($name, $singleCommands)) {
                            continue;
                        }
                    }

                    $this->writeText("\n");
                    $alias = '';
                    if ($description->getCommand($name)->getAliases()) {
                        $alias = sprintf(
                            '(%s)',
                            implode(',', $description->getCommand($name)->getAliases())
                        );
                    }

                    $spacingWidth = $width - strlen($name.$alias);
                    if ($spacingWidth < 0) {
                        $spacingWidth = 0;
                    }

                    $this->writeText(
                        sprintf(
                            '  <info>%s</info> <comment>%s</comment> %s%s',
                            $name,
                            $alias,
                            str_repeat(' ', $spacingWidth),
                            $description->getCommand($name)->getDescription(
                            )
                        ),
                        $options
                    );
                }
            }
            $this->writeText("\n");
        }
    }
    /**
     * {@inheritdoc}
     */
    private function writeText($content, array $options = [])
    {
        $this->write(
            isset($options['raw_text']) && $options['raw_text'] ? strip_tags($content) : $content,
            isset($options['raw_output']) ? !$options['raw_output'] : true
        );
    }
    /**
     * Formats input option/argument default value.
     *
     * @param mixed $default
     *
     * @return string
     */
    private function formatDefaultValue($default)
    {
        if (PHP_VERSION_ID < 50400) {
            return str_replace('\/', '/', json_encode($default));
        }
        return json_encode($default, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    }
    /**
     * @param Command[] $commands
     *
     * @return int
     */
    private function getColumnWidth(array $commands)
    {
        $width = 0;
        foreach ($commands as $command) {
            $width = strlen($command->getName()) > $width ? strlen($command->getName()) : $width;
        }
        return $width + 2;
    }
    /**
     * @param InputOption[] $options
     *
     * @return int
     */
    private function calculateTotalWidthForOptions($options)
    {
        $totalWidth = 0;
        foreach ($options as $option) {
            // "-" + shortcut + ", --" + name
            $nameLength = 1 + max(strlen($option->getShortcut()), 1) + 4 + strlen($option->getName());
            if ($option->acceptValue()) {
                $valueLength = 1 + strlen($option->getName()); // = + value
                $valueLength += $option->isValueOptional() ? 2 : 0; // [ + ]
                $nameLength += $valueLength;
            }
            $totalWidth = max($totalWidth, $nameLength);
        }
        return $totalWidth;
    }
}
