/**
 * Client side Ajax implementation
 *
 * Copyright (c) 2007 TOLRA Micro Systems Limited. All rights reserved.
 *
 * THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 *
 * @package fusionLib
 */

/**
 * Core library functions
 */
var fusionLib = new function() {
	var self = this;

	// Find the URL to the library folder
	this.libPath = '';
	var script = document.getElementsByTagName('script').item(0);
	while(script && !this.libPath.length) {
		if(script.src && script.src.indexOf('js/fusionLib.js') != -1)
			this.libPath = script.src.replace(/fusionLib.js$/, '');
		script = script.nextSibling;
	}

	// Load the CSS for the overlay
	document.write('<link type="text/css" href="' + this.libPath + 'overlay.css" rel="stylesheet" media="all" />');

	/**
	 * Events
	 */
	var handlers = [];

	this._findHandler = function(target, eventName, fnHandler) {
		for(i in handlers) {
			if(handlers[i].target == target && handlers[i].eventName == eventName && handlers[i].fnHandler == fnHandler)
				return i;
		}

		return -1;
	};

	// Attach to an event
	this.observe = function(target, eventName, fnHandler) {
		// if given a string look up the object
		if(typeof target == "string") target = document.getElementById(target);

		// Look for an existing handler
		if(self._findHandler(target, eventName, fnHandler) != -1) return;

		// create wrapper event function to fix browser differences
		var fn = function(evt) {
			evt = evt ? evt : (window.event ? window.event : null);
			if(evt) {
				// For IE convert srcElement to target
				if(!evt.target) evt.target = evt.srcElement;

				// if handler returns false don't run default
				if(!fnHandler(evt)) {
					if(evt.stopPropagation) {
						evt.stopPropagation();
						evt.preventDefault();
					}
					else if(evt.cancelBubble) {
						evt.cancelBubble = true;
						evt.returnValue = false;
					}
					return false;
				}
			}
		};

		// Remember handler
		handlers.push({target: target, eventName: eventName, fnHandler: fnHandler, fn: fn});

		// Attach event to object
		if(target.addEventListener)
			target.addEventListener(eventName, fn, false);
		else {
			target.attachEvent("on" + eventName, fn);

			// For IE add unloader to remove all handlers
			if(!window._fusionLibEventUnload) {
				window._fusionLibEventUnload = true;
				self._removeAllEvents = function() {
					for(i in handlers) {
						handlers[i].target.detachEvent("on" + handlers[i].eventName, handlers[i].fn);
					}
				}

				var _oldOnUnload = window.onunload;
				if(typeof window.onunload != 'function') window.onunload = self._removeAllEvents;
				else window.onunload = function() { self._removeAllEvents(); _oldOnUnload(); };
			}
		}
	};

	// Detach event
	this.stopObserving = function(target, eventName, fnHandler) {
		// if given a string look up the object
		if(typeof target == "string") target = document.getElementById(target);

		var idx = self._findHandler(target, eventName, fnHandler);
		if(idx == -1) return;

		// Dettach event from object
		if(target.removeEventListener)
			target.removeEventListener(eventName, handlers[idx].fn, false);
		else
			target.detachEvent("on" + eventName, handlers[idx].fn);

		delete handlers[idx];
	};

	/**
	 * Overlay
	 */
	var xPosOverlay, yPosOverlay;
	var _overlayInit = false;
	var overlayObj;				// Overlay div
	var overlayCtntObj;			// Overlay content div
	var overlayBdyObj;			// Overlay body div
	var overlayCaptionObj;		// Overlay caption div
	var overlayCaption = false;	// Overlay click to close or caption

	// Setup the page to support the overlay
	this._initOverlay = function() {
		if(_overlayInit) return;
		_overlayInit = true;

		// Add divs to body for overlay
		overlayObj = document.createElement("div");
		overlayObj.id = 'fusionLibOverlay';
		overlayObj.style.filter = 'alpha(opacity=80)';
		document.getElementsByTagName('body')[0].appendChild(overlayObj);

		overlayCtntObj = document.createElement("div");
		overlayCtntObj.id = 'fusionLibOvBox';
		document.getElementsByTagName('body')[0].appendChild(overlayCtntObj);

		overlayBdyObj = document.createElement("div");
		overlayBdyObj.id = 'fusionLibOvBdy';
		overlayCtntObj.appendChild(overlayBdyObj);

		overlayCaptionObj = document.createElement("div");
		overlayCaptionObj.id = 'fusionLibOvCap';
		overlayCtntObj.appendChild(overlayCaptionObj);
	};

	/**
	 * Common overlay enable/disable code
	 * Fix IE6 body and HTML, requires height: 100% and overflow: hidden
	 * Also requires scroll position reset
	 */
	this._doOverlay = function(active){

		// Before IE7
		if(!window.XMLHttpRequest) {
			height = active ? '100%' : 'auto';
			overflow = active ? 'hidden' : 'auto';

			// Scroll to top of page
			if(active) {
				xPosOverlay = document.documentElement.scrollLeft;
				yPosOverlay = document.documentElement.scrollTop;
				window.scrollTo(0, 0);
			}

			bodyObj = document.getElementsByTagName('body')[0];
			bodyObj.style.height = height;
			bodyObj.style.overflow = overflow;

			htmlObj = document.getElementsByTagName('html')[0];
			htmlObj.style.height = height;
			htmlObj.style.overflow = overflow;

			// Restore scroll position
			if(!active)
				window.scrollTo(xPosOverlay, yPosOverlay);
		}

		// Hide selects to stop them showing through
		var visibility = active ? 'hidden' : 'visible';
		selects = document.getElementsByTagName('select');
		for(i = 0; i < selects.length; i++) {
			selects[i].style.visibility = visibility;
		}
	};

	// Activate the overlay
	this.overlayActivate = function(click, w, h, caption) {
		overlayCaption = click;
		self._initOverlay();
		self._doOverlay(true);

		// Remove any content
		if(overlayBdyObj.firstChild)
			overlayBdyObj.removeChild(overlayBdyObj.firstChild);
		if(overlayCaptionObj.firstChild)
			overlayCaptionObj.removeChild(overlayCaptionObj.firstChild);

		// Set busy animation
		overlayCtntObj.style.backgroundImage = 'url(' + self.libPath + 'loading.gif)';

		overlayCaptionObj.style.display = 'none';
		overlayCaptionObj.style.backgroundImage = 'none';

		if(click) {
			self.observe(overlayObj, "click", self.overlayDeactivate);
			self.observe(overlayCtntObj, "click", self.overlayDeactivate);
			overlayCaptionObj.style.display = 'block';
			overlayCaptionObj.style.backgroundImage = 'url(' + self.libPath + 'close.gif)';
		}

		if(caption) {
			overlayCaption = true;
			overlayCaptionObj.style.display = 'block';
			overlayCaptionObj.innerHTML = caption;
		}

		// Size and make visible
		this.overlaySize(w || 250, h || 150);
		overlayObj.style.display = 'block';
		overlayCtntObj.style.display = 'block';
	};

	// Activate the overlay
	this.overlayDeactivate = function() {
		self._initOverlay();
		self._doOverlay(false);
		overlayObj.style.display = 'none';
		overlayCtntObj.style.display = 'none';

		// Remove event handlers
		self.stopObserving(overlayObj, "click", self.overlayDeactivate);
		self.stopObserving(overlayCtntObj, "click", self.overlayDeactivate);
	};

	// Set the size of the overlay content window
	this.overlaySize = function(w, h) {
		overlayBdyObj.style.height = h.toString() + 'px';
		if(overlayCaption) h += 26;
		overlayCtntObj.style.width = w.toString() + 'px';
		overlayCtntObj.style.marginLeft = (-Math.floor(w * .5)).toString() + 'px';
		overlayCtntObj.style.height = h.toString() + 'px';
		overlayCtntObj.style.marginTop = (-Math.floor(h * .5)).toString() + 'px';
	};

	// Write content to overlay window
	this.overlayContent = function(content) {
		// Remove busy animation and set content
		overlayCtntObj.style.backgroundImage = 'none';
		overlayBdyObj.innerHTML = content;
	};

	// Get the content window object
	this.overlayObject = function() {
		// Remove busy animation and return the overlay content object
		overlayCtntObj.style.backgroundImage = 'none';
		return overlayBdyObj;
	};
};

/**
 * Ajax engine.
 */
function flAjaxEngine(uri, properties) {
	var timer;

	// Get the transport object
	this._getTransport = function() {
		var req = null;

		// If native XMLHttpRequest object
		if(window.XMLHttpRequest)
			req = new XMLHttpRequest();
		// If ActiveX
		else if(window.ActiveXObject) {
			var progIDs = ["Msxml2.XMLHTTP", "Microsoft.XMLHTTP"];
			for(var i = 0; i<progIDs.length && req == null; i++) {
				try {
					req = new ActiveXObject(progIDs[i]);
				}
				catch(e) {
					req = null;
				}
			}
		}

		return req;
	};

	var method = properties.method || "POST";
	method = method.toUpperCase();

	// Save the success and failure handlers
	this.onSuccess = properties.onSuccess || null;
	this.onFailure = properties.onFailure || function() {
		// default error handler
		alert('An error has occurred during an AJAX transaction.\n\n' +
			  'Code: ' + this.req.status + '\n' +
			  'Description: ' + this.req.statusText);
	}
	this.onDone = properties.onDone || null;
	this.update = properties.update || null;
	this.onTimeout = properties.onTimeout || null;

	// Intialise
	var self = this;
	this.req = this._getTransport();
	if(!this.req) return false;
	this.req.onreadystatechange = function() {
		if(self.req.readyState == 4) {
			if(timer) clearTimeout(timer);
			if(self.onDone) self.onDone();

			if(self.req.status >= 200 && self.req.status <= 299) {
				var data = self.req.responseText.parseJSON();
				if(self.update) document.getElementById(self.update).innerHTML = data;
				else if(self.onSuccess) self.onSuccess(data);
			}
			else
				self.onFailure();
		}
	};
	this.req.open(method, uri, true);
	this.req.setRequestHeader('X-Requested-With', 'XMLHttpRequest');

	var data = '';

	if(method == 'POST') {
		this.req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
		if(this.req.overrideMimeType) this.req.setRequestHeader('Connection', 'close');

		// Build the post data JSON encode any objects
		if(typeof properties.postData == 'object') {
			var v;
			for(key in properties.postData) {
				v = properties.postData[key];
				switch(typeof v) {
					case 'object':
						// Serialize only if not null and have a toJSONString function
						if(v) {
							if(typeof v.toJSONString === 'function') {
								data = data + '&' + key + '=' + escape(v.toJSONString());
							}
							else {
								data = data + '&' + key + '=' + escape(objectToJSONString(v));
							}
						}
						break;

					case 'string':
					case 'number':
					case 'boolean':
						data = data + '&' + key + '=' + escape(v.toString());
						break;
				}
			}
			data = data.slice(1);
		}
		else
			data = properties.postData;
	}

	// Start timeout if any
	if(properties.timeout) {
		timer = setTimeout(function() {
			self.abort();
			if(self.onTimeout) self.onTimeout();
		}, properties.timeout);
	}

	// Send the request
	if(properties.onStart) properties.onStart();
	this.req.send(data);

	// Abort the current transaction
	this.abort = function() {
		if(self.req) {
			if(timer) clearTimeout(timer);
			self.req.onreadystatechange = function() {};
			self.req.abort();
			self.req = null;
		}
	};

	return true;
};

/**
 * JSON support
 * See http://www.json.org/json.js
 */
if (!Object.prototype.toJSONString) {

	Array.prototype.toJSONString = function() {
		var a = [],		// Array of encoded members
				v,		// Current value
				l = this.length;

		// Loop through array
		for(var i = 0; i < l; i++) {
			v = this[i];
			switch(typeof v) {
				case 'object':
					// Serialize only if not null and have a toJSONString function
					if(v) {
						if(typeof v.toJSONString === 'function') {
							a.push(v.toJSONString());
						}
					} else {
						a.push('null');
					}
					break;

				case 'string':
				case 'number':
				case 'boolean':
					a.push(v.toJSONString());
					break;
			}
		}

		return '[' + a.join(',') + ']';
	};

	Boolean.prototype.toJSONString = function() {
		return String(this);
	};

	Number.prototype.toJSONString = function () {
		return isFinite(this) ? String(this) : 'null';
	};

	// 	Should be 'Object.prototype.toJSONString = function () {}' but breaks for..in
	function objectToJSONString(o) {
		var a = [],		// Array of encoded members
				v;		// Current value.

		// Iterate through all of the keys in the object, ignoring the proto chain.
		for(var k in o) {
			if(o.hasOwnProperty(k)) {
				v = o[k];
				switch(typeof v) {
					case 'object':
						// Serialize only if not null and have a toJSONString function
						if(v) {
							if(typeof v.toJSONString === 'function') {
								a.push(k.toJSONString() + ':' + v.toJSONString());
							}
							else {
								a.push(k.toJSONString() + ':' + objectToJSONString(v));
							}
						} else {
							a.push(k.toJSONString() + ':null');
						}
						break;

					case 'string':
					case 'number':
					case 'boolean':
						a.push(k.toJSONString() + ':' + v.toJSONString());
						break;
				}
			}
		}

		return '{' + a.join(',') + '}';
	};

	Date.prototype.toJSONString = function () {
		// Ultimately, this method will be equivalent to the date.toISOString method.
		function f(n) {
			// Format integers to have at least two digits.
			return n < 10 ? '0' + n : n;
		}

		return '"' + this.getFullYear() + '-' + f(this.getMonth() + 1) + '-' + f(this.getDate()) + 'T' + f(this.getHours()) + ':' + f(this.getMinutes()) + ':' + f(this.getSeconds()) + '"';
	};

	(function (s) {
		// Character substitutions.
		var m = { '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '\\': '\\\\' };

		s.parseJSON = function() {
			// Strip out characters and see if just JSON characters are left, if so it's safe to run
			if(/^\s*|[,:{}\[\]0-9\.\-+Ee \n\r\t]+$/.test(this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, ''))) {
				return eval('(' + this + ')');
			}

			// If the text is not JSON parseable, then throw a SyntaxError.
			throw new SyntaxError('parseJSON');
		};

		s.toJSONString = function () {
			if (/["\\\x00-\x1f]/.test(this)) {
				return '"' + this.replace(/([\x00-\x1f\\"])/g, function (a, b) {
					var c = m[b];
					return c ? c : b;
				}) + '"';
			}
			return '"' + this + '"';
		};
	})(String.prototype);
}