<?php
namespace nulib\php\access;

use ArrayAccess;
use nulib\cl;

/**
 * Class KeyAccess: accès
 * - soit à une valeur d'un chemin de clé dans un tableau (si $key !== null)
 * - soit à une valeur scalaire (si $key === null)
 */
class KeyAccess extends AbstractAccess {
  const ALLOW_NULL = null;
  const ALLOW_FALSE = null;
  const PROTECT_DEST = false;

  function __construct(&$dest, $key=null, ?array $params=null) {
    parent::__construct($params);
    $this->protectDest = $params["protect_dest"] ?? static::PROTECT_DEST;
    $this->dest =& $dest;
    $this->key = $key;
    $this->allowNull = $params["allow_null"] ?? static::ALLOW_NULL;
    $this->allowFalse = $params["allow_false"] ?? static::ALLOW_FALSE;
  }

  protected bool $protectDest;

  /** @var mixed|array|ArrayAccess */
  protected $dest;

  /** @var null|int|string|array */
  protected $key;

  function reset(&$dest, $key=null): self {
    $this->dest =& $dest;
    $this->key = $key;
    return $this;
  }

  function resetKey($key=null): self {
    $this->key = $key;
    return $this;
  }

  protected ?bool $allowNull;

  protected function isAllowNull(): bool {
    $allowNull = $this->allowNull;
    if ($allowNull !== null) return $allowNull;
    return $this->key !== null;
  }

  protected ?bool $allowFalse;

  protected function isAllowFalse(): bool {
    $allowFalse = $this->allowFalse;
    if ($allowFalse !== null) return $allowFalse;
    return $this->key === null;
  }

  function exists(): bool {
    $key = $this->key;
    if ($key === null) {
      return $this->isAllowNull() || $this->dest !== null;
    } else {
      return cl::phas($this->dest, $key);
    }
  }

  function available(): bool {
    if (!$this->exists()) return false;
    $key = $this->key;
    if ($key === null) $value = $this->dest;
    else $value = cl::pget($this->dest, $key);
    if ($value === "") return $this->allowEmpty;
    if ($value === null) return $this->isAllowNull();
    if ($value === false) return $this->isAllowFalse();
    return true;
  }

  function get($default=null) {
    $key = $this->key;
    if ($key === null) $value = $this->dest;
    else $value = cl::pget($this->dest, $key, $default);
    if ($value === "" && !$this->allowEmpty) return $default;
    if ($value === null && !$this->isAllowNull()) return $default;
    if ($value === false && !$this->isAllowFalse()) return $default;
    return $value;
  }

  function set($value): void {
    $key = $this->key;
    if ($key === null) {
      if (!$this->protectDest) $this->dest = $value;
    } else {
      cl::pset($this->dest, $key, $value);
    }
  }

  function del(): void {
    $key = $this->key;
    if ($key === null) {
      if (!$this->protectDest) $this->dest = null;
    } else {
      cl::pdel($this->dest, $key);
    }
  }

  function addKey($key): self {
    if ($key === null) return $this;
    if ($this->key !== null) $key = cl::merge($this->key, $key);
    return new KeyAccess($this->dest, $key, [
      "allow_empty" => $this->allowEmpty,
      "allow_null" => $this->allowNull,
      "allow_false" => $this->allowFalse,
      "protect_dest" => $this->protectDest,
    ]);
  }

  function ensureAssoc(array $keys, ?array $params=null): void {
    $dest =& $this->dest;
    $prefix = $params["key_prefix"] ?? null;
    $suffix = $params["key_suffix"] ?? null;
    $index = 0;
    foreach ($keys as $key) {
      if ($prefix !== null || $suffix !== null) {
        $destKey = "$prefix$key$suffix";
      } else {
        # préserver les clés numériques
        $destKey = $key;
      }
      if ($dest !== null && array_key_exists($destKey, $dest)) continue;
      while (in_array($index, $keys, true)) {
        $index++;
      }
      if ($dest !== null && array_key_exists($index, $dest)) {
        $dest[$destKey] = $dest[$index];
        unset($dest[$index]);
        $index++;
      }
    }
  }

  function ensureKeys(array $defaults, ?array $missings, ?array $params=null): void {
    $dest =& $this->dest;
    $keys = array_keys($defaults);
    $prefix = $params["key_prefix"] ?? null;
    $suffix = $params["key_suffix"] ?? null;
    foreach ($keys as $key) {
      $destKey = "$prefix$key$suffix";
      $haveMissing = $missings !== null && array_key_exists($key, $missings);
      if ($dest === null || !array_key_exists($destKey, $dest)) {
        $dest[$destKey] = $defaults[$key];
      } elseif ($haveMissing && $dest[$destKey] === $missings[$key]) {
        $dest[$destKey] = $defaults[$key];
      }
    }
  }

  function deleteMissings(array $missings, ?array $params=null): void {
    $dest =& $this->dest;
    $prefix = $params["key_prefix"] ?? null;
    $suffix = $params["key_suffix"] ?? null;
    foreach ($missings as $key => $missing) {
      $destKey = "$prefix$key$suffix";
      if (array_key_exists($destKey, $dest) && $dest[$destKey] === $missing) {
        unset($dest[$destKey]);
      }
    }
  }

  function ensureOrder(array $keys, ?array $params=null): void {
    $dest =& $this->dest;
    if ($dest === null) return;

    $prefix = $params["key_prefix"] ?? null;
    $suffix = $params["key_suffix"] ?? null;
    if ($prefix !== null || $suffix !== null) {
      foreach ($keys as &$key) {
        $key = "$prefix$key$suffix";
      }; unset($key);
    }

    $destKeys = array_keys($dest);
    $keyCount = count($keys);
    if (array_slice($destKeys, 0, $keyCount) === $keys) {
      # si le tableau a déjà les bonnes clés dans le bon ordre, rien à faire
      return;
    }

    $ordered = [];
    foreach ($keys as $key) {
      if (array_key_exists($key, $dest)) {
        $ordered[$key] = $dest[$key];
        unset($dest[$key]);
      }
    }
    $preserveKeys = $params["preserve_keys"] ?? false;
    if ($preserveKeys) $dest = cl::merge2($ordered, $dest);
    else $dest = array_merge($ordered, $dest);
  }
}
