// XMLHttpRequest.js Copyright (C) 2008 Sergey Ilinsky (http://www.ilinsky.com)
//
// This work is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.

// This work is distributed in the hope that it will be useful,
// but without any warranty; without even the implied warranty of
// merchantability or fitness for a particular purpose. See the
// GNU Lesser General Public License for more details.

// You should have received a copy of the GNU Lesser General Public License
// along with this library; if not, write to the Free Software Foundation, Inc.,
// 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

(function() {

	// Save reference to earlier defined object implementation (if any)
	var oXMLHttpRequest = window.XMLHttpRequest;

	// Define on browser type
	var bGecko = !!window.controllers,
		bIE = window.document.all && !window.opera;

	// Constructor
	function cXMLHttpRequest() {
		this._object = oXMLHttpRequest ? new oXMLHttpRequest : new window.ActiveXObject("Microsoft.XMLHTTP");
		this._listeners = [];
	};

	// BUGFIX: Firefox with Firebug installed would break pages if not executed
	if (bGecko && oXMLHttpRequest.wrapped)
		cXMLHttpRequest.wrapped = oXMLHttpRequest.wrapped;

	// Constants
	cXMLHttpRequest.UNSENT = 0;
	cXMLHttpRequest.OPENED = 1;
	cXMLHttpRequest.HEADERS_RECEIVED = 2;
	cXMLHttpRequest.LOADING = 3;
	cXMLHttpRequest.DONE = 4;

	// Public Properties
	cXMLHttpRequest.prototype.readyState = cXMLHttpRequest.UNSENT;
	cXMLHttpRequest.prototype.responseText = '';
	cXMLHttpRequest.prototype.responseXML = null;
	cXMLHttpRequest.prototype.status = 0;
	cXMLHttpRequest.prototype.statusText = '';

	// Instance-level Events Handlers
	cXMLHttpRequest.prototype.onreadystatechange = null;

	// Class-level Events Handlers
	cXMLHttpRequest.onreadystatechange = null;
	cXMLHttpRequest.onopen = null;
	cXMLHttpRequest.onsend = null;
	cXMLHttpRequest.onabort = null;

	// Public Methods
	cXMLHttpRequest.prototype.open = function(sMethod, sUrl, bAsync, sUser, sPassword) {

		// When bAsync parameter value is ommited, use true as default
		if (arguments.length < 3)
			bAsync = true;

		// Save async parameter for fixing Gecko bug with missing readystatechange in synchronous requests
		this._async = bAsync;

		// Set the onreadystatechange handler
		var oRequest = this,
			nState = this.readyState;

		// BUGFIX: IE - memory leak on page unload (inter-page leak)
		if (bIE) {
			var fOnUnload = function() {
				if (oRequest._object.readyState != cXMLHttpRequest.DONE) {
					fCleanTransport(oRequest);
					// Safe to abort here since onreadystatechange handler removed
					oRequest.abort();
				}
			};
			if (bAsync)
				window.attachEvent("onunload", fOnUnload);
		}

		this._object.onreadystatechange = function() {
			if (bGecko && !bAsync)
				return;

			// Synchronize state
			oRequest.readyState = oRequest._object.readyState;

			//
			fSynchronizeValues(oRequest);

			// BUGFIX: Firefox fires unneccesary DONE when aborting
			if (oRequest._aborted) {
				// Reset readyState to UNSENT
				oRequest.readyState = cXMLHttpRequest.UNSENT;

				// Return now
				return;
			}

			if (oRequest.readyState == cXMLHttpRequest.DONE) {
				//
				fCleanTransport(oRequest);
				// Uncomment this block if you need a fix for IE cache
				/*
				// BUGFIX: IE - cache issue
				if (!oRequest._object.getResponseHeader("Date")) {
				// Save object to cache
				oRequest._cached	= oRequest._object;

					// Instantiate a new transport object
				cXMLHttpRequest.call(oRequest);

					// Re-send request
				oRequest._object.open(sMethod, sUrl, bAsync, sUser, sPassword);
				oRequest._object.setRequestHeader("If-Modified-Since", oRequest._cached.getResponseHeader("Last-Modified") || new window.Date(0));
				// Copy headers set
				if (oRequest._headers)
				for (var sHeader in oRequest._headers)
				if (typeof oRequest._headers[sHeader] == "string")	// Some frameworks prototype objects with functions
				oRequest._object.setRequestHeader(sHeader, oRequest._headers[sHeader]);

					oRequest._object.onreadystatechange	= function() {
				// Synchronize state
				oRequest.readyState		= oRequest._object.readyState;

						if (oRequest._aborted) {
				//
				oRequest.readyState	= cXMLHttpRequest.UNSENT;

							// Return
				return;
				}

						if (oRequest.readyState == cXMLHttpRequest.DONE) {
				// Clean Object
				fCleanTransport(oRequest);

							// get cached request
				if (oRequest.status == 304)
				oRequest._object	= oRequest._cached;

							//
				delete oRequest._cached;

							//
				fSynchronizeValues(oRequest);

							//
				fReadyStateChange(oRequest);

							// BUGFIX: IE - memory leak in interrupted
				if (bIE && bAsync)
				window.detachEvent("onunload", fOnUnload);
				}
				};
				oRequest._object.send(null);

					// Return now - wait untill re-sent request is finished
				return;
				};
				*/
				// BUGFIX: IE - memory leak in interrupted
				if (bIE && bAsync)
					window.detachEvent("onunload", fOnUnload);
			}

			// BUGFIX: Some browsers (Internet Explorer, Gecko) fire OPEN readystate twice
			if (nState != oRequest.readyState)
				fReadyStateChange(oRequest);

			nState = oRequest.readyState;
		};

		// Add method sniffer
		if (cXMLHttpRequest.onopen)
			cXMLHttpRequest.onopen.apply(this, arguments);

		if (arguments.length > 4)
			this._object.open(sMethod, sUrl, bAsync, sUser, sPassword);
		else
			if (arguments.length > 3)
			this._object.open(sMethod, sUrl, bAsync, sUser);
		else
			this._object.open(sMethod, sUrl, bAsync);

		// BUGFIX: Gecko - missing readystatechange calls in synchronous requests
		if (!bAsync && bGecko) {
			this.readyState = cXMLHttpRequest.OPENED;

			fReadyStateChange(this);
		}
	};
	cXMLHttpRequest.prototype.send = function(vData) {
		// Add method sniffer
		if (cXMLHttpRequest.onsend)
			cXMLHttpRequest.onsend.apply(this, arguments);

		// BUGFIX: Safari - fails sending documents created/modified dynamically, so an explicit serialization required
		// BUGFIX: IE - rewrites any custom mime-type to "text/xml" in case an XMLNode is sent
		// BUGFIX: Gecko - fails sending Element (this is up to the implementation either to standard)
		if (vData && vData.nodeType) {
			vData = window.XMLSerializer ? new window.XMLSerializer().serializeToString(vData) : vData.xml;
			if (!this._headers["Content-Type"])
				this._object.setRequestHeader("Content-Type", "application/xml");
		}

		this._object.send(vData);

		// BUGFIX: Gecko - missing readystatechange calls in synchronous requests
		if (bGecko && !this._async) {
			this.readyState = cXMLHttpRequest.OPENED;

			// Synchronize state
			fSynchronizeValues(this);

			// Simulate missing states
			while (this.readyState < cXMLHttpRequest.DONE) {
				this.readyState++;
				fReadyStateChange(this);
				// Check if we are aborted
				if (this._aborted)
					return;
			}
		}
	};
	cXMLHttpRequest.prototype.abort = function() {
		// Add method sniffer
		if (cXMLHttpRequest.onabort)
			cXMLHttpRequest.onabort.apply(this, arguments);

		// BUGFIX: Gecko - unneccesary DONE when aborting
		if (this.readyState > cXMLHttpRequest.UNSENT)
			this._aborted = true;

		this._object.abort();

		// BUGFIX: IE - memory leak
		fCleanTransport(this);
	};
	cXMLHttpRequest.prototype.getAllResponseHeaders = function() {
		return this._object.getAllResponseHeaders();
	};
	cXMLHttpRequest.prototype.getResponseHeader = function(sName) {
		return this._object.getResponseHeader(sName);
	};
	cXMLHttpRequest.prototype.setRequestHeader = function(sName, sValue) {
		// BUGFIX: IE - cache issue
		if (!this._headers)
			this._headers = {};
		this._headers[sName] = sValue;

		return this._object.setRequestHeader(sName, sValue);
	};

	// EventTarget interface implementation
	cXMLHttpRequest.prototype.addEventListener = function(sName, fHandler, bUseCapture) {
		for (var nIndex = 0, oListener; oListener = this._listeners[nIndex]; nIndex++)
			if (oListener[0] == sName && oListener[1] == fHandler && oListener[2] == bUseCapture)
			return;
		// Add listener
		this._listeners.push([sName, fHandler, bUseCapture]);
	};

	cXMLHttpRequest.prototype.removeEventListener = function(sName, fHandler, bUseCapture) {
		for (var nIndex = 0, oListener; oListener = this._listeners[nIndex]; nIndex++)
			if (oListener[0] == sName && oListener[1] == fHandler && oListener[2] == bUseCapture)
			break;
		// Remove listener
		if (oListener)
			this._listeners.splice(nIndex, 1);
	};

	cXMLHttpRequest.prototype.dispatchEvent = function(oEvent) {
		var oEvent = {
			'type': oEvent.type,
			'target': this,
			'currentTarget': this,
			'eventPhase': 2,
			'bubbles': oEvent.bubbles,
			'cancelable': oEvent.cancelable,
			'timeStamp': oEvent.timeStamp,
			'stopPropagation': function() { }, // There is no flow
			'preventDefault': function() { }, // There is no default action
			'initEvent': function() { } // Original event object should be inited
		};

		// Execute onreadystatechange
		if (oEvent.type == "readystatechange" && this.onreadystatechange)
			(this.onreadystatechange.handleEvent || this.onreadystatechange).apply(this, [oEvent]);

		// Execute listeners
		for (var nIndex = 0, oListener; oListener = this._listeners[nIndex]; nIndex++)
			if (oListener[0] == oEvent.type && !oListener[2])
			(oListener[1].handleEvent || oListener[1]).apply(this, [oEvent]);
	};

	//
	cXMLHttpRequest.prototype.toString = function() {
		return '[' + "object" + ' ' + "XMLHttpRequest" + ']';
	};

	cXMLHttpRequest.toString = function() {
		return '[' + "XMLHttpRequest" + ']';
	};

	// Helper function
	function fReadyStateChange(oRequest) {
		// Sniffing code
		if (cXMLHttpRequest.onreadystatechange)
			cXMLHttpRequest.onreadystatechange.apply(oRequest);

		// Fake event
		oRequest.dispatchEvent({
			'type': "readystatechange",
			'bubbles': false,
			'cancelable': false,
			'timeStamp': new Date + 0
		});
	};

	function fGetDocument(oRequest) {
		var oDocument = oRequest.responseXML;
		// Try parsing responseText
		if (bIE && oDocument && !oDocument.documentElement && oRequest.getResponseHeader("Content-Type").match(/[^\/]+\/[^\+]+\+xml/)) {
			oDocument = new window.ActiveXObject("Microsoft.XMLDOM");
			oDocument.loadXML(oRequest.responseText);
		}
		// Check if there is no error in document
		if (oDocument)
			if ((bIE && oDocument.parseError != 0) || (oDocument.documentElement && oDocument.documentElement.tagName == "parsererror"))
			return null;
		return oDocument;
	};

	function fSynchronizeValues(oRequest) {
		try { oRequest.responseText = oRequest._object.responseText; } catch (e) { }
		try { oRequest.responseXML = fGetDocument(oRequest._object); } catch (e) { }
		try { oRequest.status = oRequest._object.status; } catch (e) { }
		try { oRequest.statusText = oRequest._object.statusText; } catch (e) { }
	};

	function fCleanTransport(oRequest) {
		// BUGFIX: IE - memory leak (on-page leak)
		oRequest._object.onreadystatechange = new window.Function;

		// Delete private properties
		delete oRequest._headers;
	};

	// Internet Explorer 5.0 (missing apply)
	if (!window.Function.prototype.apply) {
		window.Function.prototype.apply = function(oRequest, oArguments) {
			if (!oArguments)
				oArguments = [];
			oRequest.__func = this;
			oRequest.__func(oArguments[0], oArguments[1], oArguments[2], oArguments[3], oArguments[4]);
			delete oRequest.__func;
		};
	};

	// Register new object with window
	window.XMLHttpRequest = cXMLHttpRequest;
})();