<?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\CharsetInterface;
use 
Zend\Console\Exception;
use 
Zend\Console\Charset;

/**
 * MS Windows with ANSICON console adapter
 *
 * This adapter requires ANSICON extension to be installed. It can be obtained from:
 *      https://github.com/adoxa/ansicon
 *
 * ANSICON has to be loaded and enabled before using this adapter. It's best to install
 * it using following command:
 *      ansicon -I
 *
 * Console should not run in UTF8 code page (65001), because ANSICON does not behave well with it.
 * It's best to use non-unicode code page 437, 850, 851, 852 or similar. Run "help mode" for more
 * information on how to change Windows console code page.
 */
class WindowsAnsicon extends Posix
{
    
/**
     * Whether or not mbstring is enabled
     *
     * @var null|bool
     */
    
protected static $hasMBString;

    
/**
     * 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 ANSICON env var
        
if (preg_match('/\((\d+)x/'getenv('ANSICON'), $matches)) {
            
$width $matches[1];
        } else {
            
$width AbstractAdapter::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 ANSICON env var
        
if (preg_match('/\(\d+x(\d+)/'getenv('ANSICON'), $matches)) {
            
$height $matches[1];
        } else {
            
$height AbstractAdapter::getHeight();
        }
        return 
$height;
    }

    
/**
     * 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");
    }

    
/**
     * Clear console screen
     */
    
public function clear()
    {
        echo 
chr(27) . '[1J' chr(27) . '[u';
    }

    
/**
     * Clear line at cursor position
     */
    
public function clearLine()
    {
        echo 
chr(27) . '[1K';
    }

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

    
/**
     * Get charset currently in use by this adapter.
     *

     * @return 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();
    }

    
/**
     * Read a single character from the console input
     *
     * @param  string|null $mask A list of allowed chars
     * @return string
     * @throws Exception\RuntimeException
     */
    
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 98+ "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 || $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 11);
            } 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))) {
                
// We have obtained an ASCII code, check if it is a carriage
                // return and normalize it as needed
                
if ($char == 13) {
                    
$char 10;
                }

                
// Convert to a character
                
$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), 01);
        } while (!
$char || ($mask !== null && !stristr($mask$char)));
        
fclose($stream);

        return 
$char;
    }
}