<?php
namespace nur\php;

use nur\A;
use nur\b\io\TmpfileWriter;
use nur\b\ValueException;
use nur\base;
use nur\func;
use nur\log;
use nur\reader;
use ReflectionClass;

/**
 * Class Updater: met à jour une fichier source
 */
class SrcUpdater {
  protected $allowUndefined = false;

  function setAllowUndefined(bool $allowUndefined): void {
    $this->allowUndefined = $allowUndefined;
  }

  /**
   * analyser le fichier $pf et trouver le namespace et la classe. retourner un
   * tableau ["namespace" => $namespace, "class" => $class]
   */
  function parseFile(string $pf): array {
    $lines = reader::read_lines($pf);

    $namespace = false;
    $class_name = null;
    $consts = [];
    foreach ($lines as $line) {
      if (preg_match('/^\s*namespace\s+(.+);/', $line, $ms)) {
        $namespace = $ms[1];
      } elseif (preg_match('/^\s*(?:abstract\s*)?class\s+(\w+)/', $line, $ms)) {
        $class_name = $ms[1];
      } elseif (preg_match('/^\s*const\s+(\w+)/', $line, $ms)) {
        $consts[$ms[1]] = true;
      }
    }

    $class = $class_name;
    if ($namespace) $class = "$namespace\\$class";
    return [
      "namespace" => $namespace,
      "class_name" => $class_name,
      "class" => $class,
      "defined_consts" => $consts,
      "file" => $pf,
      "lines" => $lines,
    ];
  }

  function _initParams(?array &$params, string $file) {
    A::update_n($params, $this->parseFile($file));
    if (base::z($params["class"])) throw new ValueException("class is required");
  }

  protected static function undefined_const_msg(string $name, string $class): string {
    return "$name: is not defined in class $class";
  }

  protected static function undefined_const(string $name, string $class): ValueException {
    return new ValueException(self::undefined_const_msg($name, $class));
  }

  function _updateConsts(array &$params, ?bool $allowUndefined=null) {
    if ($allowUndefined === null) $allowUndefined = $this->allowUndefined;

    $class = $params["class"];
    $rc = new ReflectionClass($class);
    $availableConsts = $rc->getConstants();
    if (!A::has($availableConsts, "_AUTOGEN_CONSTS")) {
      # il n'y a rien à mettre à jour
      return;
    }

    # normaliser _AUTOGEN_CONSTS
    # chaque élement séquentiel $value de $autogenConsts est remplacé par
    # $value => [$class, "_AUTOGEN_$value"]
    $autogenConsts = [];
    $index = 0;
    foreach ($availableConsts["_AUTOGEN_CONSTS"] as $key => $value) {
      if ($key === $index) {
        # clé séquentielle
        $index++;
        $autogenConsts[$value] = [$class, "_AUTOGEN_$value"];
      } else {
        $autogenConsts[$key] = $value;
      }
    }

    # calculer la liste dynamique de constantes et leur valeur
    if (A::has($autogenConsts, "")) {
      # liste dynamique de constantes et leurs valeurs associées
      $func = A::getdel($autogenConsts, "");
      $args = [];
      func::fix_static($func, $class);
      func::fix_args($func, $args);
      $dynamicConsts = A::with(func::call($func, ...$args));
      foreach (array_keys($dynamicConsts) as $name) {
        if (!A::has($availableConsts, $name) && !$allowUndefined) {
          throw self::undefined_const($name, $class);
        }
      }
    } else {
      $dynamicConsts = [];
    }
    # liste de tuples [$value, $literal_value]
    $literals = A::get($dynamicConsts, "_AUTOGEN_LITERALS");
    if ($literals === null) $literals = A::get($availableConsts, "_AUTOGEN_LITERALS");

    # lister les constantes à calculer
    # une valeur mentionée dans _AUTOGEN_CONSTS prend le pas sur la même valeur
    # retournée dans la liste dynamique de constantes
    $definedConsts = $params["defined_consts"];
    $consts = [];
    foreach (array_keys($autogenConsts) as $name) {
      if (A::has($availableConsts, $name)) {
        $value = $availableConsts[$name];
        A::del($dynamicConsts, $name);
      } elseif ($allowUndefined) {
        $value = null;
      } else {
        throw self::undefined_const($name, $class);
      }
      if (!A::has($definedConsts, $name)) {
        log::warning(self::undefined_const_msg($name, $class));
      }
      $consts[$name] = $value;
    }

    # calculer les valeurs des constantes
    foreach ($consts as $name => &$const) {
      if (!A::has($autogenConsts, $name)) continue;
      $func = $autogenConsts[$name];
      $args = [];
      func::fix_static($func, $class);
      func::fix_args($func, $args);
      $args[] = $const;
      $const = func::call($func, ...$args);
    }; unset($const);

    # puis mettre à jour le fichier
    $lines = [];
    $rewriteConst = false;
    $prefix = null;
    foreach ($params["lines"] as $line) {
      if (preg_match('/^(\s*)const\s+(\w+)/', $line, $ms)) {
        $prefix = $ms[1];
        $name = $ms[2];
        if (A::has($consts, $name)) {
          # cette constante doit être mise à jour
          $rewriteConst = true;
        } elseif (A::has($dynamicConsts, $name)) {
          $consts[$name] = A::getdel($dynamicConsts, $name);
          $rewriteConst = true;
        }
      } elseif (!$rewriteConst && $dynamicConsts &&
        preg_match('/^(\s*)#+\s*--autogen-dynamic--/', $line, $ms)) {
        # il faut écrire les constantes dynamiques restantes
        $prefix = $ms[1];
        foreach ($dynamicConsts as $name => $value) {
          $generator = new SrcGenerator($prefix);
          $generator->genConst($name, $value, null, $literals, false, "/*autogen*/");
          $generator->mergeInto($lines);
        }
        $dynamicConsts = false;
        $prefix = null;
      }
      if ($rewriteConst) {
        if (preg_match('/;\s*$/', $line, $ms)) {
          # écrire la constante mise à jour
          $generator = new SrcGenerator($prefix);
          $generator->genConst($name, $consts[$name], null, $literals, false, "/*autogen*/");
          $generator->mergeInto($lines);

          $rewriteConst = false;
          $prefix = null;
        }
      } else {
        $lines[] = $line;
      }
    }
    if ($dynamicConsts) {
      $count = count($dynamicConsts);
      log::warning("$params[file]: missing #--autogen-dynamic-- section, $count dynamic const(s) skipped", log::NORMAL);
    }
    if ($rewriteConst) {
      throw new ValueException("$params[file]: parse error");
    }

    $params["lines"] = $lines;
  }

  private static function call_autogens(?array $autogens, string $class) {
    $dest = [];
    if ($autogens !== null) {
      foreach ($autogens as $func) {
        $args = [];
        func::fix_static($func, $class);
        func::fix_args($func, $args);
        $fcontext = func::_prepare($func);
        func::_fill($fcontext, $args);
        $minArgs = $fcontext[3];
        $maxArgs = $fcontext[4];
        if ($maxArgs > $minArgs) {
          # ne rajouter la classe qu'en tant que dernier argument optionnel
          $maxArgs--;
          $minArgs = count($args); # prendre le nombre effectif
          while ($minArgs < $maxArgs) {
            $args[] = null;
            $minArgs++;
          }
          $args[] = $class;
        }
        A::merge($dest, func::_call($fcontext, $args));
      };
    }
    return $dest;
  }

  function _updatePropertiesAndMethods(array &$params) {
    $class = $params["class"];
    $rc = new ReflectionClass($class);
    $defined_consts = $rc->getConstants();
    $autogenProperties = A::get($defined_consts, "_AUTOGEN_PROPERTIES");
    $autogenMethods = A::get($defined_consts, "_AUTOGEN_METHODS");
    if ($autogenProperties === null && $autogenMethods === null) {
      # il n'y a rien à mettre à jour
      return;
    }

    $properties = self::call_autogens($autogenProperties, $class);
    $methods = self::call_autogens($autogenMethods, $class);

    $lines = [];
    $done = false;
    $in_comment = false;
    $found_section = false;
    foreach ($params["lines"] as $line) {
      if ($done) {
        $lines[] = $line;
        continue;
      }
      if (!$in_comment) {
        if (preg_match('/^\s*\/\*\*/', $line)) {
          $in_comment = true;
        }
        $lines[] = $line;
        continue;
      }
      # ici, $in_comment == true
      if (preg_match('/^(\s*)\*\//', $line, $ms)) {
        $prefix = "$ms[1]* ";
        $lines[] = "$prefix--autogen-properties-and-methods--";
        foreach ($properties as $property) {
          $lines[] = "$prefix@property $property";
        }
        foreach ($methods as $method) {
          $lines[] = "$prefix@method $method";
        }
        $lines[] = $line;
        $in_comment = false;
        $done = true;
        continue;
      }
      if (!$found_section) {
        if (preg_match('/^\s*\*?\s*--autogen-(?:properties-and-)?methods--\s*$/', $line)) {
          $found_section = true;
        } else {
          $lines[] = $line;
          continue;
        }
      }
      # ici, $found_section == true
    }
    if ($in_comment) {
      throw new ValueException("$params[file]: parse error: check class comment");
    }

    $params["lines"] = $lines;
  }

  function _writeFile(array $params) {
    $file = $params["file"];
    $outf = new TmpfileWriter(dirname($file));
    $outf->writeLines($params["lines"]);
    $outf->rename($file);
  }

  function updateConsts(string $file, ?bool $allowUndefined=null, ?array $params=null) {
    $this->_initParams($params, $file);
    require($file);

    $this->_updateConsts($params, $allowUndefined);
    $this->_writeFile($params);
  }

  function updatePropertiesAndMethods(string $file, ?array $params=null) {
    $this->_initParams($params, $file);
    require($file);

    $this->_updatePropertiesAndMethods($params);
    $this->_writeFile($params);
  }

  function update(string $file, ?array $params=null) {
    $this->_initParams($params, $file);
    require($file);

    $this->_updateConsts($params);
    $this->_updatePropertiesAndMethods($params);
    $this->_writeFile($params);
  }
}