<?php
namespace nur\ldap\schemas;

use nur\A;
use nur\b\IllegalAccessException;
use nur\b\ValueException;
use nur\data\types\md_utils;
use nur\data\types\Metadata;
use nur\ldap\consts;
use nur\ldap\LdapAttr;
use nur\ldap\LdapConn;
use nur\ldap\syntaxes\BinarySyntax;
use nur\ldap\syntaxes\StringSyntax;

/**
 * Class LdapSchemaExtractor: extracteur de schéma LDAP, pour utilisation avec
 * PHP
 */
class LdapSchemaExtractor {
  function __construct(?array $schemaInfos=null) {
    if ($schemaInfos !== null) {
      [
        "ldap_syntaxes" => $this->ldapSyntaxes,
        "attribute_types" => $this->attributeTypes,
        "object_classes" => $this->objectClasses,
      ] = $schemaInfos;
    }
  }

  protected $ldapSyntaxes;

  protected $attributeTypes;

  protected $objectClasses;

  function loadSchema(LdapConn $conn): array {
    $schema = null;
    $schemaDn = $conn->getRootDse()->first("subschemaSubentry");
    if ($schemaDn !== null) {
      $schema = $conn->empty()->load($schemaDn, $conn->_search($schemaDn, [
        "suffix" => "",
        "attrs" => [
          "ldapSyntaxes",
          "attributeTypes",
          "objectClasses",
        ],
        "scope" => "base",
      ])->first());
    }
    if ($schema === null) {
      throw new IllegalAccessException("unable to find subschemaSubentry attribute");
    }

    $parser = new LseSyntax();
    $ldapSyntaxes = [];
    foreach ($schema->get("ldapSyntaxes", []) as $ldapSyntax) {
      $ldapSyntax = $parser->parse($ldapSyntax);
      $ldapSyntaxes[$ldapSyntax["oid"]] = $ldapSyntax;
    }
    $parser = new LseAttribute();
    $attributeTypes = [];
    foreach ($schema->get("attributeTypes", []) as $attributeType) {
      $attributeType = $parser->parse($attributeType);
      $attributeTypes[$attributeType["oid"]] = $attributeType;
    }
    $parser = new LseObjectClass();
    $objectClasses = [];
    foreach ($schema->get("objectClasses", []) as $objectClass) {
      $objectClass = $parser->parse($objectClass);
      $objectClasses[$objectClass["oid"]] = $objectClass;
    }
    return [
      "ldap_syntaxes" => $this->ldapSyntaxes = $ldapSyntaxes,
      "attribute_types" => $this->attributeTypes = $attributeTypes,
      "object_classes" => $this->objectClasses = $objectClasses,
    ];
  }

  protected $syntaxes;
  protected $attributes;
  protected $canonAttrs;
  protected $classes;
  protected $canonClasses;

  function init(): array {
    ## calculer la liste des syntaxes, et les classer par OID
    $ldapSyntaxes = $this->ldapSyntaxes;
    # rajouter une liste connue de syntaxes
    A::merge($ldapSyntaxes, consts::KNOWN_SLAPD_SYNTAXES);
    $syntaxes = [];
    foreach ($ldapSyntaxes as $syntax) {
      $oid = $syntax["oid"];
      # si la syntaxe a déjà été définie, ignorer
      if (array_key_exists($oid, $syntaxes)) continue;
      $class = A::get(consts::KNOWN_SYNTAX_CLASSES, $oid);
      if ($class === null) {
        $binary = $syntax["x_not_human_readable"] || $syntax["x_binary_transfer_required"];
        $class = $binary? BinarySyntax::class: StringSyntax::class;
      }
      $syntax["class"] = $class;
      $syntaxes[$oid] = $syntax;
    }

    ## calculer la liste des attributs, et les classer par nom canonique
    $attributes = [];
    $canonAttrs = [];
    foreach ($this->attributeTypes as $attribute) {
      $names = $attribute["names"];
      $canonName = $names[0];
      $attribute["name"] = $canonName;
      foreach ($names as $name) {
        $canonAttrs[strtolower($name)] = $canonName;
      }
      $attribute["class"] = A::_pget($syntaxes, [$attribute["syntax"], "class"]);
      $attributes[strtolower($canonName)] = $attribute;
    }
    # résoudre l'héritage des attributs
    foreach ($attributes as &$attribute) {
      foreach ($attribute["sups"] as $sup) {
        $sup = strtolower(A::get($canonAttrs, strtolower($sup), $sup));
        A::update_n($attribute, $attributes[$sup]);
      }
    }; unset($attribute);
    # puis mettre à false les valeurs booléennes nulles
    foreach ($attributes as &$attribute) {
      foreach (LseAttribute::BOOL_ATTRS as $name) {
        $attribute[$name] = boolval($attribute[$name]);
      }
    }; unset($attribute);

    ## calculer la liste des classes, et les classer par nom canonique.
    ## les noms des attributs sont aussi canonisés
    $classes = [];
    $canonClasses = [];
    foreach ($this->objectClasses as $class) {
      $names = $class["names"];
      $canonName = $names[0];
      $class["name"] = $canonName;
      foreach ($names as $name) {
        $canonClasses[strtolower($name)] = $canonName;
      }
      $musts = A::with($class["musts"]);
      foreach ($musts as &$name) {
        $name = A::get($canonAttrs, strtolower($name), $name);
      }; unset($name);
      $class["musts"] = $musts;
      $mays = A::with($class["mays"]);
      foreach ($mays as &$name) {
        $name = A::get($canonAttrs, strtolower($name), $name);
      }; unset($name);
      $class["mays"] = $mays;
      $class["attrs"] = array_merge($musts, $mays);
      $classes[strtolower($canonName)] = $class;
    }
    # résoudre l'héritage des classes
    foreach ($classes as &$class) {
      foreach ($class["sups"] as $sup) {
        $sup = strtolower(A::get($canonAttrs, strtolower($sup), $sup));
        $sup = $classes[$sup];
        A::update_n($class, $sup);
        A::merge($class["musts"], $sup["musts"]);
        A::merge($class["mays"], $sup["mays"]);
      }
    }; unset($class);

    ## fin de l'initialisation
    return [
      "syntaxes" => $this->syntaxes = $syntaxes,
      "attributes" => $this->attributes = $attributes,
      "canon_attrs" => $this->canonAttrs = $canonAttrs,
      "classes" => $this->classes = $classes,
      "canon_classes" => $this->canonClasses = $canonClasses,
    ];
  }

  const getAttributes_overrides_SCHEMA = [
    "name" => "string",
    "class" => "?string",
    "set" => "?int",
    "reset" => "?int",
  ];
  /** @var Metadata */
  private static $getAttributes_overrides_md;

  function getAttributes(array $objectClasses, ?array $overrides=null): array {
    if ($overrides !== null) {
      $tmp = [];
      foreach ($overrides as $name => $override) {
        $attribute = ValueException::check_nn(
          A::get($this->attributes, strtolower($name))
          , "$name: attribut non défini");
        $tmp[$attribute["name"]] = $override;
      }
      $overrides = $tmp;
      $md = md_utils::ensure_md(self::$getAttributes_overrides_md, self::getAttributes_overrides_SCHEMA);
      $md->eachEnsureSchema($overrides);
    }

    $nameRequired = [];
    foreach ($objectClasses as $name) {
      $name = A::get($this->canonClasses, strtolower($name), $name);
      $class = ValueException::check_nn(
        A::get($this->classes, strtolower($name))
        , "$name: classe non définie");
      foreach ($class["musts"] as $must) {
        $nameRequired[$must] = true;
      }
      foreach ($class["mays"] as $may) {
        A::replace_nx($nameRequired, $may, false);
      }
    }
    $attributes = [
      "dn" => [
        "name" => "dn",
        "class" => StringSyntax::class,
        "flags" => LdapAttr::MONOVALUED,
      ],
    ];
    foreach ($nameRequired as $name => $required) {
      $lname = strtolower($name);
      $attribute = ValueException::check_nn(
        A::get($this->attributes, $lname)
        , "$name: attribut non défini");
      $syntax = ValueException::check_nn(
        A::get($this->syntaxes, $attribute["syntax"])
        , "$attribute[syntax]: syntaxe non définie");
      $class = $attribute["class"];
      $monovalued = $attribute["single_value"]? LdapAttr::MONOVALUED: 0;
      $binary = $syntax["x_binary_transfer_required"]? LdapAttr::BINARY: 0;
      $ordered = $attribute["x_ordered"]? LdapAttr::ORDERED: 0;
      $notHumanReadable = $syntax["x_not_human_readable"]? LdapAttr::NOT_HUMAN_READABLE: 0;
      $flags = $monovalued + $binary + $ordered + $notHumanReadable;
      $override = A::get($overrides, $name);
      if ($override !== null) {
        if ($override["class"] !== null) $class = $override["class"];
        if ($override["set"] !== null) $flags = $flags | $override["set"];
        if ($override["reset"] !== null) $flags = $flags & ~$override["reset"];
      }
      $attributes[$lname] = [
        "name" => $name,
        "class" => $class,
        "flags" => $flags,
      ];
    }
    return $attributes;
  }
}
