<?php
/**
 * ManiaPlanet dedicated server Xml-RPC client
 *
 * @license     http://www.gnu.org/licenses/lgpl.html LGPL License 3
 */

namespace Maniaplanet\DedicatedServer\Xmlrpc;

if (!defined('LF'))
{
	define('LF', "\n");
}

if (!defined('SIZE_MAX'))
{
	define('SIZE_MAX', 4096*1024);
}

class Client 
{
	public $socket;
	public $message = false;
	public $cb_message = array();
	public $reqhandle;
	public $protocol = 0;

	static $received;
	static $sent;
	
	function bigEndianTest() 
	{
		list($endiantest) = array_values(unpack('L1L', pack('V', 1)));
		if ($endiantest != 1) 
		{
			if(!function_exists(__NAMESPACE__.'\\unpack'))
			{
				/**
				 * The following code is a workaround for php's unpack function which
				 * does not have the capability of unpacking double precision floats
				 * that were packed in the opposite byte order of the current machine.
				 */
				function unpack($format, $data) 
				{
					$ar = unpack($format, $data);
					$vals = array_values($ar);
					$f = explode('/', $format);
					$i = 0;
					foreach ($f as $f_k => $f_v) 
					{
						$repeater = intval(substr($f_v, 1));
						if ($repeater == 0)
						{
							$repeater = 1;
						}
						if ($f_v{1} == '*') 
						{
							$repeater = count($ar) - $i;
						}
						if ($f_v{0} != 'd') 
						{
							$i += $repeater;
							continue;
						}
						$j = $i + $repeater;
						for ($a = $i; $a < $j; ++$a) 
						{
							$p = pack('d', $vals[$i]);
							$p = strrev($p);
							list($vals[$i]) = array_values(unpack('d1d', $p));
							++$i;
						}
					}
					$a = 0;
					foreach ($ar as $ar_k => $ar_v) 
					{
						$ar[$ar_k] = $vals[$a];
						++$a;
					}
					return $ar;
				}
			}
		}
	}

	function __construct($hostname = 'localhost', $port = 5000, $timeout) 
	{
		$this->socket = false;
		$this->reqhandle = 0x80000000;
		$this->init($hostname, $port, $timeout);
	}
	
	function __destruct()
	{
		$this->terminate();
	}

	protected function init($hostname, $port, $timeout) 
	{

		$this->bigEndianTest();

		// open connection
		$this->socket = @fsockopen($hostname, $port, $errno, $errstr, $timeout);
		if (!$this->socket) 
		{
			throw new Exception("transport error - could not open socket (error: $errno, $errstr)", -32300);
		}
		// handshake
		$array_result = unpack('Vsize', fread($this->socket, 4));
		$size = $array_result['size'];
		if ($size > 64) 
		{
			throw new Exception('transport error - wrong lowlevel protocol header', -32300);
		}
		$handshake = fread($this->socket, $size);
		if ($handshake == 'GBXRemote 1') 
		{
			$this->protocol = 1;
		} 
		elseif ($handshake == 'GBXRemote 2') 
		{
			$this->protocol = 2;
		} 
		else 
		{
			throw new Exception('transport error - wrong lowlevel protocol version', -32300);
		}
	}
	
	function terminate() 
	{
		if ($this->socket) 
		{
			fclose($this->socket);
			$this->socket = false;
		}
	}

	protected function sendRequest(Request $request) 
	{
		$xml = $request->getXml();

		@stream_set_timeout($this->socket, 20);  // timeout 20 s (to write the request)
		// send request
		$this->reqhandle++;
		if ($this->protocol == 1) 
		{
			$bytes = pack('Va*', strlen($xml), $xml);
		} 
		else 
		{
			$bytes = pack('VVa*', strlen($xml), $this->reqhandle, $xml);
		}

		$bytes_to_write = strlen($bytes);
		
		// increase sent counter ...
		self::$sent += $bytes_to_write;
		
		while ($bytes_to_write > 0) 
		{
			$r = fwrite($this->socket, $bytes);
			if ($r === false || $r == 0) 
			{
				throw new Exception('Connection interupted');
			}

			$bytes_to_write -= $r;
			if ($bytes_to_write == 0)
			{
				break;
			}

			$bytes = substr($bytes, $r);
		}
	}

	protected function getResult() 
	{
		$contents = '';
		$contents_length = 0;
		do 
		{
			$size = 0;
			$recvhandle = 0;
			@stream_set_timeout($this->socket, 5);  // timeout 20 s (to read the reply header)
			// Get result
			if ($this->protocol == 1) 
			{
				$contents = fread($this->socket, 4);
				if (strlen($contents) == 0) 
				{
					throw new Exception('transport error - connection interrupted!', -32700);
				}
				$array_result = unpack('Vsize', $contents);
				$size = $array_result['size'];
				$recvhandle = $this->reqhandle;
			} 
			else 
			{
				$contents = fread($this->socket, 8);
				if (strlen($contents) == 0) 
				{
					throw new Exception('transport error - connection interrupted!', -32700);
				}
				$array_result = unpack('Vsize/Vhandle', $contents);
				$size = $array_result['size'];
				$recvhandle = $array_result['handle'];
				// -- amd64 support --
				$bits = sprintf('%b', $recvhandle);
				if (strlen($bits) == 64) 
				{
					$recvhandle = bindec(substr($bits, 32));
				}
			}

			if ($recvhandle == 0 || $size == 0) 
			{
				throw new Exception('transport error - connection interrupted!', -32700);
			}
			
			if ($size > SIZE_MAX) 
			{
				throw new Exception("transport error - answer too big ($size)", -32700);
			}

			self::$received += $size;
			
			$contents = '';
			$contents_length = 0;
			@stream_set_timeout($this->socket, 0, 10000);  // timeout 10 ms (for successive reads until end)
			while ($contents_length < $size) 
			{
				$contents .= fread($this->socket, $size-$contents_length);
				$contents_length = strlen($contents);
			}

			if (($recvhandle & 0x80000000) == 0) 
			{
				// this is a callback, not our answer! handle= $recvhandle, xml-rpc= $contents
				// just add it to the message list for the user to read
				$new_cb_message = new Message($contents);
				if ($new_cb_message->parse() && $new_cb_message->messageType != 'fault') 
				{
					array_push($this->cb_message, array($new_cb_message->methodName, $new_cb_message->params));
				}
			}
		} 
		while ((int)$recvhandle != (int)$this->reqhandle);

		$this->message = new Message($contents);
		if (!$this->message->parse()) 
		{
			// XML error
			throw new Exception('parse error. not well formed', -32700);
		}
		// Is the message a fault?
		if ($this->message->messageType == 'fault') 
		{
			throw new Exception($this->message->faultString, $this->message->faultCode);
		}
		
		return $this->message;
	}


	function query() 
	{
		$args = func_get_args();
		$method = array_shift($args);

		if (!$this->socket || $this->protocol == 0) 
		{
			throw new Exception('transport error - Client not initialized', -32300);
		}

		$request = new Request($method, $args);

		// Check if request is larger than 512 Kbytes
		if ($request->getLength() > 1024*1024-8) //TODO changed temporary to 1024 * 1024
		{
			throw new Exception('transport error - request too large!', -32700);
		}

		$this->sendRequest($request);
		return $this->getResult();
	}

	// Non-blocking query method: doesn't read the response
	function queryIgnoreResult() 
	{
		$args = func_get_args();
		$method = array_shift($args);

		if (!$this->socket || $this->protocol == 0) 
		{
			throw new Exception('transport error - Client not initialized', -32300);
		}

		$request = new Request($method, $args);

		// Check if the request is greater than 512 Kbytes to avoid errors
		// If the method is system.multicall, make two calls (possibly recursively)
		if ($request->getLength() > 512*1024-8) 
		{
			if ($method == 'system.multicall' && isset($args[0])) 
			{
				$count = count($args[0]);
				// If count is 1, query cannot be reduced
				if ($count < 2) 
				{
					throw new Exception('transport error - request too large!', -32700);
				}
				$length = floor($count/2);

				$args1 = array_slice($args[0], 0, $length);
				$args2 = array_slice($args[0], $length, ($count-$length));

				$res1 = $this->queryIgnoreResult('system.multicall', $args1);
				$res2 = $this->queryIgnoreResult('system.multicall', $args2);
				return ($res1 && $res2);
			}
			// If the method is not a multicall, just stop
			else 
			{
				throw new Exception('transport error - request too large!', -32700);
			}
		}

		$this->sendRequest($request);
	}
	
	function getResponse() 
	{
		// methodResponses can only have one param - return that
		return $this->message->params[0];
	}
	
	function readCallbacks($timeout = 2000) 
	{
		if (!$this->socket || $this->protocol == 0) 
			throw new Exception('transport error - Client not initialized', -32300);
		if ($this->protocol == 1)
			return false;

		// flo: moved to end
		//$something_received = count($this->cb_message)>0;
		$contents = '';
		$contents_length = 0;

		@stream_set_timeout($this->socket, 0, 10000);  // timeout 10 ms (to read available data)
		// (assignment in arguments is forbidden since php 5.1.1)
		$read = array($this->socket);
		$write = NULL;
		$except = NULL;
		$nb = false;
		
		try
		{
			$nb = @stream_select($read, $write, $except, 0, $timeout);
		}
		catch (\Exception $e)
		{
			if (strpos($e->getMessage(), 'Invalid CRT') !== false)
			{
				$nb = true;
			}
			elseif (strpos($e->getMessage(), 'Interrupted system call') !== false)
			{
				return;
			}
			else
			{
				throw $e;
			}
		}
		
		// workaround for stream_select bug with amd64
		if ($nb !== false)
		{
			$nb = count($read);
		}

		while ($nb !== false && $nb > 0) 
		{
			$timeout = 0;  // we don't want to wait for the full time again, just flush the available data

			$size = 0;
			$recvhandle = 0;
			// Get result
			$contents = fread($this->socket, 8);
			if (strlen($contents) == 0) 
			{
				throw new Exception('transport error - connection interrupted!', -32700);
			}
			$array_result = unpack('Vsize/Vhandle', $contents);
			$size = $array_result['size'];
			$recvhandle = $array_result['handle'];

			if ($recvhandle == 0 || $size == 0) 
			{
				throw new Exception('transport error - connection interrupted!', -32700);
			}
			if ($size > SIZE_MAX) 
			{
				throw new Exception("transport error - answer too big ($size)", -32700);
			}
			
			self::$received += $size;

			$contents = '';
			$contents_length = 0;
			while ($contents_length < $size) 
			{
				$contents .= fread($this->socket, $size-$contents_length);
				$contents_length = strlen($contents);
			}

			if (($recvhandle & 0x80000000) == 0) 
			{
				// this is a callback. handle= $recvhandle, xml-rpc= $contents
				//echo 'CALLBACK('.$contents_length.')[ '.$contents.' ]' . LF;
				$new_cb_message = new Message($contents);
				if ($new_cb_message->parse() && $new_cb_message->messageType != 'fault') 
				{
					array_push($this->cb_message, array($new_cb_message->methodName, $new_cb_message->params));
				}
				// flo: moved to end ...
				// $something_received = true;
			}

			// (assignment in arguments is forbidden since php 5.1.1)
			$read = array($this->socket);
			$write = NULL;
			$except = NULL;
			
			try
			{
				$nb = @stream_select($read, $write, $except, 0, $timeout);
			}
			catch (\Exception $e)
			{
				if (strpos($e->getMessage(), 'Invalid CRT') !== false)
				{
					$nb = true;
				}
				else
				{
					throw $e;
				}
			}
			
			// workaround for stream_select bug with amd64
			if ($nb !== false)
			{
				$nb = count($read);
			}
		}
		return !empty($this->cb_message);
	}

	function getCallbackResponses() 
	{
		// (look at the end of basic.php for an example)
		$messages = $this->cb_message;
		$this->cb_message = array();
		return $messages;
	}
}

?>