<?php
namespace nur\data\template;

use nur\A;
use nur\b\io\IOException;
use nur\data\expr\SimpleContext;
use nur\func;
use nur\session;
use ZipArchive;

/**
 * Class InterpTemplate: réimplémentation de l'ancienne classe interp
 */
class InterpTemplate extends SimpleContext implements ITemplate {
  use TTemplate;

  function __construct(?string $text=null, $data=null, $quote=true, bool $allowPattern=true) {
    if ($data === null) $data = [];
    parent::__construct($data);
    $this->context = $this;
    $this->setText($text, $data, $quote, $allowPattern);
  }

  /** @var string */
  protected $text;
  /** @var bool|array */
  protected $quote;
  /** @var bool */
  protected $allowPattern;

  function setText(?string $text, $data=null, $quote=true, bool $allowPattern=true): void {
    $this->text = $text;
    if ($data !== null) {
      $this->data = A::with($data);
      $this->quote = $quote;
      $this->allowPattern = $allowPattern;
    }
  }

  /**
   * Traiter la valeur $value selon la valeur de $quote
   *
   * D'abord la transformer en chaine, puis:
   * - si $quote===true, la mettre en échappement avec htmlspecialchars()
   * - si $quote===false ou null, ne pas la mettre en échappement
   * - sinon, ce doit être une fonction qui met la valeur en échappement
   */
  private static function quote($value, $quote) {
    if (A::is_array($value)) $value = print_r(A::with($value), true);
    elseif (!is_string($value)) $value = strval($value);
    if ($quote === true) return htmlspecialchars($value);
    else if ($quote === false || $quote === null) return $value;
    else return func::call($quote, $value);
  }

  /**
   * Traiter la valeur $value selon la valeur de $quote. Le traitement final est
   * fait avec la méthode quote()
   *
   * - Si $quote est un tableau, alors $quote[$name] est la valeur utilisée pour
   *   décider comment traiter la valeur et sa valeur par défaut est true.
   * - Sinon prendre la valeur $quote telle quelle
   */
  private static function quote_nv(string $name, $value, $quote) {
    if (is_array($quote)) {
      if (isset($quote[$name])) {
        $value = self::quote($value, $quote[$name]);
      } else {
        # quoter par défaut quand on fournit un tableau
        $value = self::quote($value, true);
      }
    } else {
      $value = self::quote($value, $quote);
    }
    return $value;
  }

  /**
   * Résoudre la valeur du nom $name
   * - Si $name est de la forme '+keyp', prendre la valeur dans la configuration
   * - Si $name est de la forme '*keyp', prendre la valeur dans la session
   * - Sinon, $name est le chemin de clé dans le tableau $values
   */
  private function resolve(string $name, ?array $values, $quote) {
    switch (substr($name, 0, 1)) {
    case "+":
      return $this->getConfig(substr($name, 1));
    case "*":
      if (!session::started()) return "";
      return $this->getSession(substr($name, 1));
    default:
      $value = A::pget_s($values, $name, "");
      $value = self::quote_nv($name, $value, $quote);
      return $value;
    }
  }

  function apply() {
    $text = $this->text;
    $data = $this->data;
    $quote = $this->quote;
    ## d'abord les remplacements complexes
    if ($this->allowPattern) {
      if ($data !== null) {
        $names = array_keys($data);
        $name_keys_pattern = '('.implode("|", $names).')((?:\.[a-zA-Z0-9_]+)*)';
        $or_name_pattern = '|(?:(?:'.implode("|", $names).')(?:\.[a-zA-Z0-9_]+)*)';
      } else {
        $name_keys_pattern = null;
        $or_name_pattern = '';
      }
      # patterns conditionnels
      # autoriser un niveau de variable complexe à l'intérieur de la condition, i.e {{if(cond){{var.value}}}}
      $pattern = '/\{\{(if|unless)\(((?:[+*][a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*)'.$or_name_pattern.')\)((?:[^{}]*(?:\{\{[^{}]*\}\})?(?:\{[^{}]*\})?)*)\}\}/s';
      $text = preg_replace_callback($pattern, function($matches) use($data, $quote) {
        $cond = $this->resolve($matches[2], $data, $quote);
        if ($matches[1] == "if") {
          if ($cond) $value = $matches[3];
          else return "";
        } elseif ($matches[1] == "unless") {
          if (!$cond) $value = $matches[3];
          else return "";
        } else {
          return $matches[0];
        }
        $value = self::xml($value, $data, $quote);
        return $value;
      }, $text);
      # valeurs de config
      $pattern = '/\{\{(\+[a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*)\}\}/s';
      $text = preg_replace_callback($pattern, function($matches) use($data, $quote) {
        $name = $matches[1];
        $value = $this->resolve($name, $data, $quote);
        return $value;
      }, $text);
      # valeurs de la session
      $pattern = '/\{\{(\*[a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*)\}\}/s';
      $text = preg_replace_callback($pattern, function($matches) use($data, $quote) {
        $name = $matches[1];
        $value = $this->resolve($name, $data, $quote);
        return $value;
      }, $text);
      if ($data !== null) {
        # indirections
        $pattern = '/\{\{'.$name_keys_pattern.'\}\}/s';
        $text = preg_replace_callback($pattern, function($matches) use($data, $quote) {
          $name = $matches[1];
          $value = $this->resolve("$name$matches[2]", $data, $quote);
          return $value;
        }, $text);
      }
    }
    if ($data !== null) {
      ## ensuite les remplacements simples
      $froms = array();
      $tos = array();
      foreach ($data as $name => $value) {
        $froms[] = "{".$name."}";
        $tos[] = self::quote_nv($name, $value, $quote);
      }
      $text = str_replace($froms, $tos, $text);
    }
    return $text;
  }

  function xml(?string $text, $data=null, $quote=true, bool $allowPattern=true): string {
    $this->setText($text, $data, $quote, $allowPattern);
    return $this->apply();
  }

  /**
   * Comme {@link xml()} mais les valeurs ne sont pas mises en échappement par
   * défaut.
   */
  function string(?string $text, $data=null): string {
    return $this->xml($text, $data, false);
  }

  /** Comme {@link xml()} pour un fichier au format LibreOffice Writer */
  function odt(string $file, $data=null, $quote=true): void {
    $zip = new ZipArchive();
    $error = $zip->open($file);
    if ($error !== true) {
      throw new IOException("$file: error $error occured on open");
    }
    $oldContent = $zip->getFromName("content.xml");
    if ($oldContent !== false) {
      $newContent = $this->xml($oldContent, $data, $quote);
      if ($newContent !== $oldContent) {
        $zip->deleteName("content.xml");
        $zip->addFromString("content.xml", $newContent);
      }
    }
    $oldStyles = $zip->getFromName("styles.xml");
    if ($oldStyles !== false) {
      $newStyles = $this->xml($oldStyles, null, $quote);
      if ($newStyles != $oldStyles) {
        $zip->deleteName("styles.xml");
        $zip->addFromString("styles.xml", $newStyles);
      }
    }
    $zip->close();
  }
}
