<?php
namespace nur\cli;

use Exception;
use nur\b\ExitException;
use nur\b\ValueException;
use nur\config;
use nur\config\ArrayConfig;
use nur\msg;
use nur\os;
use nur\path;
use nur\sery\output\log as nlog;
use nur\sery\output\msg as nmsg;
use nur\sery\output\say as nsay;
use nur\sery\output\std\StdMessenger as nStdMessenger;

/**
 * Class Application: application de base
 */
abstract class Application {
  protected static function _app_init(): void {
    config::set_fact(config::FACT_CLI_APP);

    nmsg::set_messenger_class(nStdMessenger::class);
    nmsg::reset_params(["min_level" => nmsg::DEBUG]);

    msg::set_messenger_class(Console::class);
    msg::get()->setParametrableParams([
      # En ligne de commande, on peut afficher les messages loggués
      "display_log" => true,
    ]);
    # avant que l'application soit configurée, rester en mode debug
    msg::get()->setLevels(msg::DEBUG_LEVELS, msg::DEBUG_LEVELS, msg::DEBUG);
    # si un fichier nommé .default-profile-devel existe dans le répertoire de
    # l'application ou du projet, alors le profil par défaut est devel
    global $argv;
    $homedir = os::homedir();
    $projdir = path::abspath(path::dirname($argv[0]));
    while (true) {
      if (file_exists("$projdir/.default-profile-devel")) {
        config::set_default_profile(config::DEVEL);
        break;
      }
      # s'arrêter au répertoire du projet, ou à $HOMEDIR, ou à la racine
      if (file_exists("$projdir/composer.json")) break;
      if ($projdir == $homedir) break;
      $projdir = path::dirname($projdir);
      if ($projdir == "/") break;
    }
  }

  protected static function _app_configure(Application $app): void {
    config::configure(config::CONFIGURE_INITIAL_ONLY);
    # revenir à la configuration par défaut une fois que la configuration
    # initiale est faite
    msg::get()->setLevels(msg::PRINT_LEVELS, msg::LOG_LEVELS, msg::TYPE_LEVELS);
    nsay::reset_params(["min_level" => nmsg::NORMAL]);

    $app->parseArgs();
    config::configure();
  }
  
  protected static function _app_main(Application $app): void {
    $retcode = $app->main();
    if (is_int($retcode)) exit($retcode);
    elseif (is_bool($retcode)) exit($retcode? 0: 1);
    elseif ($retcode !== null) exit(strval($retcode));
  }

  static function run(?Application $app=null): void {
    try {
      static::_app_init();
      if ($app === null) $app = new static();
      static::_app_configure($app);
      static::_app_main($app);
    } catch (ExitException $e) {
      msg::error($e);
      exit($e->getCode());
    } catch (Exception $e) {
      msg::error($e);
      exit(1);
    }
  }

  /**
   * sortir de l'application avec un code d'erreur, qui est 0 par défaut (i.e
   * pas d'erreur)
   *
   * équivalent à lancer l'exception {@link ExitException}
   */
  protected static final function exit(int $exitcode=0, $message=null) {
    throw new ExitException($exitcode, $message);
  }

  /**
   * sortir de l'application avec un code d'erreur, qui vaut 1 par défaut (i.e
   * une erreur s'est produite)
   *
   * équivalent à lancer l'exception {@link ExitException}
   */
  protected static final function die($message=null, int $exitcode=1) {
    throw new ExitException($exitcode, $message);
  }

  const PROFILE_SECTION = [
    "title" => "PROFILS D'EXECUTION",
    ["group",
      ["-p", "--profile", "--app-profile",
        "args" => 1, "argsdesc" => "PROFILE",
        "action" => [null, "set_application_profile"],
        "help" => "spécifier le profil d'exécution",
      ],
      ["-P", "--prod", "action" => [config::class, "set_profile", config::PROD]],
      ["-T", "--test", "action" => [config::class, "set_profile", config::TEST]],
      ["--devel", "action" => [config::class, "set_profile", config::DEVEL]],
    ],
  ];

  static function set_application_profile(string $profile): void {
    config::set_profile($profile);
  }

  const VERBOSITY_SECTION = [
    "title" => "NIVEAU D'INFORMATION",
    "show" => false,
    ["group",
      ["--verbosity",
        "args" => 1, "argsdesc" => "silent|very-quiet|quiet|verbose|debug|trace",
        "action" => [null, "set_application_verbosity"],
        "help" => "spécifier le niveau d'informations affiché",
      ],
      ["-q", "--quiet", "action" => [null, "set_application_verbosity", "quiet"]],
      ["-v", "--verbose", "action" => [null, "set_application_verbosity", "verbose"]],
      ["-D", "--debug", "action" => [null, "set_application_verbosity", "debug"]],
      ["--sql-trace", "action" => [null, "set_application_sql_trace"]],
    ],
    ["-L", "--logfile",
      "args" => "file", "argsdesc" => "OUTPUT",
      "action" => [null, "set_application_log_output"],
      "help" => "Logger les messages de l'application dans le fichier spécifié",
    ],
    ["group",
      ["--color",
        "action" => [null, "set_application_color", true],
        "help" => "Afficher (resp. ne pas afficher) la sortie en couleur par défaut",
      ],
      ["--no-color", "action" => [null, "set_application_color", false]],
    ],
  ];

  static function set_application_verbosity(string $verbosity): void {
    $msg = msg::get();
    $nsay = nsay::get();
    switch ($verbosity) {
    case "s":
    case "silent":
      $msg->setLevels([
        msg::USER => msg::NEVER,
        msg::TECH => msg::NEVER,
        msg::EXCEPTION => msg::NEVER,
      ]);
      $nsay->resetParams([
        "min_level" => nmsg::NONE,
      ]);
      break;
    case "Q":
    case "very-quiet":
      $msg->setLevels([
        msg::USER => msg::CRITICAL,
        msg::TECH => msg::CRITICAL,
        msg::EXCEPTION => msg::NEVER,
      ]);
      $nsay->resetParams([
        "min_level" => nmsg::MAJOR,
      ]);
      break;
    case "q":
    case "quiet":
      $msg->setLevels([
        msg::USER => msg::MAJOR,
        msg::TECH => msg::MAJOR,
        msg::EXCEPTION => msg::NEVER,
      ]);
      $nsay->resetParams([
        "min_level" => nmsg::MAJOR,
      ]);
      break;
    case "v":
    case "verbose":
      $msg->setLevels([
        msg::USER => msg::MINOR,
        msg::TECH => msg::MINOR,
        msg::EXCEPTION => msg::NEVER,
      ]);
      $nsay->resetParams([
        "min_level" => nmsg::MINOR,
      ]);
      break;
    case "D":
    case "debug":
      config::set_debug();
      $msg->setLevels([
        msg::USER => msg::MINOR,
        msg::TECH => msg::MINOR,
        msg::EXCEPTION => msg::NORMAL,
      ], null, msg::DEBUG);
      $nsay->resetParams([
        "min_level" => nmsg::DEBUG,
      ]);
      break;
    case "T":
    case "trace":
      config::set_debug();
      $msg->setLevels([
        msg::USER => msg::MINOR,
        msg::TECH => msg::MINOR,
        msg::EXCEPTION => msg::MINOR,
      ], null, msg::DEBUG);
      $nsay->resetParams([
        "min_level" => nmsg::DEBUG,
      ]);
      break;
    default:
      throw ValueException::invalid_value($verbosity, "verbosity");
    }
  }

  static function set_application_sql_trace(): void {
    config::add(new ArrayConfig(["app" => ["trace_sql" => true]]));
  }

  static function set_application_log_output(string $logfile): void {
    msg::get()->setParametrableParams(["log_output" => $logfile]);
    nlog::create_or_reset_params([
      "output" => $logfile,
    ], nStdMessenger::class, [
      "add_date" => true,
      "min_level" => nlog::MINOR,
    ]);
  }
  static function set_application_color(bool $color): void {
    msg::get()->setParametrableParams(["color" => $color]);
    nsay::reset_params([
      "color" => $color,
    ]);
  }
  const ARGS = [
    "sections" => [
      self::PROFILE_SECTION,
      self::VERBOSITY_SECTION,
    ],
  ];

  /** @throws ArgsException */
  function parseArgs(array $args=null): void {
    $parser = new ArgsParser(static::ARGS);
    $parser->parse($this, $args);
  }

  const PROFILE_COLORS = [
    "prod" => "@r",
    "test" => "@g",
    "devel" => "@w",
  ];
  const DEFAULT_PROFILE_COLOR = "y";

  /** retourner le profil courant en couleur */
  static function profile(?string $profile=null): string {
    if ($profile === null) $profile = config::get_profile();
    foreach (static::PROFILE_COLORS as $text => $color) {
      if (strpos($profile, $text) !== false) {
        return $color? "<color $color>$profile</color>": $profile;
      }
    }
    $color = static::DEFAULT_PROFILE_COLOR;
    return $color? "<color $color>$profile</color>": $profile;
  }

  abstract function main();
}