/* A small Ajax framework
 * (c) 2007 crisp (crisp@tweakers.net) & Tweakers.net
 * This may be re-used only for non-commercial purposes
 */

if (!window.XMLHttpRequest)
{
	window.XMLHttpRequest = function()
	{
		// http://blogs.msdn.com/xmlteam/archive/2006/10/23/using-the-right-version-of-msxml-in-internet-explorer.aspx
		var types = [
			'MSXML2.XMLHTTP.6.0',
			'MSXML2.XMLHTTP.3.0'
		];

		for (var i = 0; i < types.length; i++)
		{
			try
			{
				return new ActiveXObject(types[i]);
			}
			catch(e) {}
		}

		return undefined;
	}
}

function Ajax(debug)
{
	// this is a singleton implementation :)

	if (this == window)
		return new Ajax();

	if (Ajax.instance)
		return Ajax.instance;

	// default options
	this.options = {
		type:		'xml',
		method:		'GET',
		async:		true,
		contentType:	'application/x-www-form-urlencoded',
		encoding:	'UTF-8',
		nocache:	true,
		handler:	null
	};
	this.requestObjectAsync = null;
	this.requestObjectSync = null;
	this.queue = [];
	this.busy = false;
	this.debug = debug;

	return (Ajax.instance = this);
}

Object.extend(Ajax.prototype,
{
	getRequestObject: function(async, forceNewObject)
	{
		var requestObjectName = 'requestObject' + (async ? 'Async' : 'Sync');
		if (this[requestObjectName] === null || forceNewObject)
			this[requestObjectName] = new XMLHttpRequest();

		return this[requestObjectName];
	},
	sendRequest: function(url, options, data)
	{
		options = Object.extend(options || {}, this.options);

		// Opera doesn't seem to like to re-use XHR-instances for async requests, possibly fixed in 9.2?
		var requestObject = this.getRequestObject(options['async'], window.opera && options['async']);

		if (requestObject)
		{
			if (options['async'] && this.busy)
			{
				this.addToQueue(url, options, data);
			}
			else
			{
				if (options['async'])
					this.busy = true;

				url += (url.indexOf('?') == -1 ? '?' : '&') + 'output=' + options['type'].toLowerCase();

				if (!options['headers'])
					options['headers'] = {};

				if (options['method'].toUpperCase() == 'POST')
				{
					/* Force "Connection: close" for older Mozilla browsers to work
					 * around a bug where XMLHttpRequest sends an incorrect
					 * Content-length header. See Mozilla Bugzilla #246651.
					*/
					if (requestObject.overrideMimeType && (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
						options['headers']['Connection'] = 'close';

					if (!('Content-Type' in options['headers']))
						options['headers']['Content-Type'] = options.contentType +
							(options.encoding ? '; charset=' + options.encoding : '');
				}
				else
				{
					options['method'] = 'GET';
					data = null;

					if (options['nocache'])
					{
						options['headers']['Cache-Control'] = 'no-cache';
						url += '&nocache=' + new Date().getTime();
					}
				}

				requestObject.open(options['method'].toUpperCase(), url, !!options['async']);

				for (var header in options['headers'])
				{
					if (options['headers'].hasOwnProperty(header))
					{
						requestObject.setRequestHeader(header, options['headers'][header]);
					}
				}

				if (options['async'])
				{
					requestObject.onreadystatechange = this.defaultHandler.bind(this, options);
					requestObject.send(data);
				}
				else
				{
					requestObject.send(data);
					return this.defaultHandler(options);
				}
			}

			return true;
		}

		if (this.debug) alert('Could not generate XMLHttpRequest object');

		return false;
	},
	addToQueue: function(url, options, data)
	{
		this.queue.push({url: url, options: options, data: data});
		this.checkQueue();
	},
	checkQueue: function()
	{
		if (!this.busy && this.queue.length)
		{
			var request = this.queue.shift();
			this.sendRequest(request['url'], request['options'], request['data']);
		}
	},
	defaultHandler: function(options)
	{
		var result = null;
		var requestObject = this.getRequestObject(options['async']);

		if (requestObject.readyState == 4)
		{
			if (!requestObject.status || (requestObject.status >= 200 && requestObject.status < 400))
			{
				switch (options['type'].toLowerCase())
				{
					case 'json':
						result = this.parseJSON(requestObject.responseText);
						break;
					case 'xml':
						result = this.validateXML(requestObject.responseXML);
						break;
					case 'text':
					default:
						result = requestObject.responseText;
				}

				if (options['handler'])
					result = options['handler'](result);
			}

			if (options['async'])
			{
				requestObject.onreadystatechange = function() {}
				this.busy = false;
				this.checkQueue();
			}
		}

		return result;
	},
	validateXML: function(xml)
	{
		if (xml)
		{
			if (xml.documentElement)
			{
				if (xml.documentElement.tagName == 'parsererror')
				{
					if (this.debug) alert('Error parsing XML respons:\n' + xml.documentElement.firstChild.nodeValue);
				}
				else
				{
					return xml;
				}
			}
			else
			{
				if (xml.parseError)
				{
					if (this.debug) alert('Error parsing XML respons:\n' + xml.parseError.reason + '\n' + url);
				}
				else
				{
					if (this.debug) alert('No valid data in XML respons');
				}
			}
		}
		else
		{
			if (this.debug) alert('The responseXML object was empty');
		}

		return false;
	},
	parseJSON: function(string)
	{
		try
		{
			return /^("(\\.|[^"\\\n\r])*"|[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t])+$/.test(string)
				&& eval('(' + string + ')');
		}
		catch (e) {}

		if (this.debug) alert('Error parsing JSON respons: ' + string);

		return false;
	},
	createPostBody: function(form)
	{
		var elements = form.elements, element;
		var qName, qValues, qParts = [];
		var i, j, k, o;
		for (i = 0; i < elements.length; i++)
		{
			element = elements[i];

			if (element.name && !element.disabled)
			{
				qValues = [];

				switch (element.tagName.toLowerCase())
				{
					case 'input':
						k = element.type.toLowerCase();
						if ((k == 'checkbox' || k == 'radio') && !element.checked)
						{
							break;
						}
						else if (k == 'file')
						{
							if (this.debug) alert('File uploads are not supported');
							break;
						}
					case 'textarea':
						qValues.push(element.value);
						break;
					case 'select':
						k = element.options, o = [];
						if (element.multiple)
						{
							for (j = 0; j < k.length; j++)
								if (k[j].selected) o.push(k[j]);
						}
						else
							o.push(k[element.selectedIndex]);

						for (j = 0; j < o.length; j++)
						{
							k = o[j].value;
							if (!k && !('value' in k))
								k = o.text;
							qValues.push(k);
						}

						break;
				}

				if ((k = qValues.length))
				{
					qName = encodeURIComponent(element.name);
					for (j = 0; j < k; j++)
						qParts.push(qName + '=' + encodeURIComponent(qValues[j]));
				}
			}
		}

		return qParts.join('&');
	}
});
