<?php
namespace nur\sery\os;

use nur\sery\cl;
use nur\sery\ExitError;
use nur\sery\StateException;
use nur\sery\app;

class sh {
  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)) {
      $parts = [];
      if (cl::is_list($value)) {
        foreach ($value as $part) {
          $parts[] = self::_quote(strval($part));
        }
      } else {
        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 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 verifix_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;
  }

  static function _waitpid(int $pid, ?int &$retcode=null): bool {
    pcntl_waitpid($pid, $status);
    if (pcntl_wifexited($status)) $retcode = pcntl_wexitstatus($status);
    elseif (pcntl_wifsignaled($status)) $retcode = -pcntl_wtermsig($status);
    else $retcode = app::EC_FORK_CHILD;
    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 $wait=true): bool {
    $pid = pcntl_fork();
    if ($pid == -1) {
      // parent, impossible de forker
      throw new ExitError(app::EC_FORK_PARENT, "unable to fork");
    } elseif ($pid) {
      // parent, fork ok
      if ($wait) return self::_waitpid($pid, $retcode);
      $retcode = null;
      return true;
    }
    // child, fork ok
    pcntl_exec("/bin/sh", ["-c", $cmd]);
    throw StateException::unexpected_state();
  }

  /**
   * Corriger la commande spécifiée avec {@link verifix_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::verifix_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::verifix_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::verifix_cmd($cmd, $redir);
    return self::_exec($cmd, $output, $retcode);
  }

  /**
   * Corriger la commande spécifiée avec {@link verifix_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::verifix_cmd($cmd, $redir);
    return self::_fork_exec("exec $cmd", $retcode);
  }

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

  /** retourner le répertoire $HOME */
  static final function homedir(): string {
    $homedir = getenv("HOME");
    if ($homedir === false) {
      $homedir = posix_getpwuid(posix_getuid())["dir"];
    }
    return path::abspath($homedir);
  }

  /** s'assurer que le répertoire $dir existe */
  static final function mkdirp(string $dir): bool {
    if (is_dir($dir)) return true;
    return mkdir($dir, 0777, true);
  }

  /** créer le répertoire qui va contenir le fichier $file */
  static final function mkdirof(string $file): bool {
    if (file_exists($file)) return true;
    $dir = path::dirname($file);
    if (file_exists($dir)) return true;
    return mkdir($dir, 0777, true);
  }

  /**
   * créer un répertoire avec un nom unique. ce répertoire doit être supprimé
   * manuellement quand il n'est plus utilisé.
   *
   * @return string le chemin du répertoire
   * @throws IOException si une erreur se produit (impossible de créer un
   * répertoire unique après 2560 essais)
   */
  static final function mktempdir(?string $prefix=null, ?string $basedir=null): string {
    if ($basedir === null) $basedir = sys_get_temp_dir();
    if ($prefix !== null) $prefix .= "-";
    $max = 2560;
    do {
      $dir = "$basedir/$prefix".uniqid();
      $r = @mkdir($dir);
      $max--;
    } while ($r === false && $max > 0);
    if ($r === false) {
      throw IOException::last_error("$dir: unable to create directory");
    }
    return $dir;
  }

  /**
   * Supprimer un répertoire créé avec mktempdir
   *
   * un minimum de vérification est effectué qu'il s'agit bien d'un répertoire
   * généré par mktempdir
   */
  static final function rmtempdir(string $tmpdir, ?string $prefix=null, ?string $basedir=null): void {
    if ($basedir === null) $basedir = sys_get_temp_dir();
    if ($prefix !== null) $prefix .= "-";
    // 13 '?' parce que c'est la taille d'une chaine générée par uniqid()
    if (fnmatch("$basedir/$prefix?????????????", $tmpdir)) {
      self::exec(["rm", "-rf", $tmpdir]);
    } else {
      throw new IOException("$tmpdir: n'est pas un répertoire temporaire");
    }
  }

  /**
   * supprimer tous les répertoires temporaires qui ont été créés avec le
   * suffixe spécifié dans le répertoire $basedir
   */
  static final function cleantempdirs(string $prefix, ?string $basedir=null): void {
    if ($basedir === null) $basedir = sys_get_temp_dir();
    $prefix .= "-";
    // 13 '?' parce que c'est la taille d'une chaine générée par uniqid()
    $tmpdirs = glob("$basedir/$prefix?????????????", GLOB_ONLYDIR);
    if ($tmpdirs) {
      self::exec(["rm", "-rf", ...$tmpdirs]);
    }
  }

  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);
  }
}
