/** This class allows the user to load data files from the server using the browser's
 * native XML loader class.
 */
Base.XMLLoader = Base.Comp.inherits({

	/** This property indicates whether the instance is currently loading data
	 * from the server.
	 */
	isLoading:	false,

	/** This property contains the native XML loader object when data is being
	 * loaded or null when the loader isn't active.
	 */
	xmlObject:	null,

	/** This property contains the loader's timer when data is being retrieved.
	 * The timer will be used to force the loader object to timeout. When no
	 * data is being loaded, the property contains the null value.
	 */
	timer:		null,


	/** The constructor initialises the component itself and sets default values
	 * for the URI, method and timeout to use.
	 * @param	uri	a string containing the URI from which the data is
	 *				to be fetched
	 * @param	usePost	a boolean indicating whether the loader should use
	 *				the HTTP POST method (true) or GET method
	 * @param	timeout	an optionnal integer containing the amount of seconds
	 *				before the loader times out; this defaults to
	 *				20 seconds if the parameter is missing
	 */
	constructor: function (uri, usePost, timeout) {
		this.base();

		this.addEvent('Aborted');
		this.addEvent('Loaded');
		this.addEvent('NetworkError');
		this.addEvent('ServerError');
		this.addEvent('Timeout');
		this.addEvent('Unsupported');

		this.addSlot('load');
		this.addSlot('abort');
		this.addSlot('timedOut');

		this.uri	= uri;
		this.usePost	= usePost;
		this.timeout	= timeout ? timeout : 20;
	},

	/** The destructor starts by aborting the loader's operation, then calls
	 * the standard component destructor.
	 */
	destroy: function () {
		this.abort();
		this.base();
	},


	/** This method allows the user to change the parameters such as the URI, the
	 * method to be used and the timeout value. It will only be able to do so if
	 * the loader isn't operating.
	 * @param	uri	the new data URI (ignored if null)
	 * @param	usePost	the new method to use (ignored if null)
	 * @param	timeout	the new value for the loader's timeout (ignored if
	 *				null)
	 * @returns		true if the values were set, false if they weren't
	 */
	changeParameters: function (uri, usePost, timeout) {
		if (this.isLoading) {
			return false;
		}

		this.uri	= uri ? uri : this.uri;
		this.usePost	= (typeof usePost == 'boolean') ? usePost : this.usePost;
		this.timeout	= timeout ? timeout : 20;

		return true;
	},


	/** This method loads the data using the object's URI and method using the
	 * specified parameters.
	 * @param parameters	a hashtable associating to each of the parameters'
	 *				name a value that is either a string or an
	 *				array
	 * @returns		true if the loader was successfully started
	 */
	load: function (parameters) {
		// Make sure we're not already loading
		if (this.isLoading) {
			return false;
		}
		this.isLoading = true;
		Base.Log.write('Base.XMLLoader.load(): ' + (this.usePost ? 'POST' : 'GET') + ' ' + this.uri);

		// Initialise the native loader object
		var obj = Base.XMLLoader.makeNativeInstance();
		if (!obj) {
			this.isLoading = false;
			Base.Log.write('Base.XMLLoader.load(): XMLHttpRequest unsupported');
			this.onUnsupported();
			return false;
		}

		// Generate the parameters string and the URI
		var uri = this.uri, ps;
		if (parameters) {
			ps = Base.XMLLoader.encodeParameters(parameters); 
			if (!this.usePost) {
				uri += '?' + ps;
			}
		} else {
			ps = "";
		}

/*
		// Safari and IE are stupid
		if (Base.Browser.get().safari || Base.Browser.get().IE) {
			if (this.usePost || ps == '') {
				uri += '?stoopid_browsa=' + (new Date()).getTime();
			} else {
				uri += '&stoopid_browsa=' + (new Date()).getTime();
			}
		}
*/

		// Get ready to send the request
		try {
			obj.open(this.usePost ? "POST" : "GET", uri, true);
		} catch (e) {
			// We got a permission problem
			this.isLoading = false;
			Base.Log.write('Base.XMLLoader.load(): XMLHttpRequest.open() failed, possible permission problem');
			Base.Log.write('Base.XMLLoader.load(): exception was ' + (e.toString ? e.toString() : e));
			this.onUnsupported();
			return false;
		}
		if (this.usePost) {
			obj.setRequestHeader("Method", "POST " + uri + " HTTP/1.1");
			obj.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
		}
		// Safari is still stupid
		if (Base.Browser.get().safari) {
			obj.setRequestHeader('If-Modified-Since', 'Wed, 15 Nov 1995 00:00:00 GMT');
			obj.setRequestHeader("Cache-Control", "no-cache");
		}

		// Initialise the timer
		this.timer = new Base.Timer(this.timeout * 1000);
		this.timer.bindEvent('Tick', 'timedOut', this);

		// Prepare the callback
		var _id = this._cid;
		obj.onreadystatechange = function () {
			if (typeof Base != 'undefined') {
				Base.Comp.get(_id).loaderEvent();
			}
		};

		// Start the timer and send the request
		this.xmlObject = obj;
		this.timer.start();
		obj.send((parameters && this.usePost) ? ps : null);
		return true;
	},

	/** This method cancels the current operation if there is one.
	 */
	abort: function () {
		if (!this.isLoading) {
			return;
		}

		this.doAbort();
		this.onAborted();
	},


	/** This method is used as a callback for the native loader object.
	 */
	loaderEvent: function () {
		if (!this.isLoading) {
			return;
		}

		var obj = this.xmlObject;
		try {
			if (obj.readyState != 4) {
				return;
			}

			if (obj.status != 200) {
				this.doAbort();
				Base.Log.write('Base.XMLLoader.load(): server returned ' + obj.status);
				this.onServerError(obj.status);
				return;
			}
		} catch	(e) {
			this.doAbort();
			Base.Log.write('Base.XMLLoader.load(): exception ' + (e.toString ? e.toString() : e) + ' while receiving');
			this.onNetworkError(e);
			return;
		}

		var _r = obj.responseText;
		this.doAbort();
		this.onLoaded(_r);
	},

	/** This method is responsible for deleting existing objects such as
	 * the native loader and the timer component.
	 */
	doAbort: function () {
		this.isLoading = false;

		if (this.xmlObject) {
			this.xmlObject.abort();
			delete this.xmlObject;
			this.xmlObject = null;
		}

		if (this.timer) {
			this.timer.destroy();
			this.timer = null;
		}
	},

	/** This method is called by the timer when the time imparted for loading the
	 * data expires.
	 */
	timedOut: function () {
		if (!this.isLoading) {
			return;
		}
		Base.Log.write('Base.XMLLoader: timeout :(');
		this.doAbort();
		this.onTimeout();
	}

}, {

	/** This class property stores the code to be used to generate native
	 * loader objects. If it is empty, the code hasn't been determined yet,
	 * and the "E" value indicates an error.
	 */
	objCode:	"",

	/** This class method generates native loader objects. If the objCode
	 * class property has been initialised, it uses its content to generate
	 * the new object. Otherwise it explores available options to try and
	 * generate a native loader object.
	 * @returns	the native loader instance on success, null on failure
	 */
	makeNativeInstance: function () {
		var obj;

		if (this.objCode == "E") {
			return null;
		}

		if (this.objCode != "") {
			eval('obj=' + this.objCode);
		} else {
			try {
				obj = new ActiveXObject("Msxml2.XMLHTTP");
				this.objCode = 'new ActiveXObject("Msxml2.XMLHTTP")';
			} catch (e) {
				try {
					obj = new ActiveXObject("Microsoft.XMLHTTP");
					this.objCode = 'new ActiveXObject("Microsoft.XMLHTTP")';
				} catch (oc) {
					obj = null;
				}
			}
			if (!obj && typeof XMLHttpRequest != "undefined") {
				obj = new XMLHttpRequest();
				this.objCode = "new XMLHttpRequest()";
			}
		}

		if (!obj && typeof obj == "object" && this.objCode != "E") {
			this.objCode = "E";
			obj = null;
		}

		return obj;
	},

	/** This class method encodes a hashtable containing request parameters into
	 * a string usable when loading the data.
	 * @param parameters	the hashtable containing the parameters to be encoded.
	 * @returns		the encoded string
	 */
	encodeParameters: function (parameters) {
		var mk = new Array(), pl = parameters.keys();
		for (var i in pl) {
			var _k = pl[i], _pn = encodeURIComponent(_k), _v = parameters.get(_k);

			if (_v instanceof Array) {
				for (var j in _v) {
					mk.push(_pn + '[]=' + encodeURIComponent(_v[j]));
				}
			} else {
				mk.push(_pn + '=' + encodeURIComponent(_v));
			}
		}

		return mk.join('&');
	}

});