<?php
namespace nur\cli;

use nur\A;
use nur\b\ExitError;
use nur\b\io\IWriter;
use nur\b\params\Tparametrable;
use nur\b\ui\AbstractMessenger;
use nur\b\UserException;
use nur\data\types\md_utils;
use nur\data\types\Metadata;
use nur\writer;

/**
 * Class Console: affichage de messages sur la console
 */
class Console extends AbstractMessenger {
  use Tparametrable;

  function __construct(?array $params=null) {
    self::set_parametrable_params_defaults($params, [
      "output" => null,
    ], false);
    parent::__construct($params);
  }

  const PARAMETRABLE_PARAMS_SCHEMA = [
    "output" => [null, null, "destination des messages affichés"],
    "color" => ["?bool", null, "la sortie dans la destination se fait-elle en couleur?"],
  ];

  /** @var IWriter */
  protected $ppOutput;

  /** @var bool */
  protected $ppColor;

  /** @var IWriter */
  protected $output;

  /** @var ?bool */
  protected $color;

  protected function afterSetParametrableParams(array $modifiedKeys, ?Metadata $md=null): void {
    parent::afterSetParametrableParams($modifiedKeys, $md);
    if (self::was_parametrable_param_modified($modifiedKeys, "color")) {
      $this->color = $this->ppColor;
    }
    if (self::was_parametrable_param_modified($modifiedKeys, "output")) {
      $output = $this->ppOutput;
      if ($output === null) $output = STDERR;
      $this->output = $output = writer::with($output);
      if (!self::was_parametrable_param_modified($modifiedKeys, "color")) {
        $this->color = $output->isatty();
      }
    }
  }

  const KEY_EXIT = "exit";
  const MESSAGE_OPTIONS_SCHEMA = [
    self::KEY_EXIT => [null, null, "faut-il arrêter le script après avoir affiché le message?",
      # la valeur peut être numérique auquel cas c'est le code de retour
      # sinon ce doit être un booléan
    ],
  ];

  /** @var Metadata */
  private static $message_md;

  protected static function message_md(): Metadata {
    return md_utils::ensure_md(self::$message_md
      , array_merge(self::MESSAGE_SCHEMA, self::MESSAGE_OPTIONS_SCHEMA));
  }

  protected function processMsgOptions(array $options): void {
    $exit = $options[self::KEY_EXIT];
    if ($exit !== null && $exit !== false) {
      throw new ExitError(
        is_int($exit)? $exit: 1,
        is_string($exit)? $exit: null,
      );
    }
  }

  const TYPE_PREFIXES = [
    # les clés doivent être ordonnées de la plus grande à la plus petite
    # la 4ème valeur indique s'il faut garder le préfixe s'il y a un result
    self::LEVEL_CRITICAL => [
      self::TYPE_ERROR => ["CRITICAL!", "<color @r>E!", "</color>", true],
      self::TYPE_WARNING => ["ATTENTION!", "<color @y>W!", "</color>", true],
      self::TYPE_DEBUG => ["IMPORTANT!", "<color @g>N!", "</color>", true],
    ],
    self::LEVEL_MAJOR => [
      self::TYPE_ERROR => ["ERROR:", "<color @r>E</color><color r>", "</color>", true],
      self::TYPE_WARNING => ["WARN:", "<color @y>W</color><color y>", "</color>", true],
      self::TYPE_INFO => ["INFO:", "<color @b>I</color>", "", false],
      self::TYPE_DEBUG => ["DEBUG:", "<color @w>D</color><color w>", "</color>", true],
    ],
    self::LEVEL_NORMAL => [
      self::TYPE_ERROR => ["E", "<color r>", "</color>", true],
      self::TYPE_WARNING => ["W", "<color y>", "</color>", true],
      self::TYPE_INFO => ["", "", "", false],
      self::TYPE_DEBUG => ["D", "<color w>", "</color>", true],
    ],
    self::LEVEL_MINOR => [
      self::TYPE_ERROR => ["e", "<color -r>", "</color>", true],
      self::TYPE_WARNING => ["w", "<color -y>", "</color>", true],
      self::TYPE_INFO => ["i", "<color -b>", "</color>", false],
      self::TYPE_DEBUG => ["d", "<color -w>", "</color>", true],
    ],
  ];
  const RESULT_PREFIXES = [
    self::RESULT_FAILURE => ["(FAILURE)", "<color r>✘</color>"],
    self::RESULT_SUCCESS => ["(SUCCESS)", "<color @g>✔</color>"],
    self::RESULT_NEUTRAL => ["*", "<color @w>.</color>"],
    self::RESULT_NONE => [null, null],
  ];

  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",
  ];

  private static function replace_colors(array $ms): string {
    $colors = [];
    foreach (preg_split('/\s+/', $ms[1]) as $color) {
      while ($color && !A::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;
  }
  protected function filterTags(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);
    return parent::filterTags($text);
  }
  protected function filterColors(string $text): string {
    return preg_replace('/\x1B\[.*?m/', "", $text);
  }

  protected function printStartSection($title, ?int $msgType, ?int $msgLevel): void {
    $datetime = $this->ppAddDate? self::date()." ": "\n";
    $title = "$datetime>>>> $title <<<<";
    $this->wnl($this->output, $this->color, "", $title);
  }

  protected function printEndSection(): void {
  }

  protected function getResultPrefix(int $result, bool $color): ?string {
    return self::RESULT_PREFIXES[$result][$color ? 1 : 0];
  }

  protected function getTypePrefixSuffix(int $type, int $level, bool $color, bool $haveResultPrefix): array {
    $typePrefixSuffix = false;
    foreach (self::TYPE_PREFIXES as $prefixLevel => $prefixes) {
      if ($level >= $prefixLevel) {
        foreach ($prefixes as $prefixType => $prefixValue) {
          if ($type >= $prefixType) {
            $typePrefixSuffix = $prefixValue;
            break;
          }
        }
      }
      if ($typePrefixSuffix !== false) break;
    }
    if ($typePrefixSuffix === false) return [null, null];
    elseif ($haveResultPrefix && !$typePrefixSuffix[3]) return [null, null];
    $prefix = $typePrefixSuffix[$color ? 1 : 0];
    $suffix = $color? $typePrefixSuffix[2]: "";
    return [$prefix, $suffix];
  }

  protected function printStartGroup(array $group, ?array $groups): void {
    [
      "indent" => $indent,
      "prefix" => $groupPrefix,
      "count" => $count,
      "type" => $type,
      "level" => $level,
    ] = $group;
    $count = $this->getGroupCount($group);
    if ($count > 1) {
      $groupIndent = $this->getGroupIndent($group, $indent - 1);
      if ($this->ppAddDate) $groupIndent = self::date()." $groupIndent";
      if ($type === null) $type = self::TYPE_INFO + self::RESULT_NEUTRAL;
      if ($level === null) $level = self::LEVEL_ALWAYS;
      $color = $this->color;
      $resultPrefix = $this->getResultPrefix($type & self::RESULT_MASK, $color);
      [$typePrefix, $typeSuffix] = $this->getTypePrefixSuffix($type & self::TYPE_MASK, $level, $color, $resultPrefix !== null);
      $this->wnl($this->output, $color, " ", $groupIndent.$typePrefix.$resultPrefix, $groupPrefix, $typeSuffix);
    }
  }

  protected function printEndGroup(array $group): void {
  }

  protected function getUserMsg($msg): array {
    return [$msg];
  }

  protected function getTechMsg($msg): array {
    return [$msg];
  }

  protected function getExceptionMsg($exception, $user, $tech, bool $haveTechMsgOrSummary): array {
    $msg = [null, UserException::get_traceback($exception)];
    if (!$haveTechMsgOrSummary || ($exception !== $user && $exception !== $tech)) {
      $msg[0] = UserException::get_summary($exception);
    }
    return $msg;
  }

  protected function _printMsg(
    IWriter $output, bool $color,
    ?string $groupIndent, ?string $groupPrefix,
    bool $showUser, $userMsg,
    bool $showTech, $techMsg,
    bool $showException, $exceptionMsg,
    int $type, int $level
  ): void {
    $result = $type & self::RESULT_MASK;
    $type = $type & self::TYPE_MASK;
    $resultPrefix = $this->getResultPrefix($result, $color);
    [$typePrefix, $typeSuffix] = $this->getTypePrefixSuffix($type, $level, $color, $resultPrefix !== null);
    $prefix = $groupIndent.$typePrefix.$resultPrefix;

    if ($showUser) $this->wnl($output, $color, " ", $prefix, $groupPrefix, $userMsg, $typeSuffix);
    if ($showTech) $this->wnl($output, $color, " ", $prefix, "TECH:", $techMsg, $typeSuffix);
    if ($showException) {
      [$summary, $traceback] = $exceptionMsg;
      $this->wnl($output, $color, " ", $prefix, "TRACEBACK:", $summary, $typeSuffix);
      $this->wnl($output, false, "", $traceback);
    }
  }

  protected function printMsg(
    ?array $groups,
    bool $printUser, $userMsg,
    bool $printTech, $techMsg,
    bool $printException, $exceptionMsg,
    int $type, int $level, array $options
  ): void {
    $group = A::last($groups);
    if ($group === false) {
      # groupe neutralisé
      return;
    }
    $groupIndent = $this->getGroupIndent($group);
    $groupPrefix = $this->getGroupPrefix($group);
    if ($this->ppAddDate) $groupIndent = self::date()." $groupIndent";

    $this->_printMsg($this->output, $this->color
      , $groupIndent, $groupPrefix
      , $printUser, $userMsg
      , $printTech, $techMsg
      , $printException, $exceptionMsg
      , $type, $level);
  }
}