<?php

/**
 * jsonRPCClient.php
 *
 * Written using the JSON RPC specification -
 * http://json-rpc.org/wiki/specification
 *
 * @author Kacper Rowinski <krowinski@implix.com>
 * http://implix.com
 */
if (!class_exists('jsonRPCClient'))
{

	class jsonRPCClient
	{

		protected $url = null, $is_debug = false, $parameters_structure = 'array';
		// default options for curl
		protected $curl_options = array(
			CURLOPT_CONNECTTIMEOUT => 8,
			CURLOPT_TIMEOUT => 8
		);
		// http errors - more can be found at
		// http://en.wikipedia.org/wiki/List_of_HTTP_status_codes
		public $http_errors = array
			(
			400 => '400 Bad Request',
			500 => '500 Internal Server Error'
		);

		/**
		 * Takes the connection parameter and checks for extentions
		 *
		 * @param string $url - url name like http://example.com/
		 * @return void
		 */
		public function __construct($url)
		{
			$validateParams = array
				(
				false === extension_loaded('curl') => 'The curl extension must be loaded for using this class !',
				false === extension_loaded('json') => 'The json extension must be loaded for using this class !'
			);
			$this->checkForErrors($validateParams);

			// set an url to connect to
			$this->url = $url;
		}

		/**
		 * Set debug mode
		 *
		 * @param boolean $is_debug
		 * @return void
		 */
		public function setDebug($is_debug)
		{
			$this->is_debug = !empty($is_debug);
		}

		/**
		 * Set structure to use for parameters
		 *
		 * @param string $parameters_structure 'array' or 'object'
		 * @return void
		 */
		public function setParametersStructure($parameters_structure)
		{
			if (in_array($parameters_structure, array('array', 'object')))
			{
				$this->parameters_structure = $parameters_structure;
			}
			else
			{
				throw new Exception('Invalid parameters structure type.');
			}
		}

		/**
		 * Set extra options for curl connection
		 * @param array $options_array 
		 */
		public function setCurlOptions($options_array)
		{
			if (is_array($options_array))
			{
				$this->curl_options = $options_array + $this->curl_options;
			}
			else
			{
				throw new Exception('Invalid options type.');
			}
		}

		/**
		 * Performs a request and gets the results
		 *
		 * @param string $method - A String containing the name of the method to be invoked.
		 * @param array $params - An Array of objects to pass as arguments to the method.
		 * @return array
		 */
		public function __call($method, $params)
		{
			static $requestId;

			// generating uniuqe id per process
			$requestId++;

			// check if given params are correct
			$validateParams = array
				(
				false === is_scalar($method) => 'Method name has no scalar value',
				false === is_array($params) => 'Params must be given as array'
			);
			$this->checkForErrors($validateParams);

			// send params as an object or an array
			$params = ($this->parameters_structure == 'object') ? $params[0] : array_values($params);

			// Request (method invocation)
			$request = json_encode(array('jsonrpc' => '2.0', 'method' => $method, 'params' => $params, 'id' => $requestId));

			// if is_debug mode is true then add url and request to is_debug
			$this->debug('Url: ' . $this->url . "\r\n", false);
			$this->debug('Request: ' . $request . "\r\n", false);

			$response = $this->getResponse($request);

			// if is_debug mode is true then add response to is_debug and display it
			$this->debug('Response: ' . $response . "\r\n", true);

			// decode and create array ( can be object, just set to false )
			$response = json_decode($response, true);

			// check if response is correct
			$validateParams = array
				(
				$response['id'] != $requestId => 'Request id: ' . $requestId . ' is different from Response id: ' . $response['id'],
			);
			if (isset($response['error']))
			{
				$response['error']['data'] = '';
				$validateParams[!is_null($response['error'])] = 'Request have return error: ' . $response['error']['message'] . '; ' . "\n" .
						'Request: ' . $request . '; ' . "\n" .
						'Error data: ' . $response['error']['data'];
			}

			$this->checkForErrors($validateParams);

			return $response['result'];
		}

		/**
		 * When the method invocation completes, the service must reply with a response.
		 * The response is a single object serialized using JSON
		 *
		 * @param string $request
		 * @return string
		 */
		protected function & getResponse(& $request)
		{
			// do the actual connection
			$ch = curl_init();
			// set URL
			curl_setopt($ch, CURLOPT_URL, $this->url);
			curl_setopt($ch, CURLOPT_POST, 1);
			curl_setopt($ch, CURLOPT_POSTFIELDS, $request);
			curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-type: application/json'));
			curl_setopt($ch, CURLOPT_ENCODING, 'gzip,deflate');
			curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
			curl_setopt_array($ch, $this->curl_options);
			// send the request
			$response = curl_exec($ch);
			// check http status code
			$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
			if (isset($this->http_errors[$http_code]))
			{
				throw new Exception('Response Http Error - ' . $this->http_errors[$http_code]);
			}
			// check for curl error
			if (0 < curl_errno($ch))
			{
				throw new Exception('Unable to connect to ' . $this->url . ' Error: ' . curl_error($ch));
			}
			// close the connection
			curl_close($ch);
			return $response;
		}

		/**
		 * Check for errors
		 *
		 * @param array $validateArray
		 * @return void
		 */
		protected function checkForErrors(& $validateArray)
		{
			foreach ($validateArray as $test => $error)
			{
				if ($test)
				{
					throw new Exception($error);
				}
			}
		}

		/**
		 * For is_debug and performance stats
		 *
		 * @param string $add
		 * @param boolean $show
		 * @return void
		 */
		protected function debug($add, $show = false)
		{
			static $debug, $startTime;
			// is_debug off return
			if (false === $this->is_debug)
			{
				return;
			}
			// add
			$debug .= $add;
			// get starttime
			$startTime = empty($startTime) ? array_sum(explode(' ', microtime())) : $startTime;
			if (true === $show and !empty($debug))
			{
				// get endtime
				$endTime = array_sum(explode(' ', microtime()));
				// performance summary
				$debug .= 'Request time: ' . round($endTime - $startTime, 3) . ' s Memory usage: ' . round(memory_get_usage() / 1024) . " kb\r\n";
				echo nl2br($debug);
				// send output imidiately
				flush();
				// clean static
				$debug = $startTime = null;
			}
		}

	}

}
?>