// <!--

g_TransactionSerialNumber = 1; // the current transaction serial number, incremented each transaction
g_CompletedTransactionQueue = new Array(); // queue for completed out-of-order transactions.
g_TransactionSerialNumberLast = 0; // serial number of the last completed transaction, should be initialized to 
	// g_TransactionSerialNumber - 1 but scoping can be weird here, so just set to 0

// object for asynchronous requests.
// exposes:
//   r = new async_request(url = null, method = GET, 
// 			  postdata = null,  uname=undefined, pword=undefined)
//	Parameters:
//	 	url, method, postdata: if url is specified, automatically call 
//			self.retrieve(url, method, postdata) after construction.
//	 	uname, pword: Username and password for HTTP authentication, usually undefined.
//  
//  XMLHttpRequest r.new_request_object(): Create a new XML HTTP request.
//
//  XMLHttpRequest r.request: Assigned by r.retrieve(), this is the actual request object. It is valid only
//     after a call to r.retrieve() and is overwritten by later calls.
// 
//  r.default_onreadystatechange(): the default onreadystatechange handler, assigned to the request object
//     when using async requests. it calls dispatch() when ready state == complete (4)
//
//  bool r.get(url): shortcut for r.retrieve("GET", url, null)
//  bool r.post(url, postdata): shortcut for r.retrieve("POST", url, postdata)
//  bool r.retrieve(method, url, postdata):
//  Parameters:
//    method: GET or POST
//    url: URL to retrieve
//    postdata: Post data or NULL, not URI encoded.
//  Returns: true if successfully initiated (or completed, if synchronous), false on error
//
//  r.request contains:
//    Before readyState==4:
//    	int readyState: Ready state. Shouldn't need to deal with this, 0 or 4 are all we care.
//    	void abort(): Abort the request (only valid if readyState in [1,3]
//	  After readyState==4:
//    	string responseText: Response text as string.
//  	string responseXML: Response text as DOM-compliant XML (ref: http://www.javascriptkit.com/domref/index.shtml)
//  	int status: HTTP request status
//  	string statusText: Friendly HTTP request status string
//  	string contentType: Content-type of resopnseText (ie text/xml, text/html)
//    

function async_request(url, method, postdata, async, uname, pword, oncomplete, ocp) {
	var self = this;
	
	self.new_request_object = function() {
		var rval = null;
		Trace("+async_request::new_request_object");
		if(window.XMLHttpRequest) { rval = new XMLHttpRequest(); }
		else if(window.ActiveXObject) { rval = new ActiveXObject("Microsoft.XMLHTTP"); }
		Trace("-async_request::new_request_object");
		return(rval);
	}
	
	self.default_onreadystatechange = function() {
		Trace("+async_request::onreadystatechange");
		if(self.request.readyState == 0) { /* uninitialized */ }
		else if(self.request.readyState == 1) { /* loading */ }
		else if(self.request.readyState == 2) { /* loaded */ }
		else if(self.request.readyState == 3) { /* interactive? */ }
		else if(self.request.readyState == 4) { 
			/* complete */
			self.time_1 = new Date();
			Perf("CLI PERF: retrieve url '"+self.url+"' " + (self.time_1.getTime() - self.time_0.getTime()) + " ms");
			dispatch(self);
		}
		Trace("-async_request::onreadystatechange");
	}
	
	self.get = function(url) { return(self.retrieve("GET", url, null)); }
	self.post = function(url, data) { return(self.retrieve("POST", url, data)); }
	self.retrieve = function(method, url, postdata) {
		var rval = false;
		Trace("+async_request::retrieve "+method + " "+url+" async: "+self.async);
		self.url = url;
		self.response = null;
		self.response_ok = null;
		if(self.request == null || self.request.readyState == 4)
			{ self.request = self.new_request_object(); } // recycling is good for the environment
		if(self.request.readyState != 0) { 
			Info(" readyState != 0, invalid operation");
			rval = false; /* invalid operation. */
		} else {
			if(self.async) { self.request.onreadystatechange = self.default_onreadystatechange; } // otherwise unused
			self.request.open(method, url, self.async, self.username, self.password);	
			if(method.toLowerCase() == "post") // ref. http://www.javascriptkit.com/jsref/ajax.shtml
				{ self.request.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); }
			self.time_0 = new Date();
			self.request.send(postdata);
			if(!async) {
				self.time_1 = new Date(); 
				Perf("CLI PERF: retrieve url "+self.url+" " + (self.time_1.getTime() - self.time_0.getTime()) + " ms");
			}
			
			rval = true;
		}
		Trace("-async_request::retrieve");
		return(rval);		
	}

	self.check_response_valid = function() {
		var ok = true;
		if(self.response_ok != null) return(self.response_ok); // already done.
		Trace("+async_request::check_response_valid");
		if(ok) {
			if(self.request.readyState != 4) {
				Info(" response: status != 4, "+self.request.status);
				ok = false;
			}
		}
		if(ok) {
			if(self.request.responseText == null) {
				Warn("response text null! status: "+self.request.status+", responseText: "+self.request.responseText);
				ok = false;
			}
		}
		if(ok) {
			if(self.request.responseText == '') {
				Warn("response text blank? wtf, status: " + self.request.status);
				ok = false;
			}
		}
		if(ok) {
			try { self.response = eval('(' + self.request.responseText + ')'); } 
			catch(e) {
				var etext = "JSON parsing failed, exception: ";
				for(x in e) { etext += x + ": " + e[x].toString(); }
				ok = false;
				Warn(etext);
				Debug(self.request.responseText);
			}
		}
		if(ok) {
			var d = self.response.dispatch;
			if(typeof d == 'undefined' || d == null) {
				Error("Invalid response: No dispatcher specified.");
				Debug("Response Text: "+self.request.responseText);
				ok = false;
			}
		}
		if(ok) {
			var d = self.response.debug; // debug spew, if present
			if(typeof d != 'undefined' && d != null && d != '')  { // server's loglevel settings always win
				Log(CriticalLevel, "+++ SERVER SPEW");
				Log(CriticalLevel, d);
				Log(CriticalLevel, "--- SERVER SPEW");
			}
		}

		self.response_ok = ok;
		if(!ok) {
			Info(" check_response_valid failed!");
			Debug("response: " + self.request.responseText);
		}
		Trace("-async_request::check_response_valid");
		return(ok);
	}
	
	self.request = null;
	self.oncomplete = oncomplete;
	self.oncompleteparam = ocp;
	if(typeof url == 'undefined') { url = null; }
	if(typeof method == 'undefined' || method == null) { method = "GET"; }
	if(typeof postdata == 'undefined') { postdata = null; }	
	if(typeof async == 'undefined' || async == null) { async = true; }

	if(method != "GET" && method != "POST") { return(null); } /* invalid arguments */ 
	
	self.async = async;
	self.username = uname;
	self.password = pword;

	self.serialNumber = g_TransactionSerialNumber++;
	
	if(url != null) { self.retrieve(method, url, postdata); }
	return(self);
}


// we're assuming that any given run of the async handler and scan transaction queue is atomic. 
// generic_async_handler/scan_transaction_queue are NOT reentrant and without locks, there's nothing we can do to make them so.

// we're also assuming we never get an error. Documentation for XMLHttpRequest indicates that there's *NO* built-in error reporting
// for asynchronous transactions. I guess the assumption is the browser will keep retrying for us. If not, then nice going, W3C.
// now iterate through any pending transactions.
// only the inorder call to dispatch may execute this.
function scan_transaction_queue() {
	var j;
	for(j=0; j < g_CompletedTransactionQueue.length; j++) {
		if(j == 0) Noise("checking trans q (len: "+g_CompletedTransactionQueue.length+") for expected serial "+(g_TransactionSerialNumberLast+1)); 
		Noise("&nbsp;&nbsp;&nbsp;&nbsp;q["+j+"] serial "+g_CompletedTransactionQueue[j].serialNumber);
		if(g_CompletedTransactionQueue[j].serialNumber == g_TransactionSerialNumberLast+1) {
			a = g_CompletedTransactionQueue.splice(j, 1)[0]; 
			Noise(" executing pended transaction "+a.serialNumber);
			dispatch(a, true); // which will incr g_TransactionSerialNumberLast
			j = -1; // reset loop
		}
	}	
}

function dispatch(a, norecurse) {
	Trace("+dispatch");
	var expectedSerial = g_TransactionSerialNumberLast + 1;
	var outoforder = false;
	var type = null;
	if(typeof norecurse == 'undefined' || norecurse == null) norecurse = false;
	if(expectedSerial != a.serialNumber) {
		Debug(" out-of-order response, expected xaction "+expectedSerial+
			" but got "+a.serialNumber+"; pending this transaction; "+
			"current trans q len: "+g_CompletedTransactionQueue.length);
		g_CompletedTransactionQueue.push(a);
		outoforder = true;
	} else { g_TransactionSerialNumberLast++; }
	if(!outoforder || norecurse) {
	 	if(a.check_response_valid()) {
			try { disp = eval(a.response.dispatch); }
			catch(e) {
				var etext = "exception caught evaluating dispatch: ";
				for(x in e) { etext += x + ": " + e[x].toString(); }
				ok = false;
				Warn(etext);
			}

			if(typeof disp == 'undefined' || disp == null) { Error("Undefined dispatcher: "+a.response.dispatch); }
			else { 
				Noise("invoking dispatcher..."); 
				disp(a); 
				if(typeof a.response.postprocess != 'undefined') {
					try { eval('try { '+a.response.postprocess+' } catch(e) { Error(\'Postprocessing error: \' + e.toString()); }'); }
					catch(e) { 
						Error('Postprocessing evaluation error: '+e.toString()); 
						Debug('Postprocessing Instructions: '+a.response.postprocess);
					}
				}
				if(a.oncomplete) {
					Noise("Invoking client-side postprocessing");
					a.oncomplete(a, a.oncompleteparam);
				}
			} 
		} else {
			Error("check_response_valid failed, response: \n"+a.request.responseText);
		}
	}
	// we were called by the async transaction's completion.
	// scan through and execute any pended transactions.
	if(!outoforder && !norecurse) { scan_transaction_queue(); } 
	Trace("-dispatch");
}

// some built-in dispatchers
function dispatcher_replace_element(o) {
	var div = document.getElementById(o.response.elementId);
	Debug("replacing element "+o.response.elementId);
	if(div == null) {
		Error("dispatcher_replace_element: div '"+o.response.elementId+"' not found");
	} else {
		div.innerHTML = o.response.content;
	}
}

function _create_tag(tagName, tagAttributes, children, text) {
	var newTag = document.createElement(tagName);
	
	if(typeof tagAttributes != 'undefined' && tagAttributes != null) {
		for(var attr in tagAttributes) {
			if(typeof tagAttributes[attr] != 'undefined' && tagAttributes[attr] != null) {
				newTag[attr] = tagAttributes[attr];
			}
		}
	}

	if(typeof children != 'undefined' && children != null) {
		switch(tagName) {
		case 'select': // this is architecturally identical, but because it's Javascript, we have to special-case some tags.
			for(var k = 0; k < children.length; k++) {
				newTag.options.length++;
				newTag.options[newTag.options.length-1].value = children[k].tagAttributes.value;
				newTag.options[newTag.options.length-1].text = children[k].text;
			}
			break;
		default:
			for(var k = 0; k < children.length; k++) {
				var childTag = _create_tag(
					children[k].tagName, 
					children[k].tagAttributes, 
					children[k].children,
					children[k].text);
				newTag.append_child(childTag);
			}
			break;
		}
	}

	if(typeof text != 'undefined' && text != null) {
		newTag.textContent = text;
	}
	return newTag;
	
}
// replaces an entire element but tries to preserve its id, classname, and name fields
// also capable of replacing it with a DOM tree, as given by children.
function dispatcher_replace_whole_element(o) {
	var div = document.getElementById(o.response.elementId);
	var paren = div.parentNode;
	var newTag = _create_tag(o.response.tagName, o.response.tagAttributes, o.response.children, o.response.text);
	copyAttrs = [ 'id', 'className', 'name' ];
	for(var k = 0; k < copyAttrs.length; k++) {
		// only in Javascript would it take THREE FUCKING CHECKS to determine
		// whether something is set or not.
		if(typeof div[copyAttrs[k]] != 'undefined' && 
				div[copyAttrs[k]] != null &&
				div[copyAttrs[k]] != '' && 
				(typeof newTag[copyAttrs[k]] == 'undefined' || 
				 newTag[copyAttrs[k]] == null ||
				 newTag[copyAttrs[k]] == '')) {
			newTag[copyAttrs[k]] = div[copyAttrs[k]];
		}
	}
	paren.replaceChild(newTag, div);
}

function dispatcher_redirect(o) { location.href = o.response.href; }
function dispatcher_nop(o) { }
					
function dispatcher_script(o) {
	script = 'try { '+o.response.script+' } catch(_err) { txt = "Script evaluation failed, error info: <br/>"; txt += dump_error_info(_err); Error(txt); }'; 
	Noise(script);
	eval(script);
}

// this creates an async request, sends an optional form
// back to the server, then processes some server instructions when the request completes
// the server instructions should be complete, you shouldn't have to worry about postprocessing ever
// again (thank the gods...)
// we also implicitly set the "s=1" GET parameter, which is used to tell the server "hey, this is 
// a "transact" call"
function transact(url, form, oncomplete, ocp) {
	Trace("+transact "+url);
	var fd = null;
	var method = "get";
	if(url.indexOf("?") >= 0) { url += "&s=1"; }
	else { url += "?s=1"; }
	if(typeof form != 'undefined' && form != null) { 
		fd = form_get_data(form);
		method = "post"; 
	}
	var a = new async_request(null, null, null, true, null, null, oncomplete, ocp); 
	a.retrieve(method, url, fd);
	Trace("-transact "+url);
	return(false); // so that transact can be used for onclick. if we do do error handling we'll need exceptions, not rvals
}

// -->
