<?php
namespace nur;

use nur\b\IllegalAccessException;

/**
 * Class shell: outils pour gérer l'interaction avec le shell unix
 */
class shell {
  static final function _quote(string $value): string {
    if (preg_match('/^[a-zA-Z0-9_.@:,\/+-]+$/', $value)) return $value;
    return escapeshellarg($value);
  }

  /**
   * obtenir la valeur $value quotée pour le shell
   *
   * si c'est une valeur scalaire, elle sera quotée sous la forme 'value'
   * si c'est un tableau séquentiel, elle sera quoté sous la forme ('value'...)
   * sinon, elle sera quotée sour la forme ([key]=value...)
   */
  static final function quote($value): string {
    if (is_array($value)) {
      if (A::is_seq($value)) {
        $parts = [];
        foreach ($value as $part) {
          $parts[] = self::_quote(strval($part));
        }
        return "(".implode(" ", $parts).")";
      } else {
        $parts = [];
        foreach ($value as $key => $part) {
          $key = self::_quote($key);
          $val = self::_quote($part);
          $parts[] = "[$key]=$val";
        }
        return "(".implode(" ", $parts).")";
      }
    } else {
      return self::_quote(strval($value));
    }
  }

  /**
   * obtenir une commande shell à partir du tableau des arguments.
   * à utiliser avec exec()
   */
  static final function join(array $parts): string {
    $count = count($parts);
    for($i = 0; $i < $count; $i++) {
      $parts[$i] = self::_quote(strval($parts[$i]));
    }
    return implode(" ", $parts);
  }

  private static final function add_redir(string &$cmd, ?string $redir, ?string $input, ?string $output): void {
    if ($redir !== null) {
      switch ($redir) {
      case "outonly":
      case "noerr":
        $redir = "2>/dev/null";
        break;
      case "erronly":
      case "noout":
        $redir = "2>&1 >/dev/null";
        break;
      case "both":
      case "err2out":
        $redir = "2>&1";
        break;
      case "none":
      case "null":
        $redir = ">/dev/null 2>&1";
        break;
      case "default":
        $redir = null;
        break;
      }
    }
    if ($input !== null) {
      $redir = $redir !== null? "$redir ": "";
      $redir .= "<".escapeshellarg($input); 
    }
    if ($output !== null) {
      $redir = $redir !== null? "$redir ": "";
      $redir .= ">".escapeshellarg($output);
    }
    if ($redir !== null) $cmd .= " $redir";
  }

  /**
   * Corriger la commande $cmd:
   * - si c'est tableau, joindre les arguments avec {@link join()}
   * - sinon, mettre les caractères en échappement avec {@link escapeshellarg()}
   */
  static final function fix_cmd(&$cmd, ?string $redir=null, ?string $input=null, ?string $output=null): void {
    if (is_array($cmd)) $cmd = self::join($cmd);
    else $cmd = escapeshellcmd(strval($cmd));
    self::add_redir($cmd, $redir, $input, $output);
  }

  /**
   * Lancer la commande spécifiée avec passthru() et retourner le code de retour
   * dans la variable $retcode. $cmd doit déjà être formaté comme il convient
   *
   * voici la différence entre system(), passthru() et exec()
   * +----------------+-----------------+----------------+----------------+
   * |    Command     | Displays Output | Can Get Output | Gets Exit Code |
   * +----------------+-----------------+----------------+----------------+
   * | passthru()     | Yes (raw)       | No             | Yes            |
   * | system()       | Yes (as text)   | Last line only | Yes            |
   * | exec()         | No              | Yes (array)    | Yes            |
   * +----------------+-----------------+----------------+----------------+
   *
   * @return bool true si la commande s'est lancée sans erreur, false sinon
   */
  static final function _passthru(string $cmd, int &$retcode=null): bool {
    passthru($cmd, $retcode);
    return $retcode == 0;
  }

  /**
   * Comme {@link _passthru()} mais lancer la commande spécifiée avec system().
   * cf la doc de {@link _passthru()} pour les autres détails
   */
  static final function _system(string $cmd, string &$output=null, int &$retcode=null): bool {
    $last_line = system($cmd, $retcode);
    if ($last_line !== false) $output = $last_line;
    return $retcode == 0;
  }

  /**
   * Comme {@link _passthru()} mais lancer la commande spécifiée avec exec().
   * cf la doc de {@link _passthru()} pour les autres détails
   */
  static final function _exec(string $cmd, array &$output=null, int &$retcode=null): bool {
    exec($cmd, $output, $retcode);
    return $retcode == 0;
  }

  /**
   * Lancer la commande $cmd dans un processus fils via un shell et attendre la
   * fin de son exécution.
   *
   * $cmd doit déjà être formaté comme il convient
   */
  static final function _fork_exec(string $cmd, int &$retcode=null): bool {
    $pid = pcntl_fork();
    if ($pid == -1) {
      // parent, impossible de forker
      throw new IllegalAccessException("unable to fork");
    } elseif ($pid) {
      // parent, fork ok
      pcntl_waitpid($pid, $status);
      if (pcntl_wifexited($status)) {
        $retcode = pcntl_wexitstatus($status);
      } else {
        $retcode = 127;
      }
      return $retcode == 0;
    }
    // child, fork ok
    pcntl_exec("/bin/sh", ["-c", $cmd]);
    return false;
  }

  /**
   * Corriger la commande spécifiée avec {@link fix_cmd()} puis la lancer
   * avec passthru() et retourner le code de retour dans la variable $retcode
   *
   * $redir spécifie le type de redirection demandée:
   * - "default" | null: $output reçoit STDOUT et STDERR n'est pas redirigé
   * - "outonly" | "noerr": $output ne reçoit que STDOUT et STDERR est perdu
   * - "erronly" | "noout": $output ne reçoit que STDERR et STDOUT est perdu
   * - "both" | "err2out": $output reçoit STDOUT et STDERR
   * - "none" | "null": STDOUT et STDERR sont perdus
   * - sinon c'est une redirection spécifique, et la valeur est rajoutée telle
   * quelle à la ligne de commande
   *
   * @return bool true si la commande s'est lancée sans erreur, false sinon
   */
  static final function passthru($cmd, int &$retcode=null, ?string $redir=null): bool {
    self::fix_cmd($cmd, $redir);
    return self::_passthru($cmd, $retcode);
  }

  /**
   * Comme {@link passthru()} mais lancer la commande spécifiée avec system().
   * Cf la doc de {@link passthru()} pour les autres détails
   */
  static final function system($cmd, string &$output=null, int &$retcode=null, ?string $redir=null): bool {
    self::fix_cmd($cmd, $redir);
    return self::_system($cmd, $output, $retcode);
  }

  /**
   * Comme {@link passthru()} mais lancer la commande spécifiée avec exec().
   * Cf la doc de {@link passthru()} pour les autres détails
   */
  static final function exec($cmd, array &$output=null, int &$retcode=null, ?string $redir=null): bool {
    self::fix_cmd($cmd, $redir);
    return self::_exec($cmd, $output, $retcode);
  }

  /**
   * Corriger la commande spécifiée avec {@link fix_cmd()}, la préfixer de
   * "exec" puis la lancer avec {@link _fork_exec()}
   */
  static final function fork_exec($cmd, int &$retcode=null, ?string $redir=null): bool {
    self::fix_cmd($cmd, $redir);
    return self::_fork_exec("exec $cmd", $retcode);
  }

  #############################################################################

  static final function is_diff_file(string $f, string $g): bool {
    if (!is_file($f) || !is_file($g)) return true;
    self::exec(array("diff", "-q", $f, $g), $output, $retcode);
    return $retcode !== 0;
  }

  static final function is_same_file(string $f, string $g): bool {
    if (!is_file($f) || !is_file($g)) return false;
    self::exec(array("diff", "-q", $f, $g), $output, $retcode);
    return $retcode === 0;
  }

  static final function is_diff_link(string $f, string $g): bool {
    if (!is_link($f) || !is_link($g)) return true;
    return @readlink($f) !== @readlink($g);
  }

  static final function is_same_link(string $f, string $g): bool {
    if (!is_link($f) || !is_link($g)) return false;
    return @readlink($f) === @readlink($g);
  }
}