/**
 * EGroupware clientside API object
 *
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
 * @package etemplate
 * @subpackage api
 * @link http://www.egroupware.org
 * @author Andreas Stöckel (as AT stylite.de)
 * @author Ralf Becker <RalfBecker@outdoor-training.de>
 */

/*egw:uses
	vendor.bower-asset.jquery.dist.jquery;

	egw_core;
	egw_utils;
	egw_files;
	egw_debug;
*/

/**
 * Module sending json requests
 *
 * @param {string} _app application name object is instanciated for
 * @param {object} _wnd window object is instanciated for
 */
egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd)
{
	"use strict";

	/**
	 * Object which contains all registered handlers for JS responses.
	 * The handlers are organized per response type in the top level of the
	 * object, where each response type can have an array of handlers attached
	 * to it.
	 */
	var plugins = {};

	/**
	 * Global json handlers are from global modules, not window level
	 */
	if(typeof egw._global_json_handlers == 'undefined')
	{
		egw._global_json_handlers = {};
	}
	var global_plugins = egw._global_json_handlers;

	/**
	 * Internal implementation of the JSON request object.
	 *
	 * @param {string} _menuaction
	 * @param {array} _parameters
	 * @param {function} _callback
	 * @param {object} _context
	 * @param {boolean|"keepalive"} _async true: asynchronious request, false: synchronious request,
	 * 	"keepalive": async. request with keepalive===true / sendBeacon, to be used in boforeunload event
	 * @param {object} _sender
	 * @param {egw} _egw
	 */
	function json_request(_menuaction, _parameters, _callback, _context,
		_async, _sender, _egw)
	{
		// Copy the parameters
		this.url = _egw.ajaxUrl(_menuaction);
		// IE JSON-serializes arrays passed in from different window contextx (eg. popups)
		// as objects (it looses object-type of array), causing them to be JSON serialized
		// as objects and loosing parameters which are undefined
		// JSON.strigify([123,undefined]) --> '{"0":123}' instead of '[123,null]'
		this.parameters = _parameters ? [].concat(_parameters) : [];
		this.async = typeof _async != 'undefined' ? _async : true;
		this.callback = _callback ? _callback : null;
		this.context = _context ? _context : null;
		this.sender = _sender ? _sender : null;
		this.egw = _egw;

		// We currently don't have a request object
		this.request = null;

		// Some variables needed for notification about a JS files done loading
		this.onLoadFinish = null;
		this.jsFiles = 0;
		this.jsCount = 0;

		// Function which is currently used to display alerts -- may be replaced by
		// some API function.
		this.alertHandler = function(_message, _details) {
			// we need to use the alert function of the window of the request, not just the main window
			(this.egw ? this.egw.window : window).alert(_message);

			if (_details)
			{
				_egw.debug('info', _message, _details);
			}
		};
	}

	/**
	 * Open websocket to push server (and keeps it open)
	 *
	 * @param {string} url this.websocket(s)://host:port
	 * @param {array} tokens tokens to subscribe too: sesssion-, user- and instance-token (in that order!)
	 * @param {number} account_id to connect for
	 * @param {function} error option error callback(_msg) used instead our default this.error
	 * @param {int} reconnect timeout in ms (internal)
	 */
	json_request.prototype.openWebSocket = function(url, tokens, account_id, error, reconnect)
	{
		const min_reconnect_time = 1000;
		const max_reconnect_time = 300000;
		const check_interval = 30000;	// 30 sec
		const max_ping_response_time = 1000;
		let reconnect_time = reconnect || min_reconnect_time;
		let check_timer;
		let check = function()
		{
			this.websocket.send('ping');
			check_timer = window.setTimeout(function()
			{
				console.log("Server did not respond to ping in "+max_ping_response_time+" seconds --> try reconnecting");
				check_timer = null;
				this.websocket.onclose = function()
				{
					this.websocket = null;
					this.openWebSocket(url, tokens, account_id, error, reconnect_time);
				}.bind(this);
				this.websocket.close();	// closing it now, before reopening it, to not end up with multiple connections
			}.bind(this), max_ping_response_time);
		}.bind(this);

		this.websocket = new WebSocket(url);
		this.websocket.onopen = jQuery.proxy(function(e)
		{
			check_timer = window.setTimeout(check, check_interval);
			this.websocket.send(JSON.stringify({
				subscribe: tokens,
				account_id: parseInt(account_id)
			}));
		}, this);

		this.websocket.onmessage = jQuery.proxy(function(event)
		{
			reconnect_time = min_reconnect_time;
			console.log(event);
			if (check_timer) window.clearTimeout(check_timer);
			check_timer = window.setTimeout(check, check_interval);
			if (event.data === 'pong') return;	// just a keepalive message
			let data = JSON.parse(event.data);
			if (data && data.type)
			{
				this.handleResponse({ response: [data]});
			}
		}, this);

		this.websocket.onerror = jQuery.proxy(function(error)
		{
			reconnect_time *= 2;
			if (reconnect_time > max_reconnect_time) reconnect_time = max_reconnect_time;

			console.log(error);
			(error||this.handleError({}, error));
		}, this);

		this.websocket.onclose = jQuery.proxy(function(event)
		{
			if (event.wasClean)
			{
				reconnect_time = min_reconnect_time;
				console.log(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`);
			}
			else
			{
				reconnect_time *= 2;
				if (reconnect_time > max_reconnect_time) reconnect_time = max_reconnect_time;

				// e.g. server process killed or network down
				// event.code is usually 1006 in this case
				console.log('[close] Connection died --> reconnect in '+reconnect_time+'ms');
				if (check_timer) window.clearTimeout(check_timer);
				check_timer = null;
				window.setTimeout(jQuery.proxy(this.openWebSocket, this, url, tokens, account_id, error, reconnect_time), reconnect_time);
			}
		}, this);
	},

	/**
	 * Sends the assembled request to the server
	 * @param {boolean|"keepalive"} _async Overrides async provided in constructor: true: asynchronious request,
	 * 	false: synchronious request, "keepalive": async. request with keepalive===true / sendBeacon, to be used in beforeunload event
	 * @param {string} method ='POST' allow to eg. use a (cachable) 'GET' request instead of POST
	 * @param {function} error option error callback(_xmlhttp, _err) used instead our default this.error
	 *
	 * @return {jqXHR|boolean} jQuery jqXHR request object or for async==="keepalive" boolean is returned
	 */
	json_request.prototype.sendRequest = function(async, method, error)
	{
		if(typeof async != "undefined")
		{
			this.async = async;
		}

		if (typeof method === 'undefined') method = 'POST';

		// Assemble the complete request
		var request_obj = JSON.stringify({
			request: {
				parameters: this.parameters
			}
		});

		// send with keepalive===true or sendBeacon to be used in beforeunload event
		if (this.async === "keepalive" && typeof navigator.sendBeacon !== "undefined")
		{
			const data = new FormData();
			data.append('json_data', request_obj);
			//(window.opener||window).console.log("navigator.sendBeacon", this.url, request_obj, data.getAll('json_data'));
			return navigator.sendBeacon(this.url, data);
		}

		// Send the request via AJAX using the jquery ajax function
		// we need to use jQuery of window of egw object, as otherwise the one from main window is used!
		// (causing eg. apply from server with app.$app.method to run in main window instead of popup)
		this.request = (this.egw.window?this.egw.window.jQuery:jQuery).ajax({
			url: this.url,
			async: this.async,
			context: this,
			// only POST can send JSON as direct payload, GET can not
			data: method === 'GET' ? { json_data: request_obj } : request_obj,
			contentType: method === 'GET' ? false : 'application/json',
			dataType: 'json',
			type: method,
			success: this.handleResponse,
			jsonp: false,
			error: error || this.handleError
		});

		return this.request;
	};

	/**
	 * Default error callback displaying error via egw.message
	 *
	 * @param {XMLHTTP} _xmlhttp
	 * @param {string} _err
	 */
	json_request.prototype.handleError = function(_xmlhttp, _err) {
		// Don't error about an abort
		if(_err !== 'abort')
		{
			this.egw.message.call(this.egw,
				this.egw.lang('A request to the EGroupware server returned with an error')+
				': '+_xmlhttp.statusText+' ('+_xmlhttp.status+")\n\n"+
				this.egw.lang('Please reload the EGroupware desktop (F5 / Cmd+r).')+"\n"+
				this.egw.lang('If the error persists, contact your administrator for help and ask to check the error-log of the webserver.')+
				"\n\nURL: "+this.url+"\n"+
				(_xmlhttp.getAllResponseHeaders() ? (_xmlhttp.getAllResponseHeaders().match(/^Date:.*$/mi) ? _xmlhttp.getAllResponseHeaders().match(/^Date:.*$/mi)[0]:''):'')+
				// if EGroupware send JSON payload with error, errno show it here too
				(_err === 'error' && _xmlhttp.status === 400 && typeof _xmlhttp.responseJSON === 'object' && _xmlhttp.responseJSON.error ?
				"\nError: "+_xmlhttp.responseJSON.error+' ('+_xmlhttp.responseJSON.errno+')' : '')
			);

			this.egw.debug('error', 'Ajax request to', this.url, ' failed: ', _err, _xmlhttp.status, _xmlhttp.statusText, _xmlhttp.responseJSON);

			// check of unparsable JSON on server-side, which might be caused by some network problem --> resend max. twice
			if (_err === 'error' && _xmlhttp.status === 400 && typeof _xmlhttp.responseJSON === 'object' &&
				_xmlhttp.responseJSON.errno && _xmlhttp.responseJSON.error.substr(0, 5) === 'JSON ')
			{
				// ToDo: resend request max. twice
			}
		}
	};

	json_request.prototype.handleResponse = function(data) {
		if (data && typeof data.response != 'undefined')
		{
			if (egw.preference('show_generation_time', 'common', false) == "1")
			{
				var gen_time_div = jQuery('#divGenTime').length > 0 ? jQuery('#divGenTime')
				:jQuery('<div id="divGenTime" class="pageGenTime"><span class="pageTime"></span></div>').appendTo('#egw_fw_footer');
			}
			// Load files first
			var js_files = [];
			for (var i = data.response.length - 1; i >= 0; --i)
			{
				var res = data.response[i];
				if(res.type == 'js' && typeof res.data == 'string')
				{
					js_files.unshift(res.data);
					data.response.splice(i,1);
				}
			}
			if(js_files.length > 0)
			{
				var start_time = (new Date).getTime();
				this.egw.includeJS(js_files, function() {
					var end_time = (new Date).getTime();
					this.handleResponse(data);
					if (egw.preference('show_generation_time', 'common', false) == "1")
					{
						var gen_time_div = jQuery('#divGenTime');
						if (!gen_time_div.length) gen_time_div = jQuery('.pageGenTime');
						var gen_time_async = jQuery('.asyncIncludeTime').length > 0 ? jQuery('.asyncIncludeTime'):
							gen_time_div.append('<span class="asyncIncludeTime"></span>').find('.asyncIncludeTime');
						gen_time_async.text(egw.lang('async includes took %1s', (end_time-start_time)/1000));
					}
				}, this);
				return;
			}

			// Flag for only data response - don't call callback if only data
			var only_data = (data.response.length > 0);

			for (var i = 0; i < data.response.length; i++)
			{
				// Get the response object
				var res = data.response[i];
				if(typeof res.type == 'string' && res.type != 'data') only_data = false;

				// Check whether a plugin for the given type exists
				var handlers = [plugins, global_plugins];
				for(var handler_idx = 0; handler_idx < handlers.length; handler_idx++)
				{
					var handler_level = handlers[handler_idx];
					if (typeof handler_level[res.type] !== 'undefined')
					{
						for (var j = 0; j < handler_level[res.type].length; j++) {
							try {
								// Get a reference to the plugin
								var plugin = handler_level[res.type][j];
								if (res.type.match(/et2_load/))
								{
									if (egw.preference('show_generation_time', 'common', false) == "1")
									{
										if (gen_time_div.length > 0)
										{
											gen_time_div.find('span.pageTime').text(egw.lang("Page was generated in %1 seconds ", data.page_generation_time));
											if (data.session_restore_time)
											{
												var gen_time_session_span = gen_time_div.find('span.session').length > 0 ? gen_time_div.find('span.session'):
														gen_time_div.append('<span class="session"></span>').find('.session');
												gen_time_session_span.text(egw.lang("session restore time in %1 seconds ", data.page_generation_time));
											}
										}
									}
								}
								// Call the plugin callback
								plugin.callback.call(
									plugin.context ? plugin.context : this.context,
									res.type, res, this
								);
							} catch(e) {
								var msg = e.message ? e.message : e + '';
								var stack = e.stack ? "\n-- Stack trace --\n" + e.stack : "";
								this.egw.debug('error', 'Exception "' + msg + '" while handling JSON response from ' +
									this.url + ' [' + JSON.stringify(this.parameters) + '] type "' + res.type +
									'", plugin', plugin, 'response', res, stack);
							}
						}
					}
				}
			}
			// Call request callback, if provided
			if(this.callback != null && !only_data)
			{
				this.callback.call(this.context,res);
			}
		}
		this.request = null;
	};

	var json = {

		/** The constructor of the egw_json_request class.
		 *
		 * @param _menuaction the menuaction function which should be called and
		 * 	which handles the actual request. If the menuaction is a full featured
		 * 	url, this one will be used instead.
		 * @param _parameters which should be passed to the menuaction function.
		 * @param _async specifies whether the request should be asynchronous or
		 * 	not.
		 * @param _callback specifies the callback function which should be
		 * 	called, once the request has been sucessfully executed.
		 * @param _context is the context which will be used for the callback function
		 * @param _sender is a parameter being passed to the _callback function
		 */
		json: function(_menuaction, _parameters, _callback, _context, _async,
			_sender)
		{
			return new json_request(_menuaction, _parameters, _callback,
				_context, _async, _sender, this);
		},

		/**
		 * Do an AJAX call and get a javascript promise, which will be resolved with the returned data.
		 *
		 * egw.request() returns immediately with a Promise.  The promise will be resolved with just the returned data,
		 * any other "piggybacked" responses will be handled by registered handlers.  The data will also be passed to
		 * any registered data handlers (egw.data) before it is passed to your handler.
		 *
		 * To use:
		 * @example
		 * 	egw.request(
		 * 		"EGroupware\\Api\\Etemplate\\Widget\\Select::ajax_get_options",
		 * 		["select-cat"]
	 	 * 	)
		 * 	.then(function(data) {
		 * 		// Deal with the returned data here.  data may be undefined if no data was returned.
		 * 		console.log("Here's the categories:",data);
		 * 	});
		 *
		 *
		 * 	The return is a Promise, so multiple .then() can be chained in the usual ways:
		 * 	@example
		 * 	egw.request(...)
		 * 		.then(function(data) {
		 * 		  if(debug) console.log("Requested data", data);
		 * 		}
		 * 		.then(function(data) {
		 * 			// Change the data for the rest of the chain
		 * 		    if(typeof data === "undefined") return [];
		 * 		}
		 * 		.then(function(data) {
		 * 			// data is never undefined now, if it was before it's an empty array now
		 * 		 	for(let i = 0; i < data.length; i++)
		 * 			{
		 * 		 		...
		 * 			}
		 * 		}
		 *
		 *
		 * 	You can also fire off multiple requests, and wait for them to all be answered:
		 * 	@example
		 * 	let first = egw.request(...);
		 * 	let second = egw.request(...);
		 * 	Promise.all([first, second])
		 * 		.then(function(values) {
		 * 		 	console.log("First:", values[0], "Second:", values[1]);
		 * 		}
		 *
		 *
		 * @param {string} _menuaction
		 * @param {any[]} _parameters
		 *
		 * @return Promise
		 */
		request: function(_menuaction, _parameters)
		{
			let request = new json_request(_menuaction, _parameters, null, this, true, this, this);
			let ajax_promise = request.sendRequest();

			// This happens first, immediately
			let resolvePromise = function(resolve, reject) {
				// Bind to ajax response - this is called _after_ any other handling
				ajax_promise.always(function(response, status, p) {
					if(status !== "success") reject();

					// The ajax request has completed, get just the data & pass it on
					if(response && response.response)
					{
						for(let value of response.response)
						{
							if(value.type && value.type === "data" && typeof value.data !== "undefined")
							{
								// Data was packed in response
								resolve(value.data);
							}
							else if (value && typeof value.type === "undefined" && typeof value.data === "undefined")
							{
								// Just raw data
								resolve(value);
							}
						}
					}

					// No data? Resolve the promise with nothing
					resolve();
				});
			};

			const myPromise = new Promise(resolvePromise);

			return myPromise;
		},

		/**
		 * Call a function specified by it's name (possibly dot separated, eg. "app.myapp.myfunc")
		 *
		 * @param {string|Function} _func dot-separated function name or function
		 * @param {mixed} ...args variable number of arguments
		 * @returns {mixed|Promise}
		 */
		callFunc: function(_func)
		{
			return this.applyFunc(_func, [].slice.call(arguments, 1));
		},

		/**
		 * Call a function specified by it's name (possibly dot separated, eg. "app.myapp.myfunc")
		 *
		 * @param {string|Function} _func dot-separated function name or function
		 * @param {array} args arguments
		 * @param {object} _context
		 * @returns {mixed|Promise}
		 */
		applyFunc: function(_func, args, _context)
		{
			let parent = _context || _wnd;
			let func = _func;

			if (typeof _func === 'string')
			{
				let parts = _func.split('.');
				func = parts.pop();
				for(var i=0; i < parts.length; ++i)
				{
					if (typeof parent[parts[i]] !== 'undefined')
					{
						parent = parent[parts[i]];
					}
					// check if we need a not yet included app.js object --> include it now and return a Promise
					else if (i == 1 && parts[0] == 'app' && typeof app.classes[parts[1]] === 'undefined')
					{
						const self = this;
						return new Promise(function(resolve, reject)
						{
							// cache for a day, better then no invalidation
							self.includeJS('/'+parts[1]+'/js/app.js?'+((new Date).valueOf()/86400|0).toString(), function ()
							{
								resolve(self.applyFunc(_func, args, _context));
							}, self, self.webserverUrl);
						});
					}
					// check if we need a not yet instanciated app.js object --> instanciate it now
					else if (i == 1 && parts[0] == 'app' && typeof app.classes[parts[1]] === 'function')
					{
						parent = parent[parts[1]] = new app.classes[parts[1]]();
					}
				}
				if (typeof parent[func] == 'function')
				{
					func = parent[func];
				}
			}
			if (typeof func != 'function')
			{
				throw _func+" is not a function!";
			}
			return func.apply(parent, args);
		},

		/**
		 * Registers a new handler plugin.
		 *
		 * @param _callback is the callback function which should be called
		 * 	whenever a response is comming from the server.
		 * @param _context is the context in which the callback function should
		 * 	be called. If null is given, the plugin is executed in the context
		 * 	of the request object context.
		 * @param _type is an optional parameter defaulting to 'global'.
		 * 	it describes the response type which this plugin should be
		 * 	handling.
		 * @param {boolean} [_global=false] Register the handler globally or
		 *	locally.  Global handlers must stay around, so should be used
		 *	for global modules.
		 */
		registerJSONPlugin: function(_callback, _context, _type, _global)
		{
			// _type defaults to 'global'
			if (typeof _type === 'undefined')
			{
				_type = 'global';
			}
			// _global defaults to false
			if (typeof _global === 'undefined')
			{
				_global = false;
			}
			var scoped = _global ? global_plugins : plugins;

			// Create an array for the given category inside the plugins object
			if (typeof scoped[_type] === 'undefined')
			{
				scoped[_type] = [];
			}

			// Add the entry
			scoped[_type].push({
				'callback': _callback,
				'context': _context
			});
		},

		/**
		 * Removes a previously registered plugin.
		 *
		 * @param _callback is the callback function which should be called
		 * 	whenever a response is comming from the server.
		 * @param _context is the context in which the callback function should
		 * 	be called.
		 * @param _type is an optional parameter defaulting to 'global'.
		 * 	it describes the response type which this plugin should be
		 * 	handling.
		 * @param {boolean} [_global=false] Remove a global or local handler.
		 */
		unregisterJSONPlugin: function(_callback, _context, _type, _global)
		{
			// _type defaults to 'global'
			if (typeof _type === 'undefined')
			{
				_type = 'global';
			}
			// _global defaults to false
			if (typeof _global === 'undefined')
			{
				_global = false;
			}
			var scoped = _global ? global_plugins : plugins;
			if (typeof scoped[_type] !== 'undefined') {
				for (var i = 0; i < scoped[_type].length; i++)
				{
					if (scoped[_type][i].callback == _callback &&
						scoped[_type][i].context == _context)
					{
						scoped[_type].slice(i, 1);
						break;
					}
				}
			}
		}
	};

	// Regisert the "alert" plugin
	json.registerJSONPlugin(function(type, res, req) {
		//Check whether all needed parameters have been passed and call the alertHandler function
		if ((typeof res.data.message != 'undefined') &&
			(typeof res.data.details != 'undefined'))
		{
			req.alertHandler(
				res.data.message,
				res.data.details);
			return true;
		}
		throw 'Invalid parameters';
	}, null, 'alert');

	// Regisert the "message" plugin
	json.registerJSONPlugin(function(type, res, req) {
		//Check whether all needed parameters have been passed and call the alertHandler function
		if ((typeof res.data.message != 'undefined'))
		{
			req.egw.message(res.data.message, res.data.type);
			return true;
		}
		throw 'Invalid parameters';
	}, null, 'message');

	// Register the "assign" plugin
	json.registerJSONPlugin(function(type, res, req) {
		//Check whether all needed parameters have been passed and call the alertHandler function
		if ((typeof res.data.id != 'undefined') &&
			(typeof res.data.key != 'undefined') &&
			(typeof res.data.value != 'undefined'))
		{
			var obj = _wnd.document.getElementById(res.data.id);
			if (obj)
			{
				obj[res.data.key] = res.data.value;

				if (res.data.key == "innerHTML")
				{
					egw_insertJS(res.data.value);
				}

				return true;
			}

			return false;
		}
		throw 'Invalid parameters';
	}, null, 'assign');

	// Register the "data" plugin
	json.registerJSONPlugin(function(type, res, req) {
		//Callback the caller in order to allow him to handle the data
		if (req.callback)
		{
			req.callback.call(req.sender, res.data);
			return true;
		}
	}, null, 'data');

	// Register the "script" plugin
	json.registerJSONPlugin(function(type, res, req) {
		if (typeof res.data == 'string')
		{
			try
			{
				var func = new Function(res.data);
				func.call(req.egw ? req.egw.window : window);
			}
			catch (e)
			{
				req.egw.debug('error', 'Error while executing script: ',
					res.data,e);
			}
			return true;
		}
		throw 'Invalid parameters';
	}, null, 'script');

	// Register the "apply" plugin
	json.registerJSONPlugin(function(type, res, req) {
		if (typeof res.data.func == 'string')
		{
			req.egw.applyFunc(res.data.func, res.data.parms, req.egw.window);
			return true;
		}
		throw 'Invalid parameters';
	}, null, 'apply');

	// Register the "jquery" plugin
	json.registerJSONPlugin(function(type, res, req) {
		if (typeof res.data.select == 'string' &&
			typeof res.data.func == 'string')
		{
			try
			{
				var jQueryObject = jQuery(res.data.select, req.context);
				jQueryObject[res.data.func].apply(jQueryObject,	res.data.parms);
			}
			catch (e)
			{
				req.egw.debug('error', 'Function', res.data.func,
					'Parameters', res.data.parms);
			}
			return true;
		}
		throw 'Invalid parameters';
	}, _wnd, 'jquery');

	// Register the "redirect" plugin
	json.registerJSONPlugin(function(type, res, req) {
		//console.log(res.data.url);
		if (typeof res.data.url == 'string' &&
			typeof res.data.global == 'boolean')
		{
			//Special handling for framework reload
			res.data.global |= (res.data.url.indexOf("?cd=10") > 0);

			if (res.data.global)
			{
				egw_topWindow().location.href = res.data.url;
			}
			// json request was originating from a different popup --> redirect that one
			else if(this && this.DOMContainer && this.DOMContainer.ownerDocument.defaultView != window &&
				egw(this.DOMContainer.ownerDocument.defaultView).is_popup())
			{
				this.DOMContainer.ownerDocument.location.href = res.data.url;
			}
			// main window, open url in respective tab
			else
			{
				egw_appWindowOpen(res.data.app, res.data.url);
			}
			return true;
		}
		throw 'Invalid parameters';
	}, null, 'redirect');

	// Register the 'css' plugin
	json.registerJSONPlugin(function(type, res, req) {
		if (typeof res.data == 'string')
		{
			req.egw.includeCSS(res.data);
			return true;
		}
		throw 'Invalid parameters';
	}, null, 'css');

	// Register the 'js' plugin
	json.registerJSONPlugin(function(type, res, req) {
		if (typeof res.data == 'string')
		{
			req.jsCount++;
			req.egw.includeJS(res.data, function() {
				req.jsFiles++;
				if (req.jsFiles == req.jsCount && req.onLoadFinish)
				{
					req.onLoadFinish.call(req.sender);
				}
			});
			return true;
		}
		throw 'Invalid parameters';
	}, null, 'js');

	// Register the 'html' plugin, replacing document content with send html
	json.registerJSONPlugin(function(type, res, req) {
		if (typeof res.data == 'string')
		{
			// Empty the document tree
			while (_wnd.document.childNodes.length > 0)
			{
				_wnd.document.removeChild(_wnd.document.childNodes[0]);
			}

			// Write the given content
			_wnd.document.write(res.data);

			// Close the document
			_wnd.document.close();
			return true;
		}
		throw 'Invalid parameters';
	}, null, 'html');

	// Return the extension
	return json;
});