/**
*	Ajax Class
*
*	Makes asynchronous remote calls to the server without refreshing the client
*	page.
*
*/

function Ajax(name) {
	this._name = name;
	this._http = this._generateXMLHttpRequest();
	if (this._http == null) {
		throw new Error("unable to create an XmlHttpRequest object, unsupported browser");
	}
}

Ajax.prototype = {
	serverPage: 'ajax/serve.php',
	
	/**
	 * @var {Function} set this for custom error handling...
	 */
	onAjaxError: null,

	_encodeCall: function(methodName, parameters) {
		var callString = "call[methodName]=" + methodName;

		// parameters must be in an array to be properly parsed, so put them in one...
		switch (typeof parameters) {
			case "string":
				parameters = new String(parameters);
				parameters = parameters.split(",");
				break;
			case "boolean":
			case "number":
				parameters = [parameters];
				break;
		}

		// encode parameters into the call string...
		if ( typeof(parameters) == "object" && parameters.length > 0 ) {
			callString += this._encodeParams(parameters, '&call[parameters]');
			return callString;
		} else {
			throw new Error('unrecognised parameters');
		}
	},

	_encodeParams: function(parms, prefix) {
		var enc = '';
		if (typeof(parms) == 'object') {
			for (var i in parms) {
				enc += ( this._encodeParams(parms[i], prefix + '[' + i + ']') );
			}
		} else {
			enc = prefix + '=' + parms;
		}
		return enc;
	},

	_generateXMLHttpRequest: function() {
		switch (true) {
			case (window.XMLHttpRequest != undefined): // decent browsers...
				return new XMLHttpRequest();
			case (window.ActiveXObject != undefined): // IE6 and before...
				var msxmls = new Array ('Msxml2.XMLHTTP.5.0',	'Msxml2.XMLHTTP.4.0',	'Msxml2.XMLHTTP.3.0',	'Msxml2.XMLHTTP',	'Microsoft.XMLHTTP');
				for (var i = 0; i < msxmls.length; i ++) {
					try {
						var xmlHttpRequest = new ActiveXObject(msxmls[i]);
						break;
					} catch (e) { /* fail silently and try the next version... */ }
				}
				return xmlHttpRequest;
		}
		return null;
	},

	/**
	 * _httpStateChange - checks the xmlHttpResponse for whether we've got a result back yet
	 */
	_httpStateChange: function() {
		if (this._http.readyState == 4) { // check for complete...
			this.busy = false;
			var result;

			// get the result...
			try {
				result = this._xmlParseResponse(this._http.responseXML);

				// return child nodes of result to client callback...
				this.callback(result);
			} catch (e) { // result is an error, so alert it or call the client event handler should one be defined...
				if (this.onAjaxError == null) {
					alert(this._http.responseText);
				} else {
					this.onAjaxError(e);
				}
			}

		}
	},

	_xmlError: function(e) {
		throw new Error("error processing server response: " + e);
	},

	/**
	 * _xmlGetResult: retrieves the <result> node from the response xml
	 * @scope private
	 * @param {Object} XMLDom object, obtained from XMLHttpRequest.ResponseXML
	 * @return {Object} XMLElement object
	 */
	_xmlGetResult: function(response) {
		switch (true) {
			case (window.XPathEvaluator != undefined): // DOM compliant...
				var xpath = new XPathEvaluator();
				var res = xpath.evaluate("/result", response, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
				return res.singleNodeValue;
			case (response.documentElement != undefined): // Msxml2.XMLDOCUMENT of some flavour...
				var nodes = response.documentElement.selectNodes('/result');
				return nodes[0];
			default:
				throw new Error("Unable to parse result from server, browser doesn't support XPath.")
		}
	},

	/**
	 * parses the XMLHttpRequest.ResponseXML into a javascript object
	 * @scope private
	 * @param {Object} XMLDom object, obtained from XMLHttpRequest.ResponseXML
	 * @return {Object} server response as a javascript object
	 */
	_xmlParseResponse: function(response) {
		try {
			var result = this._xmlGetResult(response);
			return this._xmlToJsArray(result)[0];
		} catch (err) {
			throw new Error("Unable to parse server response: " + err.message);
		}
	},
	

	/**
	* transforms the <result> node into a javascript object
	* @param {Object} XMLNode result node
	* @return {Object} server response
	*/
	_xmlToJsArray: function(resultElement) {
		var xx, currentNode, jsArray, nodeParsedValue, nodeKey, resultChildNodes;
		jsArray = new Array();
		// get child nodes for the result element...
		//		resultChildNodes = resultElement.getElements();

		// iterate through them, adding to our array...
		for(xx = 0; xx < resultElement.childNodes.length; xx ++) {
			currentNode = resultElement.childNodes[xx];
			switch(currentNode.getAttribute("type")) {
				case "error":
					alert("Error: " + currentNode.getText());
					if (currentNode.firstChild != null && currentNode.firstChild.data != null) {
						nodeParsedValue = new Error(currentNode.firstChild.data);
					} else {
						nodeParsedValue = new Error('');
					}
					break;
				case "array":
					if (currentNode.childNodes.length > 0) {
						nodeParsedValue = this._xmlToJsArray(currentNode);
					} else {
						nodeParsedValue = new Array();
					}
					break;
				case "boolean":
					if (currentNode.firstChild != null && currentNode.firstChild.data != null) {
						nodeParsedValue = Boolean(Number(currentNode.firstChild.data));
					} else {
						throw new Error('invalid server response: empty boolean variable');
					}
					break;
				case "integer":
				case "double":
					if (currentNode.firstChild != null && currentNode.firstChild.data != null) {
						nodeParsedValue = Number(currentNode.firstChild.data);
					} else {
						throw new Error('invalid server response: empty numeric variable');
					}
					break;
				case "string":
					if (currentNode.firstChild != null && currentNode.firstChild.data != null) {
						nodeParsedValue = String(currentNode.firstChild.data);
					} else {
						nodeParsedValue = '';
					}
					break;
				case "date":
					if (currentNode.firstChild != null && currentNode.firstChild.data != null) {
						nodeParsedValue = new Date(Number(currentNode.firstChild.data));
					} else {
						throw new Error('invalid server response: empty date variable');
					}
					break;
				default:
					nodeParsedValue = null;
					break;
			}

			// get the key for entry into the array - if one isn't provided by the xml,
			// then it should be the length of the array (this will make it unique)...
			if (attributeExists(currentNode, 'key')) {
				nodeKey = currentNode.getAttribute('key');
			} else {
				nodeKey = jsArray.length;
			}

			jsArray[nodeKey] = nodeParsedValue;
		}
		return jsArray;
	},

	/**
	 * executes a server call
	 * @param {Function} callback function, can be null if a callback isn't required
	 * @param {String} method name to call on the server
	 * @param {Array} array of parameters for the method call on the server
	 */
	execute: function(callback, methodName, parameters) {
		if (this.busy) {
			alert('request already in progress');
		} else {
			if (callback != null) {
				// presume this is a data-returning call, best practice here is to
				// send via a get request...
				var encodedCall = this._encodeCall(methodName, parameters);
				// replace new lines (\n) with %0d
				encodedCall = encodedCall.replace(/\n/g, '%0d');
				this._http.open('get', this.serverPage + "?" + encodedCall);
				this.callback = callback;
				this.busy = true;
				var self = this;
				this._http.onreadystatechange = function() {self._httpStateChange();}
				this._http.send(null);
			} else {
				// presume this is a non-data-returning call, best practice here is to
				// send via a post request...
				this._http.open('post', this.serverPage);
				this._http.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
				this._http.send(this._encodeCall(methodName, parameters));
			}
		}
	}
}