<?php
/**
 * Zend Framework (http://framework.zend.com/)
 *
 * @link      http://github.com/zendframework/zf2 for the canonical source repository
 * @copyright Copyright (c) 2005-2013 Zend Technologies USA Inc. (http://www.zend.com)
 * @license   http://framework.zend.com/license/new-bsd New BSD License
 */

namespace Zend\Console\Adapter;

use 
Zend\Console\Charset;
use 
Zend\Console\Exception;

class 
Windows extends Virtual
{
    
/**
     * Whether or not mbstring is enabled
     *
     * @var null|bool
     */
    
protected static $hasMBString;

    
/**
     * Results of probing system capabilities
     *
     * @var mixed
     */
    
protected $probeResult;

    
/**
     * Results of mode command
     *
     * @var mixed
     */
    
protected $modeResult;

    
/**
     * Determine and return current console width.
     *
     * @return int
     */
    
public function getWidth()
    {
        static 
$width;
        if (
$width > 0) {
            return 
$width;
        }

        
// Try to read console size from "mode" command
        
if ($this->probeResult === null) {
            
$this->runProbeCommand();
        }

        if (
count($this->probeResult) && (int) $this->probeResult[0]) {
            
$width = (int) $this->probeResult[0];
        } else {
            
$width = parent::getWidth();
        }

        return 
$width;
    }

    
/**
     * Determine and return current console height.
     *
     * @return false|int
     */
    
public function getHeight()
    {
        static 
$height;
        if (
$height > 0) {
            return 
$height;
        }

        
// Try to read console size from "mode" command
        
if ($this->probeResult === null) {
            
$this->runProbeCommand();
        }

        if (
count($this->probeResult) && (int) $this->probeResult[1]) {
            
$height = (int) $this->probeResult[1];
        } else {
            
$height = parent::getheight();
        }

        return 
$height;
    }

    
/**
     * Probe for system capabilities and cache results
     *
     * Run a Windows Powershell command that determines parameters of console window. The command is fed through
     * standard input (with echo) to prevent Powershell from creating a sub-thread and hanging PHP when run through
     * a debugger/IDE.
     *
     * @return void
     */
    
protected function runProbeCommand()
    {
        
exec(
            
'echo $size = $Host.ui.rawui.windowsize; write $($size.width) $($size.height) | powershell -NonInteractive -NoProfile -NoLogo -OutputFormat Text -Command -',
            
$output,
            
$return
        
);
        if (
$return || empty($output)) {
            
$this->probeResult = '';
        } else {
            
$this->probeResult = $output;
        }
    }

    
/**
     * Run and cache results of mode command
     *
     * @return void
     */
    
protected function runModeCommand()
    {
        
exec('mode', $output, $return);
        if (
$return || !count($output)) {
            
$this->modeResult = '';
        } else {
            
$this->modeResult = trim(implode('', $output));
        }
    }

    
/**
     * Check if console is UTF-8 compatible
     *
     * @return bool
     */
    
public function isUtf8()
    {
        
// Try to read code page info from "mode" command
        
if ($this->modeResult === null) {
            
$this->runModeCommand();
        }

        if (
preg_match('/Code page\:\s+(\d+)/', $this->modeResult, $matches)) {
            return (int) 
$matches[1] == 65001;
        }

        return 
false;
    }

    
/**
     * Return current console window title.
     *
     * @return string
     */
    
public function getTitle()
    {
        
// Try to use powershell to retrieve console window title
        
exec('powershell -command "write $Host.UI.RawUI.WindowTitle"', $output, $result);
        if (
$result || !$output) {
            return 
'';
        }

        return 
trim($output, "\r\n");
    }

    
/**
     * Set Console charset to use.
     *
     * @param Charset\CharsetInterface $charset
     */
    
public function setCharset(Charset\CharsetInterface $charset)
    {
        
$this->charset = $charset;
    }

    
/**
     * Get charset currently in use by this adapter.
     *
     * @return Charset\CharsetInterface $charset
     */
    
public function getCharset()
    {
        if (
$this->charset === null) {
            
$this->charset = $this->getDefaultCharset();
        }

        return 
$this->charset;
    }

    
/**
     * @return Charset\AsciiExtended
     */
    
public function getDefaultCharset()
    {
        return new 
Charset\AsciiExtended;
    }

    
/**
     * Switch to utf-8 encoding
     *
     * @return void
     */
    
protected function switchToUtf8()
    {
        
shell_exec('mode con cp select=65001');
    }

    
/**
     * Clear console screen
     */
    
public function clear()
    {
        
// Attempt to clear the screen using PowerShell command
        
exec("powershell -NonInteractive -NoProfile -NoLogo -OutputFormat Text -Command Clear-Host", $output, $return);

        if (
$return) {
            
// Could not run powershell... fall back to filling the buffer with newlines
            
echo str_repeat("\r\n", $this->getHeight());
        }
    }

    
/**
     * Clear line at cursor position
     */
    
public function clearLine()
    {
        echo 
"\r" . str_repeat(' ', $this->getWidth()) . "\r";
    }

    
/**
     * Read a single character from the console input
     *
     * @param  string|null $mask A list of allowed chars
     * @throws Exception\RuntimeException
     * @return string
     */
    
public function readChar($mask = null)
    {
        
// Decide if we can use `choice` tool
        
$useChoice = $mask !== null && preg_match('/^[a-zA-Z0-9]+$/D', $mask);

        if (
$useChoice) {
            
// Use Windows 95+ "choice" command, which allows for reading a
            // single character matching a mask, but is limited to lower ASCII
            // range.
            
do {
                
exec('choice /n /cs /c:' . $mask, $output, $return);
                if (
$return == 255 || $return < 1 || $return > strlen($mask)) {
                    throw new 
Exception\RuntimeException('"choice" command failed to run. Are you using Windows XP or newer?');
                }

                
// Fetch the char from mask
                
$char = substr($mask, $return - 1, 1);
            } while (
"" === $char || ($mask !== null && false === strstr($mask, $char)));

            return 
$char;
        }

        
// Try to use PowerShell, giving it console access. Because PowersShell
        // interpreter can take a short while to load, we are emptying the
        // whole keyboard buffer and picking the last key that has been pressed
        // before or after PowerShell command has started. The ASCII code for
        // that key is then converted to a character.
        
if ($mask === null) {
            
exec(
                
'powershell -NonInteractive -NoProfile -NoLogo -OutputFormat Text -Command "'
                    
. 'while ($Host.UI.RawUI.KeyAvailable) {$key = $Host.UI.RawUI.ReadKey(\'NoEcho,IncludeKeyDown\');}'
                    
. 'write $key.VirtualKeyCode;'
                    
. '"',
                
$result,
                
$return
            
);

            
// Retrieve char from the result.
            
$char = !empty($result) ? implode('', $result) : null;

            if (!empty(
$char) && !$return) {
                
// We have obtained an ASCII code, convert back to a char ...
                
$char = chr($char);

                
// ... and return it...
                
return $char;
            }
        } else {
            
// Windows and DOS will return carriage-return char (ASCII 13) when
            // the user presses [ENTER] key, but Console Adapter user might
            // have provided a \n Newline (ASCII 10) in the mask, to allow [ENTER].
            // We are going to replace all CR with NL to conform.
            
$mask = strtr($mask, "\n", "\r");

            
// Prepare a list of ASCII codes from mask chars
            
$asciiMask = array_map(function ($char) {
                return 
ord($char);
            }, 
str_split($mask));
            
$asciiMask = array_unique($asciiMask);

            
// Char mask filtering is now handled by the PowerShell itself,
            // because it's a much faster method than invoking PS interpreter
            // after each mismatch. The command should return ASCII code of a
            // matching key.
            
$result = $return = null;

            
exec(
                
'powershell -NonInteractive -NoProfile -NoLogo -OutputFormat Text -Command "'
                    
. '[int[]] $mask = ' . join(',', $asciiMask) . ';'
                    
. 'do {'
                        
. '$key = $Host.UI.RawUI.ReadKey(\'NoEcho,IncludeKeyDown\').VirtualKeyCode;'
                    
. '} while( !($mask -contains $key) );'
                    
. 'write $key;'
                    
. '"',
                
$result,
                
$return
            
);

            
$char = !empty($result) ? trim(implode('', $result)) : null;

            if (!
$return && $char && ($mask === null || in_array($char, $asciiMask))) {
                
// Normalize CR to LF
                
if ($char == 13) {
                    
$char = 10;
                }

                
// Convert to a char
                
$char = chr($char);

                
// ... and return it...
                
return $char;
            }
        }

        
// Fall back to standard input, which on Windows does not allow reading
        // a single character. This is a limitation of Windows streams
        // implementation (not PHP) and this behavior cannot be changed with a
        // command like "stty", known to POSIX systems.
        
$stream = fopen('php://stdin', 'rb');
        do {
            
$char = fgetc($stream);
            
$char = substr(trim($char), 0, 1);
        } while (!
$char || ($mask !== null && !stristr($mask, $char)));
        
fclose($stream);

        return 
$char;
    }

    
/**
     * Read a single line from the console input.
     *
     * @param  int $maxLength Maximum response length
     * @return string
     */
    
public function readLine($maxLength = 2048)
    {
        
$f    = fopen('php://stdin','r');
        
$line = rtrim(fread($f, $maxLength),"\r\n");
        
fclose($f);

        return 
$line;
    }
}