<?php
/**
 * Console class file.
 *
 * @author Antonio Ramirez <amigo.cobos@gmail.com>
 * @author Nofriandi Ramenta <nramenta@gmail.com>
 * @link http://www.ramirezcobos.com/
 * @link http://www.2amigos.us/
 * @link https://github.com/nramenta/clio
 * @copyright 2013 2amigOS! Consultation Group LLC
 * @license http://www.opensource.org/licenses/bsd-license.php New BSD License
 */
namespace Yiinitializr\Cli;
/**
 * Console provides a set of useful functions to work on the terminal
 *
 * @author Antonio Ramirez <amigo.cobos@gmail.com>
 * @package Yiinitializr.Cli
 * @since 1.0
 */
class Console
{
	/**
	 * Text foreground colors.
	 */
	protected static $FGCOLOR = array(
		'black'  => 30,
		'red'    => 31,
		'green'  => 32,
		'brown'  => 33,
		'blue'   => 34,
		'purple' => 35,
		'cyan'   => 36,
		'grey'   => 37,
		'yellow' => 33,
	);

	/**
	 * Text styling.
	 */
	protected static $STYLE = array(
		'normal'     => 0,
		'bold'       => 1,
		'light'      => 1,
		'underscore' => 4,
		'underline'  => 4,
		'blink'      => 5,
		'inverse'    => 6,
		'hidden'     => 8,
		'concealed'  => 8,
	);

	/**
	 * Text background color.
	 */
	protected static $BGCOLOR = array(
		'black'  => 40,
		'red'    => 41,
		'green'  => 42,
		'brown'  => 43,
		'yellow' => 43,
		'blue'   => 44,
		'purple' => 45,
		'cyan'   => 46,
		'grey'   => 47,
	);

	/**
	 * Color specifier conversion table. Taken from PEAR's Console_Color.
	 */
	protected static $CONVERSIONS = array(
		'%y' => array('yellow', null, null),
		'%g' => array('green', null, null),
		'%b' => array('blue', null, null),
		'%r' => array('red', null, null),
		'%p' => array('purple', null, null),
		'%m' => array('purple', null, null),
		'%c' => array('cyan', null, null),
		'%w' => array('grey', null, null),
		'%k' => array('black', null, null),
		'%n' => array('reset', null, null),
		'%Y' => array('yellow', 'light', null),
		'%G' => array('green', 'light', null),
		'%B' => array('blue', 'light', null),
		'%R' => array('red', 'light', null),
		'%P' => array('purple', 'light', null),
		'%M' => array('purple', 'light', null),
		'%C' => array('cyan', 'light', null),
		'%W' => array('grey', 'light', null),
		'%K' => array('black', 'light', null),
		'%N' => array('reset', 'light', null),
		'%3' => array(null, null, 'yellow'),
		'%2' => array(null, null, 'green'),
		'%4' => array(null, null, 'blue'),
		'%1' => array(null, null, 'red'),
		'%5' => array(null, null, 'purple'),
		'%6' => array(null, null, 'cyan'),
		'%7' => array(null, null, 'grey'),
		'%0' => array(null, null, 'black'),
		'%F' => array(null, 'blink', null),
		'%U' => array(null, 'underline', null),
		'%8' => array(null, 'inverse', null),
		'%9' => array(null, 'bold', null),
		'%_' => array(null, 'bold', null),
	);

	/**
	 * Create ANSI-control codes for text foreground and background colors, and
	 * styling.
	 *
	 * @param string $fgcolor Text foreground color
	 * @param string $style   Text style
	 * @param string $bgcolor Text background color
	 *
	 * @return string ANSI-control code
	 */
	public static function color($fgcolor, $style, $bgcolor)
	{
		$code = array();
		if ($fgcolor == 'reset') {
			return "\033[0m";
		}
		if (isset(static::$FGCOLOR[$fgcolor])) {
			$code[] = static::$FGCOLOR[$fgcolor];
		}
		if (isset(static::$STYLE[$style])) {
			$code[] = static::$STYLE[$style];
		}
		if (isset(static::$BGCOLOR[$bgcolor])) {
			$code[] = static::$BGCOLOR[$bgcolor];
		}
		if (empty($code)) {
			$code[] = 0;
		}
		return "\033[" . implode(';', $code) . 'm';
	}

	/**
	 * aken from PEAR's Console_Color:
	 *
	 * Converts colorcodes in the format %y (for yellow) into ansi-control
	 * codes. The conversion table is: ('bold' meaning 'light' on some
	 * terminals). It's almost the same conversion table irssi uses.
	 * <pre>
	 *                  text      text            background
	 *      ------------------------------------------------
	 *      %k %K %0    black     dark grey       black
	 *      %r %R %1    red       bold red        red
	 *      %g %G %2    green     bold green      green
	 *      %y %Y %3    yellow    bold yellow     yellow
	 *      %b %B %4    blue      bold blue       blue
	 *      %m %M %5    magenta   bold magenta    magenta
	 *      %p %P       magenta (think: purple)
	 *      %c %C %6    cyan      bold cyan       cyan
	 *      %w %W %7    white     bold white      white
	 *
	 *      %F     Blinking, Flashing
	 *      %U     Underline
	 *      %8     Reverse
	 *      %_,%9  Bold
	 *
	 *      %n     Resets the color
	 *      %%     A single %
	 * </pre>
	 * First param is the string to convert, second is an optional flag if
	 * colors should be used. It defaults to true, if set to false, the
	 * colorcodes will just be removed (And %% will be transformed into %)
	 *
	 * @param $text
	 * @param bool $color
	 * @return mixed
	 */
	public static function colorize($text, $color = true)
	{
		$text = str_replace('%%', '% ', $text);
		foreach (static::$CONVERSIONS as $key => $value) {
			list($fgcolor, $style, $bgcolor) = $value;
			$text = str_replace(
				$key,
				$color ? static::color($fgcolor, $style, $bgcolor) : '',
				$text
			);
		}
		return str_replace('% ', '%', $text);
	}

	/**
	 * Strips a string from color specifiers.
	 *
	 * @param string $text String to strip
	 *
	 * @return string
	 */
	public static function decolorize($text)
	{
		return static::colorize($text, false);
	}

	/**
	 * Strips a string of ansi-control codes.
	 *
	 * @param string $text String to strip
	 *
	 * @return string
	 */
	public static function strip($text)
	{
		return preg_replace('/\033\[(\d+)(;\d+)*m/', '', $text);
	}

	/**
	 * Gets input from STDIN and returns a string right-trimmed for EOLs.
	 *
	 * @param bool $raw If set to true, returns the raw string without trimming
	 *
	 * @return string
	 */
	public static function stdin($raw = false)
	{
		return $raw ? fgets(STDIN) : rtrim(fgets(STDIN), PHP_EOL);
	}

	/**
	 * Prints text to STDOUT.
	 *
	 * @param string $text
	 * @param bool   $raw
	 *
	 * @return int|false Number of bytes printed or false on error
	 */
	public static function stdout($text, $raw = false)
	{
		if ($raw) {
			return fwrite(STDOUT, $text);
		} elseif (extension_loaded('posix') && posix_isatty(STDOUT)) {
			return fwrite(STDOUT, static::colorize($text));
		} else {
			return fwrite(STDOUT, static::decolorize($text));
		}
	}

	/**
	 * Prints text to STDERR.
	 *
	 * @param string $text
	 * @param bool   $raw
	 *
	 * @return int|false Number of bytes printed or false on error
	 */
	public static function stderr($text, $raw = false)
	{
		if ($raw) {
			return fwrite(STDERR, $text);
		} elseif (extension_loaded('posix') && posix_isatty(STDERR)) {
			return fwrite(STDERR, static::colorize($text));
		} else {
			return fwrite(STDERR, static::decolorize($text));
		}
	}

	/**
	 * Prints text to STDERR appended with a PHP_EOL.
	 *
	 * @param string $text
	 * @param bool   $raw
	 *
	 * @return int|false Number of bytes printed or false on error
	 */
	public static function error($text = null, $raw = false)
	{
		return static::stderr($text . PHP_EOL, $raw);
	}

	/**
	 * Asks the user for input. Ends when the user types a PHP_EOL. Optionally
	 * provide a prompt.
	 *
	 * @param string $prompt String prompt (optional)
	 *
	 * @return string User input
	 */
	public static function input($prompt = null)
	{
		if (isset($prompt)) {
			static::stdout($prompt);
		}
		return static::stdin();
	}

	/**
	 * Prints text to STDOUT appended with a PHP_EOL.
	 *
	 * @param string $text
	 * @param bool   $raw
	 *
	 * @return int|false Number of bytes printed or false on error
	 */
	public static function output($text = null, $raw = false)
	{
		return static::stdout($text . PHP_EOL, $raw);
	}

	/**
	 * Prompts the user for input
	 *
	 * @param string $text    Prompt string
	 * @param array  $options Set of options
	 *
	 * @return string
	 */
	public static function prompt($text, $options = array())
	{
		$options = $options + array(
			'required'  => false,
			'default'   => null,
			'pattern'   => null,
			'validator' => null,
			'error'     => 'Input unacceptable.',
		);

		top:
		if ($options['default']) {
			$input = static::input("$text [" . $options['default'] . ']: ');
		} else {
			$input = static::input("$text: ");
		}

		if (!strlen($input)) {
			if (isset($options['default'])) {
				$input = $options['default'];
			} elseif ($options['required']) {
				static::output($options['error']);
				goto top;
			}
		} elseif ($options['pattern'] && !preg_match($options['pattern'], $input)) {
			static::output($options['error']);
			goto top;
		} elseif ($options['validator'] &&
			!call_user_func_array($options['validator'], array($input, &$error))) {
			static::output(isset($error) ? $error : $options['error']);
			goto top;
		}

		return $input;
	}

	/**
	 * Asks the user for a simple yes/no confirmation.
	 *
	 * @param string $text    Prompt string
	 *
	 * @return bool Either true or false
	 */
	public static function confirm($text)
	{
		top:
		$input = strtolower(static::input("$text [y/n]: "));
		if (!in_array($input, array('y', 'n'))) goto top;
		return $input === 'y' ? true : false;
	}

	/**
	 * Gives the user an option to choose from. Giving '?' as an input will show
	 * a list of options to choose from and their explanations.
	 *
	 * @param string $text    Prompt string
	 * @param array  $options Key-value array of options to choose from
	 *
	 * @return string An option character the user chose
	 */
	public static function select($text, $options = array())
	{
		top:
		static::stdout("$text [" . implode(',', array_keys($options)) . ",?]: ");
		$input = static::stdin();
		if ($input === '?') {
			foreach ($options as $key => $value) {
				echo " $key - $value\n";
			}
			echo " ? - Show help\n";
			goto top;
		} elseif (!in_array($input, array_keys($options))) goto top;
		return $input;
	}

	/**
	 * Execute a Closure as another process in the background while showing a
	 * status update. The status update can be an indefinite spinner or a string
	 * periodically sent from the background process, depending on whether the
	 * provided Closure object has a $socket parameter or not. Messaging to the
	 * main process is done by socket_* functions. The return value is either
	 * the return value of the background process, or false if the process fork
	 * failed.
	 *
	 * @param callable $callable Closure object
	 * @return bool|int
	 * @throws \Exception
	 */
	public static function work(\Closure $callable)
	{
		if (!extension_loaded('pcntl')) {
			throw new \Exception('pcntl extension required');
		}

		if (!extension_loaded('sockets')) {
			throw new \Exception('sockets extension required');
		}

		$spinner = array('|', '/', '-', '\\');
		$i = 0; $l = count($spinner);
		$delay = 100000;

		$func = new \ReflectionFunction($callable);

		$socket = (bool)$func->getNumberOfParameters();

		if ($socket) {
			$sockets = array();
			if (socket_create_pair(AF_UNIX, SOCK_STREAM, 0, $sockets) === false) {
				return false;
			}
		}

		$pid = pcntl_fork();

		if ($pid > 0) {
			$done   = false;
			$retval = 0;
			pcntl_signal(SIGCHLD, function() use ($pid, &$done, &$retval) {
				$child_pid = pcntl_waitpid($pid, $status);
				if (pcntl_wifexited($status)) {
					$retval = pcntl_wexitstatus($status);
				}
				$done = true;
			});

			if ($socket) {
				$text = '';
				while (!$done) {
					$r = array($sockets[1]);
					$w = null;
					$e = null;
					if ($status = socket_select($r, $w, $e, 0)) {
						$data = socket_read($sockets[1], 4096, PHP_NORMAL_READ);
						if ($data === false) {
							throw new \Exception(
								sprintf(
									'socket write error %s',
									socket_strerror(socket_last_error($sockets[1]))
								)
							);
						}
						echo str_repeat(chr(8), strlen($text));
						$text = rtrim($data, "\n");
						Console::stdout($text);
					} else {
						pcntl_signal_dispatch();
					}
					usleep($delay);
				}
				echo str_repeat(chr(8), strlen($text));
				socket_close($sockets[0]);
				socket_close($sockets[1]);
			} else {
				while (!$done) {
					pcntl_signal_dispatch();
					echo $spinner[$i];
					usleep($delay);
					echo chr(8);
					$i = $i === $l - 1 ? 0 : $i + 1;
				}
			}

			return $retval;
		} elseif ($pid === 0) {
			if ($socket) {
				call_user_func($callable, $sockets[0]);
			} else {
				call_user_func($callable);
			}
			exit;
		} else {
			// Unable to fork process.
			return false;
		}
	}
}