<?php
namespace nur\sery\os\proc;

use nur\sery\A;
use nur\sery\cv;
use nur\sery\os\sh;

abstract class AbstractCmd implements ICmd {
  private bool $needsStdin;

  private bool $needsTty;

  protected ?array $sources;

  protected ?array $vars;

  protected array $cmds;

  function __construct() {
    $this->needsStdin = true;
    $this->needsTty = true;
    $this->sources = null;
    $this->vars = null;
    $this->cmds = [];
  }

  function then($cmd, ?string $input=null, ?string $output=null): Cmd {
    if ($this instanceof Cmd) {
      $this->add($cmd, $input, $output);
      return $this;
    } else {
      return (new Cmd($this))->add($cmd, $input, $output);
    }
  }

  function or($cmd, ?string $input=null, ?string $output=null): CmdOr {
    if ($this instanceof CmdOr) {
      $this->add($cmd, $input, $output);
      return $this;
    } else {
      return (new CmdOr($this))->add($cmd, $input, $output);
    }
  }

  function and($cmd, ?string $input=null, ?string $output=null): CmdAnd {
    if ($this instanceof CmdAnd) {
      $this->add($cmd, $input, $output);
      return $this;
    } else {
      return (new CmdAnd($this))->add($cmd, $input, $output);
    }
  }

  function pipe($cmd): CmdPipe {
    if ($this instanceof CmdPipe) {
      $this->add($cmd);
      return $this;
    } else {
      return new CmdPipe([$this, $cmd]);
    }
  }

  function isNeedsStdin(): bool {
    return $this->needsStdin;
  }

  function setNeedsStdin(bool $needsStdin): void {
    $this->needsStdin = $needsStdin;
  }

  function isNeedsTty(): bool {
    return $this->needsTty;
  }

  function setNeedsTty(bool $needsTty): void {
    $this->needsTty = $needsTty;
  }

  function addSource(?string $source, bool $onlyIfExists=true): void {
    if ($source === null) return;
    if (!$onlyIfExists || file_exists($source)) {
      $source = implode(" ", [".", sh::quote($source)]);
      $this->sources[] = $source;
    }
  }

  function getSources(?string $sep=null): ?string {
    if ($this->sources === null) return null;
    if ($sep === null) $sep = "\n";
    return implode($sep, $this->sources);
  }

  function addLiteralVars($vars, ?string $sep=null): void {
    if (cv::z($vars)) return;
    if (is_array($vars)) {
      if ($sep === null) $sep = "\n";
      $vars = implode($sep, $vars);
    }
    $this->vars[] = strval($vars);
  }

  function addVars(?array $vars): void {
    if ($vars === null) return;
    foreach ($vars as $name => $value) {
      $var = [];
      if (!is_array($value)) $var[] = "export ";
      A::merge($var, [$name, "=", sh::quote($value)]);
      $this->vars[] = implode("", $var);
    }
  }

  function getVars(?string $sep=null): ?string {
    if ($this->vars === null) return null;
    if ($sep === null) $sep = "\n";
    return implode($sep, $this->vars);
  }

  function addPrefix($prefix): void {
    $count = count($this->cmds);
    if ($count == 0) return;
    $cmd =& $this->cmds[$count - 1];
    if ($cmd instanceof ICmd) {
      $cmd->addPrefix($prefix);
    } elseif (is_array($prefix)) {
      $prefix = sh::join($prefix);
      $cmd = "$prefix $cmd";
    } else {
      $cmd = "$prefix $cmd";
    }
  }

  function addRedir(?string $redir, $output=null, bool $append=false, $input=null): void {
    $count = count($this->cmds);
    if ($count == 0) return;

    if ($output !== null) $output = escapeshellarg($output);
    if ($input !== null) $input = escapeshellarg($input);
    if ($redir === "default") $redir = null;
    $gt = $append? ">>": ">";
    if ($redir === null) {
      $redirs = [];
      if ($input !== null) $redirs[] = "<$input";
      if ($output !== null) $redirs[] = "$gt$output";
      if ($redirs) $redir = implode(" ", $redir);
    } else {
      switch ($redir) {
      case "outonly":
      case "noerr":
        if ($output !== null) $redir = "$gt$output 2>/dev/null";
        else $redir = "2>/dev/null";
        break;
      case "erronly":
      case "noout":
        if ($output !== null) $redir = "2$gt$output >/dev/null";
        else $redir = "2>&1 >/dev/null";
        break;
      case "both":
      case "err2out":
        if ($output !== null) $redir = "$gt$output 2>&1";
        else $redir = "2>&1";
        break;
      case "none":
      case "null":
        $redir = ">/dev/null 2>&1";
        break;
      }
    }
    if ($redir !== null) {
      $cmd =& $this->cmds[$count - 1];
      if ($cmd instanceof ICmd) {
        $cmd->addRedir($redir);
      } else {
        $cmd = "$cmd $redir";
      }
    }
  }

  abstract function getCmd(?string $sep=null, bool $exec=false): string;

  function passthru(int &$retcode=null): bool {
    passthru($this->getCmd(), $retcode);
    return $retcode == 0;
  }

  function system(string &$output=null, int &$retcode=null): bool {
    $lastLine = system($this->getCmd(), $retcode);
    if ($lastLine !== false) $output = $lastLine;
    return $retcode == 0;
  }

  function exec(array &$output=null, int &$retcode=null): bool {
    exec($this->getCmd(), $output, $retcode);
    return $retcode == 0;
  }

  function fork_exec(?int &$retcode=null, bool $wait=true): bool {
    $cmd = $this->getCmd(null, true);
    sh::_fork_exec($cmd, $retcode, $wait);
    return $retcode == 0;
  }
}
