<?php
namespace nur\data\types;

use nur\A;
use nur\func;
use nur\ref\ref_type;

class PhpIncarnation implements IIncarnation {
  function __construct() {
    # non nullable
    $this->types[ref_type::MIXED] = new MixedType(["allow_null" => false]);
    $this->types[ref_type::BOOL] = new BoolType(["allow_null" => false]);
    $this->types[ref_type::INT] = new IntType(["allow_null" => false]);
    $this->types[ref_type::FLOAT] = new FloatType(["allow_null" => false]);
    $this->types[ref_type::RAWSTRING] = new RawStringType(["allow_null" => false]);
    $this->types[ref_type::STRING] = new StringType(["allow_null" => false]);
    $this->types[ref_type::TEXT] = new TextType(["allow_null" => false]);
    $this->types[ref_type::KEY] = new KeyType(["allow_null" => false]);
    $this->types[ref_type::CONTENT] = new ContentType(["allow_null" => false]);
    $this->types[ref_type::FILE] = new FileType(["allow_null" => false]);
    $this->types[ref_type::DATETIME] = new SDatetimeType(["allow_null" => false]);
    $this->types[ref_type::DATE] = new SDateType(["allow_null" => false]);
    $this->types[ref_type::TIME] = new STimeType(["allow_null" => false]);
    $this->types[ref_type::HOUR] = new SHourType(["allow_null" => false]);
    # nullable
    $this->types[ref_type::NMIXED] = new MixedType(["allow_null" => true]);
    $this->types[ref_type::NBOOL] = new BoolType(["allow_null" => true]);
    $this->types[ref_type::TRIBOOL] = new TriboolType(["allow_null" => true]);
    $this->types[ref_type::NTRIBOOL] = new TriboolType(["allow_null" => true]);
    $this->types[ref_type::NINT] = new IntType(["allow_null" => true]);
    $this->types[ref_type::NFLOAT] = new FloatType(["allow_null" => true]);
    $this->types[ref_type::NRAWSTRING] = new RawStringType(["allow_null" => true]);
    $this->types[ref_type::NSTRING] = new StringType(["allow_null" => true]);
    $this->types[ref_type::NTEXT] = new TextType(["allow_null" => true]);
    $this->types[ref_type::NKEY] = new KeyType(["allow_null" => true]);
    $this->types[ref_type::NCONTENT] = new ContentType(["allow_null" => true]);
    $this->types[ref_type::NFILE] = new FileType(["allow_null" => true]);
    $this->types[ref_type::NDATETIME] = new SDatetimeType(["allow_null" => true]);
    $this->types[ref_type::NDATE] = new SDateType(["allow_null" => true]);
    $this->types[ref_type::NTIME] = new STimeType(["allow_null" => true]);
    $this->types[ref_type::NHOUR] = new SHourType(["allow_null" => true]);
    # generic
    $this->types[ref_type::NARRAY] = new GenericType(GenericType::ARRAY, ["allow_null" => true]);
    $this->types[ref_type::ARRAY] = new GenericType(GenericType::ARRAY, ["allow_null" => false]);
    $this->types[ref_type::NARRAY_ARRAY] = new GenericType(GenericType::ARRAY_ARRAY, ["allow_null" => true]);
    $this->types[ref_type::ARRAY_ARRAY] = new GenericType(GenericType::ARRAY_ARRAY, ["allow_null" => false]);
    $this->types[ref_type::NITERABLE] = new GenericType(GenericType::ITERABLE, ["allow_null" => true]);
    $this->types[ref_type::ITERABLE] = new GenericType(GenericType::ITERABLE, ["allow_null" => false]);
    $this->types[ref_type::NRESOURCE] = new GenericType(GenericType::RESOURCE, ["allow_null" => true]);
    $this->types[ref_type::RESOURCE] = new GenericType(GenericType::RESOURCE, ["allow_null" => false]);
  }

  const CLASS_ALIASES = [
    ref_type::BOOL => BoolType::class,
    ref_type::TRIBOOL => TriboolType::class,
    ref_type::INT => IntType::class,
    ref_type::FLOAT => FloatType::class,
    ref_type::RAWSTRING => RawStringType::class,
    ref_type::STRING => StringType::class,
    ref_type::TEXT => TextType::class,
    ref_type::KEY => KeyType::class,
    ref_type::CONTENT => ContentType::class,
    ref_type::FILE => FileType::class,
    ref_type::DATETIME => SDatetimeType::class,
    ref_type::DATE => SDateType::class,
    ref_type::TIME => STimeType::class,
    ref_type::HOUR => SHourType::class,
  ];

  /** @var array liste d'associations {nom type => classe} */
  protected $typeClasses = [];

  /** @var array liste d'associations {nom type => instance de types} */
  protected $types = [];

  /** @var array liste de définitions de types avec leurs arguments */
  protected $dynamicTypes = [];

  private static function fix_name(&$name) {
    if ($name === null) $name = "mixed";
  }

  function hasType($name): bool {
    self::fix_name($name);

    if (is_array($name)) {
      $key = array_key_first($name);
      if ($key !== 0) {
        # type dynamique nommé
        $name = $key;
      } else {
        # type dynamique
        foreach ($this->dynamicTypes as $dtype) {
          if ($name === $dtype) return true;
        }
        return false;
      }
    } elseif (!array_key_exists($name, $this->types)) {
      $name = A::get(ref_type::ALIASES, $name, $name);
    }
    # type statique ou dynamique nommé
    $type = A::get($this->types, $name);
    if ($type !== null) return true;
    return A::get($this->typeClasses, $name) !== null;
  }

  function getType($name, bool $required=true): ?IType {
    self::fix_name($name);

    ## vérifier si le type est déjà défini
    $dclass = null;
    $dtype = null;
    if (is_array($name)) {
      $dtype = $name;
      $key = array_key_first($dtype);
      if ($key !== 0) {
        # type dynamique nommé
        $name = $key;
        $dclass = $dtype[$name];
        unset($dtype[$name]);
        array_unshift($dtype, $dclass);
      } else {
        # type dynamique anonyme
        $name = null;
      }
    } elseif (!array_key_exists($name, $this->types)) {
      $name = A::get(ref_type::ALIASES, $name, $name);
    }
    if ($name === null) {
      ## type dynamique anonyme
      foreach ($this->dynamicTypes as $dname => $dynamicType) {
        if ($dtype === $dynamicType) {
          $name = $dname;
          break;
        }
      }
    }
    if ($name !== null) {
      $type = A::get($this->types, $name);
      if ($type !== null) return $type;
    }

    ## il faut créer le type
    if ($name === null) {
      # type dynamique
      $name = count($this->dynamicTypes) + 1;
      $this->dynamicTypes[$name] = $dtype;
    }

    if ($dtype !== null) {
      $class = $dtype[0];
      $class = A::get(ref_type::ALIASES, $class, $class);
      $class = A::get(self::CLASS_ALIASES, $class, $class);
      $args = array_slice($dtype, 1);
      if (!is_subclass_of($class, IType::class)) {
        # si la classe n'implémente pas IType, prendre le type générique
        $class = [GenericType::class, $class];
      }
    } else {
      $class = A::get($this->typeClasses, $name);
      $args = [];
      if ($class !== null) func::fix_class_args($class, $args);
      if ($class === null) {
        # assumer que $name est une classe
        if (is_subclass_of($name, IType::class)) $class = $name;
        elseif ($required) $class = [GenericType::class, $name];
        else return null;
      } elseif (!is_subclass_of($class, IType::class)) {
        # si la classe n'implémente pas IType, prendre le type générique
        $class = [GenericType::class, $class];
      }
    }
    func::fix_class_args($class, $args);
    $type = func::cons($class, ...$args);

    return $this->types[$name] = $type;
  }

  function addType($typeOrClass, ?string $name=null): void {
    if ($typeOrClass instanceof IType) {
      if ($name === null) $name = get_class($typeOrClass);
      $this->types[$name] = $typeOrClass;
      return;
    }
    if (is_array($typeOrClass)) {
      # normaliser si nécessaire
      $key = array_key_first($typeOrClass);
      if ($key === 0) {
        if ($name === null) {
          foreach ($this->dynamicTypes as $dname => $dynamicType) {
            if ($typeOrClass === $dynamicType) {
              # type dynamique existant
              # c'est le seul cas où le type n'écrase pas celui existant
              return;
            }
          }
          # nouveau type dynamique
          $name = count($this->dynamicTypes) + 1;
          $this->dynamicTypes[$name] = $typeOrClass;
        }
      } else {
        # type dynamique nommé
        if ($name === null) $name = $key;
        $class = $typeOrClass[$key];
        unset($typeOrClass[$key]);
        array_unshift($typeOrClass, $class);
      }
    } elseif ($name === null) {
      $name = $typeOrClass;
    }
    $this->typeClasses[$name] = $typeOrClass;
  }
}
