<?php
namespace nur\sery;

use ArrayAccess;
use nur\sery\php\nur_func;
use Traversable;

/**
 * Class cl: gestion de tableaux ou d'instances de {@link ArrayAccess} le cas
 * échéant
 *
 * contrairement à {@link A}, les méthodes de cette classes sont plutôt conçues
 * pour retourner un nouveau tableau
 */
class cl {
  /**
   * retourner un array avec les éléments retournés par l'itérateur. les clés
   * numériques sont réordonnées, les clés chaine sont laissées en l'état
   */
  static final function all(?iterable $iterable): array {
    if ($iterable === null) return [];
    if (is_array($iterable)) return $iterable;
    $array = [];
    foreach ($iterable as $key => $value) {
      if (is_int($key)) $array[] = $value;
      else $array[$key] = $value;
    }
    return $array;
  }

  /**
   * construire un tableau avec le résultat de $row[$key] pour chaque élément
   * de $rows
   */
  static function all_get($key, ?iterable $rows): array {
    $array = [];
    if ($rows !== null) {
      foreach ($rows as $row) {
        $array[] = self::get($row, $key);
      }
    }
    return $array;
  }

  /**
   * retourner la première valeur de $array ou $default si le tableau est null
   * ou vide
   */
  static final function first(?iterable $iterable, $default=null) {
    if (is_array($iterable)) {
      $key = array_key_first($iterable);
      if ($key === null) return $default;
      return $iterable[$key];
    }
    if (is_iterable($iterable)) {
      foreach ($iterable as $value) {
        return $value;
      }
    }
    return $default;
  }

  /**
   * retourner la dernière valeur de $array ou $default si le tableau est null
   * ou vide
   */
  static final function last(?iterable $iterable, $default=null) {
    if (is_array($iterable)) {
      $key = array_key_last($iterable);
      if ($key === null) return $default;
      return $iterable[$key];
    }
    $value = $default;
    if (is_iterable($iterable)) {
      foreach ($iterable as $value) {
        # parcourir tout l'iterateur pour avoir le dernier élément
      }
    }
    return $value;
  }

  /** retourner un array non null à partir de $array */
  static final function with($array): array {
    if ($array instanceof IArrayWrapper) $array = $array->wrappedArray();
    if (is_array($array)) return $array;
    elseif ($array === null || $array === false) return [];
    elseif ($array instanceof Traversable) return self::all($array);
    else return [$array];
  }

  /** retourner un array à partir de $array, ou null */
  static final function withn($array): ?array {
    if ($array instanceof IArrayWrapper) $array = $array->wrappedArray();
    if (is_array($array)) return $array;
    elseif ($array === null || $array === false) return null;
    elseif ($array instanceof Traversable) return self::all($array);
    else return [$array];
  }

  /** tester si $array a au moins une clé numérique */
  static final function have_num_keys(?array $array): bool {
    if ($array === null) return false;
    foreach ($array as $key => $value) {
      if (is_int($key)) return true;
    }
    return false;
  }

  /**
   * tester si $array est une liste, c'est à dire un tableau non null avec
   * uniquement des clés numériques séquentielles commençant à zéro
   *
   * NB: is_list(null) === false
   * et  is_list([])   === true
   */
  static final function is_list(?array $array): bool {
    if ($array === null) return false;
    $index = -1;
    foreach ($array as $key => $value) {
      ++$index;
      if ($key !== $index) return false;
    }
    return true;
  }

  /**
   * tester si $array contient la clé $key
   *
   * @param array|ArrayAccess $array
   */
  static final function has($array, $key): bool {
    if (is_array($array)) {
      return array_key_exists($key, $array);
    } elseif ($array instanceof ArrayAccess) {
      return $array->offsetExists($key);
    }
    return false;
  }

  /**
   * retourner $array[$key] ou $default si la clé n'existe pas
   *
   * @param array|ArrayAccess $array
   */
  static final function get($array, $key, $default=null) {
    if (is_array($array)) {
      if (array_key_exists($key, $array)) return $array[$key];
    } elseif ($array instanceof ArrayAccess) {
      if ($array->offsetExists($key)) return $array->offsetGet($key);
    }
    return $default;
  }

  /**
   * retourner un tableau construit à partir des clés de $keys
   * - [$to => $from]  --> $dest[$to] = self::get($array, $from)
   * - [$to => null]   --> $dest[$to] = null
   * - [$to => false]  --> NOP
   * - [$to]           --> $dest[$to] = self::get($array, $to)
   * - [null]          --> $dest[] = null
   * - [false]         --> NOP
   *
   * Si $inverse===true, le mapping est inversé:
   * - [$to => $from]  --> $dest[$from] = self::get($array, $to)
   * - [$to => null]   --> $dest[$to] = self::get($array, $to)
   * - [$to => false]  --> NOP
   * - [$to]           --> $dest[$to] = self::get($array, $to)
   * - [null]          --> NOP         (XXX que faire dans ce cas?)
   * - [false]         --> NOP
   *
   * notez que l'ordre est inversé par rapport à {@link self::rekey()} qui
   * attend des mappings [$from => $to], alors que cette méthode attend des
   * mappings [$to => $from]
   */
  static final function select($array, ?array $mappings, bool $inverse=false): array {
    $dest = [];
    $index = 0;
    if (!$inverse) {
      foreach ($mappings as $to => $from) {
        if ($to === $index) {
          $index++;
          $to = $from;
          if ($to === false) continue;
          elseif ($to === null) $dest[] = null;
          else $dest[$to] = self::get($array, $to);
        } elseif ($from === false) {
          continue;
        } elseif ($from === null) {
          $dest[$to] = null;
        } else {
          $dest[$to] = self::get($array, $from);
        }
      }
    } else {
      foreach ($mappings as $to => $from) {
        if ($to === $index) {
          $index++;
          $to = $from;
          if ($to === false) continue;
          elseif ($to === null) continue;
          else $dest[$to] = self::get($array, $to);
        } elseif ($from === false) {
          continue;
        } elseif ($from === null) {
          $dest[$to] = self::get($array, $to);
        } else {
          $dest[$from] = self::get($array, $to);
        }
      }
    }
    return $dest;
  }

  /**
   * obtenir la liste des clés finalement obtenues après l'appel à
   * {@link self::select()} avec le mapping spécifié
   */
  static final function selected_keys(?array $mappings): array {
    if ($mappings === null) return [];
    $keys = [];
    $index = 0;
    foreach ($mappings as $to => $from) {
      if ($to === $index) {
        if ($from === false) continue;
        elseif ($from === null) $keys[] = $index;
        else $keys[] = $from;
        $index++;
      } elseif ($from === false) {
        continue;
      } else {
        $keys[] = $to;
      }
    }
    return $keys;
  }

  /**
   * méthode de convenance qui sélectionne certaines clés de $array avec
   * {@link self::select()} puis merge le tableau $merge au résultat.
   */
  static final function selectm($array, ?array $mappings, ?array $merge=null): array {
    return cl::merge(self::select($array, $mappings), $merge);
  }

  /**
   * méthode de convenance qui merge $merge dans $array puis sélectionne
   * certaines clés avec {@link self::select()}
   */
  static final function mselect($array, ?array $merge, ?array $mappings): array {
    return self::select(cl::merge($array, $merge), $mappings);
  }

  /**
   * construire un sous-ensemble du tableau $array en sélectionnant les clés de
   * $includes qui ne sont pas mentionnées dans $excludes.
   *
   * - si $includes===null && $excludes===null, retourner le tableau inchangé
   * - si $includes vaut null, prendre toutes les clés
   *
   */
  static final function xselect($array, ?array $includes, ?array $excludes=null): ?array {
    if ($array === null) return null;
    $array = self::withn($array);
    if ($includes === null && $excludes === null) return $array;
    if ($includes === null) $includes = array_keys($array);
    if ($excludes === null) $excludes = [];
    $result = [];
    foreach ($array as $key => $value) {
      if (!in_array($key, $includes)) continue;
      if (in_array($key, $excludes)) continue;
      $result[$key] = $value;
    }
    return $result;
  }

  /**
   * si $array est un array ou une instance de ArrayAccess, créer ou modifier
   * l'élément dont la clé est $key
   *
   * @param array|ArrayAccess $array
   */
  static final function set(&$array, $key, $value): void {
    if (is_array($array) || $array === null) {
      if ($key === null) $array[] = $value;
      else $array[$key] = $value;
    } elseif ($array instanceof ArrayAccess) {
      $array->offsetSet($key, $value);
    }
  }

  /**
   * si $array est un array ou une instance de ArrayAccess, supprimer l'élément
   * dont la clé est $key
   *
   * @param array|ArrayAccess $array
   */
  static final function del(&$array, $key): void {
    if (is_array($array)) {
      unset($array[$key]);
    } elseif ($array instanceof ArrayAccess) {
      $array->offsetUnset($key);
    }
  }

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

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

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

  /**
   * Fusionner tous les tableaux spécifiés. Les valeurs null sont ignorées.
   * IMPORTANT: les clés numériques sont réordonnées.
   * Retourner null si aucun tableau n'est fourni ou s'ils étaient tous null.
   */
  static final function merge(...$arrays): ?array {
    $merges = [];
    foreach ($arrays as $array) {
      A::ensure_narray($array);
      if ($array !== null) $merges[] = $array;
    }
    return $merges? array_merge(...$merges): null;
  }

  /**
   * Fusionner tous les tableaux spécifiés. Les valeurs null sont ignorées.
   * IMPORTANT: les clés numériques NE SONT PAS réordonnées.
   * Retourner null si aucun tableau n'est fourni ou s'ils étaient tous null.
   */
  static final function merge2(...$arrays): ?array {
    $merged = null;
    foreach ($arrays as $array) {
      $array = self::withn($array);
      if ($array === null) continue;
      $merged ??= [];
      foreach ($array as $key => $value) {
        $merged[$key] = $value;
      }
    }
    return $merged;
  }

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

  static final function map(callable $callback, ?iterable $array): array {
    $result = [];
    if ($array !== null) {
      $ctx = nur_func::_prepare($callback);
      foreach ($array as $key => $value) {
        $result[$key] = nur_func::_call($ctx, [$value, $key]);
      }
    }
    return $result;
  }

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

  /**
   * vérifier que le chemin $keys existe dans le tableau $array
   *
   * si $pkey est vide ou null, retourner true
   */
  static final function phas($array, $pkey): bool {
    # optimisations
    if ($pkey === null || $pkey === []) {
      return true;
    } elseif (is_int($pkey) || (is_string($pkey) && strpos($pkey, ".") === false)) {
      return self::has($array, $pkey);
    } elseif (!is_array($pkey)) {
      $pkey = explode(".", strval($pkey));
    }
    # phas
    $first = true;
    foreach($pkey 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;
  }

  static final function each_phas($array, ?array $pkeys): array {
    $result = [];
    if ($pkeys !== null) {
      foreach ($pkeys as $pkey) {
        $result[] = self::phas($array, $pkey);
      }
    }
    return $result;
  }

  /**
   * obtenir la valeur correspondant au chemin $keys dans $array
   *
   * si $pkey est vide ou null, retourner $default
   */
  static final function pget($array, $pkey, $default=null) {
    # optimisations
    if ($pkey === null || $pkey === []) return $default;
    elseif ($pkey === "") return $array;
    elseif (is_int($pkey) || (is_string($pkey) && strpos($pkey, ".") === false)) {
      return self::get($array, $pkey, $default);
    } elseif (!is_array($pkey)) {
      $pkey = explode(".", strval($pkey));
    }
    # pget
    $value = $array;
    $first = true;
    foreach($pkey 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;
  }

  static final function each_pget($array, ?array $pkeys): array {
    $result = [];
    if ($pkeys !== null) {
      foreach ($pkeys as $key => $pkey) {
        $result[$key] = self::pget($array, $pkey);
      }
    }
    return $result;
  }

  /**
   * retourner un tableau construit à partir des chemins de clé de $pkeys
   * ces chemins peuvent être exprimés de plusieurs façon:
   * - [$key => $pkey] --> $dest[$key] = self::pget($array, $pkey)
   * - [$key => null]  --> $dest[$key] = null
   * - [$pkey]         --> $dest[$key] = self::pget($array, $pkey)
   *                       avec $key = implode("__", $pkey))
   * - [null]          --> $dest[] = null
   * - [false]         --> NOP
   */
  static final function pselect($array, ?array $pkeys): array {
    $dest = [];
    $index = 0;
    foreach ($pkeys as $key => $pkey) {
      if ($key === $index) {
        $index++;
        if ($pkey === null) continue;
        $value = self::pget($array, $pkey);
        if (!is_array($pkey)) $pkey = explode(".", strval($pkey));
        $key = implode("__", $pkey);
      } elseif ($pkey === null) {
        $value = null;
      } else {
        $value = self::pget($array, $pkey);
      }
      $dest[$key] = $value;
    }
    return $dest;
  }

  /**
   * méthode de convenance qui sélectionne certaines clés de $array avec
   * {@link self::pselect()} puis merge le tableau $merge au résultat.
   */
  static final function pselectm($array, ?array $pkeys, ?array $merge=null): array {
    return cl::merge(self::pselect($array, $pkeys), $merge);
  }

  /**
   * méthode de convenance qui merge $merge dans $array puis sélectionne
   * certaines clés avec {@link self::pselect()}
   */
  static final function mpselect($array, ?array $merge, ?array $mappings): array {
    return self::pselect(cl::merge($array, $merge), $mappings);
  }

  /**
   * modifier la valeur au chemin de clé $keys dans le tableau $array
   *
   * 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 $pkey est vide ou null, $array est remplacé par $value
   */
  static final function pset(&$array, $pkey, $value): void {
    # optimisations
    if ($pkey === null || $pkey === []) {
      $array = $value;
      return;
    } elseif ($pkey === "") {
      $array[] = $value;
      return;
    } elseif (is_int($pkey) || (is_string($pkey) && strpos($pkey, ".") === false)) {
      self::set($array, $pkey, $value);
      return;
    } elseif (!is_array($pkey)) {
      $pkey = explode(".", strval($pkey));
    }
    # pset
    A::ensure_array($array);
    $current =& $array;
    $key = null;
    $last = count($pkey) - 1;
    $i = 0;
    foreach ($pkey 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 {
        A::ensure_array($current[$key]);
        $current =& $current[$key];
      }
      $i++;
    }
    if ($key === "") $current[] = $value;
    else $current[$key] = $value;
  }

  static final function each_pset(&$array, ?array $values): void {
    if ($values !== null) {
      foreach ($values as $pkey => $value) {
        self::pset($array, $pkey, $value);
      }
    }
  }

  /**
   * supprimer la valeur au chemin de clé $keys dans $array
   *
   * si $array vaut null ou false, sa valeur est inchangée.
   * si $pkey est vide ou null, $array devient null
   */
  static final function pdel(&$array, $pkey): void {
    # optimisations
    if ($array === null || $array === false) {
      return;
    } elseif ($pkey === null || $pkey === []) {
      $array = null;
      return;
    } elseif (is_int($pkey) || (is_string($pkey) && strpos($pkey, ".") === false)) {
      self::del($array, $pkey);
      return;
    } elseif (!is_array($pkey)) {
      $pkey = explode(".", strval($pkey));
    }
    # pdel
    A::ensure_array($array);
    $current =& $array;
    $key = null;
    $last = count($pkey) - 1;
    $i = 0;
    foreach ($pkey as $key) {
      if ($i == $last) break;
      if ($current instanceof ArrayAccess) {
        if (!$current->offsetExists($key)) break;
      } elseif (is_array($current)) {
        if (!array_key_exists($key, $current)) break;
      } else {
        break;
      }
      $current =& $current[$key];
      $i++;
    }
    if ($i == $last) {
      if ($current instanceof ArrayAccess) {
        $current->offsetUnset($key);
      } elseif (is_array($current)) {
        unset($current[$key]);
      }
    }
  }

  static final function each_pdel(&$array, ?array $pkeys): void {
    if ($pkeys !== null) {
      foreach ($pkeys as $pkey) {
        self::pdel($array, $pkey);
      }
    }
  }

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

  /**
   * retourner le tableau $array en "renommant" les clés selon le tableau
   * $mappings qui contient des associations de la forme [$from => $to]
   *
   * Si $inverse===true, renommer dans le sens $to => $from
   */
  static function rekey(?array $array, ?array $mappings, bool $inverse=false): ?array {
    if ($array === null || $mappings === null) return $array;
    if ($inverse) $mappings = array_flip($mappings);
    $mapped = [];
    foreach ($array as $key => $value) {
      if (array_key_exists($key, $mappings)) $key = $mappings[$key];
      $mapped[$key] = $value;
    }
    return $mapped;
  }

  /**
   * indiquer si {@link self::rekey()} modifierai le tableau indiqué (s'il y a
   * des modifications à faire)
   */
  static function would_rekey(?array $array, ?array $mappings, bool $inverse=false): bool {
    if ($array === null || $mappings === null) return false;
    if ($inverse) $mappings = array_flip($mappings);
    foreach ($array as $key => $value) {
      if (array_key_exists($key, $mappings)) return true;
    }
    return false;
  }

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

  /** tester si tous les éléments du tableau satisfont la condition */
  static final function all_if(?array $array, callable $cond): bool {
    if ($array !== null) {
      foreach ($array as $value) {
        if (!$cond($value)) return false;
      }
    }
    return true;
  }

  static final function all_z(?array $array): bool { return self::all_if($array, [cv::class, "z"]);}
  static final function all_nz(?array $array): bool { return self::all_if($array, [cv::class, "nz"]);}
  static final function all_n(?array $array): bool { return self::all_if($array, [cv::class, "n"]);}
  static final function all_nn(?array $array): bool { return self::all_if($array, [cv::class, "nn"]);}
  static final function all_t(?array $array): bool { return self::all_if($array, [cv::class, "t"]);}
  static final function all_f(?array $array): bool { return self::all_if($array, [cv::class, "f"]);}
  static final function all_pt(?array $array): bool { return self::all_if($array, [cv::class, "pt"]);}
  static final function all_pf(?array $array): bool { return self::all_if($array, [cv::class, "pf"]);}
  static final function all_equals(?array $array, $value): bool { return self::all_if($array, cv::equals($value)); }
  static final function all_not_equals(?array $array, $value): bool { return self::all_if($array, cv::not_equals($value)); }
  static final function all_same(?array $array, $value): bool { return self::all_if($array, cv::same($value)); }
  static final function all_not_same(?array $array, $value): bool { return self::all_if($array, cv::not_same($value)); }

  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

  /** tester si au moins un élément du tableau satisfait la condition */
  static final function any_if(?array $array, callable $cond): bool {
    if ($array !== null) {
      foreach ($array as $value) {
        if ($cond($value)) return true;
      }
    }
    return false;
  }

  static final function any_z(?array $array): bool { return self::any_if($array, [cv::class, "z"]);}
  static final function any_nz(?array $array): bool { return self::any_if($array, [cv::class, "nz"]);}
  static final function any_n(?array $array): bool { return self::any_if($array, [cv::class, "n"]);}
  static final function any_nn(?array $array): bool { return self::any_if($array, [cv::class, "nn"]);}
  static final function any_t(?array $array): bool { return self::any_if($array, [cv::class, "t"]);}
  static final function any_f(?array $array): bool { return self::any_if($array, [cv::class, "f"]);}
  static final function any_pt(?array $array): bool { return self::any_if($array, [cv::class, "pt"]);}
  static final function any_pf(?array $array): bool { return self::any_if($array, [cv::class, "pf"]);}
  static final function any_equals(?array $array, $value): bool { return self::any_if($array, cv::equals($value)); }
  static final function any_not_equals(?array $array, $value): bool { return self::any_if($array, cv::not_equals($value)); }
  static final function any_same(?array $array, $value): bool { return self::any_if($array, cv::same($value)); }
  static final function any_not_same(?array $array, $value): bool { return self::any_if($array, cv::not_same($value)); }

  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
  static final function filter_if(?array $array, callable $cond): ?array {
    if ($array === null) return null;
    $filtered = [];
    $index = 0;
    foreach ($array as $key => $value) {
      if (!$cond($value)) {
        if ($key === $index) {
          $index++;
          $filtered[] = $value;
        } else {
          $filtered[$key] = $value;
        }
      } elseif ($key === $index) {
        $index++;
      }
    }
    return $filtered;
  }

  static final function filter_z(?array $array): ?array { return self::filter_if($array, [cv::class, "z"]);}
  static final function filter_nz(?array $array): ?array { return self::filter_if($array, [cv::class, "nz"]);}
  static final function filter_n(?array $array): ?array { return self::filter_if($array, [cv::class, "n"]);}
  static final function filter_nn(?array $array): ?array { return self::filter_if($array, [cv::class, "nn"]);}
  static final function filter_t(?array $array): ?array { return self::filter_if($array, [cv::class, "t"]);}
  static final function filter_f(?array $array): ?array { return self::filter_if($array, [cv::class, "f"]);}
  static final function filter_pt(?array $array): ?array { return self::filter_if($array, [cv::class, "pt"]);}
  static final function filter_pf(?array $array): ?array { return self::filter_if($array, [cv::class, "pf"]);}
  static final function filter_equals(?array $array, $value): ?array { return self::filter_if($array, cv::equals($value)); }
  static final function filter_not_equals(?array $array, $value): ?array { return self::filter_if($array, cv::not_equals($value)); }
  static final function filter_same(?array $array, $value): ?array { return self::filter_if($array, cv::same($value)); }
  static final function filter_not_same(?array $array, $value): ?array { return self::filter_if($array, cv::not_same($value)); }

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

  static final function sorted(?array $array, int $flags=SORT_REGULAR, bool $assoc=false): ?array {
    A::sort($array, $flags, $assoc);
    return $array;
  }

  static final function ksorted(?array $array, int $flags=SORT_REGULAR): ?array {
    A::ksort($array, $flags);
    return $array;
  }

  /**
   * retourner une fonction permettant de trier un tableau sur les clés
   * spécifiées.
   *
   * - les clés ayant le préfixe '+' ou le suffixe '|asc' indiquent un tri
   * ascendant
   * - les clés ayant le préfixe '-' ou le suffixe '|desc' indiquent un tri
   * descendant
   * - sinon, par défaut, le tri est ascendant
   */
  static final function compare(array $keys): callable {
    return function ($a, $b) use ($keys) {
      foreach ($keys as $key) {
        if (str::del_prefix($key, "+")) $w = 1;
        elseif (str::del_prefix($key, "-")) $w = -1;
        elseif (str::del_suffix($key, "|asc")) $w = 1;
        elseif (str::del_suffix($key, "|desc")) $w = -1;
        else $w = 1;
        if ($c = $w * cv::compare(cl::get($a, $key), cl::get($b, $key))) {
          return $c;
        }
      }
      return 0;
    };
  }

  static final function usorted(?array $array, array $keys, bool $assoc=false): ?array {
    A::usort($array, $keys, $assoc);
    return $array;
  }
}
