<?php
/************************************************** 
 *                 Bot Gadu-Gadu                  * 
 ************************************************** 
 * Wersja: 2.1                                    * 
 * Data: 13.02.2012 (19:41)                       * 
 * Autor: Jacek Kowalski (http://jacekk.info)     * 
 *                                                * 
 * Utwor rozprowadzany na licencji                * 
 * http://creativecommons.org/licenses/by-nc/2.5/ * 
 **************************************************/

/**
 * Klasa konwertuje wiadomość ({@link BotMsg}) do formatu specyficznego dla Gadu-Gadu
 */
class BotMsgGG implements BotMsgInterface {
	private $parser;
	private $html = '';
	private $old = '';
	private $img = '';
	private $format = '';
	
	private $images = array();
	
	private $f_handl = FALSE;
	private $f_old = '';
	private $f_type = 0x00;
	private $f_color = '';
	
	const FORMAT_BOLD =	0x01;
	const FORMAT_ITALIC =	0x02;
	const FORMAT_UNDERLINE =0x04;
	const FORMAT_COLOR =	0x08;
	const FORMAT_IMAGE =	0x80;
	
	const COLOR_RED =	0x00010000;
	const COLOR_GREEN =	0x00000100;
	const COLOR_BLUE =	0x00000001;
	
	/**
	 * @param BotMsg $msg Wiadomość do przekonwertowania
	 */
	function __construct(BotMsg $msg) {
		$parser = $msg->getParser();
		unset($msg);
		
		$this->parser = new DOMDocument('1.0', 'utf-8');
		$this->parser->loadHTML('<html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"></head><body></body></html>');
		
		$this->rewrite( $parser->getElementsByTagName('body')->item(0), $this->parser->getElementsByTagName('body')->item(0) );
		unset($parser);
		
		$this->parse( $this->parser->getElementsByTagName('body')->item(0) );
		
		$this->html = strtr(
			(string)substr($this->parser->saveXML( $this->parser->getElementsByTagName('body')->item(0) ), 6, -7),
			array('/>' => '>') // Tak! GG nie lubi XML!
		);
	}
	
	/**
	 * Zwraca wiadomość zgodną z BotAPI Gadu-Gadu, którą można przekazać bezpośrednio do BotMastera
	 * @param bool $img Czy dołączać obrazki?
	 * @return string
	 */
	function getGG($image = TRUE) {
		if($image) {
			$image = $this->img;
		}
		else
		{
			$image = '';
			foreach($this->images as $img) {
				$image .= $img[2];
			}
		}
		
		$format = $this->getFormat();
		
		return pack('VVVV', strlen($this->html)+1, strlen($this->old)+1, strlen($image), strlen($format)).$this->html."\0".$this->old."\0".$image.$format;
	}
	
	/**
	 * Zwraca wiadomość zgodną z BotAPI Gadu-Gadu, którą można przekazać bezpośrednio do BotMastera
	 * @return string
	 */
	function __toString() {
		return $this->getGG();
	}
	
	/**
	 * Zwraca wiadomość w formacie HTML przekonwertowaną tak, by zawierała jedynie dozwolone tagi.
	 * @return string
	 */
	function getHTML() {
		return $this->html;
	}
	
	/**
	 * Zwraca wiadomość jako tekst
	 * @return string
	 */
	function getText() {
		return $this->old;
	}
	
	/**
	 * Zwraca formatowanie wiadomości tekstowej zgodne z BotAPI Gadu-Gadu
	 * @see BotMsgGG::getText()
	 * @return string
	 */
	function getFormat() {
		return pack('Cv', 0x02, strlen($this->format)).$this->format;
	}
	
	/**
	 * Wyślij wiadomość na standardowe wyjście w sposób właściwy dla BotAPI
	 */
	function sendPullResponse() {
		header('Content-Type: application/x-gadu-gadu; charset=utf-8');
		echo $this;
	}
	
	private function rewriteOne($node, $saveto) {
		
	}
	
	private function rewrite($dom, $saveto, $top = TRUE) {
		if(!($dom instanceof DOMElement)) {
			throw new BotMsgException('Nieznany element DOM: '.get_class($dom));
		}
		
		foreach($dom->childNodes as $node) {
			if($node instanceof DOMElement) {
				switch(strtolower($node->tagName)) {
					case 'b':
					case 'i':
					case 'u':
					case 'sup':
					case 'sub':
					case 'span':
						$tag = DOMHelper::cloneNode($node, $saveto);
						$this->rewrite($node, $tag, FALSE);
						$saveto->appendChild($tag);
					break;
					
					case 'strong':
						$tag = DOMHelper::cloneNode($node, $saveto, 'b');
						$this->rewrite($node, $tag, FALSE);
						$saveto->appendChild($tag);
					break;
					
					case 'script':
					case 'style':
					break;
					
					case 'p':
						DOMHelper::rtrim($saveto);
						DOMHelper::insertElement('br', $saveto);
						DOMHelper::insertElement('br', $saveto);
						
						$tag = DOMHelper::cloneNode($node, $saveto, 'span');
						$this->rewrite($node, $tag, FALSE);
						$saveto->appendChild($tag);
						
						
						DOMHelper::insertElement('br', $saveto);
						DOMHelper::insertElement('br', $saveto);
					break;
					case 'a':
						$tag = DOMHelper::cloneNode($node, $saveto, 'span');
						
						$this->rewrite($node, $tag, FALSE);
						if($node->getAttribute('href') != $node->textContent) {
							$tag->appendChild($tag->ownerDocument->createTextNode(' ('.$node->getAttribute('href').')'));
						}
						
						$saveto->appendChild($tag);
					break;
					
					case 'h1':
						DOMHelper::rtrim($saveto);
						DOMHelper::insertElement('br', $saveto);
						DOMHelper::insertElement('br', $saveto);
						
						$tag = DOMHelper::cloneNode($node, $saveto, 'b');
						$tag2 = $tag->ownerDocument->createElement('u');
						$tag->appendChild($tag2);
						
						$this->rewrite($node, $tag2, FALSE);
						
						$saveto->appendChild($tag);
						
						DOMHelper::insertElement('br', $saveto);
					break;
					case 'h2':
						DOMHelper::rtrim($saveto);
						DOMHelper::insertElement('br', $saveto);
						DOMHelper::insertElement('br', $saveto);
						
						$tag = DOMHelper::cloneNode($node, $saveto, 'b');
						$this->rewrite($node, $tag, FALSE);
						$saveto->appendChild($tag);
						
						
						DOMHelper::insertElement('br', $saveto);
					break;
					case 'h3':
						DOMHelper::rtrim($saveto);
						DOMHelper::insertElement('br', $saveto);
						DOMHelper::insertElement('br', $saveto);
						
						$tag = DOMHelper::cloneNode($node, $saveto, 'b');
						$this->rewrite($node, $tag, FALSE);
						$saveto->appendChild($tag);
						
						
						DOMHelper::insertElement('br', $saveto);
					break;
					
					case 'ul':
						DOMHelper::rtrim($saveto);
						DOMHelper::insertElement('br', $saveto);
						
						$tag = DOMHelper::cloneNode($node, $saveto, 'u');
						$this->rewrite($node, $tag, FALSE);
						$saveto->appendChild($tag);
					break;
					case 'ol':
						DOMHelper::rtrim($saveto);
						DOMHelper::insertElement('br', $saveto);
						
						if(!$node->hasAttribute('start') || !ctype_digit($node->getAttribute('start'))) {
							$node->setAttribute('start', 1);
						}
						
						$tag = DOMHelper::cloneNode($node, $saveto, 'u');
						$this->rewrite($node, $tag, FALSE);
						$saveto->appendChild($tag);
					break;
					case 'li':
						$tag = DOMHelper::cloneNode($node, $saveto, 'span');
						
						if(strtolower($dom->tagName) == 'ul') {
							$tag->appendChild($tag->ownerDocument->createTextNode('- '));
						}
						elseif(strtolower($dom->tagName) == 'ol') {
							$tag->appendChild($tag->ownerDocument->createTextNode($dom->getAttribute('start').'. '));
							
							$dom->setAttribute('start', $dom->getAttribute('start')+1);
						}
						
						$this->rewrite($node, $tag, FALSE);
						$saveto->appendChild($tag);
						
						$saveto->appendChild($saveto->ownerDocument->createElement('br'));
					break;
					
					case 'br':
					case 'img':
						$tag = DOMHelper::cloneNode($node, $saveto);
						$saveto->appendChild($tag);
					break;
					
					default:
						$tag = DOMHelper::cloneNode($node, $saveto, 'span');
						$this->rewrite($node, $tag, FALSE);
						$saveto->appendChild($tag);
					break;
				}
			}
			elseif($node instanceof DOMText) {
				$val = strtr($node->nodeValue, array("\n" => '', "\r" => ''));
				if($val) {
					$saveto->appendChild($saveto->ownerDocument->createTextNode($val));
				}
			}
			else
			{
				$saveto->appendChild($saveto->ownerDocument->importNode($node, TRUE));
			}
		}
		
		if($top) {
			DOMHelper::trim($saveto);
		}
		
		foreach($saveto->childNodes as $node) {
			if(($node instanceof DOMElement) && $node->tagName == 'span' && $node->attributes->length == 0) {
				while($node->hasChildNodes()) {
					$node->parentNode->insertBefore($node->firstChild, $node);
				}
				$node->parentNode->removeChild($node);
			}
		}
		
		foreach($saveto->childNodes as $node) {
			if(($node instanceof DOMElement) && $node->hasAttribute('auto')) {
				$node->removeAttribute('auto');
			}
		}
	}
	
	private function image($node) {
		if($node->hasAttribute('src')) {
			$src = $node->getAttribute('src');
			$node->removeAttribute('src');
			
			if(!is_file($src)) {
				return;
			}
			
			if(isset($this->images[$src])) {
				list($crc, $size, $name) = $this->images[$src];
			}
			else
			{
				$size = filesize($src);
				if($size<0 || $size>262144) {
					return;
				}
				
				$crc = hash_file('crc32b', $src);
				$name = sprintf('%08s%08x', $crc, $size);
				
				$this->images[$src] = array($crc, $size, $name);
			}
			
			$node->setAttribute('name', $name);
			$this->img .= $name.file_get_contents($src);
			
			$this->format .= pack('vC', mb_strlen($this->old), self::FORMAT_IMAGE/*|$this->f_type*/)
			//		//.$this->f_color
					.pack('CCVV', 0x09, 0x01, $size, hexdec($crc));
		}
	}
	
	private function format(&$node) {
		$node->setAttribute('beforeFormatType', ord($this->f_type));
		$node->setAttribute('beforeFormatColor', base64_encode($this->f_color));
		
		if($node->hasAttribute('color')) {
			$color = trim($node->getAttribute('color'));
			if(substr($color, 0, 1)=='#' AND (strlen($color)==4 OR strlen($color)==7) AND ctype_xdigit(substr($color, 1))) {
				$node->setAttribute('style', 'color:'.$color.';'.$node->getAttribute('style'));
				
				$R = $G = $B = 0;
				if(strlen($color)==4) {
					$R = hexdec(str_repeat(substr($color, 1, 1), 2));
					$G = hexdec(str_repeat(substr($color, 2, 1), 2));
					$B = hexdec(str_repeat(substr($color, 3, 1), 2));
				}
				else
				{
					$R = hexdec(substr($color, 1, 2));
					$G = hexdec(substr($color, 3, 2));
					$B = hexdec(substr($color, 5, 2));
				}
				
				$this->f_color = chr($R).chr($G).chr($B);
				$this->f_type |= self::FORMAT_COLOR;
			}
			$node->removeAttribute('color');
		}
		
		switch(strtolower($node->tagName)) {
			case 'b':
				$this->f_type |= self::FORMAT_BOLD;
			break;
			case 'i':
				$this->f_type |= self::FORMAT_ITALIC;
			break;
			case 'u':
				$this->f_type |= self::FORMAT_UNDERLINE;
			break;
		}
	}
	
	private function unformat($node) {
		$this->f_type = chr($node->getAttribute('beforeFormatType'));
		$node->removeAttribute('beforeFormatType');
		
		$this->f_color = base64_decode($node->getAttribute('beforeFormatColor'));
		$node->removeAttribute('beforeFormatColor');
		
		return TRUE;
	}
	
	private function cf() {
		$format = pack('C', $this->f_type).$this->f_color;
		
		if($this->f_old != $format) {
			$this->format .= pack('v', mb_strlen($this->old)).$format;
			$this->f_old = $format;
		}
	}
	
	private function parse($dom) {
		if(!($dom instanceof DOMElement)) {
			throw new BotMsgException('Nieznany element DOM: '.$dom);
		}
		
		foreach($dom->childNodes as $node) {
			if($node instanceof DOMText || $node instanceof DOMEntity) {
				$this->cf();
				$this->old .= $node->nodeValue;
			}
			elseif($node instanceof DOMElement) {
				if($node->tagName == 'br') {
					$this->old .= "\r\n";
					continue;
				}
				elseif($node->tagName == 'img') {
					$this->image($node);
					continue;
				}
				
				$this->format($node);
				$this->parse($node);
				$this->unformat($node);
			}
			else
			{
				throw new BotMsgException('Nieznany element DOM: '.$node);
			}
		}
	}
}
?>