<?php
namespace nur\b\ui;

use Exception;
use nur\A;
use nur\b\ExceptionShadow;
use nur\b\io\IWriter;
use nur\b\params\Parametrable;
use nur\b\params\Tparametrable;
use nur\b\UserException;
use nur\c;
use nur\config;
use nur\data\types\md_utils;
use nur\data\types\Metadata;
use nur\func;
use nur\SL;
use nur\writer;
use Throwable;

abstract class AbstractMessenger extends Parametrable implements IMessenger {
  use Tparametrable;

  const PRINT_LEVELS = [
    self::KEY_USER => [
      self::LEVEL_NORMAL,
      config::DEVEL => self::LEVEL_MINOR,
    ],
    self::KEY_TECH => [
      self::LEVEL_NORMAL,
      config::DEVEL => self::LEVEL_MINOR,
    ],
    self::KEY_EXCEPTION => [
      self::LEVEL_NEVER,
      config::DEVEL => self::LEVEL_MINOR,
    ],
  ];

  const LOG_LEVELS = [
    self::KEY_USER => [
      self::LEVEL_NORMAL,
      config::DEVEL => self::LEVEL_MINOR,
    ],
    self::KEY_TECH => [
      self::LEVEL_NORMAL,
      config::DEVEL => self::LEVEL_MINOR,
    ],
    self::KEY_EXCEPTION => [
      self::LEVEL_NORMAL,
      config::DEVEL => self::LEVEL_MINOR,
    ],
  ];

  const TYPE_LEVELS = [
    IMessenger::TYPE_INFO,
    config::DEVEL => IMessenger::TYPE_DEBUG,
  ];

  const DATE_FORMAT = 'Y-m-d\TH:i:s.u';

  function __construct(?array $params=null) {
    self::set_parametrable_params_defaults($params, [
      "print_levels" => static::PRINT_LEVELS,
      "log_levels" => static::LOG_LEVELS,
      "type_levels" => static::TYPE_LEVELS,
      "date_format" => static::DATE_FORMAT,
    ]);
    parent::__construct($params);
  }

  const PARAMETRABLE_PARAMS_SCHEMA = [
    "print_levels" => ["array", null, "niveaux par défaut pour chaque catégorie de message"],
    "log_levels" => ["array", null, "niveaux par défaut pour chaque catégorie de message"],
    "type_levels" => ["array", null, "niveaux par défaut des types de message"],
    "log_output" => [null, null, "destination des messages de logs"],
    "display_log" => ["bool", false, "faut-il afficher les logs?"],
    "add_date" => ["?bool", null, "faut-il dater les messages?"],
    "date_format" => ["string", null, "format de la date"]
  ];

  /** @var array */
  protected $ppPrintLevels;

  function pp_setPrintLevels(array $levels): void {
    A::merge_nn($this->ppPrintLevels, $levels);
  }

  /** @var array */
  protected $ppLogLevels;

  function pp_setLogLevels(array $levels): void {
    A::merge_nn($this->ppLogLevels, $levels);
  }

  /** @var array */
  protected $ppTypeLevels;

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

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

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

  /** @var ?string */
  protected $ppDateFormat;

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

  protected function afterSetParametrableParams(array $modifiedKeys, ?Metadata $md=null): void {
    if (self::was_parametrable_param_modified($modifiedKeys, "log_output")) {
      $this->logOutput = $this->ppLogOutput !== null? writer::with($this->ppLogOutput): null;
      if (!self::was_parametrable_param_modified($modifiedKeys, "add_date")) {
        $this->ppAddDate = $this->logOutput !== null;
      }
    }
  }

  function setLevels(?array $printLevels, ?array $logLevels=null, $typeLevels=null): IMessenger {
    $this->setParametrableParams(SL::filter_z([
      "print_levels" => $printLevels,
      "log_levels" => $logLevels,
      "type_levels" => $typeLevels,
    ]));
    return $this;
  }

  protected function _getLevel(string $key, array $levels): int {
    $level = A::get($levels, $key, self::LEVEL_NORMAL);
    if (is_int($level)) return $level;

    $levels = A::with($level);
    $level = A::get($levels, config::get_profile());
    if ($level === null) $level = A::get($levels, 0);
    if ($level === null) return self::LEVEL_NORMAL;

    return $level;
  }

  function getPrintLevel(string $key): int {
    return $this->_getLevel($key, $this->ppPrintLevels);
  }

  function getLogLevel(string $key): int {
    return $this->_getLevel($key, $this->ppLogLevels);
  }

  function getTypeLevel(): int {
    $levels = $this->ppTypeLevels;
    $level = A::get($levels, config::get_profile());
    if ($level === null) $level = A::get($levels, 0);
    if ($level === null) return IMessenger::TYPE_INFO;

    return $level;
  }

  function isLogMessage(?string $msg, int $level, string $msgKey): bool {
    return $msg !== null && $level >= $this->getLogLevel($msgKey);
  }

  function isPrintMessage(?string $msg, int $level, string $msgKey): bool {
    return $msg !== null && $level >= $this->getPrintLevel($msgKey);
  }

  protected function date() {
    return date_create()->format($this->ppDateFormat);
  }

  protected function getString($text): string {
    return c::string(c::nq($text));
  }
  protected function filterTags(string $text): string {
    return preg_replace('/<[^>]*>/', "", $text);
  }
  protected function filterColors(string $text): string {
    return $text;
  }
  protected function wnl(IWriter $writer, ?bool $color, string $sep, ...$values): void {
    $values = c::flatten($values);
    foreach ($values as &$value) {
      $value = $this->getString($value);
      if (!$value) $value = false;
    }; unset($value);
    $text = $this->filterTags($writer->toString($sep, $values));
    if ($color === null) $color = $writer->isatty();
    if (!$color) $text = $this->filterColors($text);
    $writer->wnl($text);
  }

  protected function fixDest(?int &$type): void {
    if ($type === null) $type = 0;
    if (($type & self::DEST_MASK) === 0) $type += self::DEST_ALL;
  }

  protected function shouldLog(?int $type): bool {
    if ($this->logOutput === null) return false;
    $this->fixDest($type);
    return ($type & self::DEST_LOG) != 0;
  }

  protected function shouldPrint(?int $type): bool {
    $this->fixDest($type);
    return ($type & self::DEST_DISPLAY) != 0 || $this->ppDisplayLog;
  }

  #############################################################################
  # Sections

  protected function logStartSection($title): void {
    $datetime = $this->ppAddDate? $this->date()." ": "\n";
    $title = "$datetime>>>> $title <<<<";
    $this->wnl($this->logOutput, false, "", $title);
  }

  protected function logEndSection(): void {
  }

  protected abstract function printStartSection($title, ?int $msgType, ?int $msgLevel): void;
  protected abstract function printEndSection(): void;

  /** @var bool */
  private $inSection;

  function isInSection(): bool {
    return $this->inSection;
  }

  function startSection($title, ?int $msgType=null, ?int $msgLevel=null): IMessenger {
    if ($this->inSection) $this->endSection();
    $allowLog = $msgLevel === null || $msgLevel >= $this->getLogLevel(self::KEY_USER);
    $allowPrint = $msgLevel === null || $msgLevel >= $this->getPrintLevel(self::KEY_USER);
    if ($allowLog || $allowPrint) {
      if ($allowLog && $this->shouldLog($msgType)) $this->logStartSection($title);
      if ($allowPrint && $this->shouldPrint($msgType)) $this->printStartSection($title, $msgType, $msgLevel);
      $this->inSection = true;
    }
    return $this;
  }

  function endSection(): IMessenger {
    if ($this->inSection) {
      $this->printEndSection();
      $this->inSection = false;
    }
    return $this;
  }

  #############################################################################
  # Groupes

  protected function getGroupCount(?array $group): int {
    if ($group === null ) return 0;
    $count = $group["count"];
    return $count !== null? $count: 2;
  }

  protected function getGroupIndent(?array $group, ?int $indent=null, int $maxCount=1): string {
    if ($group === null) return "";
    if ($indent === null) {
      $indent = $group["indent"];
      $count = $this->getGroupCount($group);
      if ($count <= $maxCount) $indent--;
    }
    return str_repeat("  ", $indent);
  }

  protected function getGroupPrefix(?array $group, int $maxCount=1): string {
    if ($group === null) return "";
    $count = $this->getGroupCount($group);
    if ($count > $maxCount) return "";
    return $group["prefix"]." :";
  }

  protected function logStartGroup(array $group): void {
    if ($this->getGroupCount($group) > 1) {
      $groupIndent = $this->getGroupIndent($group, $group["indent"] - 1);
      if ($this->ppAddDate) $groupIndent = self::date()." $groupIndent";
      $this->wnl($this->logOutput, false, " ", $groupIndent.">", $group["prefix"]);
    }
  }

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

  protected abstract function printStartGroup(array $group, ?array $groups): void;
  protected abstract function printEndGroup(array $group): void;

  private $groups;

  public function isInGroup(): bool {
    return $this->groups !== null;
  }

  function startGroup($prefix, int $count=null, ?int $msgType=null, ?int $msgLevel=null): IMessenger {
    $allowLog = $msgLevel === null || $msgLevel >= $this->getLogLevel(self::KEY_USER);
    $allowPrint = $msgLevel === null || $msgLevel >= $this->getPrintLevel(self::KEY_USER);
    if ($allowLog || $allowPrint) {
      $indent = 1;
      if ($this->groups !== null) {
        foreach ($this->groups as $group) {
          if ($group !== null) $indent++;
        }
      }
      $group = [
        "indent" => $indent,
        "prefix" => $prefix,
        "count" => $count,
        "type" => $msgType,
        "level" => $msgLevel,
      ];
      if ($allowLog && $this->shouldLog($msgType)) $this->logStartGroup($group);
      if ($allowPrint && $this->shouldPrint($msgType)) $this->printStartGroup($group, $this->groups);
    } else {
      $group = false;
    }
    $this->groups[] = $group;
    return $this;
  }

  function endGroup(): IMessenger {
    $group = array_pop($this->groups);
    if ($group) {
      $type = $group["type"];
      if ($this->shouldLog($type)) $this->logEndGroup($group);
      if ($this->shouldPrint($type)) $this->printEndGroup($group);
    }
    if (!$this->groups) $this->groups = null;
    return $this;
  }

  function end(bool $all=false): IMessenger {
    if ($all) {
      while ($this->groups) $this->endGroup();
      $this->endSection();
    } elseif ($this->groups) $this->endGroup();
    else $this->endSection();
    return $this;
  }

  #############################################################################
  # Messages

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

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

  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[1]) return [null, null];
    $prefix = $typePrefixSuffix[0];
    $suffix = "";
    return [$prefix, $suffix];
  }

  /** @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 logMsg(
    ?array $groups,
    bool $logUser, $userMsg,
    bool $logTech, $techMsg,
    bool $logException, $exceptionMsg,
    int $type, int $level
  ): 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";

    $color = false;
    $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;

    $logOutput = $this->logOutput;
    if ($logUser) $this->wnl($logOutput, $color, " ", $prefix, $groupPrefix, $userMsg, $typeSuffix);
    if ($logTech) $this->wnl($logOutput, $color, " ", $prefix, "TECH:", $techMsg, $typeSuffix);
    if ($logException) {
      [$summary, $traceback] = $exceptionMsg;
      $this->wnl($logOutput, $color, " ", $prefix, "TRACEBACK:", $summary, $typeSuffix);
      $this->wnl($logOutput, false, "", $traceback);
    }
  }

  protected abstract function getUserMsg($msg): array;
  protected abstract function getTechMsg($msg): array;
  protected abstract function getExceptionMsg($exception, $user, $tech, bool $haveTechMsgOrSummary): array;
  protected abstract function printMsg(
    ?array $groups
    , bool $printUser, $userMsg
    , bool $printTech, $techMsg
    , bool $printException, $exceptionMsg
    , int  $type, int $level, array $options
  ): void;

  protected function processMsgOptions(array $options): void {
  }

  function ensureMessage(&$message): void {
    static::message_md()->ensureSchema($message);

    $user =& $message[self::KEY_USER];
    $tech =& $message[self::KEY_TECH];
    $exception =& $message[self::KEY_EXCEPTION];
    if ($exception === null) {
      if ($tech instanceof Throwable || $tech instanceof ExceptionShadow) $exception = $tech;
      elseif ($user instanceof Throwable || $user instanceof ExceptionShadow) $exception = $user;
    }
    if ($tech === null) {
      if ($user instanceof Throwable || $user instanceof ExceptionShadow) $tech = $user;
    }
  }

  function addMessage($message, int $type, int $level): IMessenger {
    $allowType = ($type & self::TYPE_MASK) >= $this->getTypeLevel();
    if (!$allowType) return $this;

    $this->fixDest($type);
    $this->ensureMessage($message);

    $user = $message[self::KEY_USER];
    $logUser = $level >= $this->getLogLevel(self::KEY_USER);
    $printUser = $level >= $this->getPrintLevel(self::KEY_USER);
    $userMsg = false;
    if ($user !== null) {
      if ($user instanceof UserException) $msg = $user->getUserMessage();
      elseif ($user instanceof Throwable || $user instanceof ExceptionShadow) $msg = $user->getMessage();
      else $msg = $user;
      if (!$msg) $printUser = false;
      else $userMsg = $this->getUserMsg($msg);
    }
    $logUser &= $userMsg !== false;
    $printUser &= $userMsg !== false;

    $tech = $message[self::KEY_TECH];
    $logTech = $level >= $this->getLogLevel(self::KEY_TECH);
    $printTech = $level >= $this->getPrintLevel(self::KEY_TECH);
    $techMsg = false;
    $techSummary = false;
    $exception = $message[self::KEY_EXCEPTION];
    $logException = $exception !== null && $level >= $this->getLogLevel(self::KEY_EXCEPTION);;
    $printException = $exception !== null && $level >= $this->getPrintLevel(self::KEY_EXCEPTION);;
    $exceptionMsg = false;
    if ($tech !== null) {
      if ($tech instanceof UserException) {
        $msg = $tech->getTechMessage();
      } elseif ($tech instanceof Throwable || $tech instanceof ExceptionShadow) {
        $techSummary = true;
        $msg = $printException? UserException::get_summary($tech): null;
      } else {
        $msg = $tech;
      }
      if (!$msg) $printTech = false;
      else $techMsg = $this->getTechMsg($msg);
    }
    $logTech &= $techMsg !== false;
    $printTech &= $techMsg !== false;

    if ($exception !== null) {
      $exceptionMsg = $this->getExceptionMsg($exception, $user, $tech, $techMsg || $techSummary);
    }
    $logException &= $exceptionMsg !== false;
    $printException &= $exceptionMsg !== false;

    $options = $message;
    if ($this->shouldLog($type)) {
      $this->logMsg(
        $this->groups,
        $logUser, $userMsg,
        $logTech, $techMsg,
        $logException, $exceptionMsg,
        $type, $level);
    }
    if ($this->shouldPrint($type)) {
      $this->printMsg(
        $this->groups,
        $printUser, $userMsg,
        $printTech, $techMsg,
        $printException, $exceptionMsg,
        $type, $level, $options);
    }
    $this->processMsgOptions($options);
    return $this;
  }

  function aresult($result, ?array $args=null, $message=null, ?int $type=null): IMessenger {
    if ($message !== null) static::message_md()->ensureSchema($message);
    if (is_callable($result)) {
      if ($args === null) $args = [];
      try {
        $result = func::call($result, ...$args);
        if ($result === null) {
          # cas des fonctions void
          $result = true;
        }
      } catch (Exception $e) {
        $result = $e;
      }
    }

    $type = ($type?: 0) & self::DEST_MASK;
    $level = self::LEVEL_MAJOR;
    if ($result instanceof Exception) {
      $type += self::TYPE_INFO + self::RESULT_FAILURE;
      $message[self::KEY_USER] = $result;
    } elseif (is_string($result)) {
      $type += self::TYPE_INFO + self::RESULT_SUCCESS;
      $message[self::KEY_USER] = $result;
    } elseif ($result === null) {
      $type += self::TYPE_INFO + self::RESULT_NEUTRAL;
      A::replace_z($message, self::KEY_USER, "en cours");
    } elseif ($result) {
      $type += self::TYPE_INFO + self::RESULT_SUCCESS;
      A::replace_z($message, self::KEY_USER, "succès");
    } else {
      $type += self::TYPE_INFO + self::RESULT_FAILURE;
      A::replace_z($message, self::KEY_USER, "échec");
    }
    $this->addMessage($message, $type, $level);
    if ($this->isInGroup()) $this->endGroup();
    return $this;
  }

  function astep($message=null, ?int $type=null): IMessenger {
    $this->aresult(null, null, $message, $type);
    return $this;
  }

  function asuccess($message=null, ?int $type=null): IMessenger {
    $this->aresult(true, null, $message, $type);
    return $this;
  }

  function afailure($message=null, ?Throwable $e=null, ?int $type=null): IMessenger {
    static::message_md()->ensureSchema($message);
    if ($message === null) {
      $message = $e;
    } elseif ($message[self::KEY_USER] === null) {
      $message[self::KEY_USER] = $e;
    } else {
      A::replace_z($message, self::KEY_EXCEPTION, $e);
    }
    $this->aresult(false, null, $message, $type);
    return $this;
  }

  function action($message, $result=null, ?array $args=null, ?int $type=null, ?int $level=null): IMessenger {
    $this->startGroup($message, 1, $type, $level);
    if ($result !== null) $this->aresult($result, $args, null, $type);
    return $this;
  }
}
