<?php
namespace nur\sery\output\std;

use Exception;
use nur\sery\cl;
use nur\sery\file\Stream;
use nur\sery\php\content\c;

/**
 * Class StdOutput: affichage sur STDOUT, STDERR ou dans un fichier quelconque
 *
 * si la destination est un fichier, aucune gestion d'erreur n'est faite, sauf
 * lors de l'ouverture.
 */
class StdOutput {
  const COLORS = [
    "reset" => "0",
    "bold" => "1",
    "faint" => "2",
    "underlined" => "4",
    "reverse" => "7",
    "normal" => "22",
    "black" => "30",
    "red" => "31",
    "green" => "32",
    "yellow" => "33",
    "blue" => "34",
    "magenta" => "35",
    "cyan" => "36",
    "white" => "37",
    "default" => "39",
    "black-bg" => "40",
    "red-bg" => "41",
    "green-bg" => "42",
    "yellow-bg" => "43",
    "blue-bg" => "44",
    "magenta-bg" => "45",
    "cyan-bg" => "46",
    "white-bg" => "47",
    "default-bg" => "49",
  ];

  const COLOR_MAP = [
    "z" => "reset",
    "o" => "black",
    "r" => "red",
    "g" => "green",
    "y" => "yellow",
    "b" => "blue",
    "m" => "magenta",
    "c" => "cyan",
    "w" => "white",
    "O" => "black_bg",
    "R" => "red_bg",
    "G" => "green_bg",
    "Y" => "yellow_bg",
    "B" => "blue_bg",
    "M" => "magenta_bg",
    "C" => "cyan_bg",
    "W" => "white_bg",
    "@" => "bold",
    "-" => "faint",
    "_" => "underlined",
    "~" => "reverse",
    "n" => "normal",
  ];

  /**
   * @param resource|null $outf
   * @throws Exception si la destination est un fichier et que son ouverture a
   * échoué
   */
  function __construct($output=null, ?array $params=null) {
    if ($output !== null) $params["output"] = $output;
    elseif (!isset($params["output"])) $params["output"] = STDOUT;
    if (!isset($params["filter_tags"])) $params["filter_tags"] = true;
    if (!isset($params["indent"])) $params["indent"] = "  ";
    $this->resetParams($params);
  }

  function resetParams(?array $params=null): void {
    $output = cl::get($params, "output");
    $maskErrors = null;
    $color = cl::get($params, "color");
    $filterTags = cl::get($params, "filter_tags");
    $indent = cl::get($params, "indent");
    $flush = cl::get($params, "flush");

    if ($output instanceof Stream) $output = $output->getResource();
    if ($output !== null) {
      if ($output === "php://stdout") {
        $outf = STDOUT;
      } elseif ($output === "php://stderr") {
        $outf = STDERR;
      } elseif (!is_resource($output)) {
        # si $outf est un nom de fichier, vérifier que l'ouverture se fait sans
        # erreur. à partir de là, plus aucune gestion d'erreur n'est faite, à
        # part afficher les erreurs d'écriture la première fois qu'elles se
        # produisent
        $maskErrors = false;
        $outf = @fopen($output, "ab");
        if ($outf === false) {
          $error = error_get_last();
          if ($error !== null) $message = $error["message"];
          else $message = "$output: open error";
          throw new Exception($message);
        }
        if ($flush === null) $flush = true;
      } else {
        $outf = $output;
      }
      $this->outf = $outf;
      $this->maskErrors = $maskErrors;
      if ($color === null) $color = stream_isatty($outf);
      if ($flush === null) $flush = false;
    }
    if ($color !== null) $this->color = boolval($color);
    if ($filterTags !== null) $this->filterTags = boolval($filterTags);
    if ($indent !== null) $this->indent = strval($indent);
    if ($flush !== null) $this->flush = boolval($flush);
  }

  /** @var resource  */
  protected $outf;

  /** @var bool faut-il masquer les erreurs d'écriture? */
  protected $maskErrors;

  /** @var bool faut-il autoriser la sortie en couleur? */
  protected $color;

  function isColor(): bool {
    return $this->color;
  }

  /** @var bool faut-il enlever les tags dans la sortie? */
  protected $filterTags;

  /** @var string indentation unitaire */
  protected $indent;

  /** @var bool faut-il flush le fichier après l'écriture de chaque ligne */
  protected $flush;

  function isatty(): bool {
    return stream_isatty($this->outf);
  }

  private static function replace_colors(array $ms): string {
    $colors = [];
    foreach (preg_split('/\s+/', $ms[1]) as $color) {
      while ($color && !cl::has(self::COLORS, $color)) {
        $alias = substr($color, 0, 1);
        $colors[] = self::COLOR_MAP[$alias];
        $color = substr($color, 1);
      }
      if ($color) $colors[] = $color;
    }
    $text = "\x1B[";
    $first = true;
    foreach ($colors as $color) {
      if (!$color) continue;
      if ($first) $first = false;
      else $text .= ";";
      $text .= self::COLORS[$color];
    }
    $text .= "m";
    return $text;
  }
  function filterContent(string $text): string {
    # couleur au début
    $text = preg_replace_callback('/<color([^>]*)>/', [self::class, "replace_colors"], $text);
    # reset à la fin
    $text = preg_replace('/<\/color>/', "\x1B[0m", $text);
    # enlever les tags classiques
    if ($this->filterTags) {
      $text = preg_replace('/<[^>]*>/', "", $text);
    }
    return $text;
  }
  function filterColors(string $text): string {
    return preg_replace('/\x1B\[.*?m/', "", $text);
  }

  function getIndent(int $indentLevel): string {
    return str_repeat($this->indent, $indentLevel);
  }

  function getLines(bool $withNl, ...$values): array {
    $values = c::resolve($values, null, false);
    if (!$values) return [];
    $text = c::to_string($values, false);
    if ($text === "") return [""];
    $text = $this->filterContent($text);
    if (!$this->color) $text = $this->filterColors($text);
    $lines = explode("\n", $text);
    $max = count($lines) - 1;
    if ($withNl) {
      for ($i = 0; $i < $max; $i++) {
        $lines[$i] .= "\n";
      }
    }
    if ($lines[$max] === "") unset($lines[$max]);
    return $lines;
  }

  private function _fwrite($outf, string $data): void {
    if ($this->maskErrors === null) {
      # masquer les erreurs d'écriture en permanence
      @fwrite($outf, $data);
      return;
    }
    # masquer uniquement la première erreur, jusqu'à ce que l'erreur disparaisse
    if ($this->maskErrors) $r = @fwrite($outf, $data);
    else $r = fwrite($outf, $data);
    $this->maskErrors = $r === false;
  }

  function writeLines($indent, array $lines, bool $addNl=false): void {
    $outf = $this->outf;
    foreach ($lines as $line) {
      if ($indent !== null) $this->_fwrite($outf, $indent);
      $this->_fwrite($outf, $line);
      if ($addNl) $this->_fwrite($outf, "\n");
    }
    if ($this->flush) @fflush($outf);
  }

  function write(...$values): void {
    $this->writeLines(null, $this->getLines(true, ...$values));
  }

  function print(...$values): void {
    $values[] = "\n";
    $this->writeLines(null, $this->getLines(true, ...$values));
  }

  function iwrite(int $indentLevel, ...$values): void {
    $indent = $this->getIndent($indentLevel);
    $this->writeLines($indent, $this->getLines(true, ...$values));
  }

  function iprint(int $indentLevel, ...$values): void {
    $values[] = "\n";
    $indent = $this->getIndent($indentLevel);
    $this->writeLines($indent, $this->getLines(true, ...$values));
  }
}
