<?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
 */

/**
 * JavaScript-Deployer
 *
 * Diese Klasse kümmert sich um das Ausliefern von JS-Dateien. Die Dateien
 * können optional mit JSMin (in WV5_JSMin) oder dem Google Closure Compiler
 * komprimiert werden.
 *
 * Für den Google Compiler ist cURL oder aktiviertes allow_url_fopen notwendig.
 *
 * @author Christoph
 * @since  2.0
 */
class WV5_Deployer_JavaScript extends WV5_Deployer_Base {
	const COMPRESSION_NONE   = 'none';    ///< string  JS-Komprimierung deaktiviert
	const COMPRESSION_JSMIN  = 'jsmin';   ///< string  JS-Komprimierung via JSMin
	const COMPRESSION_GOOGLE = 'google';  ///< string  JS-Komprimierung via Google Closure Compiler

	protected $compression = 'jsmin';     ///< string  die Methode, die zur Komprimierung des JS verwendet werden soll

	/**
	 * Kompression setzen
	 *
	 * Mit dieser Methode wird festgelegt, wie die in diesem Deployer enthaltenen
	 * Dateien komprimiert werden sollen. Beachten Sie, dass Dateien, die auf
	 * .min.js (und einigen weiteren Endungen, siehe WV5_Deployment::isMinified)
	 * enden, niemals komprimiert werden, sondern stattdessen 1:1 in die
	 * Ergebnisdatei übernommen werden.
	 *
	 * Wird eine nicht bekannte Kompression angegeben, wird
	 * COMPRESSION_NONE angenommen.
	 *
	 * @param  string $method  die neue Kompressionsmethode
	 * @return string          die nun aktuelle Methode
	 */
	public function setCompression($method = self::COMPRESSION_JSMIN) {
		switch ($method) {
			case self::COMPRESSION_GOOGLE:
			case self::COMPRESSION_JSMIN:
			case self::COMPRESSION_NONE:
				$this->compression = $method;
				break;

			default:
				$this->compression = self::COMPRESSION_NONE;
				break;
		}

		return $this->compression;
	}

	/**
	 * Verarbeitet eine JS-Datei mit Googles Closure Compiler (GCC)
	 *
	 * Diese Methode versucht, einen Code an den öffentlichen Compiler von Google
	 * zu senden, um ihn dort komprimieren zu lassen. Dabei werden die "simple
	 * optimizations" verwendet.
	 *
	 * Sollte der Request fehlschlagen (kein allow_url_fopen, kein cURL, Google
	 * down, Doomsday), wird der ungeänderte Code zurückgegeben.
	 *
	 * @uses   WV_HTTP_Request
	 * @param  string $code  der zu komprimierende JS-Code
	 * @return string        der fertig verarbeitete JS-Code
	 */
	public static function googleCompile($code) {
		$request = new WV_HTTP_Request('http://closure-compiler.appspot.com/compile', 'POST');
		$request->addPOST('compilation_level', 'SIMPLE_OPTIMIZATIONS');
		$request->addPOST('output_format',     'json');
		$request->addPOST('output_info',       'compiled_code');
		$request->addPOST('js_code',           $code);

		try {
			$response = $request->getResponse();

			if (is_null($response)) {
				throw new WV5_Exception('Closure Compiler is temporally not available.');
			}

			$content = json_decode($response->getContent(), true);

			if ($content === null) {
				throw new WV5_Exception('Closure Compiler did not send json. Weird and not acceptable!');
			}

			if (!empty($content['serverErrors'])) {
				$msg = array();

				foreach ($content['serverErrors'] as $error) {
					$msg[] .= 'Code: '.$error['code'].' '.$error['error'].'!';
				}

				$msg = implode(' ', $msg);
				throw new WV5_Exception('Oh dear! '.$msg);
			}

			$code = $content['compiledCode'];

			if (empty($code)) {
				throw new WV5_Exception('The returned javascript is empty, but we will never know why. Obviously you are not a Javascript Ninja!');
			}
		}
		catch (Exception $e) {
			// throw warning, when minimizing via google closure compiler failed and pass uncompressed js-code.
			trigger_error('JS-Minimizing via Google Closure Compiler failed: '.$e->getMessage(), E_USER_WARNING);
			throw $e;
		}

		return $code;
	}

	/**
	 * JavaScript-Datei komprimieren
	 *
	 * Diese Methode wird eine JS-Datei komprimieren. Standardmäßig wird dazu das
	 * mitgelieferte JSMin zum Einsatz kommen. Alternativ kann die Kompression
	 * aber auch gänzlich deaktiviert (COMPRESSION_NONE) oder auf den Google
	 * Closure Compiler (COMPRESSION_GOOGLE) umgeschaltet werden.
	 *
	 * Um Zeit zu sparen, werden die komprimierten Dateiinhalte einzeln gecached.
	 * Um diesen Cache zu leeren, kann im REDAXO-Backend der allgemeine Cache
	 * geleert werden oder WV5_Deployment::clearCache() aufgerufen werden.
	 *
	 * @uses   JSMin
	 * @param  string  $filename  die zu verarbeitende JS-Datei
	 * @param  boolean $debug     wenn true, werden die Namen der Dateien als Kommentare eingefügt
	 * @return string             der fertige (ggf. komprimierte) Code
	 */
	protected function minify($filename, $debug = false) {
		$cacheKey  = md5($filename.$this->compression.md5_file($filename));
		$cacheDir  = WV5_Deployment::getCacheFolder();
		$cacheFile = $cacheDir.'/cache_'.substr($cacheKey, 0, 6).'.js';

		if (file_exists($cacheFile)) {
			return file_get_contents($cacheFile);
		}

		$content = file_get_contents($filename);

		switch ($this->compression) {
			case self::COMPRESSION_GOOGLE:
				$content = self::googleCompile($content);
				break;

			case self::COMPRESSION_JSMIN:
				$content = WV5_Util_JSMin::minify($content);
				break;
		}

		file_put_contents($cacheFile, $content);
		return $content;
	}

	/**
	 * Dateien zusammenfügen und komprimieren
	 *
	 * Diese Methode führt die Verarbeitung der Dateien durch und gibt danach
	 * ein Array mit Status-Infos zurück.
	 *
	 * Die Dateien werden alle eingelesen, komprimiert und in einer gebündelten
	 * Datei gespeichert. Die Methode merkt sich die mtime der Dateien, um nicht
	 * bei jedem Aufruf die Dateien erneut komprimieren zu müssen. Das Speichern
	 * dieser Information erfolgt in der Registry.
	 *
	 * Im Ergebnis-Array sind folgende Elemente enthalten:
	 *
	 * - file (string; der Dateiname der generierten Datei)
	 * - mtime (int; Unix-Timestamp der letzten Aktualisierung)
	 * - mtime_hex (string; wie mtime, nur in lower hex)
	 * - hash (string; ein 6-stelliger Hash über die enthaltenen Dateien)
	 *
	 * @param  boolean $debug  wenn true, werden die Namen der Dateien als Kommentare eingefügt
	 * @return mixed           ein Array mit den Ergebnisdaten oder false im Fehlerfalle
	 */
	public function create($debug = false) {
		if (empty($this->files)) {
			return false;
		}

		$hash      = substr(md5(implode('|', $this->files).$this->compression), 0, 6);
		$status    = $this->checkFileStatus($hash);
		$isFresh   = $status['ok'];
		$cacheFile = WV5_Deployment::getCacheFolder().'/compressed_'.$hash.'.js';
		$result    = array(
			'file'      => $cacheFile,
			'mtime'     => $status['mtime'],
			'mtime_hex' => strtolower(dechex($status['mtime'])),
			'hash'      => $hash
		);

		if (!$isFresh || !file_exists($cacheFile)) {
			$compressed = $this->compressFiles($hash, ";\n", $debug);
			file_put_contents($cacheFile, $compressed);
		}

		return $result;
	}

	public function getTypeString() {
		return 'js';
	}
}
