<?php

namespace EesyPHP;

use Exception;

class Cli {

  /**
   * EesyPHP core mode
   */
  private static $core_mode = false;

  /**
   * Initialize
   * @return void
   */
  public static function init() {
    Hook :: register('cli_set_core_mode', array('\\EesyPHP\\Cli', 'on_cli_set_core_mode'));
  }

  /**
   * Get/set core mode
   * @return bool
   */
  public static function core_mode($enable=null) {
    if (!is_null($enable)) {
      self :: $core_mode = boolval($enable);
      Hook :: trigger('cli_set_core_mode', array('enabled' => self :: $core_mode));
    }
    return self :: $core_mode;
  }

  /**
   * On CLI set core mode hook
   * @param \EesyPHP\HookEvent $event
   * @return void
   */
  public static function on_cli_set_core_mode($event) {
    if ($event->enabled) {
      self :: add_command(
        'new_project',
        array('\\EesyPHP\\Cli', 'cli_new_project'),
        ___("Create a new project using EesyPHP framework"),
        null,
        ___(
          "This command could be used to easily build the structure of a new project using the ".
          "EesyPHP framework.")
      );
    }
    else {
      self :: add_command(
        'serve',
        array('\\EesyPHP\\Cli', 'cli_serve'),
        ___("Start the PHP built-in HTTP server to serve the application"),
        '[-h] [-P -O path] [[A.B.C.D][:port]] [-p http://public.url]',
        ___(
"This command could be used to start the PHP built-in HTTP server to serve
the application.

Additionnal parameters:
  -p/--public-url              Define the public URL (default: based on listen address)
  -P/--enable-profiler         Enable Xdebug profiler
  -O/--profiler-output [path]  Xdebug profiler output directory path"
        )
      );
    }
  }

  /**
   * Registered commands
   * @var array<string,array>
   */
  protected static $commands = array();


  /**
   * Current CLI command
   * @var string|null
   */
  protected static $command = null;

  /**
   * Add CLI command
   * @param string $command The command name
   * @param callable $handler The command handler
   * @param string $short_desc Short command description
   * @param string|null $usage_args Argument usage short message
   * @param string|array<string>|null $long_desc Long command description
   * @return bool
   */
  public static function add_command($command, $handler, $short_desc, $usage_args=null, $long_desc=null,
                       $override=false) {
    if (array_key_exists($command, self :: $commands) && !$override) {
      Log :: error(I18n::_("The CLI command '%s' already exists."), $command);
      return False;
    }

    if (!is_callable($handler)) {
      Log :: error(I18n::_("The CLI command '%s' handler is not callable !"), $command);
      return False;
    }

    self :: $commands[$command] = array (
      'handler' => $handler,
      'short_desc' => $short_desc,
      'usage_args' => $usage_args,
      'long_desc' => $long_desc,
    );
    return True;
  }

  /**
   * Show usage message
   * @param string|false $error Error message to show (optional)
   * @param array $extra_args Extra arguments to use to compute error message using sprintf
   * @return never
   */
  public static function usage($error=false, ...$extra_args) {
    global $argv;

    // If extra arguments passed, format error message using sprintf
    if ($extra_args) {
      $error = call_user_func_array(
        'sprintf',
        array_merge(array($error), $extra_args)
      );
    }

    if ($error)
      echo "$error\n\n";
    echo(I18n::_("Usage: %s [-h] [-qd] command\n", basename($argv[0])));
    echo I18n::_("  -h                Show this message\n");
    echo I18n::_("  -q / -d           Quiet/Debug mode\n");
    echo I18n::_("  --trace           Trace mode (the most verbose)\n");
    echo I18n::_("  -l / --log-file   Overwrite log file specified in configuration\n");
    echo I18n::_("  -C / --console    Enable log on console\n");
    echo I18n::_("  command           Command to run\n");
    echo "\n";
    echo I18n::_("Available commands:\n");

    foreach (self :: $commands as $command => $info) {
      if (self :: $command && $command != self :: $command)
        continue;
      echo (
        "  ".str_replace(
          "\n", "\n    ",
          wordwrap("$command : ".I18n::_($info['short_desc'])))
        ."\n\n");
      echo (
        "    ".basename($argv[0])." $command ".
        ($info['usage_args']?I18n::_($info['usage_args']):'').
        "\n");
      if ($info['long_desc']) {
        if (is_array($info['long_desc'])) {
          $lines = array();
          foreach ($info['long_desc'] as $line)
            $lines[] = I18n::_($line);
          $info['long_desc'] = implode("\n", $lines);
        }
        else
          $info['long_desc'] = I18n::_($info['long_desc']);

        echo "\n    ".str_replace("\n", "\n    ", wordwrap($info['long_desc']))."\n";
      }
      echo "\n";
    }

    exit(($error?1:0));
  }

  /**
   * Handle command line arguments
   * @param array|null $args Command line argurment to handle (optional, default: $argv)
   * @param bool|null $core_mode Force enable/disable EesyPHP core mode (optional, default: false)
   * @return never
   */
  public static function handle_args($args=null, $core_mode=false) {
    global $argv;
    self :: core_mode($core_mode);
    Log :: register_fatal_error_handler(array('\\EesyPHP\\Cli', 'fatal_error'));
    $args = is_array($args)?$args:array_slice($argv, 1);
    $log_level_set = false;
    self :: $command = null;
    $command_args = array();
    for ($i=0; $i < count($args); $i++) {
      if (array_key_exists($args[$i], self :: $commands)) {
        if (!self :: $command)
          self :: $command = $args[$i];
        else
          self :: usage(I18n::_("Only one command could be executed !"));
      }
      else {
        switch($args[$i]) {
          case '-h':
          case '--help':
            self :: usage();
          case '-d':
          case '--debug':
            Log :: set_level('DEBUG');
            $log_level_set = true;
            break;
          case '-q':
          case '--quiet':
            Log :: set_level('WARNING');
            $log_level_set = true;
            break;
          case '--trace':
            Log :: set_level('TRACE');
            $log_level_set = true;
            break;
          case '-l':
          case '--log-file':
            $i++;
            Log :: change_filepath($args[$i]);
            break;
          case '-C':
          case '--console':
            App :: set('log.cli_console', true);
            break;
          default:
            if (self :: $command)
              $command_args[] = $args[$i];
            else
              self :: usage(
                I18n::_(
                  "Invalid parameter \"%s\".\nNote: Command's parameter/argument must be place ".
                  "after the command."
                ), $args[$i]
              );
        }
      }
    }

    if (!$log_level_set)
      Log :: set_level('INFO');

    if (!self :: $command)
      self :: usage();

    Log :: debug(
      "Run %s command %s with argument(s) '%s'.",
      basename($args[0]), self :: $command, implode("', '", $command_args)
    );

    try {
      $result = call_user_func(self :: $commands[self :: $command]['handler'], $command_args);

      exit($result?0:1);
    }
    catch(Exception $e) {
      Log :: exception($e, I18n::_("An exception occured running command %s"), self :: $command);
      exit(1);
    }
  }

  /**
   * Command to create new project based on EesyPHP framework
   *
   * @param  array $command_args  The command arguments
   * @return true
   */
  public static function cli_new_project($command_args) {
    echo "This CLI tool permit to initialize a new project using the EesyPHP framework.\n";
    readline('[Press enter to continue]');
    echo "\n";

    $root_path = null;
    while (!$root_path) {
      $root_path = getcwd();
      $input = readline("Please enter the root directory of your new project [$root_path]: ");
      if (empty($input))
        break;
      if (!is_dir($input))
        self::fatal_error("Invalid root directory specified: not found or is not a directory");
      else if (!is_writeable($input))
        self::fatal_error("Invalid root directory specified: not writeable");
      else
        $root_path = $input;
    }
    $root_path = rtrim($root_path, "/");
    $skel = __DIR__ . "/../skel";

    foreach (
      $iterator = new \RecursiveIteratorIterator(
        new \RecursiveDirectoryIterator($skel, \RecursiveDirectoryIterator::SKIP_DOTS),
        \RecursiveIteratorIterator::SELF_FIRST
      ) as $item
    ) {
      $path = "$root_path/".$iterator->getSubPathname();
      if ($item->isDir()) {
        if (!mkdir($path))
          self::fatal_error("Fail to create $path directory");
      }
      else {
        if (!copy($item, $path))
          self::fatal_error("Fail to copy file to $path");
      }
      if (!chmod($path, $item->getPerms()))
        self::fatal_error("Fail to chmod file $path");
    }
    echo "done. Start coding!\n";
    return true;
  }

  /**
   * Command to start PHP built-in HTTP server to serve the EesyPHP project
   *
   * @param  array $command_args  The command arguments
   * @return never
   */
  public static function cli_serve($command_args) {
    $listen_address = null;
    $public_url = null;
    $enable_profiler = false;
    $profiler_output_dir = realpath(getcwd());
    for($i=0; $i < count($command_args); $i++) {
      switch($command_args[$i]) {
        case '-p':
        case '--public-url':
          $i++;
          $public_url = $command_args[$i];
          break;
        case '-P':
        case '--enable-profiler':
          if (phpversion('xdebug') === false)
            self :: usage(I18n::_("The PHP XDEBUG extension is missing."));
          $enable_profiler = true;
          break;
        case '-O':
        case '--profiler-output':
          $i++;
          $profiler_output_dir = $command_args[$i];
          if (!is_dir($profiler_output_dir))
            self :: usage(
              I18n::_('Invalid profiler output directory "%s": not found'),
              $profiler_output_dir
            );
          if (!is_writable($profiler_output_dir))
            self :: usage(
              I18n::_('Invalid profiler output directory "%s": not writeable'),
              $profiler_output_dir
            );
          break;
        default:
          if (is_null($listen_address)) {
            $listen_address = $command_args[$i];
          }
          else
            self :: usage(I18n::_('Invalid parameter "%s"', $command_args[$i]));
          break;
      }
    }

    // Check listen address
    if (is_null($listen_address))
      $listen_address = '127.0.0.1:8000';
    $parts = explode(':', $listen_address);
    if (count($parts) > 2)
      self :: usage(
        I18n::_('Invalid listen address specify. Must be in format host:port, host or :port.')
      );

    if (empty($parts[0])) {
      $parts[0] = '0.0.0.0';
    }
    else if (!Check::ip_address($parts[0]))
      self :: usage(
        I18n::_('Invalid listen host specified. Must be an IPv4 or IPv6 address.')
      );

    if (count($parts) < 2)
      $parts[1] = 8000;
    else if (!Check::tcp_or_udp_port($parts[1]))
      self :: usage(
        I18n::_('Invalid listen port specified. Must be a positive integer between 1 and 65535.')
      );
    $listen_address = implode(':', $parts);
    if (is_null($public_url))
      $public_url = sprintf("http://%s:%s", $parts[0]=='0.0.0.0'?'127.0.0.1':$parts[0], $parts[1]);

    $public_html = App::get('root_directory_path')."/public_html";
    chdir($public_html) or self :: fatal_error(
      'Fail to enter in the public_html directory of the application (%s).',
      $public_html
    );
    $args = array(
      "-S", $listen_address,
    );
    if ($enable_profiler) {
      $args = array_merge(
        $args,
        array(
          "-d", "xdebug.mode=profile",
          "-d", "xdebug.profiler_enable=On",
          "-d", "xdebug.profiler_output_dir=$profiler_output_dir",
        )
      );
    }
    passthru(
      "EESYPHP_SERVE_URL=$public_url ".PHP_BINARY." ".
      implode(' ', array_map('escapeshellarg', $args))." index.php",
      $exit_code
    );
    exit($exit_code);
  }

  /**
   * Helper method to ask user to enter a password
   * Note: this method use the bash binary and a fatal error will be trigger if it's not available.
   * @param string|null $prompt The prompt message (optional)
   * @return string
   */
  public static function prompt_for_password($prompt=null) {
    // Check bash is available
  	$command = "/usr/bin/env bash -c 'echo OK'";
  	if (rtrim(shell_exec($command)) !== 'OK')
      Log::fatal(I18n::_("Can't invoke bash. Can't ask password prompt."));

  	$command = "/usr/bin/env bash -c 'read -s -p \"";
  	$command .= addslashes($prompt?_($prompt):I18n::_("Please enter password:"));
  	$command .= "\" mypassword && echo \$mypassword'";
  	$password = rtrim(shell_exec($command));
  	echo "\n";
  	return $password;
  }

  /**
   * Handle a fatal error in a CLI command
   * @param string $error The error message
   * @param array $extra_args Extra arguments to use to compute the error message using sprintf
   * @return never
   */
  public static function fatal_error($error, ...$extra_args) {
    // If extra arguments passed, format error message using sprintf
    if ($extra_args) {
      $error = call_user_func_array(
        'sprintf',
        array_merge(array($error), $extra_args)
      );
    }

    fwrite(STDERR, "FATAL ERROR : $error\n");
    exit(1);
  }

}
