<?php
/**
 * Qstat Plugin: Displays information about Quake 3 server (or compatible
 * like Openarena).
 *
 * Syntax: {{qstat}} - will be replaced with information about the server
 * 
 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
 * @author     Mathieu <mathieu@guim.info>
 */
 
if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/');
if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
require_once(DOKU_PLUGIN.'syntax.php');
 
/**
 * All DokuWiki plugins to extend the parser/rendering mechanism
 * need to inherit from this class
 */
class syntax_plugin_dwqstat extends DokuWiki_Syntax_Plugin {

	/**
	 * Get an associative array with plugin info.
	 *
	 * <p>
	 * The returned array holds the following fields:
	 * <dl>
	 * <dt>author</dt><dd>Author of the plugin</dd>
	 * <dt>email</dt><dd>Email address to contact the author</dd>
	 * <dt>date</dt><dd>Last modified date of the plugin in
	 * <tt>YYYY-MM-DD</tt> format</dd>
	 * <dt>name</dt><dd>Name of the plugin</dd>
	 * <dt>desc</dt><dd>Short description of the plugin (Text only)</dd>
	 * <dt>url</dt><dd>Website with more information on the plugin
	 * (eg. syntax description)</dd>
	 * </dl>
	 * @param none
	 * @return Array Information about this plugin class.
	 * @public
	 * @static
	 */
	function getInfo(){
		return array(
				'author' => 'Mathieu',
				'email'  => 'mathieu@guim.info',
				'date'   => rtrim(io_readFile(DOKU_PLUGIN.'dwqstat/VERSION.txt')),
				'name'   => 'DWQstat Plugin mostly for Openarena. You can use a proxy server if your PHP installation can\'t use \'stream_socket_*\' functions.',
				'desc'   => 'Provide informations about a Quake3 (or compatible) server',
				'url'    => 'http://www.guim.info/dokuwiki/dev/qstat',
			    );
	}

	/**
	 * Get the type of syntax this plugin defines.
	 *
	 * @param none
	 * @return String <tt>'substition'</tt> (i.e. 'substitution').
	 * @public
	 * @static
	 */
	function getType(){
		return 'substition';
	}

	/**
	 * Where to sort in?
	 *
	 * @param none
	 * @return Integer <tt>6</tt>.
	 * @public
	 * @static
	 */
	function getSort() {
		return 100;
	}


	/**
	 * Connect lookup pattern to lexer.
	 *
	 * @param $aMode String The desired rendermode.
	 * @return none
	 * @public
	 * @see render()
	 */
	function connectTo($mode) {
		$this->Lexer->addSpecialPattern('{{qstat}}', $mode, 'plugin_dwqstat');
		$this->Lexer->addSpecialPattern('{{qstat.+?}}', $mode, 'plugin_dwqstat');
	}

	/**
	 * Handler to prepare matched data for the rendering process.
	 *
	 * <p>
	 * The <tt>$aState</tt> parameter gives the type of pattern
	 * which triggered the call to this method:
	 * </p>
	 * <dl>
	 * <dt>DOKU_LEXER_ENTER</dt>
	 * <dd>a pattern set by <tt>addEntryPattern()</tt></dd>
	 * <dt>DOKU_LEXER_MATCHED</dt>
	 * <dd>a pattern set by <tt>addPattern()</tt></dd>
	 * <dt>DOKU_LEXER_EXIT</dt>
	 * <dd> a pattern set by <tt>addExitPattern()</tt></dd>
	 * <dt>DOKU_LEXER_SPECIAL</dt>
	 * <dd>a pattern set by <tt>addSpecialPattern()</tt></dd>
	 * <dt>DOKU_LEXER_UNMATCHED</dt>
	 * <dd>ordinary text encountered within the plugin's syntax mode
	 * which doesn't match any pattern.</dd>
	 * </dl>
	 * @param $aMatch String The text matched by the patterns.
	 * @param $aState Integer The lexer state for the match.
	 * @param $aPos Integer The character position of the matched text.
	 * @param $aHandler Object Reference to the Doku_Handler object.
	 * @return Integer The current lexer state for the match.
	 * @public
	 * @see render()
	 * @static
	 */
	function handle($match, $state, $pos, &$handler){
		$match = substr($match,7,-2);
		$match = explode(" ", trim($match));
		if (count($match) != 2) {
			return array();
		}
		$status = $this->_getStatus($match[0], $match[1]);
		return ($status === FALSE) ? array() : $status;
	}

	/**
	 * Handle the actual output creation.
	 *
	 * <p>
	 * The method checks for the given <tt>$aFormat</tt> and returns
	 * <tt>FALSE</tt> when a format isn't supported. <tt>$aRenderer</tt>
	 * contains a reference to the renderer object which is currently
	 * handling the rendering. The contents of <tt>$aData</tt> is the
	 * return value of the <tt>handle()</tt> method.
	 * </p>
	 * @param $aFormat String The output format to generate.
	 * @param $aRenderer Object A reference to the renderer object.
	 * @param $aData Array The data created by the <tt>handle()</tt>
	 * method.
	 * @return Boolean <tt>TRUE</tt> if rendered successfully, or
	 * <tt>FALSE</tt> otherwise.
	 * @public
	 * @see handle()
	 */
	function render($mode, &$renderer, $data) {
		$this->setupLocale();
		$renderer->info['cache'] = false;
		if($mode == 'xhtml') {
			if (empty($data)) {
				//$renderer->doc .= "No informations available.";
				$renderer->doc .= $this->getLang('no_info');
			}
			else {
				$infotodisplay = array('mapname');
				if (array_key_exists('defrag_vers', $data)) {
					//defrag server
					array_push($infotodisplay, 'df_promode');
				}
				else {
					// baseoa server
					if (array_key_exists('g_gametype', $data)) {
						switch($data['g_gametype']) {
							case 0:
							case 3:
								// ffa, tdm
								array_push($infotodisplay, 'g_gametype', 'fraglimit', 'timelimit');
								break;
							case 4:
								// ctf
								array_push($infotodisplay, 'g_gametype', 'capturelimit', 'timelimit');
								break;
						}
					}
				}

				$buf = '<div class="qstat">';
				// hostname
				$buf .= "<h3>".$this->_colorize($data['sv_hostname'], 'server')."</h3>";
				// informations
				/*
				$buf .= "<h4>".
					'<span class="alignleft">Serveur '.(($data['g_needpass'] == 0) ? '<span class="publicserver">public</span>' : '<span class="privateserver">priv&eacute;</span>').'</span>'.
					'<span class="alignright">'.$this->_renderInformation('protocol', $data['protocol'])."</span></h4>";
				*/
				$buf .= '<h4><span class="alignleft">'.
					(($data['g_needpass'] == 0) ?
					$this->getLang('public_server') : 
					$this->getLang('private_server')).
					'</span></h4>';
				$buf .= '<div class="information';
					if (array_key_exists('defrag_vers', $data))
						$buf .= ' defrag" ';
					else if (array_key_exists('g_gametype', $data) and $data['g_gametype'] == 4) {
						$buf .= ' ctf" ';
						unset($infotodisplay[array_search('g_gametype', $infotodisplay)]);
					}
					else if (array_key_exists('g_gametype', $data) and $data['g_gametype'] == 3) {
						$buf .= ' tdm" ';
						unset($infotodisplay[array_search('g_gametype', $infotodisplay)]);
					}
					else if (array_key_exists('g_gametype', $data) and $data['g_gametype'] == 0) {
						$buf .= ' ffa" ';
						unset($infotodisplay[array_search('g_gametype', $infotodisplay)]);
					}
					else {
						$buf .= '"';
					}
				$buf .= ">";
				foreach ($infotodisplay as $key) {
					$buf .= $this->_renderInformation($key, $data[$key]);
				}
				// flagbar
				$buf .= "</div>";
				$buf .= $this->_flagbar($data);
				// players
				$maxclient = $data['sv_maxclients'] - $data['sv_privateClients'];
				$buf .= "<h4>{$this->getLang('players')} ".count($data['players'])."/{$maxclient}</h4>";
				$buf .= '<div class="player"><ul>';
				foreach ($data['players'] as $item) {
					$buf .= "<li>{$this->_colorize($item['name'])} : {$item['score']} ({$item['ping']})</li>";
				}
				$buf .= "</ul></div>";
				$buf .= '</div>';
				$renderer->doc .= $buf;
			}
			return true;
		}
		return false;
	}

	// display formatted information
	function _renderInformation($key, $value) {
		$str = '';
		switch ($key) {
			case 'mapname':
				$str = '<p class="aligncenter">'.$this->getLang('map').': '.$value.'</p>';
				break;
			case 'capturelimit':
				$str = '<p>'.$this->getLang('flags').": $value</p>";
				break;
			case 'fraglimit':
				$str = '<p>'.$this->getLang('frags').": $value</p>";
				break;
			case 'timelimit':
				$str = '<p>'.$this->getLang('time').": $value</p>";
				break;
			case 'protocol':
				if ($value == '68')
					$str = '0.7.1';
				elseif ($value == '69')
					$str = '0.7.6';
				elseif ($value == '70')
					$str = '0.7.7';
				elseif ($value == '71')
					$str = '0.8.1';
				break;
			case 'df_promode':
				$str = '<p>'.$this->getLang('physic').': '.(($value == '0') ? 'VQ3' : 'CPM').'</p>';
				break;
			case 'g_gametype':
				$str .= '<p>';
				if ($value == 0)
					$str = 'Type : FFA';
				elseif ($value == 3)
					$str = 'Type : TDM';
				elseif ($value == 4)
					$str = 'Type : CTF';
				$str .= '</p>';
				break;
			default:
				$str = "<p>$key: $value</p>";
				break;
		}
		return $str;
	}

	// show flag option (on/off) for the server
	function _flagbar($data) {
		$flag = array(
			'g_delagHitscan' => array('desc' => 'unlagged', 'img' => 'lag.png'),
			'g_delaghitscan' => array('desc' => 'unlagged', 'img' => 'lag.png'),
			'g_instantgib' => array('desc' => 'instantgib', 'img' => 'ammo_rg.png'),
			'g_rockets' => array('desc' => 'all rocket', 'img' => 'ammo_rl.png'),
		);
		$mod = array(
			'baseoa' => array('desc' => 'OpenArena', img => 'baseoa.png'),
			'unfrag-ifs' => array('desc' => 'unfrag-ifs', img => 'unfrag-ifs.png'),
			'z3r0x' => array('desc' => 'z3r0x', img => 'z3r0x.png'),
		);
		$imgpath = DOKU_BASE.'/lib/plugins/qstat_oatforg/images/';
		$str = '<div class="flagbar">';
		// mod iteration
		foreach ($mod as $key => $value) {
			$keytotest = ((isset($data['game'])) ? 'game' : 'gamename');
			if ($data[$keytotest] == $key) {
				$str .= '<img src="'.$imgpath.$value['img'].'" title="'.$value['desc'].'"/>';
			}
		}
		// flag iteration
		foreach ($flag as $key => $value) {
			if ($data[$key] == 1) {
				$str .= '<img src="'.$imgpath.$value['img'].'" title="'.$value['desc'].'"/>';
			}
		}
		$str .= '</div>';
		return $str;
	}

	// retrieve status using the appropriate method
	function _getStatus($host, $port) {
		if ($this->getConf('use_proxy'))
			return $this->_proxy($host, $port);
		else
			return array_merge($this->_infos($host, $port), $this->_status($host, $port));
	}

	// use the proxy to retrieve information
	function _proxy($host, $port) {
		$proxy = $this->getConf('proxy_url');
		$str = file_get_contents("$proxy?host=$host&port=$port");
		$serverstatus = unserialize($str);
		if (is_string($serverstatus) and $serverstatus == "Bad host or port") {
			$serverstatus = FALSE;
		}
		return $serverstatus;
	}

	// ask server for "getinfo"
	function _infos($host, $port) {
		$serverinfos = array();
		$packet = "\xFF\xFF\xFF\xFFgetinfo\x00";
		$sock = @stream_socket_client("udp://$host:$port");
		if ($sock === FALSE)
			return FALSE;
		@stream_set_timeout($sock, 3);
		$r = @fwrite($sock, $packet, strlen($packet));
		if ($r === FALSE)
			return FALSE;
		$r = @fread($sock, 1500);
		if ($r === FALSE)
			return FALSE;
		// "\0xFF\0xFF\0xFF\0xFFinfoResponse " => 17
		$str = trim(substr($r, 17));
		$split = preg_split("/\\\/", $str);
		$key = null;
		foreach ($split as $item) {
			if ($key == null) {
				$key = $item;
			}
			else {
				$serverinfos[$key] = $item;
				$key = null;
			}
		}
		return $serverinfos;
	}

	// ask server for "getstatus"
	function _status($host, $port) {
		$serverstatus = array();
		$packet = "\xFF\xFF\xFF\xFFgetstatus\x00";
		$sock = @stream_socket_client("udp://$host:$port");
		if ($sock === FALSE)
			return FALSE;
		@stream_set_timeout($sock, 3);
		$r = @fwrite($sock, $packet, strlen($packet));
		if ($r === FALSE)
			return FALSE;
		$r = @fread($sock, 1500);
		if ($r === FALSE)
			return FALSE;
		// "\0xFF\0xFF\0xFF\0xFFstatusResponse " => 19
		$str = trim(substr($r, 19));

		$split = preg_split("/\n/", $str);
		$status = array_shift($split);
		// player parsing
		$players = array();
		foreach ($split as $p) {
			$s = preg_split("/\s/", $p);
			$tmp_name = '';
			for ($i=2; $i<count($s); $i++) {
				$tmp_name .= trim($s[$i], "\"").' ';
			}
			array_push($players, array(
						'name' => $tmp_name,
						'ping' => $s[1],
						'score' => $s[0]));
		}
		$serverstatus['players'] = $players;
		// status parsing
		$split = preg_split("/\\\/", $status);
		$key = null;
		foreach ($split as $item) {
			if ($key == null) {
				$key = $item;
			}
			else {
				$serverstatus[$key] = $item;
				$key = null;
			}
		}
		return $serverstatus;
	}

	// colorize a player name with Quake style
	function _colorize($str,$type='player') {
		$COLOR = array( 0 => 'black',
				1 => 'red',
				2 => 'green',
				3 => 'yellow',
				4 => 'blue',
				5 => 'cyan',
				6 => 'magenta',
				7 => 'white');
		$len = strlen($str);
		$tmp = '';
		$span = false;
		for ($i=0; $i<$len; $i++) {
			if ($str[$i] == '^' && $str[$i+1] == '^') {
				if ($type == 'server') {
					if ($i+2 < $len && is_numeric($str[$i+2])) {
						if ($span) $tmp .= '</span>';
						$tmp .= '<span style="color: '.$COLOR[($str[$i+2])%8].';">';
						$span = true;
						$i+=3;
					}
				}
				else {
					$tmp .= $str[$i];
				}
			}
			else if ($str[$i] == '^' && $i+1 < $len && is_numeric($str[$i+1])) {
				if ($span) $tmp .= '</span>';
				$tmp .= '<span style="color: '.$COLOR[($str[$i+1])%8].';">';
				$span = true;
				$i++;
			}
			else if ($str[$i] == '^' && $i+1 < $len && is_string($str[$i+1])) {
				if ($span) $tmp .= '</span>';
				$ascii = ord($str[$i+1]);
				#$color = (($ascii > 96) ? $ascii - 22 : $ascii) - 65;
				$color = (($ascii > 96) ? $ascii - 22 : $ascii) - 66;
				$tmp .= '<span style="color: '.$COLOR[$color%8].';">';
				$span = true;
				$i++;
			}
			else {
				$tmp .= $str[$i];
			}
		}
		if ($span) $tmp .= '</span>';
		return $tmp;
	}
}
?>
