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

/**
 * CSS-Komprimierung
 *
 * Diese Klasse kapselt eine Reihe von regulären Ausdrücken und einen großen
 * Haufen schwarzer Voodoo-Magie, die eine CSS-Datei ziemlich gut komprimieren
 * können.
 *
 * Generell sollte keinesfalls eine Datei mit fiesen Browser-Hacks komprimiert
 * werden, da das den Algorithmus aus dem Tritt bringen könnte und dann grausame
 * Dinge geschehen könnten.
 *
 * @since  1.0
 * @author Christoph Mewes
 */
abstract class WV5_Util_CSSMin {
	/**
	 * CSS-Datei komprimieren
	 *
	 * Diese Methode wendet auf eine CSS-Datei eine Folge von Transformationen
	 * an, um diese möglichst kompakt zu komprimieren. Primär werden mit
	 * regulären Ausdrücken Whitespaces entfernt, Regeln zusammengefasst und
	 * unnötige Einheiten entfernt. Auch wird eine eventuell vorhandene CHARSET
	 * Angabe entfernt und die Datei nach UTF-8 kodiert.
	 *
	 * @param  string  $filename  der Name der zu komprimierenden Datei
	 * @param  boolean $debug     wenn true, wird jede Regel in einer eigenen Zeile notiert
	 * @return string             der komprimierte Dateiinhalt
	 */
	public static function compressFile($filename, $debug = false) {
		return self::compress(file_get_contents($filename), $debug);
	}

	/**
	 * CSS-Code komprimieren
	 *
	 * Diese Methode wendet auf einen CSS-Code eine Folge von Transformationen
	 * an, um diese möglichst kompakt zu komprimieren. Primär werden mit
	 * regulären Ausdrücken Whitespaces entfernt, Regeln zusammengefasst und
	 * unnötige Einheiten entfernt. Auch wird eine eventuell vorhandene CHARSET
	 * Angabe entfernt und die Datei nach UTF-8 kodiert.
	 *
	 * @param  string  $cssCode  der zu komprimierende CSS-Code
	 * @param  boolean $debug    wenn true, wird jede Regel in einer eigenen Zeile notiert
	 * @return string            der komprimierte Dateiinhalt
	 */
	public static function compress($cssCode, $debug = false) {
		$content   = $cssCode;
		$inputEnc  = 'utf-8';
		$outputEnc = 'utf-8';

		if (preg_match('#@charset "([^"]*)";#i', $content, $match)) {
			$inputEnc = strtolower($match[1]);
			$content  = iconv($inputEnc, $outputEnc, $content);
		}

		// charset-Angabe entfernen
		$content = preg_replace('#@charset "[^"]*";#i', '', $content);
		// Kommentare entfernen
		$content = preg_replace('#/\*.*?\*/#si', '', $content);
		// Mehrfache Leerzeichen entfernen
		$content = preg_replace('# +#i', ' ', $content);
		// Zeilenumbrüche/Tabs entfernen
		$content = str_replace(array("\t","\n","\r"), '', $content);
		// Pseudoklassen vor der Entfernung der Leerzeichen retten ("elemt :hover" != "elemt:hover")
		$content = preg_replace('#\s+:(hover|link|visited|active|first-child|focus|lang)#i', '%:$1', $content);
		// Leerzeichen vor/nach Doppelpunkten entfernen
		$content = preg_replace('#\s*:\s*#si', ':', $content);
		// Pseudoklassen wiederherstellen
		$content = preg_replace('#%:(hover|link|visited|active|first-child|focus|lang)#i', ' :$1', $content);
		// Leerzeichen vor/nach { und , entfernen
		$content = preg_replace('#\s*(\{|,|\})\s*#si', '$1', $content);
		// Leerzeichen nach Zeilenumbrüchen entfernen
		$content = str_replace("\n ", "\n", $content);
		// Das letzte Semikolon einer Definition ist unnütz
		$content = str_replace(';}', '}', $content);
		// Einheiten, die ,0 notiert wurden, kürzen ("1.0" -> "1")
		$content = preg_replace('#([0-9]+)\.0(em|ex|cm|mm|in|pt|pc)#i', '$1$2', $content);
		// Angaben wie 0px auf 0 reduzieren
		$content = preg_replace('#(:| )0(em|ex|cm|mm|in|pt|pc|px)(\b)#i', '${1}0$3', $content);
		// Doppelte Semikolons entfernen
		$content = preg_replace('#;+#i', ';', $content);
		// Leerzeichen nach Semikolons entfernen (für Regeln, die direkt "a:b; c:d" notiert wurden)
		$content = preg_replace('#; +#i', ';', $content);

		// Regeln der Form EIGENSCHAFT:A A A A; zu EIGENSCHAFT:A; und
		// EIGENSHAFT:A B A B; zu EIGENSCHAFT:A B; umformen.
		$matches = false;

		preg_match_all('#(margin|padding|border-width):([0-9a-z]+)\s+([0-9a-z]+)\s+([0-9a-z]+)\s+([0-9a-z]+)\s*;#si', $content, $matches);

		if ($matches) {
			foreach ($matches[0] as $idx => $matchText) {
				$first  = $matches[2][$idx];
				$second = $matches[3][$idx];
				$third  = $matches[4][$idx];
				$fourth = $matches[5][$idx];

				if ($first == $second && $second == $third && $third == $fourth) {
					$content = str_replace($matchText, $matches[1][$idx].':'.$first.';', $content);
					continue;
				}

				if ($first == $third && $second == $fourth) {
					$content = str_replace($matchText, $matches[1][$idx].':'.$first.' '.$second.';', $content);
				}
			}
		}

		// Farben von rgb(a,b,c) zu #AABBCC umformen
		preg_match_all('#\brgb\(\s*([0-9%]+)\s*,\s*([0-9%]+)\s*,\s*([0-9%]+)\s*\)#i', $content, $matches, PREG_SET_ORDER);

		if ($matches) {
			foreach ($matches as $idx => $match) {
				$r = self::getHexColor($match[1]);
				$g = self::getHexColor($match[2]);
				$b = self::getHexColor($match[3]);

				$content = str_replace($match[0], '#'.$r.$g.$b, $content);
			}
		}

		// Farben der Form #AABBCC zu #ABC umformen
		preg_match_all('~#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})~si', $content, $matches);

		if ($matches) {
			foreach ($matches[0] as $idx => $matchText) {
				$first  = $matches[1][$idx];
				$second = $matches[2][$idx];
				$third  = $matches[3][$idx];

				if ($first[0] == $first[1] && $second[0] == $second[1] && $third[0] == $third[1]) {
					$content = str_replace($matchText, '#'.$first[0].$second[0].$third[0], $content);
				}
			}
		}

		// Farben kürzen
		static $colorRules = array(
			'aqua'    => '#0ff',
			'black'   => '#000',
			'blue'    => '#00f',
			'fuchsia' => '#f0f',
			'#808080' => 'gray',
			'#008000' => 'green',
			'lime'    => '#0f0',
			'#800000' => 'maroon',
			'#000080' => 'navy',
			'#808000' => 'olive',
			'orange'  => '#FFA500',
			'#800080' => 'purple',
			'#f00'    => 'red',
			'#c0c0c0' => 'silver',
			'#008080' => 'teal',
			'white'   => '#fff',
			'yellow'  => '#ffo'
		);

		foreach ($colorRules as $old => $new) {
			$content = preg_replace('/:'.$old.'([;} ])/i', ':'.$new.'$1', $content);
		}

		// Leere Definitionen entfernen
		$content = preg_replace('#[^{}]+?\{\}#s', '', $content);

		// Auf Wunsch jede Gruppe in eine einzelne Zeile schreiben (Debugging)
		if ($debug) {
			$content = str_replace('}', "}\n", $content);
		}

		return trim($content);
	}

	/**
	 * Farbcode in Hexwert umrechnen
	 *
	 * Diese Methode nimmt einen dezimalen Wert (Total oder in Prozent von 255)
	 * und rechnet diesen in einen hexadezimalen Wert um. Ist der Wert einstellig,
	 * wird er mit einer führenden 0 versehen.
	 *
	 * @param  string $color  die Farbe (Ganzzahl oder "Zahl%")
	 * @return string         der hexadezimale Wert
	 */
	public static function getHexColor($color) {
		if (substr($color, -1) == '%') {
			$color = substr($color, 0, -1);
			$color = 255.0 / 100 * $color;
		}
		return str_pad(dechex($color), 2, '0', STR_PAD_LEFT);
	}
}
