<?php
/**
 * Part of the Joomla Framework Console 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\Console\Descriptor;

use Joomla\Console\Application;
use Joomla\Console\Command\AbstractCommand;
use Symfony\Component\Console\Exception\CommandNotFoundException;

/**
 * Describes an application.
 *
 * @since  2.0.0
 */
final class ApplicationDescription
{
	/**
	 * Placeholder for commands in the global namespace.
	 *
	 * @var    string
	 * @since  2.0.0
	 */
	public const GLOBAL_NAMESPACE = '_global';

	/**
	 * The application's aliased commands.
	 *
	 * @var    AbstractCommand[]
	 * @since  2.0.0
	 */
	private $aliases;

	/**
	 * The application being described.
	 *
	 * @var    Application
	 * @since  2.0.0
	 */
	private $application;

	/**
	 * The application's commands.
	 *
	 * @var    AbstractCommand[]
	 * @since  2.0.0
	 */
	private $commands;

	/**
	 * The command namespace to process.
	 *
	 * @var    string
	 * @since  2.0.0
	 */
	private $namespace = '';

	/**
	 * The application's command namespaces.
	 *
	 * @var    array[]
	 * @since  2.0.0
	 */
	private $namespaces;

	/**
	 * Flag indicating hidden commands should be displayed.
	 *
	 * @var    boolean
	 * @since  2.0.0
	 */
	private $showHidden;

	/**
	 * Constructor.
	 *
	 * @param   Application  $application  The application being described.
	 * @param   string       $namespace    The command namespace to process.
	 * @param   boolean      $showHidden   Flag indicating hidden commands should be displayed.
	 *
	 * @since   2.0.0
	 */
	public function __construct(Application $application, string $namespace = '', bool $showHidden = false)
	{
		$this->application = $application;
		$this->namespace   = $namespace;
		$this->showHidden  = $showHidden;
	}

	/**
	 * Get the application's command namespaces.
	 *
	 * @return  array[]
	 *
	 * @since   2.0.0
	 */
	public function getNamespaces(): array
	{
		if ($this->namespaces === null)
		{
			$this->inspectApplication();
		}

		return $this->namespaces;
	}

	/**
	 * Get the application's commands.
	 *
	 * @return  AbstractCommand[]
	 *
	 * @since   2.0.0
	 */
	public function getCommands(): array
	{
		if ($this->commands === null)
		{
			$this->inspectApplication();
		}

		return $this->commands;
	}

	/**
	 * Get a command by name.
	 *
	 * @param   string  $name  The name of the command to retrieve.
	 *
	 * @return  AbstractCommand
	 *
	 * @since   2.0.0
	 * @throws  CommandNotFoundException
	 */
	public function getCommand(string $name): AbstractCommand
	{
		if (!isset($this->commands[$name]) && !isset($this->aliases[$name]))
		{
			throw new CommandNotFoundException(sprintf('Command %s does not exist.', $name));
		}

		return $this->commands[$name] ?? $this->aliases[$name];
	}

	/**
	 * Returns the namespace part of the command name.
	 *
	 * @param   string   $name   The command name to process
	 * @param   integer  $limit  The maximum number of parts of the namespace
	 *
	 * @return  string
	 *
	 * @since   2.0.0
	 */
	private function extractNamespace(string $name, ?int $limit = null): string
	{
		$parts = explode(':', $name);
		array_pop($parts);

		return implode(':', $limit === null ? $parts : \array_slice($parts, 0, $limit));
	}

	/**
	 * Inspects the application.
	 *
	 * @return  void
	 *
	 * @since   2.0.0
	 */
	private function inspectApplication(): void
	{
		$this->commands   = [];
		$this->namespaces = [];

		$all = $this->application->getAllCommands($this->namespace ? $this->application->findNamespace($this->namespace) : '');

		foreach ($this->sortCommands($all) as $namespace => $commands)
		{
			$names = [];

			foreach ($commands as $name => $command)
			{
				if (!$command->getName() || (!$this->showHidden && $command->isHidden()))
				{
					continue;
				}

				if ($command->getName() === $name)
				{
					$this->commands[$name] = $command;
				}
				else
				{
					$this->aliases[$name] = $command;
				}

				$names[] = $name;
			}

			$this->namespaces[$namespace] = ['id' => $namespace, 'commands' => $names];
		}
	}

	/**
	 * Sort a set of commands.
	 *
	 * @param   AbstractCommand[]  $commands  The commands to sort.
	 *
	 * @return  AbstractCommand[][]
	 *
	 * @since   2.0.0
	 */
	private function sortCommands(array $commands): array
	{
		$namespacedCommands = [];
		$globalCommands     = [];

		foreach ($commands as $name => $command)
		{
			$key = $this->extractNamespace($name, 1);

			if (!$key)
			{
				$globalCommands[self::GLOBAL_NAMESPACE][$name] = $command;
			}
			else
			{
				$namespacedCommands[$key][$name] = $command;
			}
		}

		ksort($namespacedCommands);
		$namespacedCommands = array_merge($globalCommands, $namespacedCommands);

		foreach ($namespacedCommands as &$commandsSet)
		{
			ksort($commandsSet);
		}

		// Unset reference to keep scope clear
		unset($commandsSet);

		return $namespacedCommands;
	}
}
