<?php
namespace nur\b\values;

use ArrayAccess;
use Throwable;

class ProxyValue implements IProxyValue, ArrayAccess {
  /** @var IProxyValue */
  private static $undef;

  static function undef(): IProxyValue {
    if (self::$undef === null) {
      self::$undef = new class implements IProxyValue {
        function allowUndef(): bool { return true; }
        function allowNull(): bool { return false; }
        function allowError(): bool { return false; }
        function isUndef(): bool { return true; }
        function isNull(): bool { return false; }
        function isError(): bool { return false; }
        function isValue(): bool { return false; }
        function isMutable(): bool { return false; }
        function get($default=null) { return $default; }
        function getError(): Throwable { throw ProxyValueException::notAnError(); }
        function set($value) { throw ProxyValueException::undefIsImmutable(); }
        function reset() { throw ProxyValueException::undefIsImmutable(); }
      };
    }
    return self::$undef;
  }

  /** @var IProxyValue */
  private static $null;

  static function null(): IProxyValue {
    if (self::$null === null) {
      self::$null = new class implements IProxyValue {
        function allowUndef(): bool { return false; }
        function allowNull(): bool { return true; }
        function allowError(): bool { return false; }
        function isUndef(): bool { return false; }
        function isNull(): bool { return true; }
        function isError(): bool { return false; }
        function isValue(): bool { return false; }
        function isMutable(): bool { return false; }
        function get($default=null) { return null; }
        function getError(): Throwable { throw ProxyValueException::notAnError(); }
        function set($value) { throw ProxyValueException::nullIsImmutable(); }
        function reset() { throw ProxyValueException::nullIsImmutable(); }
      };
    }
    return self::$null;
  }

  /** Retourner une instance de IProxyValue */
  static function with($value, bool $ensure_mutable=false): IProxyValue {
    if ($value instanceof IProxyValue) {
      if (!$ensure_mutable || $value->isMutable()) return $value;
      if ($value->isValue()) return new static($value->get());
      elseif ($value->isError()) return new static($value->getError());
      elseif ($value->isNull()) return new static(null);
      elseif ($value->isUndef()) return new static(false);
    } elseif (!$ensure_mutable) {
      if ($value === null) return self::null();
      elseif ($value === false) return self::undef();
    }
    return new static($value);
  }

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

  function __construct($value) {
    $this->_set($value);
  }

  function get($default=null) {
    $value = $this->value;
    if ($this->allowUndef()) {
      if ($value === false) return $default;
    } elseif ($this->allowError()) {
      if ($value instanceof Throwable) throw $value;
    }
    return $value;
  }

  function getError(): Throwable {
    if ($this->allowError()) {
      $value = $this->value;
      if ($value instanceof Throwable) return $value;
    }
    throw ProxyValueException::notAnError();
  }

  function set($value) {
    if (!$this->isMutable()) {
      throw ProxyValueException::valueIsImmutable();
    } elseif ($value === null && !$this->allowNull()) {
      throw ProxyValueException::nullNotAllowed();
    } elseif ($value === false && !$this->allowUndef()) {
      throw ProxyValueException::undefNotAllowed();
    }
    $old_value = $this->value;
    $this->_set($value);
    return $old_value;
  }

  function reset() {
    if ($this->allowUndef()) {
      $this->value = false;
    } else {
      throw ProxyValueException::undefNotAllowed();
    }
  }

  function __toString(): string { return strval($this->value); }
  function __invoke(...$args) { $func = $this->value; return $func(...$args); }
  function __set(string $name, $value) { $this->value->$name = $value; }
  function __get(string $name) { return $this->value->$name; }
  function __isset(string $name): bool { return isset($this->value->$name); }
  function __unset(string $name) { unset($this->value->$name); }
  function __call(string $name, array $args) { return $this->value->$name(...$args); }
  function offsetExists($offset) { return isset($this->value[$offset]); }
  function offsetGet($offset) { return isset($this->value[$offset])? $this->value[$offset]: null; }
  function offsetSet($offset, $value) { if ($offset === null) $this->value[] = $value; else $this->value[$offset] = $value; }
  function offsetUnset($offset) { unset($this->value[$offset]); }

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

  const ALLOW_UNDEF = true;
  function allowUndef(): bool { return static::ALLOW_UNDEF; }

  const ALLOW_NULL = true;
  function allowNull(): bool { return static::ALLOW_NULL; }

  const ALLOW_ERROR = true;
  function allowError(): bool { return static::ALLOW_ERROR; }

  const IS_MUTABLE = true;
  function isMutable(): bool { return static::IS_MUTABLE; }

  protected $value;

  protected function _set($value) {
    $this->value = $value;
  }

  function isUndef(): bool {
    return $this->allowUndef() && $this->value === false;
  }
  function isNull(): bool {
    return $this->allowNull() && $this->value === null;
  }
  function isError(): bool {
    return $this->allowError() && $this->value instanceof Throwable;
  }
  function isValue(): bool {
    return !$this->isUndef() && !$this->isNull() && !$this->isError();
  }
}