<?php
namespace nur;

use ArrayAccess;
use nur\b\coll\BaseArray;
use nur\b\coll\Flattener;
use nur\b\coll\IArray;
use Traversable;

/**
 * Class A: méthodes utilitaires pour gérer les array
 */
class A {
  static final function with($array): array { return SL::with($array); }
  static final function withn($array): ?array { return SL::withn($array); }

  /**
   * tester si $array est un tableau de type array, BaseArray ou Traversable.
   * tous ces objets peuvent être transformés en array par {@link with()}
   *
   * pour être sûr de pouvoir les parcourir en lecture seule, il faut les
   * transformer en array avec {@link with()}
   */
  static final function is_array($array): bool {
    if ($array === null || $array === false) return false;
    return is_iterable($array) || $array instanceof BaseArray;
  }

  static final function ensure_array(&$array): bool { return SL::ensure_array($array); }
  static final function ensure_narray(&$array): bool { return SL::ensure_narray($array); }

  /**
   * retourner une référence permettant de modifier $array en tant qu'array de
   * préférence ou en tant qu'instance de ArrayAccess au pire.
   */
  static final function &ensure_access(&$array) {
    if (is_array($array)) return $array;
    if ($array instanceof IArray) return $array->array();
    if ($array instanceof ArrayAccess) return $array;
    if ($array instanceof Traversable) $array = iterator_to_array($array);
    elseif ($array === null || $array === false) $array = [];
    else $array = [$array];
    return $array;
  }

  /**
   * s'assurer que $array est un tableau de $size éléments, en complétant avec
   * des occurrences de $default si nécessaire
   *
   * @return bool true si le tableau a été modifié, false sinon
   */
  static final function ensure_size(?array &$array, int $size, $default=null): bool {
    $modified = false;
    if ($array === null) {
      $array = [];
      $modified = true;
    }
    if ($size < 0) return $modified;
    $count = count($array);
    if ($count == $size) return $modified;
    if ($count < $size) {
      # agrandir le tableau
      while ($count++ < $size) {
        $array[] = $default;
      }
      return true;
    }
    # rétrécir le tableau
    $tmparray = [];
    foreach ($array as $key => $value) {
      if ($size-- == 0) break;
      $tmparray[$key] = $value;
    }
    $array = $tmparray;
    return true;
  }

  /** tester si $array contient la clé $key */
  static final function has(?array $array, $key): bool {
    return $array !== null && array_key_exists($key, $array);
  }

  /** retourner $array[$key] ou $default si la clé n'existe pas */
  static final function get(?array $array, $key, $default=null) {
    if ($array === null) return $default;
    elseif (array_key_exists($key, $array)) return $array[$key];
    else return $default;
  }

  /** s'assurer que $array est un array puis spécifier $array[$key] */
  static final function set(&$array, $key, $value): void {
    self::ensure_array($array);
    if ($key === null) $array[] = $value;
    else $array[$key] = $value;
  }

  /** s'assurer que $array est un array puis supprimer $array[$key] */
  static final function del(&$array, $key): void {
    if ($array === null || $array === false) return;
    self::ensure_array($array);
    unset($array[$key]);
  }

  /** retourner le nombre d'éléments de $array */
  static final function count(?array $array): int {
    return $array === null? 0: count($array);
  }

  /** retourner la liste des clés de $array */
  static final function keys(?array $array): array {
    return $array === null? []: array_keys($array);
  }

  static final function join(?array $array, string $sep=" "): string {
    return $array === null? "": implode($sep, $array);
  }

  #############################################################################

  /**
   * tester si $array est un tableau séquentiel.
   *
   * NB: un tableau vide est séquentiel
   */
  static final function is_seq($array): bool {
    if (!is_array($array)) return false;
    $count = count($array);
    if ($count == 0) return true;
    return array_keys($array) === range(0, $count - 1);
  }

  /**
   * tester si $array est un tableau associatif.
   *
   * NB: un tableau vide est associatif
   */
  static final function is_assoc($array): bool {
    if (!is_array($array)) return false;
    $count = count($array);
    if ($count == 0) return true;
    return array_keys($array) !== range(0, $count - 1);
  }

  #############################################################################

  /**
   * s'assurer que $array est un array puis y fusionner tous les autres tableaux
   * avec array_merge(). les clés numériques sont réordonnées, et les clés du
   * tableau destination sont écrasées par les clés correspondantes des tableaux
   * entrants. par exemple:
   * ~~~
   * $a = [10 => "a", 15 => "b",            "x" => "y"];
   * $b = [           15 => "c", 20 => "d", "x" => "z"];
   * A::merge($a, $b);
   * # $a vaut maintenant [0 => "a", 1 => "b", "x" => "z", 2 => "c", 3 => "d"]
   * ~~~
   *
   * parmi les tableaux de $arrays, ignorer les occurrences de null et false
   *
   * NB: dans $arrays, les valeurs scalaires sont traitées comme un singleton
   * [$value]. elles sont donc simplement ajoutées à $array comme avec la
   * commande "$array[] = $value;"
   */
  static final function merge(&$array, ...$arrays): void {
    self::ensure_array($array);
    if ($arrays) {
      $merges = [$array];
      foreach ($arrays as $merge) {
        if ($merge === null || $merge === false) continue;
        $merges[] = self::with($merge);
      }
      $array = array_merge(...$merges);
    }
  }

  /**
   * s'assurer que $array est un array puis y fusionner tous les autres tableaux
   * comme avec {@link merge()} mais sans réordonner les clés numériques. Cela
   * ressemble au comportement de l'opérateur union de PHP, mais la différence
   * est que les clés du tableau destination sont écrasées par les clés
   * correspondantes des tableaux entrants. par exemple:
   * ~~~
   * $a = [10 => "a", 15 => "b",            "x" => "y"];
   * $b = [           15 => "c", 20 => "d", "x" => "z"];
   * A::merge2($a, $b);
   * # $a vaut maintenant [10 => "a", 15 => "c", "x" => "z", 20 => "d"]
   * ~~~
   *
   * parmi les tableaux de $arrays, ignorer les occurrences de null et false
   *
   * NB: dans $arrays, les valeurs scalaires sont traitées comme un singleton
   * [$value]. elles écrasent donc la valeur à la clé '0' si celle-ci existe
   * déjà dans le tableau destination
   */
  static final function merge2(&$array, ...$arrays): void {
    self::ensure_array($array);
    foreach ($arrays as $merge) {
      if ($merge === null || $merge === false) continue;
      foreach (self::with($merge) as $key => $value) {
        $array[$key] = $value;
      }
    }
  }

  /**
   * s'assurer que $array est un array puis y fusionner tous les autres tableaux
   * un peu comme avec {@link merge()} mais en ne réordonnant *que* les clés
   * séquentielles. par exemple:
   * ~~~
   * $a = ["1st", "2nd",               10 => "a", 15 => "b",            "x" => "y"];
   * $b = [              "3rd", "4th",            15 => "c", 20 => "d", "x" => "z"];
   * A::merge3($a, $b);
   * # $a vaut maintenant ["1st", "2nd", 10 => "a", 15 => "c", "x" => "z", "3rd", "4th", 20 => "d"]
   * ~~~
   *
   * parmi les tableaux de $arrays, ignorer les occurrences de null et false
   */
  static final function merge3(&$array, ...$arrays): void {
    self::ensure_array($array);
    $desti = 0;
    foreach ($array as $key => $value) {
      if ($key === $desti) {
        $desti++;
      }
    }
    foreach ($arrays as $merge) {
      $srci = 0;
      if ($merge === null || $merge === false) continue;
      foreach (self::with($merge) as $key => $value) {
        if ($key === $srci) {
          $srci++;
          $array[$desti++] = $value;
        } else {
          $array[$key] = $value;
        }
      }
    }
  }

  /**
   * comme {@link merge()} mais dans chacun des tableaux sources, ceux de
   * $arrays, ignorer les valeurs null.
   */
  static final function merge_nn(&$array, ...$arrays): void {
    self::ensure_array($array);
    if ($arrays) {
      $merges = [$array];
      foreach ($arrays as $tmp) {
        if ($tmp === false || $tmp === null) continue;
        $merge = [];
        foreach (self::with($tmp) as $key => $value) {
          if ($value === null) continue;
          $merge[$key] = $value;
        }
        $merges[] = $merge;
      }
      $array = array_merge(...$merges);
    }
  }

  /**
   * comme {@link merge2()} mais dans chacun des tableaux sources, ceux de
   * $arrays, ignorer les valeurs null.
   */
  static final function merge_nn2(&$array, ...$arrays): void {
    self::ensure_array($array);
    foreach ($arrays as $merge) {
      if ($merge === false || $merge === null) continue;
      foreach (self::with($merge) as $key => $value) {
        if ($value === null) continue;
        $array[$key] = $value;
      }
    }
  }

  /**
   * comme {@link merge()} mais dans chacun des tableaux sources, ceux de
   * $arrays, ignorer les valeurs null et false.
   */
  static final function merge_nz(&$array, ...$arrays): void {
    self::ensure_array($array);
    if ($arrays) {
      $merges = [$array];
      foreach ($arrays as $tmp) {
        if ($tmp === false || $tmp === null) continue;
        $merge = [];
        foreach (self::with($tmp) as $key => $value) {
          if ($value === null || $value === false) continue;
          $merge[$key] = $value;
        }
        $merges[] = $merge;
      }
      $array = array_merge(...$merges);
    }
  }

  /**
   * comme {@link merge2()} mais dans chacun des tableaux sources, ceux de
   * $arrays, ignorer les valeurs null et false.
   */
  static final function merge_nz2(&$array, ...$arrays): void {
    self::ensure_array($array);
    foreach ($arrays as $merge) {
      if ($merge === false || $merge === null) continue;
      foreach (self::with($merge) as $key => $value) {
        if ($value === null || $value === false) continue;
        $array[$key] = $value;
      }
    }
  }

  /**
   * comme {@link merge()} mais pour chacun des tableaux sources, ceux de
   * $arrays, ne fusionner les valeurs dans la destination $array que si la clé
   * n'y existe pas
   */
  static final function update_nx(&$array, ...$arrays): void {
    self::ensure_array($array);
    if ($arrays) {
      $updates = [$array];
      foreach ($arrays as $tmp) {
        if ($tmp === false || $tmp === null) continue;
        $update = [];
        foreach (self::with($tmp) as $key => $value) {
          if (!array_key_exists($key, $array)) {
            $update[$key] = $value;
          }
        }
        $updates[] = $update;
      }
      $array = array_merge(...$updates);
    }
  }

  /**
   * comme {@link merge2()} mais pour chacun des tableaux sources, ceux de
   * $arrays, ne fusionner les valeurs dans la destination $array que si la clé
   * n'y existe pas
   *
   * NB: il s'agit de la définition du l'opérateur union. l'implémentation fait
   * d'ailleurs usage de cet opérateur
   */
  static final function update_nx2(&$array, ...$arrays): void {
    self::ensure_array($array);
    foreach ($arrays as $update) {
      if ($update === null || $update === false) continue;
      $array += self::with($update);
    }
  }

  /**
   * comme {@link merge()} mais pour chacun des tableaux sources, ceux de
   * $arrays, ne fusionner les valeurs dans la destination $array que si la clé
   * n'y existe pas ou, si elle y existe, que la valeur y est null
   */
  static final function update_n(&$array, ...$arrays): void {
    self::ensure_array($array);
    if ($arrays) {
      $updates = [$array];
      foreach ($arrays as $tmp) {
        if ($tmp === false || $tmp === null) continue;
        $update = [];
        foreach (self::with($tmp) as $key => $value) {
          if (!array_key_exists($key, $array)) {
            $update[$key] = $value;
          } elseif ($array[$key] === null) {
            $update[$key] = $value;
          }
        }
        $updates[] = $update;
      }
      $array = array_merge(...$updates);
    }
  }

  /**
   * comme {@link merge2()} mais pour chacun des tableaux sources, ceux de
   * $arrays, ne fusionner les valeurs dans la destination $array que si la clé
   * n'y existe pas ou, si elle y existe, que la valeur y est null
   */
  static final function update_n2(&$array, ...$arrays): void {
    self::ensure_array($array);
    foreach ($arrays as $update) {
      if ($update === false || $update === null) continue;
      foreach (self::with($update) as $key => $value) {
        if (!array_key_exists($key, $array)) {
          $array[$key] = $value;
        } elseif ($array[$key] === null) {
          $array[$key] = $value;
        }
      }
    }
  }

  /**
   * comme {@link merge()} mais pour chacun des tableaux sources, ceux de
   * $arrays, ne fusionner les valeurs dans la destination $array que si la clé
   * n'y existe pas ou, si elle y existe, que la valeur y est null ou false
   */
  static final function update_z(&$array, ...$arrays): void {
    self::ensure_array($array);
    if ($arrays) {
      $updates = [$array];
      foreach ($arrays as $tmp) {
        if ($tmp === false || $tmp === null) continue;
        $update = [];
        foreach (self::with($tmp) as $key => $value) {
          if (!array_key_exists($key, $array)) {
            $update[$key] = $value;
          } else {
            $rvalue = $array[$key];
            if ($rvalue === null || $rvalue === false) {
              $update[$key] = $value;
            }
          }
        }
        $updates[] = $update;
      }
      $array = array_merge(...$updates);
    }
  }

  /**
   * comme {@link merge2()} mais pour chacun des tableaux sources, ceux de
   * $arrays, ne fusionner les valeurs dans la destination $array que si la clé
   * n'y existe pas ou, si elle y existe, que la valeur y est null ou false
   */
  static final function update_z2(&$array, ...$arrays): void {
    self::ensure_array($array);
    foreach ($arrays as $tomerge) {
      if ($tomerge === false || $tomerge === null) continue;
      foreach (self::with($tomerge) as $key => $value) {
        if (!array_key_exists($key, $array)) {
          $array[$key] = $value;
        } else {
          $rvalue = $array[$key];
          if ($rvalue === null || $rvalue === false) {
            $array[$key] = $value;
          }
        }
      }
    }
  }

  #############################################################################
  ## get

  /** obtenir le premier élément du tableau */
  static final function first(?array $array, $default=null) {
    if (!$array) return $default;
    else return $array[array_key_first($array)];
  }

  /** obtenir le second élément du tableau */
  static final function second(?array $array, $default=null) {
    if (!$array) return $default;
    $first = true;
    foreach ($array as $value) {
      if ($first) $first = false;
      else return $value;
    }
    return $default;
  }

  /**
   * obtenir le n-ième élément du tableau, en commençant à 0 (i.e 0=premier,
   * 1=second, etc.)
   */
  static final function nth(?array $array, int $index, $default=null) {
    if (!$array) return $default;
    $i = 0;
    foreach ($array as $value) {
      if ($index == $i) return $value;
      $i++;
    }
    return $default;
  }

  /** obtenir la première clé du tableau */
  static final function first_key(?array $array, $default_key=null) {
    if (!$array) return $default_key;
    else return array_key_first($array);
  }

  /** obtenir la seconde clé du tableau */
  static final function second_key(?array $array, $default_key=null) {
    if (!$array) return $default_key;
    $first = true;
    foreach (array_keys($array) as $key) {
      if ($first) $first = false;
      else return $key;
    }
    return $default_key;
  }

  /**
   * obtenir la clé du n-ième élément du tableau, en commençant à 0 (i.e
   * 0=premier, 1=second, etc.)
   */
  static final function nth_key(?array $array, int $index, $default_key=null) {
    if (!$array) return $default_key;
    $i = 0;
    foreach (array_keys($array) as $key) {
      if ($index == $i) return $key;
      $i++;
    }
    return $default_key;
  }

  /** obtenir le dernier élément du tableau */
  static final function last(?array $array, $default=null) {
    if (!$array) return $default;
    else return $array[array_key_last($array)];
  }

  /** obtenir la dernière clé du tableau */
  static final function last_key(?array $array, $default_key=null) {
    if (!$array) return $default_key;
    else return array_key_last($array);
  }

  /**
   * si $array est un tableau séquentiel avec un seul élément, retourner cet élément.
   * si c'est un tableau vide ou null retourner $default
   * sinon retourner le tableau inchangé
   */
  static final function one_or_array(?array $array, $default=null) {
    if (!$array) {
      return $default;
    } elseif (count($array) == 1 && array_key_exists(0, $array)) {
      return $array[0];
    } else {
      return $array;
    }
  }

  /**
   * retourner un tableau avec pour chaque tableau $array de $arrays, la valeur
   * de get($array, $key, $default)
   */
  static final function each_get(?array $arrays, $key, $default=null): array {
    $values = [];
    foreach ($arrays as $index => $array) {
      $values[$index] = self::get($array, $key, $default);
    }
    return $values;
  }

  #############################################################################
  ## set

  /**
   * Mettre à jour la clé $key avec $value si $value !== null
   *
   * si aucune modification ne doit se faire, $array n'est pas modifié
   *
   * Retourner la valeur effective de la clé (qu'elle aie été mise à jour ou
   * non)
   */
  static final function set_nn(&$array, $key, $value) {
    if ($value !== null) {
      self::ensure_array($array);
      self::set($array, $key, $value);
      return $value;
    } else {
      return self::get($array, $key);
    }
  }

  /**
   * Mettre à jour la clé $key avec $array[$key_indirect] si cette valeur ne
   * vaut pas null
   *
   * Retourner la valeur effective de la clé (qu'elle aie été mise à jour ou
   * non)
   */
  static final function set_nn_indirect(&$array, $key, $key_indirect) {
    self::ensure_narray($array);
    $value = self::get($array, $key_indirect);
    return self::set_nn($array, $key, $value);
  }

  /**
   * Mettre à jour la clé $key avec $value si $value ne vaut ni null ni false
   *
   * si aucune modification ne doit se faire, $array n'est pas modifié
   *
   * Retourner la valeur effective de la clé (qu'elle aie été mise à jour ou
   * non)
   */
  static final function set_nz(&$array, $key, $value) {
    if ($value !== false && $value !== null) {
      self::set($array, $key, $value);
      return $value;
    } else {
      return self::get($array, $key);
    }
  }

  /**
   * Mettre à jour la clé $key avec $array[$key_indirect] si cette valeur ne
   * vaut ni null ni false
   *
   * Retourner la valeur effective de la clé (qu'elle aie été mise à jour ou
   * non)
   */
  static final function set_nz_indirect(&$array, $key, $key_indirect) {
    self::ensure_narray($array);
    $value = self::get($array, $key_indirect);
    return self::set_nz($array, $key, $value);
  }

  /**
   * Ajouter la valeur avec la clé spécifiée si elle n'existe pas déjà
   *
   * si aucune modification ne doit se faire, $array n'est pas modifié
   *
   * Retourner la valeur effective de la clé (qu'elle aie été mise à jour ou
   * non)
   */
  static final function replace_nx(&$array, $key, $value) {
    if (!is_array($array) || !array_key_exists($key, $array)) {
      self::set($array, $key, $value);
      return $value;
    } else {
      return self::get($array, $key);
    }
  }

  /**
   * Mettre à jour la clé $key avec $value si la clé $key n'existe pas ou si sa
   * valeur actuelle est null.
   *
   * Retourner la valeur effective de la clé (qu'elle aie été mise à jour ou
   * non)
   */
  static final function replace_n(&$array, $key, $value) {
    self::ensure_narray($array);
    $rvalue = self::get($array, $key);
    if ($rvalue === null) {
      self::set($array, $key, $value);
      $rvalue = $value;
    }
    return $rvalue;
  }

  /**
   * Mettre à jour la clé $key avec $array[$key_indirect] si cette clé n'existe
   * pas ou si sa valeur actuelle est null.
   *
   * Retourner la valeur effective de la clé (qu'elle aie été mise à jour ou
   * non)
   */
  static final function replace_n_indirect(&$array, $key, $key_indirect) {
    self::ensure_narray($array);
    $rvalue = self::get($array, $key);
    if ($rvalue === null) {
      $value = self::get($array, $key_indirect);
      self::set($array, $key, $value);
      $rvalue = $value;
    }
    return $rvalue;
  }

  /**
   * Mettre à jour la clé $key avec $value si la clé $key n'existe pas ou si sa
   * valeur actuelle est null ou false.
   *
   * Retourner la valeur effective de la clé (qu'elle aie été mise à jour ou
   * non)
   */
  static final function replace_z(&$array, $key, $value) {
    self::ensure_narray($array);
    $rvalue = self::get($array, $key);
    if ($rvalue === false || $rvalue === null) {
      self::set($array, $key, $value);
      $rvalue = $value;
    }
    return $rvalue;
  }

  /**
   * Mettre à jour la clé $key avec $array[$key_indirect] si cette clé n'existe
   * pas ou si sa valeur actuelle est null ou false.
   *
   * Retourner la valeur effective de la clé (qu'elle aie été mise à jour ou
   * non)
   */
  static final function replace_z_indirect(&$array, $key, $key_indirect) {
    self::ensure_narray($array);
    $rvalue = self::get($array, $key);
    if ($rvalue === false || $rvalue === null) {
      $value = self::get($array, $key_indirect);
      self::set($array, $key, $value);
      $rvalue = $value;
    }
    return $rvalue;
  }

  /**
   * Ajouter $value à la fin de $array
   *
   * Retourner la valeur ajoutée
   */
  static final function append(&$array, $value) {
    self::ensure_array($array);
    $array[] = $value;
    return $value;
  }

  /**
   * Si $value n'est pas null, ajouter $value à la fin de $array
   *
   * si aucune modification ne doit se faire, $array n'est pas modifié
   *
   * Retourner la valeur ajoutée, ou null si la valeur n'a pas été ajoutée
   */
  static final function append_nn(&$array, $value) {
    if ($value !== null) {
      self::ensure_array($array);
      $array[] = $value;
      return $value;
    } else {
      return null;
    }
  }

  /**
   * Si $value ne vaut ni null ni false, ajouter $value à la fin de $array
   *
   * si aucune modification ne doit se faire, $array n'est pas modifié
   *
   * Retourner la valeur ajoutée, ou null si la valeur n'a pas été ajoutée
   */
  static final function append_nz(&$array, $value) {
    if (base::nz($value)) {
      self::ensure_array($array);
      $array[] = $value;
      return $value;
    } else {
      return null;
    }
  }

  /**
   * Insérer $value au début de $array
   *
   * Retourner la valeur insérée
   */
  static final function prepend(&$array, $value) {
    self::ensure_array($array);
    array_unshift($array, $value);
    return $value;
  }

  /**
   * Si $value n'est pas null, insérer $value au début de $array
   *
   * si aucune modification ne doit se faire, $array n'est pas modifié
   *
   * Retourner la valeur ajoutée, ou null si la valeur n'a pas été ajoutée
   */
  static final function prepend_nn(&$array, $value) {
    if ($value !== null) {
      self::ensure_array($array);
      array_unshift($array, $value);
      return $value;
    } else {
      return null;
    }
  }

  /**
   * Si $value ne vaut ni null ni false, insérer $value au début de $array
   *
   * si aucune modification ne doit se faire, $array n'est pas modifié
   *
   * Retourner la valeur ajoutée, ou null si la valeur n'a pas été ajoutée
   */
  static final function prepend_nz(&$array, $value) {
    if (base::nz($value)) {
      self::ensure_array($array);
      array_unshift($array, $value);
      return $value;
    } else {
      return null;
    }
  }

  /**
   * dans le tableau séquentiel $array, insérer $value à la position $index et
   * décaler toutes les autres clés
   *
   * Retourner la valeur insérée
   */
  static final function insert(&$array, $index, $value) {
    self::ensure_array($array);
    $count = count($array);
    if ($count == 0 || $index >= $count) {
      $array[] = $value;
    } else {
      while ($index < 0) $index += $count;
      if ($index == 0) $prefix = [];
      else $prefix = array_slice($array, 0, $index);
      $suffix = array_slice($array, $index);
      $array = array_merge($prefix, [$value], $suffix);
    }
    return $value;
  }

  /** Ajouter une valeur dans le tableau, pour utilisation avec pop() */
  static final function push(&$array, $value): void {
    self::ensure_array($array);
    $array[] = $value;
  }

  /**
   * enlever la dernière valeur ajoutée dans le tableau avec push() et la
   * retourner
   */
  static final function pop(&$array, $default=null) {
    self::ensure_array($array);
    $count = count($array);
    if ($count > 0) {
      $value = array_pop($array);
    } else {
      $value = $default;
    }
    return $value;
  }

  #############################################################################
  ## del

  static final function del_value(&$array, $value, int $max_count=1, bool $strict=false): int {
    if ($array === null || $array === false) return 0;
    self::ensure_array($array);
    $count = 0;
    $rekey = null;
    while ($max_count <= 0 || $count < $max_count) {
      $key = array_search($value, $array, $strict);
      if ($key === false) break;
      # s'il faut supprimer des clés, vérifier d'abord si c'est un tableau
      # séquentiel, afin de refaire la numérotation le cas échéant
      if ($rekey === null) $rekey = self::is_seq($array);
      unset($array[$key]);
      $count++;
    }
    if ($rekey) $array = array_values($array);
    return $count;
  }

  static final function del_first_key(&$array, int $max_count=1): int {
    if ($array === null || $array === false) return 0;
    self::ensure_array($array);
    $count = 0;
    $rekey = null;
    while ($max_count <= 0 || $count < $max_count) {
      $key = array_key_first($array);
      if ($key === null) break;
      # s'il faut supprimer des clés, vérifier d'abord si c'est un tableau
      # séquentiel, afin de refaire la numérotation le cas échéant
      if ($rekey === null) $rekey = self::is_seq($array);
      unset($array[$key]);
      $count++;
    }
    if ($rekey) $array = array_values($array);
    return $count;
  }

  static final function del_last_key(&$array, int $max_count=1): int {
    if ($array === null || $array === false) return 0;
    self::ensure_array($array);
    $count = 0;
    while ($max_count <= 0 || $count < $max_count) {
      $key = array_key_last($array);
      if ($key === null) break;
      unset($array[$key]);
      $count++;
    }
    return $count;
  }

  static final function del_keys(&$array, ...$keys): void {
    if ($array === null || $array === false) return;
    self::ensure_array($array);
    foreach ($keys as $key) {
      unset($array[$key]);
    }
  }

  #############################################################################
  ## getdel

  /**
   * obtenir la valeur correspondant à la clé $key, ou $default si elle n'est
   * pas trouvée. puis supprimer la clé du tableau
   */
  static final function getdel(&$array, $key, $default=null) {
    self::ensure_array($array);
    $value = self::get($array, $key, $default);
    unset($array[$key]);
    return $value;
  }

  /**
   * obtenir les valeurs correspondantes aux clés $keys sous forme de tableau
   * séquentiel. supprimer les clés du tableau
   */
  static final function getdels(&$array, ?array $keys): array {
    self::ensure_array($array);
    $values = [];
    if ($keys !== null) {
      foreach ($keys as $key) {
        $values[] = self::get($array, $key);
        unset($array[$key]);
      }
    }
    return $values;
  }

  #############################################################################
  ## Chemins de clé

  /**
   * vérifier que le chemin $keys fourni sous forme de tableau existe dans le
   * tableau $array
   *
   * si $keys est vide ou null, retourner true
   */
  static final function _phas(?array $array, ?array $keys): bool {
    $first = true;
    foreach($keys as $key) {
      if ($key === "" && $first) {
        # une chaine vide en première position est ignorée
        continue;
      } elseif (is_array($array)) {
        if (!array_key_exists($key, $array)) return false;
        $array = $array[$key];
      } elseif ($array instanceof ArrayAccess) {
        if (!$array->offsetExists($key)) return false;
        $array = $array->offsetGet($key);
      } else {
        return false;
      }
      $first = false;
    }
    return true;
  }

  /**
   * Vérifier que le chemin $keys qui est de la forme key[.keys...] existe
   * dans le tableau $array
   *
   * si $keys === null, retourner true
   */
  static final function phas_s(?array $array, ?string $keys): bool {
    if ($keys === null) return true;
    $keys = explode(".", $keys);
    return self::_phas($array, $keys);
  }

  /**
   * Vérifier que le chemin $keys fourni sous forme de tableau existe dans le
   * tableau $array
   *
   * si $keys est nul ou vide, retourner true
   */
  static final function phas_a(?array $array, ?array $keys): bool {
    if (!$keys) return true;
    $keys = implode(".", $keys);
    $keys = explode(".", $keys);
    return self::_phas($array, $keys);
  }

  /** vérifier que le chemin $keys existe dans le tableau $array */
  static final function phas($array, $keys): bool {
    if ($keys === null) return true;
    elseif (is_array($keys)) return self::phas_a($array, $keys);
    else return self::phas_s($array, strval($keys));
  }

  /**
   * Parcourir les enfants de $array avec le chemin $keys fourni sous forme de
   * tableau et retourner la valeur correspondante
   *
   * si $keys est vide ou null, retourner $default
   */
  static final function _pget(?array $array, ?array $keys, $default=null) {
    if (!$keys) return $default;
    $value = $array;
    $first = true;
    foreach($keys as $key) {
      if ($key === "" && $first) {
        # une chaine vide en première position est ignorée
        continue;
      } elseif (is_array($value)) {
        if (!array_key_exists($key, $value)) return $default;
        $value = $value[$key];
      } elseif ($value instanceof ArrayAccess) {
        if (!$value->offsetExists($key)) return $default;
        $value = $value->offsetGet($key);
      } else {
        return $default;
      }
      $first = false;
    }
    return $value;
  }

  /**
   * Parcourir les enfants de $array avec le chemin $keys qui est de la forme
   * key[.keys...] et retourner la valeur correspondante
   *
   * si $keys === null, retourner $default
   */
  static final function pget_s(?array $array, ?string $keys, $default=null) {
    if ($keys === null) return $default;
    $keys = explode(".", $keys);
    return self::_pget($array, $keys, $default);
  }

  /**
   * Parcourir les enfants de $array avec le chemin $keys fourni sous forme de
   * tableau et retourner la valeur correspondante
   *
   * si $keys est nul ou vide, retourner $default
   */
  static final function pget_a(?array $array, ?array $keys, $default=null) {
    if (!$keys) return $default;
    $keys = implode(".", $keys);
    $keys = explode(".", $keys);
    return self::_pget($array, $keys, $default);
  }

  /** obtenir la valeur correspondant au chemin $keys dans $array */
  static final function pget($array, $keys, $default=null) {
    if ($keys === null) return $default;
    elseif (is_array($keys)) return self::pget_a($array, $keys, $default);
    else return self::pget_s($array, strval($keys), $default);
  }

  /**
   * Modifier la valeur au chemin $keys fourni sous forme de tableau
   *
   * utiliser la clé "" (chaine vide) en dernière position pour rajouter à la fin, e.g
   * - _pset($array, [""], $value) est équivalent à $array[] = $value
   * - _pset($array, ["a", "b", ""], $value) est équivalent à $array["a"]["b"][] = $value
   * la clé "" n'a pas de propriété particulière quand elle n'est pas en dernière position
   *
   * si $keys est vide ou null, $array est remplacé par $value
   */
  static final function _pset(&$array, ?array $keys, $value): void {
    if (!$keys) {
      $array = $value;
      return;
    }
    self::ensure_array($array);
    $current =& $array;
    $last = count($keys) - 1;
    $i = 0;
    foreach ($keys as $key) {
      if ($i == $last) break;
      if ($current instanceof ArrayAccess) {
        if (!$current->offsetExists($key)) $current->offsetSet($key, []);
        $current =& $current->offsetGet($key);
        if ($current === null) {
          $current = [];
        } elseif (!is_array($current) && !($current instanceof ArrayAccess)) {
          $current = [$current];
        }
      } else {
        self::ensure_array($current[$key]);
        $current =& $current[$key];
      }
      $i++;
    }

    if ($key === "") $current[] = $value;
    else $current[$key] = $value;
  }

  /**
   * Modifier la valeur au chemin $keys qui est de la forme key[.keys...]
   *
   * utiliser la clé "" (chaine vide) en dernière position pour rajouter à la fin, e.g
   * - pset($array, "", $value) est équivalent à $array[] = $value
   * - pset($array, "a.b.", $value) est équivalent à $array["a"]["b"][] = $value
   * la clé "" n'a pas de propriété particulière quand elle n'est pas en dernière position
   *
   * si $keys est null, $array est remplacé par $value
   */
  static final function pset_s(&$array, ?string $keys, $value): void {
    if ($keys === null) {
      $array = $value;
    } else {
      $keys = explode(".", $keys);
      self::_pset($array, $keys, $value);
    }
  }

  /**
   * Modifier la valeur au chemin $keys fourni sous forme de tableau
   *
   * utiliser la clé "" (chaine vide) en dernière position pour rajouter à la fin, e.g
   * - pset($array, "", $value) est équivalent à $array[] = $value
   * - pset($array, "a.b.", $value) est équivalent à $array["a"]["b"][] = $value
   * la clé "" n'a pas de propriété particulière quand elle n'est pas en dernière position
   *
   * si $keys est null, $array est remplacé par $value
   */
  static final function pset_a(&$array, ?array $keys, $value): void {
    if (!$keys) {
      $array = $value;
    } else {
      $keys = implode(".", $keys);
      $keys = explode(".", $keys);
      self::_pset($array, $keys, $value);
    }
  }

  /** modifier la valeur au chemin de clé $keys dans le tableau $array */
  static final function pset(&$array, $keys, $value): void {
    if ($keys === null) $array = $value;
    elseif (is_array($keys)) self::pset_a($array, $keys, $value);
    else self::pset_s($array, strval($keys), $value);
  }

  /**
   * supprimer la valeur au chemin $keys fourni sous forme de tableau
   *
   * si $array vaut null ou false, sa valeur est inchangée.
   * $keys est vide ou null, $array devient null
   */
  static final function _pdel(&$array, ?array $keys): void {
    if ($array === false || $array === null) return;
    if (!$keys) {
      $array = null;
      return;
    }
    self::ensure_array($array);
    $current =& $array;
    $last = count($keys) - 1;
    $i = 0;
    foreach ($keys as $key) {
      if ($i == $last) break;
      if ($current instanceof ArrayAccess) {
        if (!$current->offsetExists($key)) break;
      } elseif (is_array($current)) {
        if (!self::has($current, $key)) break;
      } else {
        break;
      }
      $current =& $current[$key];
      $i++;
    }
    if ($i == $last) {
      if ($current instanceof ArrayAccess) {
        $current->offsetUnset($key);
      } elseif (is_array($current)) {
        unset($current[$key]);
      }
    }
  }

  /**
   * supprimer la valeur au chemin $keys qui est de la forme key[.keys...]
   *
   * si $array vaut null ou false, sa valeur est inchangée.
   * si $keys est null, $array devient null
   */
  static final function pdel_s(&$array, ?string $keys): void {
    if ($array === false || $array === null) return;
    if ($keys === null) {
      $array = null;
    } else {
      $keys = explode(".", $keys);
      self::_pdel($array, $keys);
    }
  }

  /**
   * supprimer la valeur au chemin $keys fourni sous forme de tableau
   *
   * si $array vaut null ou false, sa valeur est inchangée.
   * si $keys est vide ou null, $array devient null
   */
  static final function pdel_a(&$array, ?array $keys): void {
    if ($array === false || $array === null) return;
    if (!$keys) {
      $array = null;
    } else {
      $keys = implode(".", $keys);
      $keys = explode(".", $keys);
      self::_pdel($array, $keys);
    }
  }

  /** supprimer la valeur au chemin de clé $keys dans $array */
  static final function pdel(&$array, $keys): void {
    if ($array === false || $array === null) return;
    if ($keys === null) $array = null;
    elseif (is_array($keys)) self::pdel_a($array, $keys);
    else self::pdel_s($array, strval($keys));
  }

  #############################################################################
  ## map

  static final function map(?array $array, callable $func): ?array {
    if ($array === null) return null;
    return array_map($func, $array);
  }

  #############################################################################
  ## filter

  static final function filter_n(?array $array): ?array { return SL::filter_n($array); }
  static final function filter_z(?array $array): ?array { return SL::filter_z($array); }
  static final function filter_f(?array $array): ?array { return SL::filter_f($array); }
  static final function filter_f2(?array $array): ?array { return SL::filter_pf($array); }

  #############################################################################
  ## Tests sur les valeurs

  static final function any_v(?array $array, $value, bool $strict=true): bool {
    if ($strict) return SL::any_same($array, $value);
    else return SL::any_equals($array, $value);
  }
  static final function all_v(?array $array, $value, bool $strict=true): bool {
    if ($strict) return SL::all_same($array, $value);
    else return SL::all_equals($array, $value);
  }
  static final function any_nv(?array $array, $value, bool $strict=true): bool {
    if ($strict) return SL::any_not_same($array, $value);
    else return SL::any_not_equals($array, $value);
  }
  static final function all_nv(?array $array, $value, bool $strict=true): bool {
    if ($strict) return SL::all_not_same($array, $value);
    else return SL::all_not_equals($array, $value);
  }
  static final function any_z(?array $array): bool { return SL::any_z($array); }
  static final function all_z(?array $array): bool { return SL::all_z($array); }
  static final function any_nz(?array $array): bool { return SL::any_nz($array); }
  static final function all_nz(?array $array): bool { return SL::all_nz($array); }
  static final function any_n(?array $array): bool { return SL::any_n($array); }
  static final function all_n(?array $array): bool { return SL::all_n($array); }
  static final function any_nn(?array $array): bool { return SL::any_nn($array); }
  static final function all_nn(?array $array): bool { return SL::all_nn($array); }
  static final function any_f(?array $array): bool { return SL::any_f($array); }
  static final function all_f(?array $array): bool { return SL::all_f($array); }
  static final function any_t(?array $array): bool { return SL::any_t($array); }
  static final function all_t(?array $array): bool { return SL::all_t($array); }
  static final function any_f2(?array $array): bool { return SL::any_pf($array); }
  static final function all_f2(?array $array): bool { return SL::all_pf($array); }
  static final function any_t2(?array $array): bool { return SL::any_pt($array); }
  static final function all_t2(?array $array): bool { return SL::all_pt($array); }

  #############################################################################
  ## Fonctions avancées

  /** dans le tableau $array, "renommer" les clés selon le tableau $key_map */
  static function map_keys(?array &$array, ?array $key_map): void {
    if ($array === null || $key_map === null) return;
    foreach ($key_map as $from => $to) {
      if (array_key_exists($from, $array)) {
        $array[$to] = $array[$from];
        unset($array[$from]);
      }
    }
  }

  private static $flattener;

  /**
   * Applatir le tableau $array
   *
   * Pour chaque élément avec une clé séquentielle:
   * - si c'est un tableau, l'applatir puis rajouter ses éléments tels quels au
   * résultat
   * - sinon ajouter l'élément tel quel
   *
   * Pour chaque élément avec une clé associative:
   * - si la valeur n'existe pas déjà, elle est rajoutée telle quelle
   * - si la valeur source est null ou false, la valeur destination n'est pas
   * modifiée
   * - sinon, les deux valeurs sont transformées en tableau le cas échéant.
   * si $flattenValue == true, alors le tableau source est applati au préalable.
   * puis cette nouvelle valeur est fusionnée avec array_merge() dans la valeur
   * précédente.
   */
  static final function flatten(?array &$array, bool $flattenValue=true): void {
    if (self::$flattener === null) self::$flattener = new Flattener();
    self::$flattener->flatten($array, $flattenValue);
  }

  /** retourner le tableau $array applati */
  static final function flattened(?array $array, bool $flattenValue=true): array {
    self::flatten($array, $flattenValue);
    return $array;
  }

  /**
   * Extraire d'un tableau les clés séquentielles
   *
   * Retourner $seq où $seq est un tableau avec uniquement les valeurs des clés
   * séquentielles. S'il n'existe aucune clé séquentielle retourner $default.
   *
   * Par exemple: extract_seq(["a", "b" => "c"]) retourne ["a"]
   */
  static final function extract_seq(?array $array, ?array $default=null): ?array {
    $seq = null;
    if ($array !== null) {
      $index = 0;
      foreach ($array as $key => $value) {
        if ($key === $index) {
          $seq[] = $value;
          $index++;
        }
      }
    }
    if ($seq === null) $seq = $default;
    return $seq;
  }

  /**
   * Extraire d'un tableau les clés associatives
   *
   * Retourner une liste $assoc où $assoc est un tableau avec uniquement les
   * valeurs des clés associatives. S'il n'existe aucune clé associative,
   * retourner $default.
   *
   * Par exemple: split_assoc(["a", "b" => "c"]) retourne ["b" => "c"]
   */
  static final function extract_assoc(?array $array, ?array $default=null): ?array {
    $assoc = null;
    if ($array !== null) {
      $index = 0;
      foreach ($array as $key => $value) {
        if ($key === $index) $index++;
        else $assoc[$key] = $value;
      }
    }
    if ($assoc === null) $assoc = $default;
    return $assoc;
  }

  /**
   * Extraire d'un tableau les clés séquentielles et les clés associatives
   *
   * Retourner une liste [$seq, $assoc] où $seq est un tableau avec uniquement
   * les valeurs des clés séquentielles et $assoc est un tableau avec uniquement
   * les valeurs des clés associatives. S'il n'existe aucune clé séquentielle
   * (resp. aucune clé associative), $seq (resp. $assoc) vaut null.
   *
   * Par exemple: split_assoc(["a", "b" => "c"]) retourne [["a"], ["b" => "c"]]
   */
  static final function split_assoc(?array $array): array {
    $seq = null;
    $assoc = null;
    if ($array !== null) {
      $i = 0;
      foreach ($array as $key => $value) {
        if ($key === $i) {
          $seq[] = $value;
          $i++;
        } else {
          $assoc[$key] = $value;
        }
      }
    }
    return [$seq, $assoc];
  }

  /**
   * Joindre en un seul tableau un tableau avec des clés séquentielles et un
   * tableau avec des clés associatives.
   *
   * Si $seq_first==true, les clés séquentielles arrivent d'abord, ensuite les
   * clés associatives. Sinon, ce sont les clés associatives qui arrivent d'abord
   */
  static final function merge_assoc(?array &$array, ?array $seq, ?array $assoc, bool $seq_first=false): void {
    if ($seq === null && $assoc === null) $array = [];
    elseif ($seq === null) $array = $assoc;
    elseif ($assoc === null) $array = $seq;
    elseif ($seq_first) $array = array_merge($seq, $assoc);
    else $array = array_merge($assoc, $seq);
  }

  /**
   * Construire un sous-ensemble du tableau $array en sélectionnant les clés
   * mentionnées dans $keys
   * .. si $keys === null, retourner $array
   * .. sinon, $keys est un tableau avec des clés séquentielles ou associatives.
   * chacune des clés séquentielles est prise telle quelle. les clés associatives
   * permettent de renommer les clés
   *
   * soit $array = ["a" => 1, "b" => 2, "c" => 3]
   * alors select($array, ["a", "b" => "x"])
   * retourne ["a" => 1, "x" => 2]
   */
  static final function select(?array $array, ?array $keys, $default=null): array {
    if ($array === null) $array = [];
    if ($keys === null) return $array;

    $index = 0;
    $result = [];
    foreach ($keys as $key => $tkey) {
      if ($key === $index) {
        # clé séquentielle
        $value = self::get($array, $tkey, $default);
        $index++;
      } else {
        # clé associative
        $value = self::get($array, $key, $default);
      }
      $result[$tkey] = $value;
    }
    return $result;
  }

  /**
   * construire un sous-ensemble du tableau $array en sélectionnant les clés
   * mentionnées dans $keys.
   * .. si $keys === null, retourner $array
   * .. sinon, $keys est un tableau avec des clés séquentielles ou associatives.
   * pour chacune des clés séquentielles, la valeur est une clé pour récupérer
   * la valeur dans $array
   * pour chacune des clés associatives, la valeur est celle fournie
   */
  static final function select_replace(?array $array, ?array $keys, $default=null): array {
    if ($array === null) $array = [];
    if ($keys === null) return $array;

    $index = 0;
    $result = [];
    foreach ($keys as $key => $value) {
      if ($key === $index) {
        # clé séquentielle
        $result[$value] = self::get($array, $value, $default);
        $index++;
      } else {
        # clé associative
        $result[$key] = $value;
      }
    }
    return $result;
  }

  /**
   * comme {@link select_replace()} mais $keys est applati d'abord
   *
   * par exemple:
   * ~~~php
   * $src = ["a" => 1, "b" => 2, "c" => 3];
   * $dest = A::select($src, ["a", "x" => 9, ["z" => 8, "c"], "d"]);
   * # $dest === ["a" => 1, "x" => 9, "z" => 8, "c" => 3, "d" => null]
   * ~~~
   */
  static final function select_replace2(?array $array, ?array $keys, $default=null): array {
    if ($array === null) $array = [];
    if ($keys === null) return $array;
    self::flatten($keys);
    return self::select_replace($array, $keys, $default);
  }

  /**
   * construire un sous-ensemble du tableau $array en sélectionnant les clés
   * mentionnées dans $keys.
   * .. si $keys === null, retourner $array
   * .. sinon, $keys est un tableau avec des clés séquentielles ou associatives.
   * pour chacune des clés séquentielles, la valeur est une clé pour récupérer
   * la valeur dans $array
   * pour chacune des clés associatives, la valeur fournie est prise par défaut
   * si la valeur correspondante n'existe pas ou vaut false dans $array
   */
  static final function select_default(?array $array, ?array $keys, $default=null): array {
    if ($array === null) $array = [];
    if ($keys === null) return $array;

    $index = 0;
    $result = [];
    foreach ($keys as $key => $value) {
      if ($key === $index) {
        # clé séquentielle
        $result[$value] = self::get($array, $value, $default);
        $index++;
      } else {
        # clé associative
        if (array_key_exists($key, $array)) {
          $arrayValue = $array[$key];
          if ($arrayValue !== false) $value = $arrayValue;
        }
        $result[$key] = $value;
      }
    }
    return $result;
  }

  /**
   * comme {@link select_default()} mais $keys est applati d'abord
   *
   * par exemple:
   * ~~~php
   * $src = ["a" => 1, "b" => 2, "c" => 3];
   * $dest = A::select($src, ["a", "x" => 9, ["z" => 8, "c"], "d"]);
   * # $dest === ["a" => 1, "x" => 9, "z" => 8, "c" => 3, "d" => null]
   * ~~~
   */
  static final function select_default2(?array $array, ?array $keys, $default=null): array {
    if ($array === null) $array = [];
    if ($keys === null) return $array;
    self::flatten($keys);
    return self::select_default($array, $keys, $default);
  }

  /**
   * Construire un sous-ensemble du tableau $array en sélectionnant les clés
   * mentionnées dans $includes et pas mentionnées dans $excludes
   *
   * soit $array = ["a" => 1, "b" => 2, "c" => 3, "d" => 4]
   * alors xselect($array, ["a", "b" => "x"], ["d"])
   * retourne ["a" => 1, "x" => 2]
   */
  static final function xselect_keys(?array $array, ?array $includes, ?array $excludes, $default=null): array {
    if ($array === null) $array = [];
    if ($includes === null) {
      if ($excludes === null) return $array;
      else $includes = array_keys($array);
    }

    $index = 0;
    $result = [];
    foreach ($includes as $fromkey => $tokey) {
      if ($fromkey === $index) {
        # clé séquentielle
        $index++;
        $fromkey = $tokey;
      }
      if ($excludes !== null && in_array($fromkey, $excludes)) continue;
      $value = self::get($array, $fromkey, $default);
      $result[$tokey] = $value;
    }
    return $result;
  }

  /**
   * Construire un sous-ensemble du tableau $array en sélectionnant les valeurs
   * mentionnées dans $includes et pas mentionnées dans $excludes
   *
   * soit $array = ["a" => 1, "b" => 2, "c" => 3, "d" => 4]
   * alors xselect($array, ["a", "b" => "x"], ["d"])
   * retourne ["a" => 1, "x" => 2]
   */
  static final function xselect(?array $array, ?array $includes, ?array $excludes, $default=null): array {
    if ($array === null) $array = [];
    if ($includes === null && $excludes === null) return $array;

    $result = [];
    foreach ($array as $key => $value) {
      if ($excludes !== null && in_array($value, $excludes)) continue;
      if ($includes !== null && !in_array($value, $includes)) continue;
      $result[$key] = $value;
    }
    return $result;
  }
}