/**
 * HTTPRequest
 * Facade à XMLHttpRequest
 *  – Implémente XMLHttpRequest-v1 (W3C) avec quelques autres facilités
 * 
 * Ce module tend à être compatible et reproduire exactement le même 
 * comportement sur tous les navigateurs implémentant de leur manière 
 * XMLHttpRequest (IE 6+, FireFox 2+, Safari). Il est activement testé sur 
 * Safari 2 et FireFox 3.
 * 
 * @version	0.1
 * @author	Julien PLÉE <julien.plee@aims-interactive.com>
 * @copyright	AIMS Interactive - 2008
 *
 * @version	0.2 (24-05-2009)
 * @author  Julien PLÉE <julienplee@gmail.com>
 * @description	Ajout du support des méthodes POST, PUT et DELETE. 
 * • Ajout du support des envois précoces d'entêtes HTTP (Ils sont stockés 
 * dans l'objet et soumis juste après l'ouverture d'une connexion).
 * • Prise en charge de la première méthode de Internet Explorer pour 
 * construire un objet XMLHTTP.
 * 
 * @version	0.3 (20-06-2009)
 * @author	Julien PLÉE <julienplee@gmail.com>
 * @description	Ajout du support JSON (systémétiquement décodé au chargement 
 * complet).
 *
 * @version	0.4 (22-06-2009)
 * @author	Julien PLÉE <julienplee@gmail.com>
 * @description	Ajout du support du type MIME application/javascript. Le 
 * javascript embarqué est alors automatiquement exécuté et les objets JSON ne 
 * sont décodés que lorsque le type MIME est application/json.
 * • Ajout d'une extention de journalisation des messages de débuggage, 
 * récupérables en surchargeant la méthode debugCallback.
 * • Correction d'un bug avec IE qui tournait en boucle lors de la tentative
 * d'envoi des entêtes.
 * 
 * @version 0.5 (27-06-2009)
 * @author	Julien PLÉE <julienplee@gmail.com>
 * @description	Renommage de l'événement onLoaded en onHeadersReceived.
 * • Renommage de l'événement onLoading en onOpened.
 * • Reformattage des tabulations.
 * • Appel de la méthode abort() lors de l'appel à open() quand le client n'est 
 * pas dans l'état UNSENT.
 * • Suppression de l'événement ontimeout() (probablement conflictuel avec 
 * IE5.5)
 * 
 * @version	0.6 (24-07-2009)
 * @author	Julien PLÉE <julienplee@gmail.com>
 * @description	Ajout de la capacité à traiter et envoyer un formulaire.
 * Prise en charge de la méthode (par défaut GET), de l'action (par défaut 
 * l'adresse de la fenêtre contenant le client HTTP) et de la compilation et 
 * l'émission des données. L'encodage des données du formulaire est un encodage 
 * URL, mais aucune information de ce genre n'est fournie au serveur et 
 * l'attribut du formulaire correspondant n'est pas traité. 
 * 
 * @version 0.7 (25-07-2009)
 * @author Julien PLÉE <julienplee@gmail.com>
 * @description Modifications au niveau de l'envoi de formulaires pour corriger 
 * la gestion des composants INPUT de type CHECKBOX et RADIO, et les composants 
 * SELECT.
 * Les composants INPUT de type FILE sont filtrés. S'ils contiennent une valeur,
 * l'envoi du formulaire est annulé puisque ces fichiers ne sont pas 
 * transmissibles via les composants XMLHttpResquest. S'ils ne contiennent pas 
 * de valeur, ils sont alors simplement ignorés.
 * 
 * @version 0.8 (02-09-2009)
 * @author Julien PLÉE <julienplee@gmail.com>
 * @description Ajout d'une méthode d'encodage des données à envoyer sous la 
 * forme x-www-form-urlencoded.
 * Ajout d'un encodage automatique dans la méthodes post() et put() pour 
 * transformer un objet en données à transporter.
 * 
 * @todo Si la synchronicité n'est pas établie, FF et IE ne mettront pas à jour 
 * les états de la connexion. Cela provoque alors un bug dans les fonctions 
 * comme setRequestHeader qui tournent en boucle. Il faudra alors reproduire le 
 * comportement attendu dans ces cas. De plus, les événements liés à l'état de 
 * la connexion ne seront pas considérés.
 */
 
// Mise à disposition de l'objet window.XMLHttpRequest sur tous les navigateurs.
if (typeof (window.XMLHttpRequest) == 'undefined'
	&& typeof (window.ActiveXObject) != 'undefined')
{
	try {
		var obj = new ActiveXObject ('Msxml2.XMLHTTP');
		window.XMLHttpRequest = function () 
		{
			return new ActiveXObject ('Msxml2.XMLHTTP');
		};
	}
	catch (e)
	{
		try {
			var obj = new ActiveXObject ('Microsoft.XMLHTTP');
			window.XMLHttpRequest = function () 
			{
				return new ActiveXObject ('Microsoft.XMLHTTP');
			};
		}
		catch (e)
		{
			; // Nothing to do. XMLHttpRequest can not be supported.
		}
	}
}

// Mise à disposition d'un objet HTTPRequest sur-implémentant XMLHttpRequest.
// Nota: Cet objet n'existera pas si XMLHttpRequest est indisponible.
if (typeof (window.XMLHttpRequest) != 'undefined')
{
	function HTTPRequest (debugCallback)
	{
		this.debugCallback = debugCallback;
		this.debug('constructor ('+debugCallback+')', "Initialisation de l'objet HTTPRequest.");
		this.handler = new XMLHttpRequest ();
		
		this._keepHandlerEventsAttached();
		this._updateProperties ();
	}
	HTTPRequest.prototype = {
		// Constantes [Ready State]
		// W3C definition
		UNSENT				: 0,
		OPENED				: 1,
		HEADERS_RECEIVED	: 2,
		LOADING				: 3,
		DONE				: 4,
		SECURITY_ERR		: 18,
		NETWORK_ERR			: 101,
		ABORT_ERR			: 102,
		
		// Properties
		constructor		: null,
		channel			: null,
		responseBody	: null,
		responseXML		: null,
		responseText	: null,
		responseJson	: null,
		status			: null,
		statusText		: null,
		readyState		: null,
		multipart		: null,
		timeout			: null,
		// Personal properties
		async			: null,
		proxy			: null,
		requestHeaders  : [],
		// Methods
		about				   : function () {
			var aboutMessage = "HTTPRequest – An easy to use XMLHttpRequest wrapper.\n"
				+ "Author :		Julien PLÉE <julienplee@gmail.com>\n"
				+ "Original work author : Julien PLÉE <julien.plee@aims-interactive.com>\n"
				+ "Original work copyright :	2008, AIMS Interactive <www.aims-interactive.com>\n"
				+ "Version :	0.8 (2009-09-02)"
				;
			alert (aboutMessage);
			},
		abort					: function () { 
			this.debug('abort ()', 'Abording the request.');
			var r = this.handler.abort (); 
			this._onreadystatechange(); 
			return r; 
			},
		getAllResponseHeaders	: function () { 
			this.debug('getAllResponseHeaders ()', 'Getting all response headers.');
			return this.handler.getAllResponseHeaders (); 
			},
		getResponseHeader		: function (headerName) { 
			this.debug('getResponseHeader ('+headerName+')', 'Getting the response headers of name '+headerName+'.');
			return this.handler.getResponseHeader (headerName); 
			},
		openRequest				: HTTPRequest.prototype.open,
		open					: function (request_method, request_url, request_async, request_user, request_password) {
			this.debug('open ('+request_method+', '+request_url+', '+request_async+', '+request_user+', '+request_password+')', "Open a new request and send buffered header data.");
			// Si request_async est indéfini dans ce mapping, il sera considéré 
			// comme valant 'false' alors que la recommandation veut qu'il valle 'true'.
			if ((typeof(request_async) == 'undefined') || (request_async == null))
				request_async = this.async;
			if ((typeof(request_async) == 'undefined') || (request_async == null))
			{
				request_async = true;
				this.async = request_async;
			}
			// Dans le cas de l'utilisation d'une passerelle, on intègre l'url cible dans une requête à la passerelle.
			this.url = this.proxy && (this.proxy.length > 0) ? this.proxy + this.urlencode(request_url) : request_url;
			// Réinitialisation, si besoin, du client HTTP
			if (this.readyState != this.UNSENT)
				this.abort();
			
			var r = this.handler.open (request_method, this.url, request_async, request_user, request_password);
			
			if ((request_async == false) && (this.readyState != this.OPENED))
				this.readyState = this.OPENED;
			for (var i=0;i<this.requestHeaders.length; ++i)
				this.setRequestHeader(this.requestHeaders[i][0], this.requestHeaders[i][1]);
			this.requestHeaders = [];
			return r; 
			},
		send					: function (request_body) { 
			this.debug('send ('+request_body+')', 'Send body of the request.');
			var r = this.handler.send (request_body);
			if (this.async == false) this._onreadystatechange();
			return r; 
			},
		sendAsBinary			: function (request_body) { 
			this.debug('sendAsBinary ('+request_body+')', 'Send binary body of the request.');
			var r = typeof (this.handler.sendAsBinary) == 'function' ? this.handler.sendAsBinary (request_body) : this.handler.send (request_body);
			if (this.async == false) this._onreadystatechange();
			return r; 
			},
		setRequestHeader		: function (header_name, header_value) { 
			if (this.readyState == this.OPENED) {
//				alert('setRequestHeader('+header_name+', '+header_value+') => Set (status:'+this.readyState+', '+this.handler.readyState+')');
				this.debug('setRequestHeader ('+header_name+', '+header_value+')', 'Setting header data during request initialisation time.');
				return this.handler.setRequestHeader (header_name, header_value); 
			} else {
//				alert('setRequestHeader('+header_name+', '+header_value+') => Store (status:'+this.readyState+', '+this.handler.readyState+')');
				this.debug('setRequestHeader ('+header_name+', '+header_value+')', 'Buffering header data for the next request.');
				return this.requestHeaders.push ([header_name, header_value]);
			};
			},
		overrideMimeType		: function (mimetype) { 
			this.debug('overrideMimeType ('+mimetype+')', 'Forcing the value of the returned MimeType during request time.');
			try {
				return this.handler.overrideMimeType (mimetype);
			} catch (e) {
				return false;
			}
			},
		// Personal methods
		urlencode				: function (url) { 
			this.debug('urlencode ('+url+')', 'Encoding a string into a formatted URL.');
			return escape(url).replace(/\+/g,'%2B').replace(/%20/g,'+').replace(/\*/g,'%2A').replace(/\//g,'%2F').replace(/@/g,'%40'); 
			},
		urldecode				: function (url) { 
			this.debug('urldecode ('+url+')', 'Decoding a URL formatted string.');
			return unescape(url.replace(/\+/g,'%20').replace(/%2B/g,'+').replace(/%2A/g,'*').replace(/%2F/g,'/').replace(/%40/g,'@')); 
			},
		get						: function (request_url, request_async, request_user, request_password) { 
			this.debug('get ('+request_url+', '+request_async+', '+request_user+', '+request_password+')', 'Getting a ressource.');
			this.open ('GET', request_url, request_async, request_user, request_password); 
			return this.send (null); 
			},
		post					: function (request_url, request_body, request_async, request_user, request_password) {
			if (typeof(request_body) == 'object')
				request_body = this.encodeData(request_body);
			this.debug('post ('+request_url+', '+request_body+', '+request_async+', '+request_user+', '+request_password+')', 'Posting data to a ressource.');
			this.open ('POST', request_url, request_async, request_user, request_password);
			this.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
			this.setRequestHeader("Content-length", request_body.length);
			return this.send (request_body); 
			},
		put					 : function (request_url, request_body, request_async, request_user, request_password) { 
			if (typeof(request_body) == 'object')
				request_body = this.encodeData(request_body);
			this.debug('put ('+request_url+', '+request_body+', '+request_async+', '+request_user+', '+request_password+')', 'Putting a new ressource.');
			this.open ('PUT', request_url, request_async, request_user, request_password); 
			this.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
			this.setRequestHeader("Content-length", request_body.length);
			return this.send (request_body); 
			},
		del					 : function (request_url, request_async, request_user, request_password) { 
			this.debug('del ('+request_url+', '+request_async+', '+request_user+', '+request_password+')', 'Demande de suppression d\'une ressource.');
			this.open ('DELETE', request_url, request_async, request_user, request_password); 
			return this.send (null); 
			},
		submit					: function (form, request_async, request_user, request_password) {
			// Compilation des données du formulaire
			var data="";
			var elt = null;
			
			for (var i=0, n=0; i<form.elements.length; ++i)
			{
				elt = form.elements[i];
				//alert(elt.nodeName);
				if (elt.nodeName == 'INPUT')
				{
					switch (elt.getAttribute('type').toLowerCase())
					{
						case 'checkbox': 
						case 'radio':
							if (!elt.checked) continue;
							if (elt.value.length == 0)
								elt.value = "on";
							break;
						case 'file':
							if (elt.value.length == 0)
							{
								continue;
							}
							else
							{
								alert('WARNING: HTTPRequest can not send files. The form submission will abort. \nYou may still send this formular if you empty values from each file field.');
								return false;
								break;
							}
					}
				}
				if (elt.nodeName == 'SELECT')
				{
					var opt = null;
					for (var j=0; j < elt.options.length; ++j)
					{
						opt = elt.options[j];
						if (!opt.selected)
							continue;

						if (n > 0) data+="&";
						data+=encodeURIComponent(elt.name)+"="+encodeURIComponent(opt.value);
						++n;
					}
					continue;
				}
				
				if (n > 0) data+="&";
				data+=encodeURIComponent(elt.name)+"="+encodeURIComponent(elt.value);
				++n;
			}
			
			var method = form.method == '' ? 'GET' : form.method.toUpperCase();
			var url = form.action == '' ? window.location.href : form.action;
			
			if (method == 'GET')
			{
				var m;
				if (m = /\?(.*?)(&?)$/.exec(url))
				{
					if (m[2].length > 0)
					{
						url += data;
					}
					else if (m[1].length > 0)
					{
						url += '&'+data;
					}
					else
					{
						url += data;
					}
				}
				else
				{
					url += '?' + data;
				}
				
				return this.get(url, request_async, request_user, request_password);
			}
			else
			{
				this.open (method, url, request_async, request_user, request_password);
				this.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
				this.setRequestHeader("Content-length", data.length);
				return this.send (data); 
			}
			
			},
		encodeData : function (dataObject)
			{
				var dataString = '';
				var curData = '';
				
				var dataCount = 0;
				for (var label in dataObject)
				{
					curData = '';
					switch(typeof(dataObject[label]))
					{
						case 'string':
						case 'number':
							curData = encodeURIComponent(label) + '=' + dataObject[label];
							break;
						case 'object':
							if (typeof(dataObject[label].length) == 'number')
							{
								var iMax = dataObject[label].length;
								for (var i = 0; i < iMax; ++i)
									curData += (i>0?'&':'') + encodeURIComponent(label) + '=' + encodeURIComponent(dataObject[label][i]);
							}
							break;
					}
					if (curData.length > 0)
					{
						dataString += (dataCount > 0 ? '&' : '') + curData;
						++dataCount;
					}
				}
				
				return dataString;
			},
		debug					: function (funcName, logString) {
			if (typeof(this.debugCallback) == 'function')
			{
				this.debugCallback('HTTPRequest.'+funcName, 
						logString 
						/*+ ((typeof(this.handler) != 'undefined') 
								? "\nonreadystatechange handler: " 
								+ this.handler.onreadystatechange
								: "")*/);
			}
			},
		JsonDecode		  	: function () {
			this.debug('JsonDecode ()', 'Decoding JSON data.');
			var r;
			if (this.handler.responseText.length > 0)
				eval ("r="+this.handler.responseText);
			else
				r = null;
			return r;
			},
		// Events handler
		onreadystatechange		: null,
		onTimeout				: null,
		// Personal events
		onUninitialized			: null,		// When a new client is created and ready to be used.
		onOpened				: null,		// When a new connection is established with the server.
		onHeadersReceived		: null,		// When response headers has been received from the server. Can't be fired when in synchronous mode.
		onInteractive			: null,		// When the body response is beeing received.
		onCompleted				: null,		// When the response has been completely received.
		_onreadystatechange		: function (e)
		{
			// La réinitialisation en cas de plusieurs requêtes vaut pour tout IE.
			this._keepHandlerEventsAttached ();
			
			this.debug('_onreadystatechange ('+e+')', 'Managing the ready state change dispatcher.');
			this._updateProperties ();
			// Lance l'événement onreadystatechange et termine si la valeur de 
			// retour est fausse.
			if ((typeof (this.onreadystatechange) == 'function') 
				&& (this.onreadystatechange(e) === false))
				return false;
			// Lance l'événement relevant de readyState.
			switch (this.readyState)
			{
				case this.UNSENT			: if (typeof (this.onUninitialized)	== 'function') return this.onUninitialized (e);	break;
				case this.OPENED			: if (typeof (this.onOpened)		== 'function') return this.onOpened (e);		break;
				case this.HEADERS_RECEIVED	: if (typeof (this.onHeadersReceived)	== 'function') return this.onHeadersReceived (e);	break;
				case this.LOADING			: if (typeof (this.onInteractive)	== 'function') return this.onInteractive (e);	break;
				case this.DONE				: if (typeof (this.onCompleted)		== 'function') return this.onCompleted (e);		break;
			}
			return true;
		},
		_updateProperties : function () {
			this.debug('_updateProperties ()', 'Updating the XMLHttpRequest mapped properties. ReadySate: '+this.handler.readyState);
			try { this.constructor	= this.handler.constructor;	} catch (e) { this.constructor	= null; };
			try { this.channel		= this.handler.status;		} catch (e) { this.channel		= null; };
			try { this.status		= this.handler.status;		} catch (e) { this.status		= null; };
			try { this.statusText	= this.handler.statusText;	} catch (e) { this.statusText	= null; };
			try { this.readyState	= this.handler.readyState;	} catch (e) { this.readyState	= null; };
			try { this.multipart	= this.handler.multipart;	} catch (e) { this.multipart	= null; };
			try { this.timeout		= this.handler.timeout;		} catch (e) { this.timeout		= null; };
			try { this.responseBody	= this.handler.responseBody;} catch (e) { this.responseBody	= null; };
			try { this.responseXML	= this.handler.responseXML;	} catch (e) { this.responseXML	= null; };
			try { this.responseText	= this.handler.responseText;} catch (e) { this.responseText	= null; };
			this.responseJson = undefined;
			if (this.readyState == this.DONE)
			{
				if (this.getResponseHeader('Content-Type').match(/(application|text)\/json/i)) {
					try { var d=null; eval('d='+this.responseText); this.responseJson = d; } catch(e) { this.responseJson = null; };
				}
				if (this.getResponseHeader('Content-Type').match(/(application|text)\/(java|j|ecma)script/i)) {
					try { eval(this.responseText); } catch(e) {};
				}
			}
		},
		_onTimeout		: function (e) {
			this.debug('_onreadystatechange ('+e+')', 'Operation timed out.');
			if (typeof (this.onTimeout) == 'function') return this.onTimeout (e);
		},
		_keepHandlerEventsAttached	: function () {
			var current = this;
			try { 
				if ((typeof (this.handler.onreadystatechange) == 'undefined') 
					|| (this.handler.onreadystatechange == null))
					this.handler.onreadystatechange = function (e) { return current._onreadystatechange(e); }; 
			} catch (e) {
				try { this.handler.onreadystatechange = function (e) { return current._onreadystatechange(e); }; } catch (e) {}; 
			};
			// Suppression de l'événement ontimeout()
			/*
			try {
				if ((typeof (this.handler.ontimeout) == 'undefined') 
						|| (this.handler.ontimeout == null))
					this.handler.ontimeout = function (e) { return current._onTimeout(e); }; 
			} catch (e) {
				try { this.handler.ontimeout = function (e) { return current._onTimeout(e); }; } catch (e) {};
			};
			*/
		}
	};
};