﻿SetupNamespaces("Pliner.Util");

Pliner.Util.Ajax = { 
	/**
	Connection
	constructor for the Connection object
	**/
	Connection: function() { 
		var private = {
			pendingMsgs: new Pliner.Util.DataStructures.PriorityQueue(Pliner.Util.Ajax.MessageComparer),
			runningMsgs: new Pliner.Util.DataStructures.LinkedList(),
			
			onAnyCompleted: null, 
			onAnyError: null, 
			
			rtt: new Pliner.Util.DataStructures.LinkedList(), 
			ttc: new Pliner.Util.DataStructures.LinkedList(), 
			estimatedRtt: 20000, 
			rttDeviation: 0, 
			
			/**
			GetXmlHttpRequestObj
			creates a cross-browser XMLHttpRequest object
			returns: the XMLHttpRequest object to be used for the current browser
			**/
			GetXmlHttpRequestObj: function() {
				var xmlhttp;
				/*@cc_on
				@if (@_jscript_version >= 5) {
					try { xmlhttp = new ActiveXObject("Msxml2.XMLHTTP"); }
					catch (e) {
						try { xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); }
						catch (E) { xmlhttp = false; }
					}
				}
				@else xmlhttp = false;
				@end @*/
				if (!xmlhttp && typeof XMLHttpRequest != "undefined") {
					try { xmlhttp = new XMLHttpRequest(); }
					catch (e) { xmlhttp = false; }
				}
				return xmlhttp;
			}, 
			
			/**
			PollQueue
			the queue of pending messages is polled for new messages that can be run, until the 
			number of running messages equals the total number of pipelined connections
			**/
			PollQueue: function() { 
				if (private.runningMsgs.count >= public.pipelinedConnections) return;
				
				while (private.pendingMsgs.count > 0 && private.runningMsgs.count < public.pipelinedConnections) { 
					var msg = private.pendingMsgs.Dequeue();
					
					if ((msg.status == 0 || msg.status == 3) && (msg.priority != 3 || private.pendingMsgs.count == 0)) { 
						private.runningMsgs.Add(msg);
						msg.Run(private.GetXmlHttpRequestObj(), private.GetTimeout());
					}
				}
			}, 
			
			/**
			PollRunning
			the list of running messages is polled for completed messages to remove.  If any messages are removed, 
			the queue is also polled for new messages to add to the list of running messages.  Messages needing a 
			retry are pushed back onto the queue
			**/
			PollRunning: function() { 
				var numRemoved = 0;
			
				for (var i=0; i < private.runningMsgs.count; i++) { 
					var msg = private.runningMsgs.Get(i);
					
					if (msg.status == 3) // needs retry
						private.pendingMsgs.Enqueue(msg);
					if (msg.status != 1 && msg.status != 2) { // not sending or receiving
						private.runningMsgs.RemoveAt(i);
						i--;
						numRemoved++;
						
						if (msg.status == 5 && private.onAnyCompleted) private.onAnyCompleted(msg);
						else if (msg.status == 4 && private.onAnyError) private.onAnyError(msg);
					}
				}
				
				if (numRemoved > 0)
					private.PollQueue();
			}, 
			
			/** 
			GetTimeout
			gets the timeout value to use for new connections, based on the current connection latency
			returns: the timeout value (in ms)
			**/
			GetTimeout: function() { 
				return Math.max(private.estimatedRtt + 4 * private.rttDeviation, 20000);
			}, 
			
			/** 
			SetMessagingTimes
			takes the round-trip-time and time-to-complete for a message that has just completed
			and updates the connection state calculations with these recent measurements
			**/
			SetMessagingTimes: function(r, t) { 
				while (private.rtt.count >= 100) private.rtt.RemoveAt(private.rtt.count - 1);
				while (private.ttc.count >= 100) private.ttc.RemoveAt(private.ttc.count - 1);
				private.rtt.Add(r);
				private.ttc.Add(t);
				private.estimatedRtt = 0.875 * private.estimatedRtt + 0.125 * r;
				private.rttDeviation = 0.75 * private.rttDeviation + 0.25 * Math.abs(r - private.estimatedRtt);
			}
		};

		var public = {
			/** the number of simultaneous requests to run.  can be modified at any time during runtime.  0 will shutdown the connection manager. **/
			pipelinedConnections: 1, 
			
			/**
			AddToQueue
			adds a message to the queue
			msg: the message to add to the queue
			returns: true if the message was added, false if the message could not be added because the queue is flooded with messages
			**/
			AddToQueue: function(msg) { 
				if (private.pendingMsgs.length > 50000) return false; // queue too large
				
				msg.AddOnCompletedEvent(function(m) { private.SetMessagingTimes(m.GetRoundTripTime(), m.GetTimeToProcess()); private.PollRunning(); });
				msg.AddOnErrorEvent(function(m) { private.PollRunning(); });
				msg.AddOnCanceledEvent(function(m) { if (m.status == 6) private.PollQueue(); else private.PollRunning(); });
			
				private.pendingMsgs.Enqueue(msg);
				private.PollQueue();
				
				return true;
			}, 
			
			/**
			AddOnAnyCompletedEvent
			adds a function pointer to the list of event handlers that will be called anytime any message's communication with 
			the server is completed without error or exception
			handler: the function pointer to add.  The given function will be passed the completed message as the first and only parameter
			**/
			AddOnAnyCompletedEvent: function(handler) { private.onAnyCompleted = AddEventHandler(private.onAnyCompleted, handler); }, 
			
			/**
			AddOnAnyErrorEvent
			adds a function pointer to the list of event handlers that will be called anytime any message's communication with 
			the server is completed with an error code
			handler: the function pointer to add.  The given function will be passed the message with an error as the first and only parameter
			**/
			AddOnAnyErrorEvent: function(handler) { private.onAnyError = AddEventHandler(private.onAnyError, handler); }, 
			
			/**
			GetQueueLength
			gets the length of the queue of pending, non-running messages
			returns: the number of messages waiting to be run
			**/
			GetQueueLength: function() { 
				return private.pendingMsgs.count;
			}, 
			
			/**
			GetAvgTimeToProcess
			gets the average time from message object instantiation to completion of sending/receiving over the last 100 messages 
			with successful completions (or fewer if there have not yet been 100).
			returns: the average time to fulfill a message (in ms)
			**/
			GetAvgTimeToProcess: function() { 
				var avg = 0;
				for (var t=0; t < private.ttc.count; t++) 
					avg += (private.ttc.Get(t) - avg) / (t + 1);
				return avg;
			}, 
			
			/**
			GetAvgRoundTripTime
			gets the average time from the start of sending/receiving until the completion of receiving over the last 100 messages 
			with successful completions (or fewer if there have not yet been 100).
			returns: the average time communicate a message and response (in ms)
			**/
			GetAvgRoundTripTime: function() { 
				var avg = 0;
				for (var t=0; t < private.rtt.count; t++) 
					avg += (private.rtt.Get(t) - avg) / (t + 1);
				return avg;
			}
		};

		return public;
	}, 

	/**
	Message
	constructor for the Message object, which represents a message to be sent to the server and the 
	server's eventual response to the submission
	msgUrl: the URL if the page this message will request.  May include querystring values
	msgPriority: (optional) the message's priority in the connection queue.  Defaults to "normal" if not present
	onFinished: (optional) an initial event to be assigned to the onCompleted event handler's list of callbacks
	**/
	Message: function(msgUrl, msgPriority, onFinished) { 
		var private = {
			onRunning: null, 
			onCompleted: null, 
			onError: null, 
			onCanceled: null, 
			postData: [], 
		
			requestObj: null, 
			timeoutTimer: null, 
			timeCreated: new Date(), 
			timeSent: null, 
			timeReceived: null, 
			
			/**
			UpdateConnState
			checks the state of the XMLHttpRequest object, and updates the message object's internal state 
			to match the object's state, launching any events that are noted.
			timeout: the timeout period for the request communication.  Each time data is received or the state changes, 
			         this function will reset the timeout timer due to this activity
			**/
			UpdateConnState: function(timeout) { 
				clearTimeout(private.timeoutTimer);
				var completed = false;
				
				if (!private.requestObj) return;
				
				switch(private.requestObj.readyState) { 
					case 0: case 1: case 2: break;
					case 3: 
						public.status = 2;
						break;
					case 4: 
						private.timeReceived = new Date();
						public.responseText = private.requestObj.responseText;
						if (private.requestObj.status == 200) { 
							public.status = 5;
							if (private.onCompleted) private.onCompleted(public);
						}
						else {  
							public.status = 4;
							if (private.onError) private.onError(public);
						}
						private.requestObj = null;
						break;
				}
				
				if (!completed)
					private.timeoutTimer = setTimeout(function() { private.RegisterTimeout(); }, timeout);
			}, 
			
			/**
			RegisterTimeout
			sets the message object into a post-timeout state, preparing it for a re-run
			**/
			RegisterTimeout: function() { 
				if (private.requestObj) { 
					private.requestObj.abort();
					private.requestObj = null;
				}
				public.status = 3;
				public.numRetries++;
			}, 
			
			/**
			PostDataPair
			constructor for the PostDataPair object, which contains a key/value pair of data to post with this message
			k: the key for this post field
			v: the value for this post field
			**/
			PostDataPair: function(k, v) { 
				var public = {
					key: k ? k : null, 
					value: v ? v : null
				};

				return public;
			}
		};

		var public = {
			/** a unique ID for this message (for sequencing and for the server's convenience). Automatically assigned as instantiation **/
			id: -1, 
			/** the address of the page that this message will request **/
			url: msgUrl ? msgUrl : "", 
			/** the status of the message.  corresponds to the named values in Connection.MessageStatus (for example MessageStatus[status] == "Waiting")  **/
			status: 0, 
			/** the priority of the message. corresponds to the named values in Connection.MessagePriority (for example MessagePriority[priority] == "High") **/
			priority: msgPriority ? msgPriority : 1, 
			/** the number of times this message has previously been tried, without success **/
			numRetries: 0, 
			/** the response from the server to the message, or null if no response has been read yet.  Until the message status is "ReceivedSuccess", 
			    this text may be partially but not completely loaded **/
			responseText: null, 
			
			/**
			Run
			submits the message to the server asynchronously
			xmlHttpRequest: the XMLHttpRequest object to used for communication.  This will be retained for as long as is needed
			timeout: the timeout for the request (in ms), after which time the communication will be aborted and marked for a retry
			**/
			Run: function(xmlHttpRequest, timeout) { 
				if (public.status == 6 || public.status == 7) return;
				
				public.status = 1;
				private.requestObj = xmlHttpRequest;
				
				var postDataStr = "";
				for (var p=0; p < private.postData.length; p++) 
					postDataStr += (p > 0 ? "&" : "") + private.postData[p].key + "=" + escape(private.postData[p].value);
				
				xmlHttpRequest.open("POST", public.url + (public.url.indexOf("?") > -1 ? "&" : "?") + "msg=" + public.id, true);
				xmlHttpRequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
				xmlHttpRequest.setRequestHeader("Content-length", postDataStr.length);
				xmlHttpRequest.onreadystatechange = function() { private.UpdateConnState(timeout); };
				private.timeoutTimer = setTimeout(function() { private.RegisterTimeout(); }, timeout);
				
				private.timeSent = new Date();
				xmlHttpRequest.send(postDataStr);
				
				if (private.onRunning) private.onRunning(public);
			}, 
			
			/**
			Cancel
			cancels a message.  If the message has not been sent, it will be set to status "Canceled" and will never 
			be sent.  If sending/receiving has started but has not completed, the message will be set to status 
			"Aborted" and the communication will be stopped.  If the message has already been sent or received or canceled 
			or aborted, nothing will be done.
			**/
			Cancel: function() { 
				if (public.status == 0) { 
					public.status = 6;
				}
				else if (public.status != 4 && public.status != 5 && public.status != 6 && public.status != 7) { 
					if (private.requestObj) {
						private.requestObj.abort();
						private.requestObj = null;
					}
					public.status = 7;
				}
				
				if (public.status == 6 || public.status == 7)
					if (private.onCanceled) private.onCanceled(public);
			}, 
			
			/**
			AddOnRunningEvent
			adds a function pointer to be called when a message is run, entering the sending/receiving phase
			handler: the function pointer to add.  The function will be passed a reference to the message as the only parameter
			**/
			AddOnRunningEvent: function(handler) { private.onRunning = AddEventHandler(private.onRunning, handler); }, 
			
			/**
			AddOnCompletedEvent
			adds a function pointer to be called when a message completes the sending/receiving phase without any error or exception
			handler: the function pointer to add.  The function will be passed a reference to the message as the only parameter
			**/
			AddOnCompletedEvent: function(handler) { private.onCompleted = AddEventHandler(private.onCompleted, handler); }, 
			
			/**
			AddOnErrorEvent
			adds a function pointer to be called when a message completes the sending/receiving phase with an error or exception
			handler: the function pointer to add.  The function will be passed a reference to the message as the only parameter
			**/
			AddOnErrorEvent: function(handler) { private.onError = AddEventHandler(private.onError, handler); }, 
			
			/**
			AddOnCanceledEvent
			adds a function pointer to be called when a message is canceled before the sending or receiving phase is completed
			handler: the function pointer to add.  The function will be passed a reference to the message as the only parameter
			**/
			AddOnCanceledEvent: function(handler) { private.onCanceled = AddEventHandler(private.onCanceled, handler); }, 
			
			/**
			AddPostDataKey
			adds the given key/value pair to the post data to be submitted to the server with this request.  Requests submit 
			post data only (GET requests are not allowed).
			key: the key to identify this data
			value: the value of the data to be sent
			**/
			AddPostDataKey: function(key, value) { 
				private.postData[private.postData.length] = new private.PostDataPair(key.toLowerCase(), value);
			}, 
			
			/**
			RemovePostDataKey
			removes the requested key/value pair from the data to be posted to the server
			key: the key to remove.  All data with keys that have case-insensitive string matches to this key will be dropped
			**/
			RemovePostDataKey: function(key) { 
				key = key.toLowerCase();
				for (var p=0; p < private.postData.length; p++) 
					if (private.postData[p] && private.postData[p].key == key) 
						private.postData[p] = null;
			}, 
			
			/**
			GetTimeToProcess
			gets the total time from object instantiation until receiving from the server completed, indicating 
			the total time to respond to this message (including queueing time)
			returns: the total processing time for this message (in ms), or -1 if this cannot be determined yet
			**/
			GetTimeToProcess: function() { // time from message creation until response, in ms
				if (!private.timeCreated || !private.timeReceived) return -1;
				return private.timeReceived.getTime() - private.timeCreated.getTime();
			}, 
			
			/**
			GetRoundTripTime
			gets the round trip time for communication with the server, from the start of sending/receiving to the end
			returns: the round trip time for communication (in ms), or -1 if this cannot be determined yet
			**/
			GetRoundTripTime: function() { // time from sending to receiving, in ms
				if (!private.timeSent || !private.timeReceived) return -1;
				return private.timeReceived.getTime() - private.timeSent.getTime();
			}
		};
		
		public.id = ++Pliner.Util.Ajax._currentMsgId;
		if (onFinished) public.AddOnCompletedEvent(onFinished);

		return public;
	}, 
	
	MessageComparer: function(a, b) { 
		if (a.priority < b.priority) return 1;
		if (a.priority > b.priority) return -1; 
		return b.id - a.id;
	}, 
	
	MessageStatus: ["Waiting", "Sending", "Receiving", "ReceivedAwaitingRetry", "ReceivedError", "ReceivedSuccess", "Canceled", "Aborted"], 
	MessagePriority: ["High", "Normal", "Low"], 
	
	_currentMsgId: 0
};
