<?php
namespace nur\sery\db;

use nur\sery\cl;
use nur\sery\str;
use Traversable;

/**
 * Class CapacitorChannel: un canal d'une instance de {@link ICapacitor}
 */
class CapacitorChannel {
  const NAME = null;

  const TABLE_NAME = null;

  const COLUMN_DEFINITIONS = null;

  const PRIMARY_KEYS = null;

  const MANAGE_TRANSACTIONS = true;

  const EACH_COMMIT_THRESHOLD = 100;

  const USE_CACHE = false;

  static function verifix_name(?string &$name, ?string &$tableName=null): void {
    if ($name !== null) {
      $name = strtolower($name);
      if ($tableName === null) {
        $tableName = str_replace("-", "_", $name) . "_channel";
      }
    } else {
      $name = static::class;
      if ($name === self::class) {
        $name = "default";
        if ($tableName === null) $tableName = "default_channel";
      } else {
        $name = preg_replace('/^.*\\\\/', "", $name);
        $name = preg_replace('/Channel$/', "", $name);
        $name = lcfirst($name);
        if ($tableName === null) $tableName = str::camel2us($name);
        $name = strtolower($name);
      }
    }
  }

  protected static function verifix_eachCommitThreshold(?int $eachCommitThreshold): ?int {
    $eachCommitThreshold ??= static::EACH_COMMIT_THRESHOLD;
    if ($eachCommitThreshold < 0) $eachCommitThreshold = null;
    return $eachCommitThreshold;
  }

  function __construct(?string $name=null, ?int $eachCommitThreshold=null, ?bool $manageTransactions=null) {
    $name ??= static::NAME;
    $tableName ??= static::TABLE_NAME;
    self::verifix_name($name, $tableName);
    $this->name = $name;
    $this->tableName = $tableName;
    $this->manageTransactions = $manageTransactions ?? static::MANAGE_TRANSACTIONS;
    $this->eachCommitThreshold = self::verifix_eachCommitThreshold($eachCommitThreshold);
    $this->useCache = static::USE_CACHE;
    $this->setup = false;
    $this->created = false;
    $columnDefinitions = cl::withn(static::COLUMN_DEFINITIONS);
    $primaryKeys = cl::withn(static::PRIMARY_KEYS);
    if ($primaryKeys === null && $columnDefinitions !== null) {
      $index = 0;
      foreach ($columnDefinitions as $col => $def) {
        if ($col === $index) {
          $index++;
          if (preg_match('/\bprimary\s+key\s+\((.+)\)/i', $def, $ms)) {
            $primaryKeys = preg_split('/\s*,\s*/', trim($ms[1]));
          }
        } else {
          if (preg_match('/\bprimary\s+key\b/i', $def)) {
            $primaryKeys[] = $col;
          }
        }
      }
    }
    $this->columnDefinitions = $columnDefinitions;
    $this->primaryKeys = $primaryKeys;
  }

  protected string $name;

  function getName(): string {
    return $this->name;
  }

  protected string $tableName;

  function getTableName(): string {
    return $this->tableName;
  }

  /**
   * @var bool indiquer si les modifications de each doivent être gérées dans
   * une transaction. si false, l'utilisateur doit lui même gérer la
   * transaction.
   */
  protected bool $manageTransactions;

  function isManageTransactions(): bool {
    return $this->manageTransactions;
  }

  function setManageTransactions(bool $manageTransactions=true): self {
    $this->manageTransactions = $manageTransactions;
    return $this;
  }

  /**
   * @var ?int nombre maximum de modifications dans une transaction avant un
   * commit automatique dans {@link Capacitor::each()}. Utiliser null pour
   * désactiver la fonctionnalité.
   *
   * ce paramètre n'a d'effet que si $manageTransactions==true
   */
  protected ?int $eachCommitThreshold;

  function getEachCommitThreshold(): ?int {
    return $this->eachCommitThreshold;
  }

  function setEachCommitThreshold(?int $eachCommitThreshold=null): self {
    $this->eachCommitThreshold = self::verifix_eachCommitThreshold($eachCommitThreshold);
    return $this;
  }

  /**
   * @var bool faut-il passer par le cache pour les requêtes de all(), each()
   * et delete()?
   * ça peut être nécessaire avec MySQL/MariaDB si on utilise les requêtes non
   * bufférisées, et que la fonction manipule la base de données
   */
  protected bool $useCache;

  function isUseCache(): bool {
    return $this->useCache;
  }

  function setUseCache(bool $useCache=true): self {
    $this->useCache = $useCache;
    return $this;
  }

  /**
   * initialiser ce channel avant sa première utilisation.
   */
  protected function setup(): void {
  }

  protected bool $setup;

  function ensureSetup() {
    if (!$this->setup) {
      $this->setup();
      $this->setup = true;
    }
  }

  protected bool $created;

  function isCreated(): bool {
    return $this->created;
  }

  function setCreated(bool $created=true): void {
    $this->created = $created;
  }

  protected ?array $columnDefinitions;

  /**
   * retourner un ensemble de définitions pour des colonnes supplémentaires à
   * insérer lors du chargement d'une valeur
   *
   * la clé primaire "id_" a pour définition "integer primary key autoincrement".
   * elle peut être redéfinie, et dans ce cas la valeur à utiliser doit être
   * retournée par {@link getItemValues()}
   *
   * la colonne "item__" contient la valeur sérialisée de l'élément chargé. bien
   * que ce soit possible techniquement, cette colonne n'a pas à être redéfinie
   *
   * les colonnes dont le nom se termine par "_" sont réservées.
   * les colonnes dont le nom se termine par "__" sont automatiquement sérialisées
   * lors de l'insertion dans la base de données, et automatiquement désérialisées
   * avant d'être retournées à l'utilisateur (sans le suffixe "__")
   */
  function getColumnDefinitions(): ?array {
    return $this->columnDefinitions;
  }

  protected ?array $primaryKeys;

  function getPrimaryKeys(): ?array {
    return $this->primaryKeys;
  }

  /**
   * calculer les valeurs des colonnes supplémentaires à insérer pour le
   * chargement de $item. pour une même valeur de $item, la valeur de retour
   * doit toujours être la même. pour rajouter des valeurs supplémentaires qui
   * dépendent de l'environnement, il faut plutôt les retournner dans
   * {@link self::onCreate()} ou {@link self::onUpdate()}
   *
   * Cette méthode est utilisée par {@link Capacitor::charge()}. Si la clé
   * primaire est incluse (il s'agit généralement de "id_"), la ligne
   * correspondate est mise à jour si elle existe.
   * Retourner la clé primaire par cette méthode est l'unique moyen de
   * déclencher une mise à jour plutôt qu'une nouvelle création.
   *
   * Retourner [false] pour annuler le chargement
   */
  function getItemValues($item): ?array {
    return null;
  }

  /**
   * Avant d'utiliser un id pour rechercher dans la base de donnée, corriger sa
   * valeur le cas échéant.
   *
   * Cette fonction assume que la clé primaire n'est pas multiple. Elle n'est
   * pas utilisée si une clé primaire multiple est définie.
   */
  function verifixId(string &$id): void {
  }

  /**
   * retourne true si un nouvel élément ou un élément mis à jour a été chargé.
   * false si l'élément chargé est identique au précédent.
   *
   * cette méthode doit être utilisée dans {@link self::onUpdate()}
   */
  function wasRowModified(array $values, array $pvalues): bool {
    return $values["item__sum_"] !== $pvalues["item__sum_"];
  }

  final function serialize($item): ?string {
    return $item !== null? serialize($item): null;
  }

  final function unserialize(?string $serial) {
    return $serial !== null? unserialize($serial): null;
  }

  const SERIAL_DEFINITION = "mediumtext";
  const SUM_DEFINITION = "varchar(40)";

  final function sum(?string $serial, $value=null): ?string {
    if ($serial === null) $serial = $this->serialize($value);
    return $serial !== null? sha1($serial): null;
  }

  final function isSerialCol(string &$key): bool {
    return str::del_suffix($key, "__");
  }

  final function getSumCols(string $key): array {
    return ["${key}__", "${key}__sum_"];
  }

  function getSum(string $key, $value): array {
    $sumCols = $this->getSumCols($key);
    $serial = $this->serialize($value);
    $sum = $this->sum($serial, $value);
    return array_combine($sumCols, [$serial, $sum]);
  }

  function wasSumModified(string $key, $value, array $pvalues): bool {
    $sumCol = $this->getSumCols($key)[1];
    $sum = $this->sum(null, $value);
    $psum = $pvalues[$sumCol] ?? $this->sum(null, $pvalues[$key] ?? null);
    return $sum !== $psum;
  }

  function _wasSumModified(string $key, array $row, array $prow): bool {
    $sumCol = $this->getSumCols($key)[1];
    $sum = $row[$sumCol] ?? null;
    $psum = $prow[$sumCol] ?? null;
    return $sum !== $psum;
  }

  /**
   * méthode appelée lors du chargement avec {@link Capacitor::charge()} pour
   * créer un nouvel élément
   *
   * @param mixed $item l'élément à charger
   * @param array $values la ligne à créer, calculée à partir de $item et des
   * valeurs retournées par {@link getItemValues()}
   * @return ?array le cas échéant, un tableau non null à merger dans $values et
   * utilisé pour provisionner la ligne nouvellement créée.
   * Retourner [false] pour annuler le chargement (la ligne n'est pas créée)
   *
   * Si $item est modifié dans cette méthode, il est possible de le retourner
   * avec la clé "item" pour mettre à jour la ligne correspondante.
   *
   * la création ou la mise à jour est uniquement décidée en fonction des
   * valeurs calculées par {@link self::getItemValues()}. Bien que cette méthode
   * peut techniquement retourner de nouvelles valeurs pour la clé primaire, ça
   * risque de créer des doublons
   */
  function onCreate($item, array $values, ?array $alwaysNull): ?array {
    return null;
  }

  /**
   * méthode appelée lors du chargement avec {@link Capacitor::charge()} pour
   * mettre à jour un élément existant
   *
   * @param mixed $item l'élément à charger
   * @param array $values la nouvelle ligne, calculée à partir de $item et
   * des valeurs retournées par {@link getItemValues()}
   * @param array $pvalues la précédente ligne, chargée depuis la base de
   * données
   * @return ?array null s'il ne faut pas mettre à jour la ligne. sinon, ce
   * tableau est mergé dans $values puis utilisé pour mettre à jour la ligne
   * existante
   * Retourner [false] pour annuler le chargement (la ligne n'est pas mise à
   * jour)
   *
   * - Il est possible de mettre à jour $item en le retourant avec la clé "item"
   * - La clé primaire (il s'agit généralement de "id_") ne peut pas être
   * modifiée. si elle est retournée, elle est ignorée
   */
  function onUpdate($item, array $values, array $pvalues): ?array {
    return null;
  }

  /**
   * méthode appelée lors du parcours des éléments avec
   * {@link Capacitor::each()}
   *
   * @param mixed $item l'élément courant
   * @param ?array $values la ligne courante
   * @return ?array le cas échéant, un tableau non null utilisé pour mettre à
   * jour la ligne courante
   *
   * - Il est possible de mettre à jour $item en le retourant avec la clé "item"
   * - La clé primaire (il s'agit généralement de "id_") ne peut pas être
   * modifiée. si elle est retournée, elle est ignorée
   */
  function onEach($item, array $values): ?array {
    return null;
  }
  const onEach = "->".[self::class, "onEach"][1];

  /**
   * méthode appelée lors du parcours des éléments avec
   * {@link Capacitor::delete()}
   *
   * @param mixed $item l'élément courant
   * @param ?array $values la ligne courante
   * @return bool true s'il faut supprimer la ligne, false sinon
   */
  function onDelete($item, array $values): bool {
    return true;
  }
  const onDelete = "->".[self::class, "onDelete"][1];

  #############################################################################
  # Méthodes déléguées pour des workflows centrés sur le channel

  /**
   * @var Capacitor|null instance de Capacitor par laquelle cette instance est
   * utilisée
   */
  protected ?Capacitor $capacitor;

  function getCapacitor(): ?Capacitor {
    return $this->capacitor;
  }

  function setCapacitor(Capacitor $capacitor): self {
    $this->capacitor = $capacitor;
    return $this;
  }

  function charge($item, $func=null, ?array $args=null, ?array &$values=null): int {
    return $this->capacitor->charge($item, $func, $args, $values);
  }

  function discharge(bool $reset=true): Traversable {
    return $this->capacitor->discharge($reset);
  }

  function count($filter=null): int {
    return $this->capacitor->count($filter);
  }

  function one($filter, ?array $mergeQuery=null): ?array {
    return $this->capacitor->one($filter, $mergeQuery);
  }

  function all($filter, ?array $mergeQuery=null): Traversable {
    return $this->capacitor->all($filter, $mergeQuery);
  }

  function each($filter, $func=null, ?array $args=null, ?array $mergeQuery=null, ?int &$nbUpdated=null): int {
    return $this->capacitor->each($filter, $func, $args, $mergeQuery, $nbUpdated);
  }

  function delete($filter, $func=null, ?array $args=null): int {
    return $this->capacitor->delete($filter, $func, $args);
  }
}
