<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Service_Console
 * @version    $Id$
 * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @copyright  Copyright (c) 2009 - 2011, RealDolmen (http://www.realdolmen.com)
 * @license    http://phpazure.codeplex.com/license
 */

/**
 * @category   Zend
 * @package    Zend_Service_Console
 * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @copyright  Copyright (c) 2009 - 2011, RealDolmen (http://www.realdolmen.com)
 * @license    http://phpazure.codeplex.com/license
 */
class Zend_Service_Console_Command
{
	/**
	 * The handler.
	 *
	 * @var array
	 */
	protected $_handler;

	/**
	 * Gets the handler.
	 *
	 * @return array
	 */
	public function getHandler()
	{
		return $this->_handler;
	}

	/**
	 * Sets the handler.
	 *
	 * @param array $handler
	 * @return Zend_Service_Console_Command
	 */
	public function setHandler($handler)
	{
		$this->_handler = $handler;
		return $this;
	}

	/**
	 * Replaces PHP's error handler
	 *
	 * @param mixed $errno
	 * @param mixed $errstr
	 * @param mixed $errfile
	 * @param mixed $errline
	 */
	public static function phpstderr($errno, $errstr, $errfile, $errline)
	{
		self::stderr($errno . ': Error in ' . $errfile . ':' . $errline . ' - ' . $errstr);
	}

	/**
	 * Replaces PHP's exception handler
	 *
	 * @param Exception $exception
	 */
	public static function phpstdex($exception)
	{
		self::stderr('Error: ' . $exception->getMessage());
	}

	/**
	 * Writes output to STDERR, followed by a newline (optional)
	 *
	 * @param string $errorMessage
	 * @param string $newLine
	 */
	public static function stderr($errorMessage, $newLine = true)
	{
		if (error_reporting() === 0) {
			return;
		}
		file_put_contents('php://stderr', $errorMessage . ($newLine ? "\r\n" : ''));
	}

	/**
	 * Bootstrap the shell command.
	 *
	 * @param array $argv PHP argument values.
	 */
	public static function bootstrap($argv)
	{
		// Abort bootstrapping depending on the MICROSOFT_CONSOLE_COMMAND_HOST constant.
		if (defined('MICROSOFT_CONSOLE_COMMAND_HOST') && strtolower(MICROSOFT_CONSOLE_COMMAND_HOST) != 'console') {
			return;
		}

		// Replace error handler
		set_error_handler(['Zend_Service_Console_Command', 'phpstderr']);
		set_exception_handler(['Zend_Service_Console_Command', 'phpstdex']);

		// Build the application model
		$model = self::_buildModel();

		// Find a class that corresponds to the $argv[0] script name
		$requiredHandlerName = str_replace('.bat', '', str_replace('.sh', '', str_replace('.php', '', strtolower(basename($argv[0])))));
		$handler = null;
		foreach ($model as $possibleHandler) {
			if ($possibleHandler->handler == strtolower($requiredHandlerName)) {
				$handler = $possibleHandler;
				break;
			}
		}
		if (is_null($handler)) {
			self::stderr("No class found that implements handler '" . $requiredHandlerName . "'. Create a class that is named '" . $requiredHandlerName . "' and extends Zend_Service_Console_Command or is decorated with a docblock comment '@command-handler " . $requiredHandlerName . "'. Make sure it is loaded either through an autoloader or explicitly using require_once().");
			die();
		}

		// Find a method that matches the command name
		$command = null;
		foreach ($handler->commands as $possibleCommand) {
			if (in_array(strtolower(isset($argv[1]) ? $argv[1] : '<default>'), $possibleCommand->aliases)) {
				$command = $possibleCommand;
				break;
			}
		}
		if (is_null($command)) {
			$commandName = (isset($argv[1]) ? $argv[1] : '<default>');
			self::stderr("No method found that implements command " . $commandName . ". Create a method in class '" . $handler->class . "' that is named '" . strtolower($commandName) . "Command' or is decorated with a docblock comment '@command-name " . $commandName . "'.");
			die();
		}

		// Parse parameter values
		$parameterValues = [];
		$missingParameterValues = [];
		$parameterInputs = array_splice($argv, 2);
		foreach ($command->parameters as $parameter) {
			// Default value: null
			$value = null;

			// Consult value providers for value. First one wins.
			foreach ($parameter->valueproviders as $valueProviderName) {
				if (!class_exists($valueProviderName)) {
					$valueProviderName = 'Zend_Service_Console_Command_ParameterSource_' . $valueProviderName;
				}
				$valueProvider = new $valueProviderName();

				$value = $valueProvider->getValueForParameter($parameter, $parameterInputs);
				if (!is_null($value)) {
					break;
				}
			}
			if (is_null($value) && $parameter->required) {
				$missingParameterValues[] = $parameter->aliases[0];
			} else if (is_null($value)) {
				$value = $parameter->defaultvalue;
			}

			// Set value
			$parameterValues[] = $value;
			$argvValues[$parameter->aliases[0]] = $value;
		}

		// Mising parameters?
		if (count($missingParameterValues) > 0) {
			self::stderr("Some parameters are missing:\r\n" . implode("\r\n", $missingParameterValues));
			die();
		}

		// Supply argv in a nice way
		$parameterValues['argv'] = $parameterInputs;

		// Run the command
		$className = $handler->class;
		$classInstance = new $className();
		$classInstance->setHandler($handler);
		call_user_func_array([$classInstance, $command->method], $parameterValues);

		// Restore error handler
		restore_exception_handler();
		restore_error_handler();
	}

	/**
	 * Builds the handler model.
	 *
	 * @return array
	 */
	protected static function _buildModel()
	{
		$model = [];

		$classes = get_declared_classes();
		foreach ($classes as $class) {
			$type = new ReflectionClass($class);

			$handlers = self::_findValueForDocComment('@command-handler', $type->getDocComment());
			if (count($handlers) == 0 && $type->isSubclassOf('Zend_Service_Console_Command')) {
				// Fallback: if the class extends Zend_Service_Console_Command, register it as
				// a command handler.
				$handlers[] = $class;
			}
			$handlerDescriptions = self::_findValueForDocComment('@command-handler-description', $type->getDocComment());
			$handlerHeaders = self::_findValueForDocComment('@command-handler-header', $type->getDocComment());
			$handlerFooters = self::_findValueForDocComment('@command-handler-footer', $type->getDocComment());

			for ($hi = 0; $hi < count($handlers); $hi++) {
				$handler = $handlers[$hi];
				$handlerDescription = $handlerDescriptions[$hi] ?? $handlerDescriptions[0] ?? '';
				$handlerDescription = str_replace(
          ['\r\n', '\n'],
          ["\r\n", "\n"],
          $handlerDescription
        );

				$handlerModel = (object)[
					'handler'     => strtolower($handler),
					'description' => $handlerDescription,
					'headers'     => $handlerHeaders,
					'footers'     => $handlerFooters,
					'class'       => $class,
					'commands'    => []
				];

				$methods = $type->getMethods();
			    foreach ($methods as $method) {
			       	$commands = self::_findValueForDocComment('@command-name', $method->getDocComment());
			    	if (substr($method->getName(), -7) == 'Command' && !in_array(substr($method->getName(), 0, -7), $commands)) {
						// Fallback: if the method is named <commandname>Command,
						// register it as a command.
						$commands[] = substr($method->getName(), 0, -7);
					}
			       	for ($x = 0; $x < count($commands); $x++) {
			       		$commands[$x] = strtolower($commands[$x]);
			       	}
			       	$commands = array_unique($commands);
			       	$commandDescriptions = self::_findValueForDocComment('@command-description', $method->getDocComment());
			       	$commandExamples = self::_findValueForDocComment('@command-example', $method->getDocComment());

			       	if (count($commands) > 0) {
						$command = $commands[0];
						$commandDescription = isset($commandDescriptions[0]) ? $commandDescriptions[0] : '';

						$commandModel = (object)[
							'command'     => $command,
							'aliases'     => $commands,
							'description' => $commandDescription,
							'examples'    => $commandExamples,
							'class'       => $class,
							'method'      => $method->getName(),
							'parameters'  => []
						];

						$parameters = $method->getParameters();
						$parametersFor = self::_findValueForDocComment('@command-parameter-for', $method->getDocComment());
						for ($pi = 0; $pi < count($parameters); $pi++) {
							// Initialize
							$parameter = $parameters[$pi];
							$parameterFor = null;
							$parameterForDefaultValue = null;

							// Is it a "catch-all" parameter?
							if ($parameter->getName() == 'argv') {
								continue;
							}

							// Find the $parametersFor with the same name defined
							foreach ($parametersFor as $possibleParameterFor) {
								$possibleParameterFor = explode(' ', $possibleParameterFor, 4);
								if ($possibleParameterFor[0] == '$' . $parameter->getName()) {
									$parameterFor = $possibleParameterFor;
									break;
								}
							}
							if (is_null($parameterFor)) {
								die('@command-parameter-for missing for parameter $' . $parameter->getName());
							}

							if (is_null($parameterForDefaultValue) && $parameter->isOptional()) {
								$parameterForDefaultValue = $parameter->getDefaultValue();
							}

							$parameterModel = (object)[
								'name'           => '$' . $parameter->getName(),
								'defaultvalue'   => $parameterForDefaultValue,
								'valueproviders' => explode('|', $parameterFor[1]),
								'aliases'        => explode('|', $parameterFor[2]),
								'description'    => (isset($parameterFor[3]) ? $parameterFor[3] : ''),
								'required'       => (isset($parameterFor[3]) ? strpos(strtolower($parameterFor[3]), 'required') !== false && strpos(strtolower($parameterFor[3]), 'required if') === false : false),
							];

							// Add to model
							$commandModel->parameters[] = $parameterModel;
						}

						// Add to model
						$handlerModel->commands[] = $commandModel;
			       	}
				}

				// Add to model
				$model[] = $handlerModel;
			}
		}

		return $model;
	}

	/**
	 * Finds the value for a specific docComment.
	 *
	 * @param string $docCommentName Comment name
	 * @param string $docComment Comment object
	 * @return array
	 */
	protected static function _findValueForDocComment($docCommentName, $docComment)
	{
		$returnValue = [];

		$commentLines = explode("\n", $docComment);
	    foreach ($commentLines as $commentLine) {
	        if (strpos($commentLine, $docCommentName . ' ') !== false) {
	            $returnValue[] = trim(substr($commentLine, strpos($commentLine, $docCommentName) + strlen($docCommentName) + 1));
	        }
	    }

	    return $returnValue;
	}

	/**
	 * Display information on an object
	 *
	 * @param object $object Object
	 * @param array $propertiesToDump Property names to display
	 */
	protected function _displayObjectInformation($object, $propertiesToDump = [])
	{
		foreach ($propertiesToDump as $property) {
			printf('%-16s: %s' . "\r\n", $property, $object->$property);
		}
		printf("\r\n");
	}

	/**
	 * Displays the help information.
	 *
	 * @command-name <default>
	 * @command-name -h
	 * @command-name -help
	 * @command-description Displays the current help information.
	 */
	public function helpCommand(...$args) {
		$handler = $this->getHandler();
		$newline = "\r\n";

		if (count($handler->headers) > 0) {
			foreach ($handler->headers as $header) {
				printf('%s%s', $header, $newline);
			}
			printf($newline);
		}
		printf('%s%s', $handler->description, $newline);
		printf($newline);
		printf('Available commands:%s', $newline);
		foreach ($handler->commands as $command) {
			$description = str_split($command->description, 50);
			printf('  %-25s %s%s', implode(', ', $command->aliases), $description[0], $newline);
			for ($di = 1; $di < count($description); $di++) {
				printf('  %-25s %s%s', '', $description[$di], $newline);
			}
			printf($newline);

			if (count($command->parameters) > 0) {
				foreach ($command->parameters as $parameter) {
					$description = str_split($parameter->description, 50);
					printf('    %-23s %s%s', implode(', ', $parameter->aliases), $description[0], $newline);
					for ($di = 1; $di < count($description); $di++) {
						printf('    %-23s %s%s', '', $description[$di], $newline);
					}
					printf($newline);
				}
			}
			printf($newline);

			if (count($command->examples) > 0) {
				printf('    Example usage:%s', $newline);
				foreach ($command->examples as $example) {
					printf('      %s%s', $example, $newline);
				}
				printf($newline);
			}
		}

		if (count($handler->footers) > 0) {
			printf($newline);
			foreach ($handler->footers as $footer) {
				printf('%s%s', $footer, $newline);
			}
			printf($newline);
		}
	}
}
