<?php
namespace nulib\php\access;

use nulib\StateException;
use nulib\str;
use ReflectionClass;
use ReflectionException;
use ReflectionProperty;

class PropertyAccess extends AbstractAccess {
  const PROTECT_DEST = true;
  const MAP_NAMES = true;
  const ALLOW_NULL = true;
  const ALLOW_FALSE = false;

  function __construct(?object $dest, ?string $name=null, ?array $params=null) {
    parent::__construct($params);
    $this->protectDest = $params["protect_dest"] ?? static::PROTECT_DEST;
    $this->mapNames = $params["map_names"] ?? static::MAP_NAMES;
    $this->_setName($name);
    $this->_setDest($dest);
    $this->allowNull = $params["allow_null"] ?? static::ALLOW_NULL;
    $this->allowFalse = $params["allow_false"] ?? static::ALLOW_FALSE;
  }

  protected bool $protectDest;

  protected ?object $dest;
  protected bool $mapNames;

  protected ?string $name;

  protected ?ReflectionProperty $property;

  private function _getName(string $key): string {
    return $this->mapNames? str::us2camel($key): $key;
  }
  private function _setName(?string $name): void {
    if ($name !== null) $name = $this->_getName($name);
    $this->name = $name;
  }

  private function _getProperty(?string $name, ?ReflectionClass $class, ?object $object=null): ?ReflectionProperty {
    $property = null;
    if ($class === null && $object !== null) {
      $class = new ReflectionClass($object);
    }
    if ($class !== null && $name !== null) {
      try {
        $property = $class->getProperty($name);
        $property->setAccessible(true);
      } catch (ReflectionException $e) {
      }
    }
    return $property;
  }

  private function _setDest(?object $dest): void {
    $this->dest = $dest;
    $this->property = $this->_getProperty($this->name, null, $dest);
  }

  function reset(?object $dest, ?string $name=null): self {
    $this->_setName($name);
    $this->_setDest($dest);
    return $this;
  }

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

  protected bool $allowNull;

  protected bool $allowFalse;

  function exists(): bool {
    $name = $this->name;
    if ($this->dest === null) return false;
    return $name === null
      || $this->property !== null
      || property_exists($this->dest, $name);
  }

  protected function _get($default=null) {
    $name = $this->name;
    $property = $this->property;
    if ($this->dest === null) {
      return $default;
    } elseif ($name === null) {
      return $this->dest;
    } elseif ($property !== null) {
      return $property->getValue($this->dest);
    } elseif (property_exists($this->dest, $name)) {
      return $this->dest->$name;
    } else {
      return $default;
    }
  }

  function available(): bool {
    if (!$this->exists()) return false;
    $value = $this->_get();
    if ($value === "") return $this->allowEmpty;
    if ($value === null) return $this->allowNull;
    if ($value === false) return $this->allowFalse;
    return true;
  }

  function get($default=null) {
    if (!$this->exists()) return $default;
    $value = $this->_get();
    if ($value === "" && !$this->allowEmpty) return $default;
    if ($value === null && !$this->allowNull) return $default;
    if ($value === false && !$this->allowFalse) return $default;
    return $value;
  }

  protected function _set($value): void {
    $name = $this->name;
    $property = $this->property;
    if ($this->dest === null) {
      throw StateException::unexpected_state("dest is null");
    } elseif ($name === null) {
      if (!$this->protectDest) $this->_setDest($value);
    } elseif ($property !== null) {
      $property->setValue($this->dest, $value);
    } else {
      $this->dest->$name = $value;
    }
  }

  function set($value): void {
    $this->_set($value);
  }

  function del(): void {
    $this->_set(null);
  }

  function addKey($key): IAccess {
    if ($key === null) return $this;
    return new ChainAccess($this, $key);
  }

  function ensureKeys(array $defaults, ?array $missings, ?array $params=null): void {
    $dest = $this->dest;
    if ($dest === null) {
      # comme ne connait pas la classe de l'objet destination, on n'essaie pas
      # de le créer
      return;
    }
    $class = new ReflectionClass($dest);
    $keys = array_keys($defaults);
    $prefix = $params["key_prefix"] ?? null;
    $suffix = $params["key_suffix"] ?? null;
    foreach ($keys as $key) {
      $name = $this->_getName("$prefix$key$suffix");
      $property = $this->_getProperty($name, $class);
      if ($property !== null) {
        $type = $property->getType();
        if ($type !== null && !$property->isInitialized($dest) && $type->allowsNull()) {
          # initialiser avec null au lieu de $defaults[$key] pour respecter le
          # type de la propriété
          $property->setValue($dest, null);
        }
      } elseif (!property_exists($dest, $name)) {
        $dest->$name = $defaults[$key];
      }
    }
  }
}
