<?php
namespace nur\ldap;

use ArrayAccess;
use Countable;
use Iterator;
use nur\A;
use nur\b\coll\TIterableArray;
use nur\ldap\syntaxes\AbstractSyntax;
use nur\str;

class LdapAttr implements ArrayAccess, Countable, Iterator {
  use TIterableArray;

  const MONOVALUED = 1, BINARY = 2, ORDERED = 4, NOT_HUMAN_READABLE = 8;

  function __construct(string $name, ?array &$values, ?AbstractSyntax $syntax, ?int $flags) {
    $this->name = $name;
    $this->syntax = $syntax;
    $this->flags = $flags;
    $this->reset($values);
  }

  /** @var string */
  protected $name;

  function name(): string {
    return $this->name;
  }

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

  function reset(?array &$values): self {
    $this->data =& $values;
    return $this;
  }

  /** @var AbstractSyntax */
  protected $syntax;

  /** @var int */
  protected $flags;

  function isMonovalued(): bool {
    return $this->flags !== null && $this->flags & self::MONOVALUED != 0;
  }

  function isBinary(): bool {
    return $this->flags !== null && $this->flags & self::BINARY != 0;
  }

  function isOrdered(): bool {
    return $this->flags !== null && $this->flags & self::ORDERED != 0;
  }

  function isNotHumanReadable(): bool {
    return $this->flags !== null && $this->flags & self::NOT_HUMAN_READABLE != 0;
  }

  protected function fromLdap($value) {
    $syntax = $this->syntax;
    if ($syntax !== null) {
      if ($this->isMonovalued()) $value = $syntax->fromMonovaluedLdap($value);
      else $value = $syntax->fromMultivaluedLdap($value);
    }
    return $value;
  }
  protected function fromPhp($value): ?iterable {
    $syntax = $this->syntax;
    if ($syntax !== null) $value = $syntax->fromPhp($value);
    else A::ensure_narray($value);
    return $value;
  }

  /** retourner un tableau si multivalué, une valeur scalaire si monovalué */
  function get($index=null) {
    $value = $this->fromLdap($this->data);
    if ($index !== null && is_array($value)) {
      $value = array_key_exists($index, $value)? $value[$index]: null;
    }
    return $value;
  }

  /**
   * retourner toutes les valeurs
   *
   * @param string $checkPrefixDel ne retourner que les valeurs qui commencent
   * par ce préfixe ET enlever le préfixe
   */
  function all(?string $checkPrefixDel=null): ?array {
    if ($this->syntax === null) $values = $this->data;
    else $values = $this->syntax->fromMultivaluedLdap($this->data);
    if ($checkPrefixDel !== null && $values !== null) {
      $filtered = [];
      foreach ($values as $value) {
        if (str::del_prefix($value, $checkPrefixDel)) {
          $filtered[] = $value;
        }
      }
      $values = $filtered;
    }
    return $values;
  }

  /** retourner la première valeur */
  function first(?string $checkPrefixDel=null) {
    return A::first($this->all($checkPrefixDel));
  }

  function set($values, bool $unlessNn=false): self {
    if ($values instanceof LdapAttr) $values = $values->array();
    if (!$unlessNn || $this->data === null) {
      $this->data = $this->fromPhp($values);
    }
    return $this;
  }

  protected static function in_array(string $needle, array $haystack, bool $strict, ?int &$index=null): bool {
    if (!$strict) $needle = strtolower($needle);
    foreach ($haystack as $index => $hay) {
      if ($strict && $hay === $needle) return true;
      if (!$strict && strtolower($hay) == $needle) return true;
    }
    return false;
  }

  /** vérifier si la valeur spécifiée figure dans l'attribut */
  function contains($value, bool $strict=false): bool {
    $value = A::first($this->fromPhp($value));
    if ($value === null || $this->data === null) return false;
    return self::in_array($value, $this->data, $strict);
  }

  /**
   * l'unicité est calculée ainsi:
   * - en mode strict, ce doit être une égalité parfaite
   * - en mode non strict, la comparaison est insensible à la casse
   * XXX à terme, implémenter la comparaison en fonction de la syntaxe
   */
  function add($value, bool $unique=true, bool $strict=false): self {
    $value = A::first($this->fromPhp($value));
    if ($value !== null) {
      if (!$unique || $this->data === null ||
        !self::in_array($value, $this->data, $strict)) {
        $this->data[] = $value;
      }
    }
    return $this;
  }

  function addAll(?iterable $values): self {
    if ($values !== null) {
      foreach ($values as $value) {
        $this->add($value);
      }
    }
    return $this;
  }

  function del($value, int $maxCount=-1, bool $strict=false): self {
    if ($value !== null && $this->data !== null) {
      $value = A::first($this->fromPhp($value));
      $rekey = false;
      while ($maxCount != 0) {
        if (!self::in_array($value, $this->data, $strict, $index)) break;
        unset($this->data[$index]);
        $rekey = true;
        if ($maxCount > 0) $maxCount--;
      }
      if ($rekey) $this->data = array_values($this->data);
    }
    return $this;
  }

  function ins(int $index, $value): self {
    $value = A::first($this->fromPhp($value));
    if ($value !== null) {
      A::insert($this->data, $index, $value);
    }
    return $this;
  }

  function unset(int $index): self {
    if ($this->data !== null) {
      $count = count($this->array());
      if ($count > 0 && $index < 0) {
        while ($index < 0) $index += $count;
      }
      unset($this->data[$index]);
      $this->data = array_values($this->data);
    }
    return $this;
  }

  function key() { return $this->_key(); }
  function current() {
    $current = $this->_current();
    $syntax = $this->syntax;
    if ($syntax !== null) $current = $syntax->ldap2php($current);
    return $current;
  }

  #############################################################################
  # données au format LDAP

  function __toString() {
    return implode("\n", $this->data);
  }
  /** retourner les données au format LDAP */
  function &array(): ?array { return $this->data; }
  function count(): int { return count($this->data); }
  function keys(): array { return array_keys($this->data); }
  function offsetExists($key) {
    return $this->data !== null && array_key_exists($key, $this->data);
  }
  function offsetGet($key) { return array_key_exists($key, $this->data)? $this->data[$key]: null; }
  function offsetSet($key, $value) { $this->data[$key] = $value; }
  function offsetUnset($key) { unset($this->data[$key]); }

  function __isset($key) { return $this->offsetExists($key); }
  function __get($key) { return $this->offsetGet($key); }
  function __set($key, $value) { $this->offsetSet($key, $value); }
  function __unset($key) { $this->offsetUnset($key); }
}