<?php
namespace nulib\php\access;

use nulib\cl;
use nulib\StateException;
use ReflectionClass;
use ReflectionException;

class ChainAccess extends AbstractAccess {
  const ACCESS_AUTO = 0, ACCESS_KEY = 1, ACCESS_PROPERTY = 2;

  private static function unexpected_access_type(): StateException {
    return StateException::unexpected_state("access_type");
  }
  
  function __construct(IAccess $access, $key, ?array $params=null) {
    parent::__construct();
    $this->access = $access;
    $this->key = $key;
    $this->accessType = $params["access_type"] ?? self::ACCESS_AUTO;
  }

  protected IAccess $access;

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

  protected int $accessType;

  protected function _accessType($access, $key): int {
    $accessType = $this->accessType;
    if ($accessType === self::ACCESS_AUTO) {
      if (is_object($access) && is_string($key)) {
        $accessType = self::ACCESS_PROPERTY;
      } else {
        $accessType = self::ACCESS_KEY;
      }
    }
    return $accessType;
  }

  protected function _has(): bool {
    $src = $this->access->get();
    $key = $this->key;
    $accessType = $this->_accessType($src, $key);
    if ($accessType === self::ACCESS_KEY) {
      return cl::phas($src, $key);
    } elseif ($accessType === self::ACCESS_PROPERTY) {
      $class = new ReflectionClass($src);
      return $class->hasProperty($key) || property_exists($src, $key);
    } else {
      throw self::unexpected_access_type();
    }
  }

  protected function _get($default=null) {
    $src = $this->access->get();
    $key = $this->key;
    $accessType = $this->_accessType($src, $key);
    if ($accessType === self::ACCESS_KEY) {
      return cl::pget($src, $key);
    } elseif ($accessType === self::ACCESS_PROPERTY) {
      $class = new ReflectionClass($src);
      try {
        $property = $class->getProperty($key);
        $property->setAccessible(true);
      } catch (ReflectionException $e) {
        $property = null;
      }
      if ($property !== null) {
        return $property->getValue($src);
      } elseif (property_exists($src, $key)) {
        return $src->$key;
      } else {
        return $default;
      }
    } else {
      throw self::unexpected_access_type();
    }
  }

  protected function _pset(object $dest, $name, $value): void {
    $class = new ReflectionClass($dest);
    try {
      $property = $class->getProperty($name);
      $property->setAccessible(true);
    } catch (ReflectionException $e) {
      $property = null;
    }
    if ($property !== null) {
      $property->setValue($dest, $value);
    } else {
      $dest->$name = $value;
    }
  }

  function isAllowEmpty(): bool {
    return $this->access->isAllowEmpty();
  }

  function exists(): bool {
    if (!$this->access->exists()) return false;
    if ($this->key === null) return true;
    return $this->_has();
  }

  function available(): bool {
    if (!$this->access->available()) return false;
    if ($this->key === null) return true;
    if (!$this->_has()) return false;
    return $this->isAllowEmpty() || $this->_get() !== "";
  }

  function get($default=null) {
    if ($this->key === null) {
      return $this->access->get($default);
    }
    return $this->_get($default);
  }

  function set($value): void {
    if ($this->key === null) {
      $this->access->set($value);
      return;
    }
    $dest = $this->access->get();
    $key = $this->key;
    $accessType = $this->_accessType($dest, $key);
    if ($accessType === self::ACCESS_KEY) {
      cl::pset($dest, $key, $value);
      $this->access->set($dest);
    } elseif ($accessType === self::ACCESS_PROPERTY) {
      $this->_pset($dest, $key, $value);
    } else {
      throw self::unexpected_access_type();
    }
  }

  function del(): void {
    if ($this->key === null) {
      $this->access->del();
      return;
    }
    $dest = $this->access->get();
    $key = $this->key;
    $accessType = $this->_accessType($dest, $key);
    if ($accessType === self::ACCESS_KEY) {
      cl::pdel($dest, $key);
      $this->access->set($dest);
    } elseif ($accessType === self::ACCESS_PROPERTY) {
      $this->_pset($dest, $key, null);
    } else {
      throw self::unexpected_access_type();
    }
  }

  function addKey($key, ?array $params=null): IAccess {
    if ($key === null) return $this;
    $accessType = $params["access_type"] ?? $this->accessType;
    if ($accessType === self::ACCESS_KEY && $accessType === $this->accessType) {
      if ($this->key !== null) $key = cl::merge($this->key, $key);
      return new ChainAccess($this->access, $key);
    } else {
      return new ChainAccess($this, $key);
    }
  }

  function ensureAssoc(array $keys, ?array $params=null): void {
    #XXX fonction de $accessType?
    #$this->access->ensureAssoc($keys, $params);
  }

  function ensureKeys(array $defaults, ?array $missings, ?array $params=null): void {
    #XXX fonction de $accessType?
    #$this->access->ensureKeys($defaults, $params);
  }

  function ensureOrder(array $keys, ?array $params=null): void {
    #XXX fonction de $accessType?
    #$this->access->ensureOrder($keys, $params);
  }
}
