function ajaxRequest(channel)
{
	this.channel = channel;

	// Controleer of de browser wel ajax support biedt en laadt het ajax object.
	if (typeof XMLHttpRequest !== 'undefined') // most browsers
		this.XMLHttpRequest = new XMLHttpRequest();

	else if (typeof ActiveXObject !== 'undefined') // old IE
	{
		try { this.XMLHttpRequest = new ActiveXObject("Msxml2.XMLHTTP"); }
		catch (err)
		{
			try { this.XMLHttpRequest = new ActiveXObject("Microsoft.XMLHTTP"); }
			catch (err) { ajax.support = false; }
		}
	}
	else
		ajax.support = false;

	if (!ajax.support)
		return;

	this.progressStatus = 'ready';
	this.waitingForRetry = 0;

	this.requestTimeout = 90;
	this.timer = 0;
	this.startTime = 0;

	this.requestData = null;
}

(function()
{
	ajaxRequest.prototype.load = function ajaxRequest_load(requestData)
	{
		if (this.progressStatus != 'ready')
			return false;

		this.requestTimeout = typeof requestData.timeout != 'number' || requestData.timeout < 0 ? 90 : requestData.timeout;

		requestData.method = (!requestData.method || requestData.method.search(/^(post|get|error)$/i) == -1) ? 'GET' : requestData.method.toUpperCase();
		requestData.contentType = requestData.contentType || 'application/x-www-form-urlencoded';
		requestData.data = requestData.data || '';

		this.requestData = requestData;
		this.progressStatus = 'loaded';

		return true;
	};

	ajaxRequest.prototype.process = function ajaxRequest_process()
	{
		if (this.progressStatus != 'loaded')
			return false;

		this.startTime = new Date();
		this.progressStatus = 'running';
		this.requestStatus = '';

		// Logdata beetje opschonen. Het is ook al keer gelogd toen deze opdracht in de queue gezet werd.
		var logData = this.requestData.method == 'ERROR' ? this.requestData.data.replace(/^.*?msg=([^&]*)&.*$/, function(ref0, ref1) { return decodeURIComponent(ref1); }) : this.requestData.data;

		ajax.log.push({
			'what': 'Send #'+this.requestData.volgnummer+' (channel '+this.channel+')',
			'time': this.startTime.valueOf()+' ('+this.startTime.toLocaleTimeString()+')',
			'url': this.requestData.url,
			'data': logData.replace(/^(.+?)[\r\n][\s\S]*$/, '$1')
		});

		var method = this.requestData.method == 'ERROR' ? 'POST' : this.requestData.method;
		var ajaxRequestObject = this;

		if (this.requestTimeout)
			this.timer = setTimeout(function ajaxRequest_timeoutHandler_timeout() { return ajaxRequestObject.timeout(); }, Math.round(this.requestTimeout * 1000));

		this.XMLHttpRequest.open(method, this.requestData.url, true);
		this.XMLHttpRequest.onreadystatechange = function ajaxRequest_readystatechangeHandler() { return ajaxRequestObject.receive(); };
		if (method == 'POST')
		{
			this.XMLHttpRequest.setRequestHeader('Content-Type', this.requestData.contentType);
			this.XMLHttpRequest.setRequestHeader('Content-Length', this.requestData.data.length);
		}
		this.XMLHttpRequest.send(this.requestData.data);

		return true;
	};

	/**
	 * Handelt de ontvangst van de response af en controleert de status.
	 */
	ajaxRequest.prototype.receive = function ajaxRequest_receive()
	{
		if (this.progressStatus != 'running' && this.progressStatus != 'timed out' && this.progressStatus != 'aborted')
			return false;

		if (this.XMLHttpRequest.readyState != 4)
			return false;

		if (this.timer)
		{
			clearTimeout(this.timer);
			this.timer = 0;
		}

		// In IE 9 zijn status en responseText niet beschikbaar als de request
		// afgebroken is (aborted of timeout). Zodra de status wordt opgevraagd
		// geeft-ie de melding "Could not complete the operation due to error
		// c00c023f". Dit is een permissionprobleem. Niet logisch, maar we moeten
		// het ermee doen.
		try { var httpStatus = this.XMLHttpRequest.status; }
		catch (err) { var httpStatus = 0; }
		try { var responseText = this.XMLHttpRequest.responseText; }
		catch (err) { var responseText = ''; }

		var responseHeaders = httpStatus >= 100 && httpStatus < 600 ? this.XMLHttpRequest.getAllResponseHeaders() : '';

		if (this.progressStatus != 'running' || httpStatus != 200)
		{
			var responseData = this.receiveFailure();

			// Als er een retry gaat plaatsvinden, moet de huidige response
			// niet verder verwerkt worden.
			if (this.waitingForRetry)
				return false;

			var responseStatus = this.progressStatus == 'running' ? 'error' : this.progressStatus;
		}
		else
		{
			responseData = this.receiveSuccess();
			responseStatus = 'ok';
		}

		this.progressStatus = 'ready';

		// RequestData object klonen. Hier mag geen copy-by-reference gemaakt
		// worden. De functie ajax.nextFromQueue kan de property this.requestData
		// overschrijven. Als we hier dan een reference hebben, is de kopie hier
		// ook automatisch overschreven.
		var requestData = { };
		for (var prop in this.requestData)
			requestData[prop] = this.requestData[prop];
		var ajaxData = {
			'timeoutSetting': this.requestTimeout,
			'responseStatus': responseStatus,
			'responseHeaders': responseHeaders,
			'httpStatus': httpStatus,
			'responseText': responseText
		};

		// De volgende request mag al beginnen voordat de callbackfunctie van de huidige
		// is aangeroepen. Er moeten hier timeouts gebruikt worden, omdat anders de nieuwe
		// request al begint voordat deze functie, die de callbackfunctie van de lopende
		// request is, afgelopen is. Na afloop worden de properties van het XMLHttpRequest
		// object gereset, dus ook de onreadystatechange. Dit geeft problemen als-ie dan
		// al met de volgende request bezig is. De timeouts staan zowel op de aanroep van
		// de volgende request als op de verdere verwerking van de callbackfunctie.
		if (ajax.doNextFromQueue == ajax.ALL_REQUESTS)
			setTimeout(function ajaxRequest_timeoutHandler_doNextFromQueue() { return ajax.nextFromQueue(); }, 0);

		// En nu de callback aanroepen.
		if (requestData.callback)
		{
			if (ajax.isIE || typeof toerEngineError === 'undefined')
			{
				// Voor IE geen try/catch, omdat-ie dan geen regelnummers meer geeft in de foutmelding
				var func = function() { requestData.callback(responseData, requestData.passthruData, responseStatus, ajaxData); return true; };
			}
			else
			{
				func = function()
				{
					try { requestData.callback(responseData, requestData.passthruData, responseStatus, ajaxData); }
					catch(ex) { toerEngineError(ex); }
					return true;
				};
			}

			func.fName = 'ajax callbackFunction for #'+requestData.volgnummer;
			setTimeout(func, 10);
		}

		return true;
	};


	/**
	 * Zorgt voor de afhandeling van een succesvolle request en geeft de ontvangen
	 * gegevens terug.
	 */
	ajaxRequest.prototype.receiveSuccess = function ajaxRequest_receiveSuccess()
	{
		var endTime = new Date();
		ajax.log.push({
			'what': 'Receive #'+this.requestData.volgnummer+' (channel '+this.channel+')',
			'time': endTime.valueOf()+' (request took '+(endTime - this.startTime)+' ms)',
			'url': this.requestData.url
		});

		var contentType = this.XMLHttpRequest.getResponseHeader('Content-Type');
		if (contentType && contentType.match(/\bjson\b/i))
		{
			try { var responseData = eval('('+this.XMLHttpRequest.responseText.replace(/(^[\s\r\n]+|[\s\r\n]+$)/g, '').replace(/\n/g, '\\n')+')'); }
			catch(err)
			{
				var responseData = this.XMLHttpRequest.responseText;
				ajax.log.push({
					'what': '#'+this.requestData.volgnummer+' Evaluation of JSON code failed (channel '+this.channel+')',
					'info': (err.name && err.message) ? err.name+': '+err.message : err+''
				});
			}
		}
		else if (contentType && contentType.match(/\bxml\b/i))
		{
			try { var responseData = this.XMLHttpRequest.responseXML; }
			catch(err) { var responseData = this.XMLHttpRequest.responseText; }
		}
		else
			var responseData = this.XMLHttpRequest.responseText;

		if (ajax.doNextFromQueue != ajax.NO)
			ajax.doNextFromQueue = ajax.ALL_REQUESTS;

		this.waitingForRetry = 0;

		return responseData;
	};


	/**
	 * Zorgt voor de afhandeling van een mislukte request.
	 */
	ajaxRequest.prototype.receiveFailure = function ajaxRequest_receiveFailure()
	{
		var lastReceiveTime = (new Date()).valueOf();
		var endTime = new Date();
		var timeDiff = 0;

		var errorType = 'ERROR';
		var responseHeaders = '- no response headers -';
		var responseText = '';
		this.waitingForRetry++;

		// In IE 9 zijn status en responseText niet beschikbaar als de request
		// afgebroken is (aborted of timeout). Zodra de status wordt opgevraagd
		// geeft-ie de melding "Could not complete the operation due to error
		// c00c023f". Dit is een permissionprobleem. Niet logisch, maar we moeten
		// het ermee doen.
		try { var httpStatus = this.XMLHttpRequest.status; }
		catch (err) { var httpStatus = 0; }

		if (this.progressStatus == 'timed out')
		{
			errorType = 'TIMEOUT';
			this.waitingForRetry = 0;
		}
		else if (this.progressStatus == 'aborted')
		{
			errorType = 'ABORT';
			this.waitingForRetry = 0;
		}
		else if (httpStatus >= 100 && httpStatus < 600)
		{
			this.waitingForRetry = 0;
			responseHeaders = this.XMLHttpRequest.getAllResponseHeaders();
			try { responseText = this.XMLHttpRequest.responseText; }
			catch(err) { responseText = ''; }
		}
		else
		{
			errorType = 'CONNECTION ERROR';

			// Dan was er een netwerkprobleem en is er geen HTTP errorcode. De meeste
			// browsers geven dan code 0, terwijl IE 12xxx teruggeeft om de aard van
			// het probleem aan te geven.
			// Dit gebeurt doorgaans als de computer wakker wordt uit de slaapstand. Dan
			// mislukt de eerste request. Eventuele vervolgrequests gaan meestal wel goed.
			// Bij wakker worden uit de slaapstand wordt de eerste keep-alive request vaak
			// al verstuurd voordat Windows de netwerkverbinding hersteld heeft, en dan
			// gaat-ie dus fout.
			//
			// IE statuscodes:
			//   12002 = ERROR_INTERNET_TIMEOUT
			//   12007 = ERROR_INTERNET_NAME_NOT_RESOLVED
			//   12023 = ERROR_INTERNET_NO_DIRECT_ACCESS
			//   12029 = ERROR_INTERNET_CANNOT_CONNECT
			//   12030 = ERROR_INTERNET_CONNECTION_ABORTED
			//   12031 = ERROR_INTERNET_CONNECTION_RESET
			// Zie voor een volledig overzicht http://support.microsoft.com/kb/193625

			// Even uitzoeken hoe lang geleden de vorige request gedaan was. Als dat
			// lang geleden was, is de kans groot dat we inderdaad te maken hebben met
			// het wakker-worden-uit-de-slaapstand syndroom.
			for (var logWalk = ajax.log.length-1; logWalk >= 0; logWalk--)
			{
				if (ajax.log[logWalk].what && ajax.log[logWalk].what.match(/^receive #/))
				{
					lastReceiveTime = parseInt(ajax.log[logWalk].time);
					timeDiff = Math.round((endTime.valueOf() - lastReceiveTime) / 1000);
					break;
				}
			}

			if (timeDiff > 3600)
			{
				// 1 uur niks gedaan? Dan zal het netwerk probleem wel door de slaapstand
				// komen. 1 uur = 60 min * 60 sec = 3.600 sec.
				errorType = 'HYBERNATE ERROR';
			}
		}

		var uren = parseInt(timeDiff / 3600);
		var minuten = parseInt((timeDiff - (uren * 3600)) / 60);
		var seconden = (timeDiff - (uren * 3600) - (minuten * 60));
		ajax.log.push({
			'what': errorType+(this.progressStatus == 'running' ? ' '+httpStatus+':' : '')+' #'+this.requestData.volgnummer+(this.waitingForRetry ? ' attempt '+this.waitingForRetry : '')+' (channel '+this.channel+')',
			'time': endTime.valueOf()+(this.progressStatus == 'running' ? ' (request took ' : ' ('+this.progressStatus+' after ')+(endTime - this.startTime)+' ms)',
			'url': this.requestData.url,
			'info': (timeDiff ? 'Last request '+uren+':'+(minuten < 10 ? '0' : '')+minuten+':'+(seconden < 10 ? '0' : '')+seconden+' ago' : '')+(httpStatus >= 12000 ? (timeDiff ? '. ' : '')+'Statuscode '+httpStatus+' - Zie http://support.microsoft.com/kb/193625' : (timeDiff ? '' : '- no info -')),
			'response headers': responseHeaders,
			'data': responseText ? responseText.substring(0, 1024)+(responseText.length > 1024 ? '...' : '') : '- no data - '+(this.waitingForRetry && this.waitingForRetry < ajax.maxRetries ? 'retry in '+ajax.retryDelay+' seconds' : 'no more retries')
		});

		if (this.waitingForRetry && this.waitingForRetry < ajax.maxRetries && this.requestData.method != 'ERROR')
		{
			this.progressStatus = 'loaded';
			var ajaxRequestObject = this;
			setTimeout(function ajaxRequest_timeoutHandler_retry() { return ajaxRequestObject.process(); }, ajax.retryDelay * 1000);
			return null;
		}

		// Bij een verbindingsprobleem wil de request niet lukken. We hebben het
		// nu meerdere keren (ajax.maxRetries) geprobeerd, maar de verbinding bleef
		// problematisch. We zetten de requestopdracht gewoon weer terug in de queue,
		// zonder een retry-tijd in te stellen. De queue wordt pas weer getriggerd
		// als er een nieuwe opdracht geplaatst wordt.
		if (this.waitingForRetry)
		{
			// Wel even zorgen dat de niet queue niet direct weer getriggerd wordt door
			// de error-meldingen die door dit netwerk probleem getriggerd worden. Dan
			// raakt-ie in een oneindige loop.
			ajax.doNextFromQueue = ajax.NORMAL_REQUESTS;
			ajax.putInQueue(this.requestData);
			this.progressStatus = 'requeued';
		}
		else if (ajax.doNextFromQueue != ajax.NO)
		{
			// Dan was het geen netwerkprobleem en kunnen we er redelijkerwijs vanuit
			// gaan dat-ie de foutmeldingen niet allemaal in de queue gaat opstapelen.
			ajax.doNextFromQueue = ajax.ALL_REQUESTS;
		}

		this.waitingForRetry  = 0;

		return null;
	};


	/**
	 * Zorgt ervoor dat de request afgehandeld wordt als er na de
	 * timeout-tijd nog geen antwoord is ontvangen. De request wordt
	 * afgebroken, de callbackfunctie wordt aangeroepen zonder gegevens,
	 * en als er nog opdrachten in de wacht staan, wordt de eerstvolgende
	 * daarvan opgepakt.
	 */
	ajaxRequest.prototype.timeout = function ajaxRequest_timeout()
	{
		this.timer = 0;
		this.progressStatus = 'timed out';
		this.XMLHttpRequest.abort();
		return true;
	};

	/**
	 * Kapt een request af net zoals de timeout functie, maar heeft als invoer
	 * een volgnummer nodig. Als de request nog in de queue zit, wordt-ie
	 * daaruit en wordt de callback functie aangeroepen op dezelfde manier
	 * alsof er een timeout is geweest, dus zonder resultaatgegevens.
	 */
	ajaxRequest.prototype.abort = function ajaxRequest_abort()
	{
		if (this.timer)
			clearTimeout(this.timer);

		this.timer = 0;
		this.progressStatus = 'aborted';
		this.XMLHttpRequest.abort();
		return true;
	};
})();



var ajax =
{
	'NO': 0,
	'NORMAL_REQUESTS': 1,
	'ALL_REQUESTS': 2,
	'isIE': navigator.userAgent && navigator.userAgent.indexOf("MSIE") > 0,

	'support': true,
	'channels': [ ],
	'maxChannels': 2,
	'maxRetries': 3,
	'retryDelay': 1.5,  // seconds
	'queue': [ ],
	'doNextFromQueue': 0,  // ajax.NO
	'volgnummer': 0,
	'log': [ ]
};

(function()
{
	/**
	 * Registreert de callbackfunctie en start de verzending. Als er nog een
	 * oude ajaxrequest loopt, wordt de nieuwe even in de wacht gezet tot-ie
	 * aan de beurt is.
	 *
	 * Parameters:
	 * @param string url         // Het webadres waarheen de request moet worden gedaan
	 * @param string data        // De gegevens die meegestuurd moeten worden
	 * @param function callback  // Reference naar de functie die de respons moet verwerken
	 * @param string method      // POST, GET of ERROR; default GET. ERROR is ook POST, maar wordt niet gelogd.
	 * @param number timeout     // Maximum aantal seconden wachttijd op antwoord van de server; default 4
	 * @param string contentType // Content mime-type voor POST request; default 'application/x-www-form-urlencoded'
	 */
	ajax.send = function ajax_send(requestData, data, callback, method, timeout, contentType, passthruData, volgnummer)
	{
		if (!this.support)
			return false;

		while (this.log.length > 100)
			this.log.shift();

		if (typeof requestData == 'string')
		{
			requestData = { 'url': requestData };
			if (typeof data != 'undefined') requestData.data = data;
			if (typeof callback != 'undefined') requestData.callback = callback;
			if (typeof method != 'undefined') requestData.method = method;
			if (typeof timeout != 'undefined') requestData.timeout = timeout;
			if (typeof contentType != 'undefined') requestData.contentType = contentType;
			if (typeof passthruData != 'undefined') requestData.passthruData = passthruData;
		}
		return this.putInQueue(requestData);
	};

	ajax.putInQueue = function ajax_putInQueue(requestData)
	{
		if (typeof requestData != 'object' || !requestData.url)
			return false;

		var startTime = new Date();
		this.queue.push(requestData);

		if (requestData.volgnummer)
		{
			// Dan wordt er waarschijnlijk een mislukte terug in de queue gezet
			// voor de volgende poging. Hij moet dan wel weer op z'n oude plek in
			// de queue terugkomen, dus even de queue sorteren op volgnummer.
			this.log.push({
				'what': 'Queue #'+requestData.volgnummer,
				'time': startTime.valueOf()+' ('+startTime.toLocaleTimeString()+')',
				'url': requestData.url,
				'data': requestData.method && requestData.method == 'ERROR' ? requestData.data.replace(/^.*?msg=([^&]*)&.*$/, function(ref0, ref1) { return decodeURIComponent(ref1); }) : requestData.data
			});
			this.queue.sort(function(a, b) { return a.volgnummer - b.volgnummer; });
		}
		else
		{
			requestData.volgnummer = ++ajax.volgnummer;
			this.log.push({
				'what': 'Queue #'+requestData.volgnummer,
				'time': startTime.valueOf()+' ('+startTime.toLocaleTimeString()+')',
				'url': requestData.url,
				'data': requestData.method && requestData.method == 'ERROR' ? requestData.data.replace(/^.*?msg=([^&]*)&.*$/, function(ref0, ref1) { return decodeURIComponent(ref1); }) : requestData.data
			});
			if (this.doNextFromQueue == this.NORMAL_REQUESTS && requestData.method == 'ERROR')
			{
				// Dan is er zojuist een netwerkprobleem geweest. En deze foutmelding is dan
				// waarschijnlijk door dat netwerkprobleem getriggerd. Maar bij een netwerkprobleem
				// kan de foutmelding natuurlijk ook niet verzonden worden... Gewoon in de queue
				// stoppen dus. We proberen later wel weer te verzenden.
				;
			}
			else
			{
				this.doNextFromQueue = this.ALL_REQUESTS;
				setTimeout(function ajax_timeoutHandler_doNextFromQueue() { return ajax.nextFromQueue(); }, 0);
			}
		}

		return requestData.volgnummer;
	};

	ajax.nextFromQueue = function ajax_nextFromQueue()
	{
		if (!this.queue.length)
			return false;

		for (var i = 0; i < this.maxChannels; i++)
		{
			if (i >= this.channels.length)
				this.channels.push(new ajaxRequest(i));

			if (this.channels[i].progressStatus == 'ready')
			{
				var requestData = this.queue[0];
				if (this.channels[i].load(requestData))
				{
					// Pas als het laden gelukt is mag-ie echt uit de queue verwijderd worden
					this.queue.shift();
					this.channels[i].process();
					break;
				}
			}
		}

		if (!this.queue.length)
			this.doNextFromQueue = this.NO;

		return i < this.maxChannels;
	};

	/**
	 * Kapt een request af net zoals de timeout functie, maar heeft als invoer
	 * een volgnummer nodig. Als de request nog in de queue zit, wordt-ie
	 * daaruit en wordt de callback functie aangeroepen op dezelfde manier
	 * alsof er een timeout is geweest, dus zonder resultaatgegevens.
	 */
	ajax.abort = function ajax_abort(volgnummer)
	{
		// Kijken of de ajaxtaak al uitgevoerd wordt. Zo ja: annuleren.
		for (var i = 0; i < this.channels.length; i++)
			if (this.channels[i].requestData && this.channels[i].requestData.volgnummer === volgnummer)
				return this.channels[i].abort();

		// Als we hier komen zit de taak waarschijnlijk nog in de queue.
		// Dan kan-ie er gewoon uitgehaald worden.
		i = 0;
		while (i < this.queue.length && this.queue[i].volgnummer !== volgnummer)
			i++;

		if (i >= this.queue.length) // dan is-ie niet gevonden
			return false;

		this.log.push({
			'what': 'ABORT #'+volgnummer+' before sending',
			'time': new Date().valueOf(),
			'url': this.queue[i].url,
			'data': this.queue[i].data
		});

		var requestData = this.queue.splice(i, 1)[0];

		if (requestData.callback)
		{
			if (this.isIE || typeof toerEngineError === 'undefined')
			{
				// Voor IE geen try/catch, omdat-ie dan geen regelnummers meer geeft in de foutmelding
				requestData.callback(null, requestData.passthruData, 'aborted', '');
			}

			else
			{
				try { requestData.callback(null, requestData.passthruData, 'aborted', ''); }
				catch(ex) { toerEngineError(ex); }
			}
		}

		return true;
	};
})();


