///<reference path="../../TESCO.js" />
///<reference path="../event.js" />
///<reference path="../eventManager.js" />
///<reference path="../exception.js" />
///<reference path="../DOM.node.js" />
///<reference path="../serialization.js" />
/*
TODO:
	1) check compatibility:
				WinIE5					.
				WinIE5.5				X (no support for abortPreviousRequest)
				WinIE6					X (no support for abortPreviousRequest)
				WInIE7					X (no support for abortPreviousRequest?)
				Mozilla 1+			.
				Firefox					X
				Netscape 7+			X
				Opera 8+				X
				Safari 1.2+			.
				Safari 2				X
				iCab 3.0b352+		.
				Konqueror				.
				...
	2) complete testing for browsers that do not support the xmlhttp object (or turned off)
	3) add an abort method to the connection2 object that will iterate through all pending requests and cancel + dispatch abort event
	4) add abortPreviousRequest support for IE
	
	Example:
	
	Use simpler AJAX wrapper for event handling
*/

TESCO.system.connection = new function() {
	TESCO.system.event.manager.call(this,
		"request",
		"beforerequest",
		"complete",
		"error",
		"abort"
	);

	this.VERSION = "2.1.0";
	this.NAME = "TESCO.system.connection";

	this.manager = this;
}

/* ===================================================================================
	TESCO.system.connection.XMLHTTP([async],					OPTIONAL - true/false - makes the connection async/sync - defauts to true
																									NOTE: the XMLHTTP object will still send requests async
															[abortPreviousRequest],	OPTIONAL - true/false - if there is a request being processed it is
															 										aborted and the new request sent. only supported when async = false - defaults to false
															[defaultMimeType],		OPTIONAL - specifies the default content type used when calling the request method
															 										can be overridden when passing a mimeType to the request method.
															[characterEncoding],		OPTIONAL - specifies the required character encoding - defaults to TESCO.system.connection.XMLHTTP.CHARACTERENCODING.UTF8
															[credentials]);			OPTIONAL - specifies a username/password object (TESCO.system.connection.XMLHTTP.credentials): {username : "username"[, password : "password"]}
----------------------------------------------------------------------------------- */
TESCO.$("system.connection").XMLHTTP = function(async, abortPreviousRequest, defaultMimeType, characterEncoding, credentials) {
	TESCO.system.event.manager.call(this,
		"request",
		"beforerequest",
		"complete",
		"error",
		"abort"
	);

	this.VERSION = "2.1.0";
	this.NAME = "TESCO.system.connection.XMLHTTP";

	var _self = this;
	var _connectionManager = TESCO.system.connection.manager;
	var _request;

	var _async = (typeof async !== "undefined" ? async : true);
	var _abortPreviousRequest = (typeof abortPreviousRequest !== "undefined" ? abortPreviousRequest : false);
	var _connectionCount = 0;
	var _requests = [];
	var _isBusy = false;
	var _credentials = (credentials ? credentials : new TESCO.system.connection.XMLHTTP.credentialDetails());

	var _defaultMimeType = (defaultMimeType ? defaultMimeType : TESCO.system.connection.XMLHTTP.MIMETYPE.xml);

	var _characterEncoding;
	if(!characterEncoding) {
		_characterEncoding = TESCO.system.connection.XMLHTTP.CHARACTERENCODING.UTF8;
	} else {
		if(!TESCO.system.connection.XMLHTTP.CHARACTERENCODING.containsValue(characterEncoding)) {
			throw new TESCO.system.connection.XMLHTTP.exceptions.invalidparameter("request", "characterEncoding", "TESCO.system.connection.XMLHTTP.METHOD");
		}
	}

	var _requests_locked = false;

	this.getNextPendingRequest = function() {
		if(_requests.length > 0) {
			return _requests[0].details.params;
		} else {
			return null;
		}
	}

	this.lockPendingRequests = function() {
		_requests_locked = true;
	}

	this.unlockPendingRequests = function() {
		// TODO: should this have a timeout to automatically unlock the requests as if an exception occurs somewhere and this
		//			 is not called then no more requests will be sent
		_requests_locked = false;
	}

	this.isBusy = function() {
		return _isBusy;
	}

	function _req(request, url, params, method, mimeType, customHeaders, customObject, callback) {
		var _self_req = this;
		this.request = createXMLHTTP();
		this.url = url;
		this.params = params;
		this.method = (method ? method : TESCO.system.connection.XMLHTTP.METHOD.post);
		this.mimeType = (mimeType ? mimeType : _defaultMimeType);
		this.customHeaders = customHeaders;
		this.customObject = customObject;
		this.callback = callback;

		var _abortRequest = false;
		var details = {url : this.url, params : this.params, method : this.method, mimeType : this.mimeType, customHeaders : this.customHeaders};

		this.destroy = function() {
			if(this.request) {
				delete this.request.onreadystatechange;
				delete this.request;
			}
			delete this.url;
			delete this.params;
			delete this.method;
			delete this.mimeType;
			delete this.customHeaders;
			delete this.customObject;
			delete this.callback;
		}

		this.abort = function() {
			_abortRequest = true;
			if(this.request) {
				this.request.abort();
			}
		}

		try {
			var paramQueryString = null;
			var content = null;

			this.request.open(this.method, this.url, true, _credentials.username, _credentials.password);

			if(this.params) {
				if(this.method == TESCO.system.connection.XMLHTTP.METHOD.get) {
					this.request.setRequestHeader("If-Modified-Since", "Sat, 1 Jan 2000 00:00:00 GMT");
					paramQueryString = this.params.toValueString(null, null, true);
					url += (paramQueryString ? "?" + paramQueryString : "");
				} else {
					this.request.setRequestHeader("Content-Type", mimeType + "; charset=" + _characterEncoding);
					switch(this.mimeType) {
						case TESCO.system.connection.XMLHTTP.MIMETYPE.form:
							content = this.params.toValueString(null, null, true);
							break;
						case TESCO.system.connection.XMLHTTP.MIMETYPE.xml:
							content = this.params.serialize();
							break;
						case TESCO.system.connection.XMLHTTP.MIMETYPE.text:
							content = this.params;
							break;
						default:
							throw new TESCO.system.connection.XMLHTTP.exceptions.invalidMimeType("request", mimeType);
					}
				}
			}

			// -------- customHeaders --------
			if(this.customHeaders) {
				for(var i in this.customHeaders) {
					if(typeof(this.customHeaders[i]) != "object" && typeof(this.customHeaders[i]) != "function") {
						this.request.setRequestHeader(i, this.customHeaders[i]);
					}
				}
			}

            function _getAjaxRedirectLocation() {
                //split because of a bug where the custom header is added twice. To be fixed.
                var responseHeader = _self_req.request.getResponseHeader("AjaxRedirect-To");
                if (responseHeader) {
                    return responseHeader.split(', ')[0];
                }
                else {
                    return null;
                }
            }

			this.request.onreadystatechange = function() {
				if(_self_req.request && _self_req.request.readyState == TESCO.system.connection.XMLHTTP.READYSTATE.completed) {
				    var ajaxRedirectLocation = _getAjaxRedirectLocation();
					try {
						if(_abortRequest) {
							_abortRequest = false;
							var oEvent = _connectionManager.dispatchEvent("abort", {exception : new TESCO.system.connection.XMLHTTP.exceptions.requestAborted("request"), details : details, customObject : _self_req.customObject}, true);
							if(oEvent.returnValue) {
								_self.dispatchEvent("abort", {exception : new TESCO.system.connection.XMLHTTP.exceptions.requestAborted("request"), details : details, customObject : _self_req.customObject});
							}
						} else if(_self_req.request.status != 200) {
                             throw new TESCO.system.connection.XMLHTTP.exceptions.invalidstatus("request", _self_req.request);
                        }
                        else if (ajaxRedirectLocation) {
                            window.location.href = ajaxRedirectLocation;
                        }
						else {
							// Get the mime type from the content-type header. format "type/subtype; parameters"
							var mimeType = _self_req.request.getResponseHeader("content-type");
							if(mimeType) {
								mimeType = mimeType.split(";")[0].toLowerCase();
							}
							switch(mimeType) {
								case TESCO.system.connection.XMLHTTP.MIMETYPE.xml:
									var event = {responseMimeType : mimeType, response : TESCO.system.serialization.deserialize(_self_req.request.responseXML), details : details, customObject : _self_req.customObject};
									var oEvent = _connectionManager.dispatchEvent("complete", event, true);
									if(oEvent.returnValue) {
										_self.dispatchEvent("complete", event);
										if(_self_req.callback) {
											_self_req.callback(event);
										}
									}
									break;
								case TESCO.system.connection.XMLHTTP.MIMETYPE.objectliteral:
									_connectionCount++;
									var responseObjectName = "response" + _connectionCount;
									try {
										eval(_self.NAME + "." + responseObjectName + " = {" + _self_req.request.responseText + "}");
										var event = {responseMimeType : mimeType, response : TESCO.system.connection.XMLHTTP[responseObjectName], details : details, customObject : _self_req.customObject};
										var oEvent = _connectionManager.dispatchEvent("complete", event, true);
										if(oEvent.returnValue) {
											_self.dispatchEvent("complete", event);
											if(_self_req.callback) {
												_self_req.callback(event);
											}
										}
									} catch(exceptionResponse) {
										throw new TESCO.system.connection.XMLHTTP.exceptions.invalidresponse("request", _self_req.request);
									} finally {
										delete TESCO.system.connection.XMLHTTP[responseObjectName];
									}
									break;
								default:
									var event = {responseMimeType : mimeType, response : _self_req.request.responseText, details : details, customObject : _self_req.customObject};
									var oEvent = _connectionManager.dispatchEvent("complete", event, true);
									if(oEvent.returnValue) {
										_self.dispatchEvent("complete", event);
										if(_self_req.callback) {
											_self_req.callback(event);
										}
									}
							}
						}
					} catch(e) {
						var oEvent = _connectionManager.dispatchEvent("error", {exception : e, details : details, customObject : _self_req.customObject}, true);
						if(oEvent.returnValue) {
							_self.dispatchEvent("error", {exception : e, details : details, customObject : _self_req.customObject});
						}
					} finally {
						_self_req.destroy();
						_request = sendNextRequest();
					}
				}
			}

			var requestBody = {body : content, queryString : paramQueryString};
			var oEvent = _connectionManager.dispatchEvent("beforerequest", {request : requestBody, details : details, customObject : this.customObject}, true);
			if(oEvent.returnValue) {
				_self.dispatchEvent("beforerequest", {request : requestBody, details : details, customObject : this.customObject});
				this.request.send(requestBody.body);
				var oEvent = _connectionManager.dispatchEvent("request", {request : requestBody.body, details : details, customObject : this.customObject}, true);
				if(oEvent.returnValue) {
					_self.dispatchEvent("request", {request : requestBody.body, details : details, customObject : this.customObject});
				}
			}
		} catch(e) {
			var oEvent = _connectionManager.dispatchEvent("error", {exception : e, details : details, customObject : this.customObject}, true);
			if(oEvent.returnValue) {
				_self.dispatchEvent("error", {exception : e, details : details, customObject : this.customObject});
			}

			this.destroy();
			_request = sendNextRequest();
		}
	}

	/* ===================================================================================
		request(url,							url of the service
						[params],					OPTIONAL - defines a parameter object to pass to the server
						[method],					OPTIONAL - TESCO.system.connection.XMLHTTP.METHOD - defaults to TESCO.system.connection.XMLHTTP.METHOD.post
						[mimeType],				OPTIONAL - TESCO.system.connection.XMLHTTP.MIMETYPE - defaults to TESCO.system.connection.XMLHTTP.MIMETYPE.xml
						[customHeaders],	OPTIONAL - defines an associative array of values to pass as HTTP headers
						[customObject],		OPTIONAL - defines an object that will be associated with the individual request
						[callback]);			OPTIONAL - callback to be executed when a specific request has completed
	----------------------------------------------------------------------------------- */
	this.request = function(url, params, method, mimeType, customHeaders, customObject, callback) {
		if(!url) {
			throw new TESCO.system.connection.XMLHTTP.exceptions.missingparameter("request", "url");
		}

		// -------- method --------
		if(!method) {
			method = TESCO.system.connection.XMLHTTP.METHOD.post;
		} else {
			if(!TESCO.system.connection.XMLHTTP.METHOD.containsValue(method)) {
				throw new TESCO.system.connection.XMLHTTP.exceptions.invalidparameter("request", "method", "TESCO.system.connection.XMLHTTP.METHOD");
			}
		}

		// -------- mimeType --------
		if(!mimeType) {
			mimeType = _defaultMimeType;
		} else {
			if(!TESCO.system.connection.XMLHTTP.MIMETYPE.containsValue(mimeType)) {
				throw new TESCO.system.connection.XMLHTTP.exceptions.invalidparameter("request", "mimeType", "TESCO.system.connection.XMLHTTP.METHOD");
			}
		}

		if(_isBusy && !_async) {
			var pendingRequest = {details : {url : url, params : params, method : method, mimeType : mimeType, customHeaders : customHeaders, customObject : customObject, callback : callback}};
			_requests.push(pendingRequest);
			if(_abortPreviousRequest) {
				_request.abort();
			}
			return;
		}

		_isBusy = true;

		_request = new _req(null, url, params, method, mimeType, customHeaders, customObject, callback);
		return _request;
	}

	function sendNextRequest() {
		if(_requests.length > 0) {
			if(_requests_locked) {
				setTimeout(sendNextRequest, 50);
			} else {
				var req = _requests.shift();
				return new _req(null, req.details.url, req.details.params, req.details.method, req.details.mimeType, req.details.customHeaders, req.details.customObject, req.details.callback);
			}
		} else {
			_isBusy = false;
			return null;
		}
	}

	function createXMLHTTP() {
		try {
			var request;
			if(window.XMLHttpRequest) {
				request = new XMLHttpRequest();
			}

			if(request) {
				return request;
			} else {
				throw new TESCO.system.connection.XMLHTTP.exceptions.notsupported("createXMLHTTP");
			}
		} catch(e) {
			throw new TESCO.system.connection.XMLHTTP.exceptions.notsupported("createXMLHTTP");
		}
	}
}

if (!window.XMLHttpRequest && window.ActiveXObject) {
	window.XMLHttpRequest = function() {
		try {
			return new ActiveXObject("Msxml2.XMLHTTP.6.0");
		} catch(e1) {
			try {
				return new ActiveXObject("Msxml2.XMLHTTP.3.0");
			} catch (e2) {
				try {
					return new ActiveXObject("Msxml2.XMLHTTP");
				} catch(e3) {
					try {
						return new ActiveXObject("Microsoft.XMLHTTP");
					} catch(e4) {
					}
				}
			}
		}
		return null;
	}
	TESCO.system.event.attach(window, "unload", 
		function() {
			window.XMLHttpRequest = null;
		}
	);
}

TESCO.system.connection.XMLHTTP.exceptions = {

	notsupported : function(functionName) {
		this.exception = TESCO.system.Exception;
		this.exception("XMLHTTP not supported", "TESCO.system.connection.XMLHTTP", functionName);
	},

	missingparameter : function(functionName, parameterName) {
		this.exception = TESCO.system.Exception;
		this.exception("Parameter '" + parameterName + "' must be defined", "TESCO.system.connection.XMLHTTP", functionName);
		this.parameterName = parameterName;
	},

	invalidparameter : function(functionName, parameterName, parameterType) {
		this.exception = TESCO.system.Exception;
		this.exception("Parameter '" + parameterName + "' is invalid. Must be of type '" + parameterType + "'", "TESCO.system.connection.XMLHTTP", functionName);
		this.parameterName = parameterName;
		this.parameterType = parameterType;
	},

	invalidstatus : function(functionName, request) {
		this.exception = TESCO.system.Exception.InvalidStatus;
		this.exception(TESCO.system.connection.XMLHTTP, functionName, request);
		this.requestStatus = request.status;
		this.requestStatusText = request.statusText;
		this.responseText = request.responseText;
	},

	invalidresponse : function(functionName, responseText) {
		this.exception = TESCO.system.Exception;
		this.exception("Invalid response from server: " + responseText, "TESCO.system.connection.XMLHTTP", functionName);
		this.responseText = responseText;
	},

	requestAborted : function(functionName) {
		this.exception = TESCO.system.Exception;
		this.exception("Request aborted", "TESCO.system.connection.XMLHTTP", functionName);
	},

	invalidMimeType : function(functionName, mimeType) {
		this.exception = TESCO.system.Exception;
		this.exception("Invalid content type: " + mimeType, "TESCO.system.connection.XMLHTTP", functionName);
		this.mimeType = mimeType;
	}
}

// TODO: support any other verbs?
TESCO.system.connection.XMLHTTP.METHOD = {
	get : "GET",
	post : "POST"
}

TESCO.system.connection.XMLHTTP.MIMETYPE = {
	form : "application/x-www-form-urlencoded",
	xml : "text/xml",
	text : "text/plain",
	objectliteral : "text/x-object-literal"
}

// TODO: add some additional encoding types...
TESCO.system.connection.XMLHTTP.CHARACTERENCODING = {
	UTF8 : "utf-8"
}

TESCO.system.connection.XMLHTTP.READYSTATE = {
	uninitialized : 0,
	loading : 1,
	loaded : 2,
	interactive : 3,
	completed : 4
}

TESCO.system.connection.XMLHTTP.credentialDetails = function(username, password) {
	this.username = username;
	this.password = password;
}

if (TESCO.sites.Configuration.application.debug) {
    /*
    TESCO.system.event.attach(TESCO.system.connection, "complete",
	    function(e) {
		    console.log("TESCO.system.connection.COMPLETE: " + e.response.serialize());
	    }
    );
    */

    TESCO.system.event.attach(TESCO.system.connection, "request",
	    function(e) {
		    console.log("TESCO.system.connection.REQUEST: " + e.request);
	    }
    );
}

TESCO.$("system.connection").eggTimer = {
	show: function(node) {
		node.style.cursor = "wait";
	},
	hide: function(node) {
		node.style.cursor = "";
	}
}
