<?php
namespace nur\mapper\base\capacitor;

use ArrayAccess;
use nur\A;
use nur\data\types\Metadata;

/**
 * Class Capacitor: classe outil qui permet d'accumuler des données pour les
 * fournir en une seule fois au moment voulu
 */
class Capacitor implements ICapacitor, ArrayAccess {
  use TCapacitor;

  /** @var Metadata */
  private static $key_md;

  private static function key_md(): Metadata {
    if (self::$key_md === null) {
      self::$key_md = new Metadata(self::KEY_SCHEMA);
    }
    return self::$key_md;
  }

  /** @var Metadata */
  private static $sort_md;

  private static function sort_md(): Metadata {
    if (self::$sort_md === null) {
      self::$sort_md = new Metadata(self::SORT_SCHEMA);
    }
    return self::$sort_md;
  }

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

  /**
   * spécifier les clés à traquer lors du chargement d'une donnée.
   *
   * si une clé est marquée comme primaire, alors les doublons éventuels sont
   * supprimés au fur et à mesure du chargement.
   */
  function setKeys(array $keys, ?string $channel=null): void {
    self::key_md()->eachEnsureSchema($keys);
    $namedkeys = [];
    $pkeys = null;
    foreach ($keys as $key) {
      $namedkeys[$key["name"]] = $key;
      if ($key["primary"]) $pkeys[] = $key;
    }
    $this->kinfos[$channel] = ["keys" => $namedkeys, "pkeys" => $pkeys];
  }

  private static function get_kvalues(array $keys, $item): array {
    $item = A::with($item);
    $kvalues = [];
    foreach ($keys as $kname => $key) {
      $kvalues[$kname] = A::get($item, $key["name"]);
    }
    return $kvalues;
  }

  private static function compute_itemkey(array $pkvalues): string {
    return implode("-", $pkvalues);
  }

  private static function get_itemkey(?array $kinfos, $item): ?string {
    if ($kinfos === null) return null;
    $pkeys = $kinfos["pkeys"];
    if ($pkeys === null) return null;
    $pkvalues = self::get_kvalues($pkeys, $item);
    return self::compute_itemkey($pkvalues);
  }

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

  /**
   * trier les données selon les clés spécifiées. NB: cette implémentation
   * autorise qu'on utilise des clés qui n'ont pas été déclarées avec
   * {@link setKeys()}
   *
   * @see ICapacitor::sort()
   */
  function sort(?array $keys=null, ?string $channel=null): void {
    $kinfos = A::get($this->kinfos, $channel);
    $asort = $kinfos !== null && $kinfos["pkeys"] !== null;
    $defaultKeys = $kinfos !== null? $kinfos["keys"]: null;
    if ($keys !== null) {
      self::sort_md()->eachEnsureSchema($keys);
    } else {
      $keys = $defaultKeys;
      if ($keys === null) return;
    }
    foreach ($keys as $kname => &$key) {
      if ($key["reverse"] === null && $defaultKeys !== null && array_key_exists($kname, $defaultKeys)) {
        $key["reverse"] = $defaultKeys[$kname]["reverse"];
      }
      if ($key["reverse"] === null) $key["reverse"] = false;
    }; unset($key);
    $sortfunc = function ($a, $b) use ($keys) {
      $akvs = self::get_kvalues($keys, $a);
      $bkvs = self::get_kvalues($keys, $b);
      foreach ($keys as $kname => $key) {
        $akv = $akvs[$kname];
        $bkv = $bkvs[$kname];
        if ($akv == $bkv) continue;
        if ($akv < $bkv) {
          return $key["reverse"] ? 1 : -1;
        } else {
          return $key["reverse"] ? -1 : 1;
        }
      }
      return 0;
    };
    if ($asort) uasort($this->data[$channel], $sortfunc);
    else usort($this->data[$channel], $sortfunc);
  }

  function charge($item, ?string $channel=null, $pkvalues=null): void {
    $kinfos = A::get($this->kinfos, $channel);
    $itemkey = self::get_itemkey($kinfos, $item);
    if ($itemkey === null && $item === null) {
      if ($pkvalues === null) return;
      $itemkey = self::compute_itemkey($pkvalues);
    }
    A::set($this->data[$channel], $itemkey, $item);
  }

  function getItem($pkvalues, ?string $channel=null, $default=null) {
    if (is_array($pkvalues)) {
      $itemkey = self::compute_itemkey($pkvalues);
    } else {
      $itemkey = $pkvalues;
    }
    return A::_pget($this->data, [$channel, $itemkey], $default);
  }

  function discharge(?string $channel=null, bool $remove=true): iterable {
    $items = A::get($this->data, $channel, []);
    if ($remove) A::del($this->data, $channel);
    return $items;
  }
}