//*
// AJAX Object
// By Francois Dupras
// created: 2006-09-10

// 2007-10-09
//	Improved the queue request with a wait list to proceed the request in the same order they came in
// 2007-09-07
//  Changed the request and replace function to take the second argument from request as a replace if a valid ID and a callback if valid function (removed the last parameter as the ID)
// 2007-03-26
//  fixed a minor bug in the IECacheProblem using post
// 2007-03-22
//  added the async property to determine if the calls should be made async or sync (not freeze the execution or freeze it until response is reseive)
// 2007-02-09
//  BUG FIXED - call queue not working properly
//  BUG FIXED - error reporting doesn't always work
//  BUG FIXED - XML validation before callback, XML value will now be false if incorrect
// 2007-01-23
//  BUG FOUND - call queue not working properly
//  BUG FOUND - error reporting doesn't always work
//  BUG FOUND - XML validation before callback, XML value will now be false if incorrect
// 2007-01-19
//  XML validation before callback, XML value will now be false if incorrect
//  errorCallback now returns error code as second parameter
//  added getLastError method [0] error code [1] error message
// 2007-01-12
//  fix IE caching problem and none return of onreadystatechange
// 2007-01-10
//  added function for global loading and loaded (setLoadingFunction and setLoadedFunction)
//  added error callback

function doNothing(){}

function ajaxObject (maxSumiltaniousConnectionParam) {
	// each XMLHttpRequest will be stored in that array
	this.ajaxRequestArray = new Array();
	this.requestWaitList = new Array();
	
	// should the requests be async
	// set this value with setAsync(true or false)
	this.async = true;
	
	// set the maximum simultatius connection, 10 is the default one if none was provided
	this.maxSumiltaniousConnection = maxSumiltaniousConnectionParam;
	if(maxSumiltaniousConnectionParam == undefined) {
		this.maxSumiltaniousConnection = 10; // hardcoded maximum connection
	}

	// this is the method to call to replace content with response from the server
	// *url is the url of the server, if you use the GET method you can put the arguments in (page.php?arg1=value1&arg2=val2) or use the arg param
	// *id is the id of the element where you want the new content, received from the server, to be inserting (replacing existing content)
	// *method can only be "GET" or "POST"
	// *arg are the url formated argument to append to the url if GET is the method and sent as header if POST is the method selected
	// *loadingFunc is the function called as soon as the remote call is made (this function should provide your user with visual aid that the application is doing something
	//    NEW if you are using the new function setLoadingFunction this parameter becomes the parameter sent to the loading callback function
	// *loadedFunc is the function called when a response from the server was received, this function is called before the callback function
	//    NEW if you are using the new function setLoadedFunction this parameter becomes the parameter sent to the loaded callback function
	this.replace = function (url, id, method, arg, loadingFunc, loadedFunc) {
		var validate = document.getElementById(id);
		if(validate != null) {
			if(typeof(validate.innerHTML) != undefined) {
				this.request(url, id, method, arg, loadingFunc, loadedFunc);
			}
		}
		else {
			this.callError(1, "replace ID is not valid", true);
		}
	}
	
	// this is the method to call to make a server call
	// *url is the url of the server, if you use the GET method you can put the arguments in (page.php?arg1=value1&arg2=val2) or use the arg param
	// *callback is the function to be called with the returned information from the server or the id of the control to be replaced
	// *method can only be "GET" or "POST"
	// *arg are the url formated argument to append to the url if GET is the method and sent as header if POST is the method selected
	// *loadingFunc is the function called as soon as the remote call is made (this function should provide your user with visual aid that the application is doing something
	//    NEW if you are using the new function setLoadingFunction this parameter becomes the parameter sent to the loading callback function
	// *loadedFunc is the function called when a response from the server was received, this function is called before the callback function
	//    NEW if you are using the new function setLoadedFunction this parameter becomes the parameter sent to the loaded callback function
	// *replaceId **UPDATE** this parameter should not be used anymore, the callback should be the name of the Id to be replaced
	this.request = function (url, callback, method, arg, loadingFunc, loadedFunc, replaceId) {
		if(replaceId != undefined) {
			callback = replaceId;
			this.callError(-1, "Parameter replaceId should not be used anymore, use the callback parameter (2) with the Id to be replaced instead.", true);
		}
		// validation of the inputs
		method = method.toLowerCase();
		if(url == "" || url == undefined) { alert("URL function must be provided"); return false; }
		else if(callback == "" || callback == undefined && callback != null) { alert("Callback function must be provided"); return false; }
		else if(method != "post" && method != "get") { alert("Method function must be provided and be POST or GET only"); return false; }

		// assign inner variable with the provided value		
		var innerCallback = callback;
		var innerLoadingFunc = loadingFunc;
		var innerLoadedFunc = loadedFunc;
		// get an empty or new XMLHttpRequest object
		var httpRequest = this.getHttpRequest();

		// continue if connection is clear
		if(httpRequest && (httpRequest.readyState == 4 || httpRequest.readyState == 0)) {

			// an exception can be thrown by the open and send method
			try {
				// call the loading function
				if(typeof(innerLoadingFunc) == "function") {
					innerLoadingFunc();
				}
				else if(typeof(innerLoadingFunc) == "object" && typeof(innerLoadingFunc.loading) == "function") {
					innerLoadingFunc.loading(innerLoadingFunc.param);
				}
				else if(typeof(outterLoadingFunc) == "function") {
					outterLoadingFunc(innerLoadingFunc);
				}
				// open and send the request
				if(method == "get") {
					arg+="&IECacheProblem="+IECacheFix_getTime();
					var urlComplete = url;
					if(arg != undefined) {
						urlComplete += '?'+arg;
					}
					httpRequest.open("get", urlComplete, this.async);
					//use the internal handle function (needs to be located here because IE sucks and need it to be between open and send)
					var readystatechange = getHandleStateChange(this)
					httpRequest.onreadystatechange = readystatechange;
					httpRequest.send(' ');
					// the readystatechange is never called if in sync mode, so call it when returned from the server
					if(!this.async)
						readystatechange();
				}
				else {
					httpRequest.open("post", url, this.async);
					httpRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset="+this.encoding);
					//use the internal handle function (needs to be located here because IE sucks and need it to be between open and send)
					var readystatechange = getHandleStateChange(this)
					httpRequest.onreadystatechange = readystatechange;
					if(arg=="" || arg == undefined) {
						arg = "IECacheProblem="+IECacheFix_getTime();
					}
					httpRequest.send(arg);
					// the readystatechange is never called if in sync mode, so call it when returned from the server
					if(!this.async)
						readystatechange();
				}
			}
			catch(e) {
				// the only reson I know to get here is if you try to open a connection to a diferent host than the one you are currently on
				this.callError(2, "An exception occurred in the script. Error name: " + e.name + ". Error message: " + e.message, true);
			}
		}
		// if the connection is not clear, try again in a little while
		else {
			// add this request to the wait list
			this.requestWaitList.push(new Array(url, callback, method, arg, loadingFunc, loadedFunc));
		}
		

		// generic handle function
		function getHandleStateChange(thisAjax) {
			function handleStateChange () {
				if(httpRequest.readyState == 4) {
					// status == 200 when the connection went well
					if(httpRequest.status == 200) {
						if(typeof(innerCallback) == "function") {
							// call the callback function with the return text and XML (if any)
							xmlResponse = httpRequest.responseXML;
							if(!xmlResponse || !xmlResponse.documentElement) {
								//IE & Opera error or invalid xml
								thisAjax.callError(5, 'AJAX Object reports: no XML returned or IE and Opera error', false);
								xmlResponse = false;
							}
							if(xmlResponse.documentElement && xmlResponse.documentElement.nodeName == 'parseerror') {
								//firefox error or invalid xml
								thisAjax.callError(6, 'AJAX Object reports: no XML returned or Firefox and Opera error', false);
								xmlResponse = false;
							}

							// call the loaded function, the reason why this is in the if AND in the else is that if this function 
							// takes to much time to return and another ajax call is made with the same object a exception can occur
							if(typeof(innerLoadedFunc) == "function") { innerLoadedFunc(); }
							else if(typeof(innerLoadedFunc) == "object" && typeof(innerLoadedFunc.loaded) == "function") {
								innerLoadedFunc.loaded(innerLoadedFunc.param);
							}
							else if(typeof(innerLoadingFunc) == "object" && typeof(innerLoadingFunc.loaded) == "function") {
								innerLoadingFunc.loaded(innerLoadingFunc.param);
							}
							else if(typeof(outterLoadedFunc) == "function") { outterLoadedFunc(innerLoadedFunc); }


							// first param is the true, complete response from (body, no headers) the server returned,
							// second param is the xml returned or false if invalid xml 
							var text = httpRequest.responseText;
							var xml = xmlResponse;
							innerCallback(httpRequest.responseText, xmlResponse);
						}
						else {
							var text = httpRequest.responseText;
							
							// call the loaded function, the reason why this is in the if AND in the else is that if this function 
							// takes to much time to return and another ajax call is made with the same object a exception can occur
							if(typeof(innerLoadedFunc) == "function") { innerLoadedFunc(); }
							else if(typeof(innerLoadedFunc) == "object" && typeof(innerLoadedFunc.loaded) == "function") {
								innerLoadedFunc.loaded(innerLoadedFunc.param);
							}
							else if(typeof(innerLoadingFunc) == "object" && typeof(innerLoadingFunc.loaded) == "function") {
								innerLoadingFunc.loaded(innerLoadingFunc.param);
							}
							else if(typeof(outterLoadedFunc) == "function") { outterLoadedFunc(innerLoadedFunc); }

							// this code is only executed with to replace an object.innerHTML with the responseText
							var r = document.getElementById(innerCallback);
							if(r) {
								r.innerHTML = text;
							}
						}
					}
					else {
						// status is not 200, display the status to the user (404: not found, 403: acces denied, 500: internal error)
						// TODO: setup a callback function to handle this event
						thisAjax.callError(3, "Error connecting; Sever status: "+httpRequest.status+"\n"+httpRequest.responseText, true);
					}
					if(thisAjax.requestWaitList.length > 0) {
						arg = thisAjax.requestWaitList.shift();
						thisAjax.request(arg[0],arg[1],arg[2],arg[3],arg[4], arg[5]);
					}
				}
			}
			return handleStateChange;
		}


		return true;
	}
	
	// get a free or newly created XMLHttpRequest object
	this.getHttpRequest = function () {
		var httpRequest = undefined;
		// loop trought the already created object to find one free
		for(i=0; i<this.ajaxRequestArray.length; i++) {
			// if one was found assign it and break the loop
			if(this.ajaxRequestArray[i].readyState == 4 || this.ajaxRequestArray[i].readyState == 0) {
				httpRequest = this.ajaxRequestArray[i];
				break;
			}
		}
		// if no free object was found and that the max simultatious connection is not reach create a new one
		if(httpRequest == undefined && this.ajaxRequestArray.length < this.maxSumiltaniousConnection) {
			// try to create a new object for the mordern browser
			try {
				httpRequest = new XMLHttpRequest();
			} catch(e) {
				// try to create object for older browser
				var xmlHttpVersion = new Array("Msxml2.XMLHTTP.6.0", "Msxml2.XMLHTTP.5.0", "Msxml2.XMLHTTP.4.0", "Msxml2.XMLHTTP.3.0", "Msxml2.XMLHTTP", "Microsoft.XMLHTTP");
				for(var i = 0; i < xmlHttpVersion.length && !httpRequest; i++) {
					try {
						httpRequest = new ActiveXObject(xmlHttpVersion[i]);
					} catch(e) { }
				}
			}
			// get the next index for keeping the object
			var nextIndex = this.ajaxRequestArray.length;
			// assign the new object to the list
			this.ajaxRequestArray[nextIndex] = httpRequest;
		}
		// return the free object, the new object or false if the max is reach and all are busy
		return httpRequest;
	}

	// these variables are used to store the callbacks function that will fire on starting a call, and end when the response is received.
	var outterLoadingFunc = '';
	var outterLoadedFunc = '';
	// call setLoadingFunction and setLoadedFunction to set the function to be called when a starting a call to the server (request or replace) and when the response is received.
	// the callback function should take 1 parameter each, this parameter will be what you pass in the request or replace call.
	this.setLoadingFunction = function (callback) {
		outterLoadingFunc = callback;
	}
	this.setLoadedFunction = function (callback) {
		outterLoadedFunc = callback;
	}
	
	// set the function that will be called if an error occurs
	var errorCallback = '';
	var lastError = new Array(0,'');
	this.setErrorCallback = function (callback) {
		errorCallback = callback;
	}
	// private function to handle error
	this.callError = function (errCode, errMsg, show) {
		// error list:
		//  1 - replace ID is not valid; called from the replace method when the id is not a valid htmlElement
		//  2 - An exception occurred in the script. Error name: {name} Error message: {message}
		//  4 - The argument passed to formToUrlArguments function is not a form ID or a form object
		// Legacy error message
		// -1 - Parameter replaceId should not be used anymore, use the callback parameter (2) with the Id to be replaced instead.; warn the programmer that this parameter should not be used anymore.
		lastError = new Array(errCode, errMsg);
		if(typeof(errorCallback) == "function") {
			errorCallback(errMsg, errCode);
		}
		else if(show == true) {
			alert(errMsg);
		}
	}
	
	// return an array with [0] last error code and [1] last error message
	this.getLastError = function () {
		return lastError;
	}

		
	// function to clear the wait list of all item to a specific URL, if no url provided, remove all wait list item
	this.cleanWaitList = function (url) {
		if(url == undefined || url == "") {
			this.requestWaitList = new Array();
		}
		else {
			function removeFrom(arr, url) {
				var r = new Array;
				for(var i=0; i<arr.length; i++) {
					if(arr[i][0] != url) {
						r.push(arr[i]);
					}
				}
				return r;
			}
			this.requestWaitList = removeFrom(this.requestWaitList, url);
		}
	}

	// text-encoding for the transmision with the server, user function setEncoding to set it
	this.encoding = 'iso-8859-1';
	// text-encoding for the transmision with the server, user function setEncoding to set it
	this.setEncoding = function(newCharset) {
		var old = this.encoding;
		this.encoding = newCharset;
		return old;
	}
	
	this.setAsync = function(newValue) {
		this.async = newValue;
	}
	
	// transform the form elements into url encoded arguments (name=value&name2=value2)
	this.formToUrl = function(formId) { return this.formToUrlArguments(formId); }
	this.formToUrlArgument = function(formId) { return this.formToUrlArguments(formId); }
	this.formToUrlArguments = function(formId) {
		var form = false;
		// you can pass ether the object or the id of the form
		if(typeof(formId) == "object") {
			form = formId;
		}
		else {
			form=document.getElementById(formId);
		}

		// if form is an object
		if(form != null && typeof(form) == "object" && form.tagName.toLowerCase() == "form") {
			// r is the return string... yes, I know, this name could be more descriptive.
			var r = "";
			// loop trought all of the form elements
			var nbFormElements = form.elements.length;
			for(var iFormElements = 0; iFormElements < nbFormElements; iFormElements++) {
				var formElement = form.elements[iFormElements];
				// will not include disabled or unamed element (like a submit button <input type="submit" value="Send"/>)
				if(formElement.name != "" && formElement.disabled == false) {
					// text, hidden, password, textarea and single select (dropdown) are all passed the same way, basic text
					if(formElement.type == "text" || formElement.type == "hidden" || formElement.type == "password" || formElement.type == "textarea" || formElement.type == "select-one") {
						r+= encodeURIComponent(formElement.name)+"="+encodeURIComponent(formElement.value)+"&";
					}
					// checkbox are send but the value is only sent if the checkbox is checked
					else if (formElement.type == "checkbox") {
						r+= encodeURIComponent(formElement.name)+"=";
						if (formElement.checked == true) {
							r+= encodeURIComponent(formElement.value)+"&";
						}
						r+= "&"
					}
					// only the selected radio button and value is send
					else if (formElement.type == "radio" && formElement.checked) {
						r+= encodeURIComponent(formElement.name)+"=";
						r+= encodeURIComponent(formElement.value)+"&";
					}
					// for a case like the multiple select (listbox) an array of value is send
					else if(formElement.type == "select-multiple") {
						var nb = formElement.options.length;
						for(var i = 0;i < nb; i++){
							if(formElement.options[i].selected == true){
								//TODO: this will work with PHP but as not been tested with ASP
								if(formElement.name.lastIndexOf('[]') == formElement.name.length-2) {
									r+= encodeURIComponent(formElement.name)+"="+encodeURIComponent(formElement.options[i].value)+"&";
								}
								else {
									r+= encodeURIComponent(formElement.name)+"[]="+encodeURIComponent(formElement.options[i].value)+"&";
								}
							}
							if(i > 100) {
								alert('bug!!!!');
								break;
							}
						}
					}
					//TODO: add the file, if possible.
					else if(formElement.type == "file") {
						//alert("DOM Input File: "+formElement.value);
					}
				}
			}
			return r;
		}
		else {
			this.callError(4, "The argument passed to formToUrlArguments function is not a form ID or a form object", true);
		}
		return false;
	}
	
	return this;
}

function IECacheFix_getTime() {
	var d = new Date();
	return d.getTime();
}
/**/