<?php
/*
 * Copyright (c) 2012, webvariants GbR, http://www.webvariants.de
 *
 * This file is released under the terms of the MIT license. You can find the
 * complete text in the attached LICENSE file or online at:
 *
 * http://www.opensource.org/licenses/mit-license.php
 */

/**
 * Generator für URLs
 *
 * Dieser Generator ist eine Möglichkeit für AddOns, selber schöne URLs über
 * realURL2 generieren zu lassen. Dazu reicht es, ein _WV24_URLGenerator-Objekt
 * zu erzeugen, Parameter zu binden und dann generate() aufzurufen.
 *
 * Jedem Generator stehen eine Reihe von vordefinierten Platzhaltern zur
 * Verfügung, die automatisch mit den passenden Werten belegt werden.
 *
 * - <code>slyartid</code>/<code>rexartid</code> (ID des Ziel-Artikels)
 * - <code>slyclangid</code> (Sprach-ID des Ziel-Artikels)
 * - <code>slyclang</code> (Alias für clangid)
 *
 * Zu jeder URL können neben den Werten für die eigentliche URL (z. B.
 * Produktnamen in einem Shop) auch noch beliebige zusätzliche Werte gebunden
 * werden, die in der Datenbank zu der URL abgelegt und bei einem Aufruf
 * wiederhergestellt werden. Damit kann zu der URL "foo/bar/produkt1.html"
 * die Produkt-ID mitgespeichert werden, um auf sie direkt im entsprechenden
 * Modul zugreifen zu können.
 *
 * Jede generierte URL enthält neben den schon erwähnten Daten noch weitere
 * Informationen, die primär zum effizienten Auffinden einer Gruppe von URLs
 * dienen.
 *
 * Im Namespace kann sich jedes AddOn verewigen, um alle von ihm generierten
 * URLs zu identifizieren. Das wird beim Deinstallation sehr praktisch, um den
 * Cache zu leeren. Namespaces dürfen maximal 32 Zeichen lang sein.
 *
 * Über den Key kann ein AddOn eine URL eindeutig identifizieren. Ein Key könnte
 * für einen Blogbeitrag zum Beispiel "1_0" für Artikel 1, Sprache 0 sein. Wie
 * ein AddOn einen Key erzeugt, ist ihm überlassen. Maximallänge ist auch hier
 * 32 Zeichen.
 *
 * Die Kombination aus Namespace und Key muss für alle Routen im System
 * eindeutig sein.
 *
 * Der Path ist vergleichbar mit der Pfadangabe für Sally-Artikel und sollte,
 * wenn überhaupt, auch in dieser Form genutzt werden. Wenn ein AddOn
 * strukturierte Daten verwaltet, kann über den Pfad einfach ein Teilbaum der
 * Struktur selektiert werden. Empfohlenes Format: "|ID|ID|ID|"; Maximallänge:
 * 128 Zeichen.
 *
 * @since  v4.0
 * @author Christoph
 */
class WV24_Generator {
	protected $route;      ///< string  die Route als String
	protected $params;     ///< array   die gebundenen Parameter
	protected $articleID;  ///< int     die ID des Ziel-Artikels
	protected $clang;      ///< int     die Sprach-ID des Ziel-Artikels
	protected $namespace;
	protected $ident;
	protected $path;
	protected $isFullUrl;
	protected $settings;
	protected $storage;

	/**
	 * Konstruktor
	 *
	 * Erzeugt eine neuen Generator. Eine Route ist dabei nur eine
	 * Bildungsvorschrift für neue URLs. Routen selbst werden nicht permanent
	 * gespeichert, dafür aber die nach ihrem Muster erzeugten URLs.
	 *
	 * In die Route können Platzhalter eingebettet werden. Platzhalter beginnen
	 * mit einem Doppelpunkt und nehmen dann alle folgenden alphanumerischen
	 * Zeichen ein. Beispiel: "hallo/:kat/:prod.html" enthält die beiden
	 * Platzhalter kat und prod, die später mit bind() mit Werten belegt
	 * werden.
	 *
	 * @throws Exception      falls die Route leer ist
	 * @param  string $route  die URL (meist mit Platzhaltern)
	 */
	public function __construct($route, _WV24_Storage $storage = null) {
		$this->setRoute($route);

		$this->params    = array();
		$this->articleID = sly_Core::getCurrentArticleId();
		$this->clang     = sly_Core::getCurrentClang();
		$this->namespace = '';
		$this->ident     = '';
		$this->path      = '';
		$this->isFullUrl = false;
		$this->settings  = null;
		$this->storage   = $storage ? $storage : WV24_Util::getStorage();
	}

	protected function prewarmSettings() {
		$this->settings = array(
			'cleanup'   => WV8_Settings::getValue('realurl2', 'cleanup', null, $this->clang),
			'lowercase' => WV8_Settings::getValue('realurl2', 'lowercase', null, $this->clang)
		);
	}

	/**
	 * Route setzen
	 *
	 * Überschreibt das interne "Muster", nach dem die URLs erzeugt werden.
	 *
	 * @throws Exception      falls die Route leer ist
	 * @param  string $route  die neue URL (meist mit Platzhaltern)
	 */
	public function setRoute($route) {
		$route = trim(strtolower($route));

		if (empty($route)) {
			throw new Exception('Leere Routen sind nicht erlaubt.');
		}

		$this->route = $route;
	}

	/**
	 * Wert binden
	 *
	 * Diese Methode füllt eine Route mit entsprechenden Werten. Dabei wird in
	 * $param der Doppelpunkt des Platzhalters nicht mit angegeben. Wird $restore
	 * auf true gesetzt, wird der gebundene Wert in der Datenbank zusätzlich
	 * gespeichert und bei einem Request wiederhergestellt.
	 *
	 * Es gibt keine Beschränkung, welche Werte an eine URL gebunden werden
	 * können. Es werden allerdings nur diejenigen verwendet, deren Platzhalter
	 * in der Route auftauchen oder die bei einem Request wiederhergestellt
	 * werden sollen. Alle weiteren hängen unmotiviert bis zum Scriptende im
	 * Speicher rum.
	 *
	 * @param  string  $param    der Name des Platzhalters
	 * @param  mixed   $value    der entsprechende Wert (keine Ressourcen oder File Handles!)
	 * @param  boolean $restore  wenn true, wird der Wert in $_GET bei einem Request wiederhergestellt
	 * @param  boolean $raw      wenn true, wird der Wert nicht URL-friendly gemacht, sondern 1:1 übernommen
	 * @return int               die Anzahl der nun gebundenen Parameter
	 */
	public function bind($param, $value, $restore = false, $raw = false) {
		$this->params[$param] = array('value' => $value, 'restore' => (boolean) $restore, 'raw' => (boolean) $raw);
		return count($this->params);
	}

	/**
	 * Mehrere Werte auf einmal binden
	 *
	 * Diese Methode füllt eine Route mit entsprechenden Werten. Dabei wird in
	 * $params ein assoziatives Array mit den Werten angegeben. Wird $restore
	 * auf true gesetzt, werden alle gebundenen Werte bei einem Request
	 * wiederhergestellt.
	 *
	 * Siehe bind() für die Beschränkungen, welche Parameter erlaubt sind.
	 *
	 * @param  array   $params   die zu bindenden Werte als assoziatives Array
	 * @param  boolean $restore  wenn true, werden alle Werte in $_GET bei einem Request wiederhergestellt
	 * @param  boolean $raw      wenn true, werden alle 1:1 übernommen und nicht nicht URL-friendly gemacht
	 * @return int               die Anzahl der nun gebundenen Parameter
	 */
	public function bindMany($params, $restore = false) {
		$restore = (boolean) $restore;
		$raw     = (boolean) $raw;

		foreach ($params as $key => $value) {
			$this->params[$key] = array('value' => $value, 'restore' => $restore, 'raw' => $raw);
		}

		return count($this->params);
	}

	/**
	 * Entfernt einen Wert wieder
	 *
	 * Diese Methode entfernt einen gesetzten Wert wieder aus der Liste.
	 *
	 * @param  string $param  der Name des Platzhalters
	 * @return int            die Anzahl der nun gebundenen Parameter
	 */
	public function unbind($param) {
		unset($this->params[$param]);
		return count($this->params);
	}

	/**
	 * Alle Werte entfernen
	 *
	 * Diese Methode entfernt alle gesetzten Werte.
	 *
	 * @return int  immer 0
	 */
	public function unbindAll() {
		$this->params = array();
		return 0;
	}

	public function setNamespace($namespace) {
		$this->namespace = trim($namespace);
		return $this->namespace;
	}

	public function setIdent($ident) {
		$this->ident = trim($ident);
		return $this->ident;
	}

	public function setPath($path) {
		$this->path = trim($path);
		return $this->path;
	}

	public function setIsFullUrl($flag = true) {
		$this->isFullUrl = (boolean) $flag;
		return $this->isFullUrl;
	}

	/**
	 * Ziel-Artikel setzen
	 *
	 * Diese Methode setzt den Artikel, auf den die erzeugten URLs zeigen sollen.
	 *
	 * @param  mixed $article  null für den aktuellen Artikel, sonst irgend etwas, das als Artikel identifiziert werden kann
	 * @return int             die ermittelte Artikel-ID
	 */
	public function setArticle($article = null) {
		$this->articleID = $article === null ? sly_Core::getCurrentArticleId() : WV_Sally::getIDForArticle($article);
		return $this->articleID;
	}

	public function getArticle() {
		return $this->articleID;
	}

	public function getClang() {
		return $this->clang;
	}

	public function getNamespace() {
		return $this->namespace;
	}

	public function getIdent() {
		return $this->ident;
	}

	public function getPath() {
		return $this->path;
	}

	public function getParams($restoredOnly = false) {
		$params = array();

		foreach ($this->params as $key => $value) {
			if ($restoredOnly && !$value['restore']) continue;
			$params[$key] = $value['value'];
		}

		return $params;
	}

	/**
	 * Ziel-Sprache setzen
	 *
	 * Diese Methode setzt die Sprache, auf die die erzeugten URLs zeigen sollen.
	 *
	 * @param  int $clang  die gewünschte Sprache
	 * @return int         die ermittelte Sprach-ID
	 */
	public function setClang($clang) {
		$this->clang    = (int) $clang;
		$this->settings = null;

		return $this->clang;
	}

	/**
	 * URL erzeugen
	 *
	 * Diese Methode erzeugt schlussendlich die fertige URL zu der aktuellen
	 * Route. Die URL wird automatisch im Cache abgelegt.
	 *
	 * @param  boolean $store  whether or not to store the generated URL
	 * @return _WV24_URL       die fertige Route
	 */
	public function generate($store = true) {
		// Immer verfügbare Variablen setzen
		$this->bindCoreVariables();

		if ($this->settings === null) {
			$this->prewarmSettings();
		}

		// Platzhalter in der Route ersetzen
		$trimmedRoute = ltrim($this->route, '/');
		preg_match_all('#(:[a-z][a-z0-9]*)#si', $trimmedRoute, $matches);

		extract($this->settings);

		if (!empty($matches)) {
			foreach ($matches[1] as $match) {
				$param    = substr($match, 1);
				$value    = isset($this->params[$param]) ? $this->params[$param]['value'] : '';
				$raw      = isset($this->params[$param]) ? $this->params[$param]['raw'] : false;
				$friendly = $raw ? $value : WV24_Util::makeURLFriendly($value, $cleanup, $lowercase);

				$trimmedRoute = str_replace($match, $friendly, $trimmedRoute);
			}
		}

		// Fehlende Platzhalter können URLs à la "foo//bar" erzeugen.
		// Das können wir problemlos immer korrigieren.
		$trimmedRoute = preg_replace('#(?<!http:)//+#', '/', $trimmedRoute);

		// Route URL zum Seitenstartartikel anhängen
		$articleID = $this->articleID;
		$clang     = $this->clang;

		if (!$this->isFullUrl) {
			$artURL = WV24_RealURL2::getURL(sly_Core::getSiteStartArticleId(), $clang, array(), '', true);
			$url    = rtrim($artURL, '/').'/'.$trimmedRoute;
		}
		else {
			$url = $trimmedRoute;
		}

		// URL und Parameter cachen
		$params = array();

		foreach ($this->params as $key => $data) {
			if ($data['restore']) {
				$params[$key] = $data['value'];
			}
		}

		if ($store) {
			$this->storage->setUrl($this->namespace, $this->ident, $url, $articleID, $clang, $this->path, $params);
		}

		// Fertig!
		return new _WV24_URL(array(
			'namespace'  => $this->namespace,
			'ident'      => $this->ident,
			'article_id' => $articleID,
			'clang'      => $clang,
			'path'       => $this->path,
			'url'        => $url,
			'created'    => date('r'),
			'params'     => $params
		));
	}

	/**
	 * Core-Variablen setzen
	 *
	 * Jeder Route stehen automatisch die in der Klassenbeschreibung genannten
	 * Platzhalter zur Verfügung. Diese werden in dieser Methode mit sinnvollen
	 * Werten belegt.
	 *
	 * Da diese Methode von generate() aufgerufen wird, ist es nicht möglich,
	 * die Core-Variablen mit eigenen Werten zu überschreiben.
	 */
	protected function bindCoreVariables() {
		$this->bind('slyartid', $this->articleID, false);
		$this->bind('slyclangid', $this->clang, false);
		$this->bind('slyclang', $this->clang, false);
	}
}
