/*
 * jQuery Stream 1.2
 * Comet Streaming JavaScript Library 
 * http://code.google.com/p/jquery-stream/
 * 
 * Copyright 2011, Donghwan Kim 
 * Licensed under the Apache License, Version 2.0
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Compatible with jQuery 1.5+
 */
(function($, undefined) {

	var // Stream object instances
		instances = {},
	
		// Streaming agents
		agents = {},
	
		// HTTP Streaming transports
		transports = {},
		
		// Does the throbber of doom exist?
		throbber = $.browser.webkit && !$.isReady;
	
	// Once the window is fully loaded, the throbber of doom will not be appearing
	if (throbber) {
		$(window).load(function() {
			throbber = false;
		});
	}
	
	// Stream is based on The WebSocket API 
	// W3C Working Draft 19 April 2011 - http://www.w3.org/TR/2011/WD-websockets-20110419/
	$.stream = function(url, options) {
		
		// Returns the first Stream in the document
		if (!arguments.length) {
			for (var i in instances) {
				return instances[i];
			}
			
			return null;
		}
		
		// Stream to which the specified url or alias is mapped
		var instance = instances[url];
		
		if (!options) {
			return instance || null;
		} else if (instance && instance.readyState < 3) {
			return instance;
		}
		
		var // Stream object
			stream = {
				
				// URL to which to connect
				url: url,
				
				// Merges options
				options: $.stream.setup({}, options),
				
				// The state of stream
				// 0: CONNECTING, 1: OPEN, 2: CLOSING, 3: CLOSED
				readyState: 0,
				
				// Fake send
				send: function() {},
				
				// Fake close
				close: function() {}
				
			},
			match = /^(http|ws)s?:/.exec(stream.url),
			open = function() {
				// Delegates open process
				agents[stream.options.type](stream);
			};
		
		// Stream type
		if (match) {
			stream.options.type = match[1];
		}
		
		// Makes arrays of event handlers
		for (var i in {open: 1, message: 1, error: 1, close: 1}) {
			stream.options[i] = $.makeArray(stream.options[i]); 
		}
		
		// The url and alias are a identifier of this instance within the document
		instances[stream.url] = stream;
		if (stream.options.alias) {
			instances[stream.options.alias] = stream;
		}
		
		// Deals with the throbber of doom
		if (stream.options.type === "ws" || !throbber) {
			open();
		} else {
			switch (stream.options.throbber.type || stream.options.throbber) {
			case "lazy":
				$(window).load(function() {
					setTimeout(open, stream.options.throbber.delay || 50);
				});
				break;
			case "reconnect":
				open();
				$(window).load(function() {
					if (stream.readyState === 0) {
						stream.options.open.push(function() {
							stream.options.open.pop();
							setTimeout(reconnect, 10);
						});
					} else {
						reconnect();
					}
					
					function reconnect() {
						stream.options.close.push(function() {
							stream.options.close.pop();
							setTimeout(function() {
								$.stream(stream.url, stream.options);
							}, stream.options.throbber.delay || 50);
						});
						
						var reconn = stream.options.reconnect;
						stream.close();
						stream.options.reconnect = reconn;
					}
				});
				break;
			}
		}
		
		return stream;
	};
	
	$.extend($.stream, {
		
		version: "1.2",
		
		// Logic borrowed from jQuery.ajaxSetup
		setup: function(target, options) {
			if (!options) {
				options = target;
				target = $.extend(true, $.stream.options, options); 
			} else {
				$.extend(true, target, $.stream.options, options);
			}
			
			for (var field in {context: 1, url: 1}) {
				if (field in options) {
					target[field] = options[field];
				} else if (field in $.stream.options) {
					target[field] = $.stream.options[field];
				}
			}
			
			return target;
		},
		
		options: {
			// Stream type
			type: window.WebSocket ? "ws" : "http",
			// Whether to automatically reconnect when stream closed
			reconnect: true,
			// Whether to trigger global stream event handlers
			global: true,
			// Only for WebKit
			throbber: "lazy",
			// Message data type
			dataType: "text",
			// Message data converters
			converters: {
				text: window.String, 
				json: $.parseJSON, 
				xml: $.parseXML
			}
			// Additional parameters for GET request
			// openData: null,
			// WebSocket constructor argument
			// protocols: null,
			// XDomainRequest transport
			// enableXDR: false,
			// rewriteURL: null
			// Polling interval
			// operaInterval: 0
			// iframeInterval: 0
		}
	
	});
	
	$.extend(agents, {

		// WebSocket wrapper
		ws: function(stream) {
			if (!window.WebSocket) {
				return;
			}
			
			var // Absolute WebSocket URL
				url = prepareURL(getAbsoluteURL(stream.url).replace(/^http/, "ws"), stream.options.openData),
				// WebSocket instance
				ws = stream.options.protocols ? new window.WebSocket(url, stream.options.protocols) : new window.WebSocket(url);
			
			// WebSocket event handlers
			$.extend(ws, {
				onopen: function(event) {
					stream.readyState = 1;
					trigger(stream, event);
				},
				onmessage: function(event) {
					trigger(stream, $.extend({}, event, {data: stream.options.converters[stream.options.dataType](event.data)}));
				},
				onerror: function(event) {
					stream.options.reconnect = false;
					trigger(stream, event);
				},
				onclose: function(event) {
					var readyState = stream.readyState; 
					
					stream.readyState = 3;
					trigger(stream, event);

					// Reconnect?
					if (stream.options.reconnect && readyState !== 0) {
						$.stream(stream.url, stream.options);
					}
				}
			});
			
			// Overrides send and close
			$.extend(stream, {
				send: function(data) {
					if (stream.readyState === 0) {
						$.error("INVALID_STATE_ERR: Stream not open");
					}
					
					ws.send(typeof data === "string" ? data : param(data));
				},
				close: function() {
					if (stream.readyState < 2) {
						stream.readyState = 2;
						stream.options.reconnect = false;
						ws.close();
					}
				}
			});
		},
		
		// HTTP Streaming
		http: function(stream) {
			var // Transport
				transportFn,
				transport,
				// Low-level request and response handler
				handleOpen,
				handleMessage,
				handleSend,
				// Latch for AJAX
				sending,
				// Data queue
				dataQueue = [],
				// Helper object for parsing response
				message = {
					// The index from which to start parsing
					index: 0,
					// The temporary data
					data: ""
				};
			
			// Chooses a proper transport
			transportFn = transports[
				// xdr
				stream.options.enableXDR && window.XDomainRequest ? "xdr" :
				// iframe
				window.ActiveXObject ? "iframe" :
				// xhr
				window.XMLHttpRequest ? "xhr" : null];
			
			if (!transportFn) {
				return;
			}
			
			// Default response handler
			handleOpen = stream.options.handleOpen || function(text, message, stream) {
				// The top of the response is made up of the id and padding
				// optional identifier within the server
				stream.id = text.substring(0, text.indexOf(";"));
				// message.index = text.indexOf(";", stream.id.length + ";".length) + ";".length;
				// tonyg 20120525: If the current chunk doesn't include the end-of-padding marker,
				// don't accept the header until it does.
				var semiPos = text.indexOf(";", stream.id.length + 1);
				if (semiPos < 0) {
					return false;
				}
				message.index = semiPos + 1;
			};
			handleMessage = stream.options.handleMessage || function(text, message) {
				// Response could contain a single message, multiple messages or a fragment of a message
				// default message format is message-size ; message-data ;
				if (message.size == null) {
					// Checks a semicolon of size part
					var sizeEnd = text.indexOf(";", message.index);
					if (sizeEnd < 0) {
						return false;
					}
					
					message.size = +text.substring(message.index, sizeEnd);
					// index: sizeEnd + ";".length,
					message.index = sizeEnd + 1;
				}
				
				var data = text.substr(message.index, message.size - message.data.length);
				message.data += data;
				message.index += data.length;

				// Has stream message been completed?
				if (message.size !== message.data.length) {
					return false;
				}
				
				// Checks a semicolon of data part
				var dataEnd = text.indexOf(";", message.index);
				if (dataEnd < 0) {
					return false;
				}
				
				// message.index = dataEnd + ";".length;
				message.index = dataEnd + 1;
				
				// Completes parsing
				delete message.size;
			};
			
			// Default request handler
			handleSend = stream.options.handleSend || function(type, options, stream) {
				var metadata = {"metadata.id": stream.id, "metadata.type": type};
				
				options.data = 
					// Close
					type === "close" ? param(metadata) :
					// Send
					// converts data if not already a string
					((typeof options.data === "string" ? options.data : param(options.data)) + "&" + param(metadata));
			};
			
			transport = transportFn(stream, {
				response: function(text) {
					if (stream.readyState === 0) {
						if (handleOpen(text, message, stream) === false) {
							return;
						}
						
						stream.readyState = 1;
						trigger(stream, "open");
					}
					
					for (;;) {
						if (handleMessage(text, message, stream) === false) {
							return;
						}
						
						if (stream.readyState < 3) {
							// Pseudo MessageEvent
							trigger(stream, "message", {
								// Converts the data type
								data: stream.options.converters[stream.options.dataType](message.data), 
								origin: "", 
								lastEventId: "", 
								source: null, 
								ports: null
							});
						}
						
						// Resets the data
						message.data = "";
					}
				},
				close: function(isError) {
					var readyState = stream.readyState;
					stream.readyState = 3;
					
					if (isError) {
						// Prevents reconnecting
						stream.options.reconnect = false;
						
						// If establishing a connection fails, fires the close event instead of the error event 
						if (readyState === 0) {
							// Pseudo CloseEvent
							trigger(stream, "close", {
								wasClean: false, 
								code: null, 
								reason: ""
							});
						} else {
							trigger(stream, "error");
						}
					} else {
						// Pseudo CloseEvent
						trigger(stream, "close", {
							// Presumes that the stream closed cleanly
							wasClean: true, 
							code: null, 
							reason: ""
						});
						
						// Reconnect?
						if (stream.options.reconnect) {
							$.stream(stream.url, stream.options);
						}
					}
				}
			}, message);
			
			transport.open();
						
			// Overrides send and close
			$.extend(stream, {
				send: function(data) {
					if (stream.readyState === 0) {
						$.error("INVALID_STATE_ERR: Stream not open");
					}
					
					// Pushes the data into the queue
					dataQueue.push(data);
					
					if (!sending) {
						sending = true;
												
						// Performs an Ajax iterating through the data queue
						(function post() {
							if (stream.readyState === 1 && dataQueue.length) {
								var options = {url: stream.url, type: "POST", data: dataQueue.shift()};
								
								if (handleSend("send", options, stream) !== false) {
									$.ajax(options).complete(post);
								} else {
									post();
								}
							} else {
								sending = false;
							}
						})();
					}
				},
				close: function() {
					// Do nothing if the readyState is in the CLOSING or CLOSED
					if (stream.readyState < 2) {
						stream.readyState = 2;
						
						var options = {url: stream.url, type: "POST"};
						
						if (handleSend("close", options, stream) !== false) {
							// Notifies the server
							$.ajax(options);
						}

						// Prevents reconnecting
						stream.options.reconnect = false;
						transport.close();
					}
				}
			});
		}
		
	});
	
	$.extend(transports, {

		// XMLHttpRequest: Modern browsers except Internet Explorer
		xhr: function(stream, handler, message) {
			var stop,
				polling, 
				preStatus, 
				xhr = new window.XMLHttpRequest();
			
			xhr.onreadystatechange = function() {
				switch (xhr.readyState) {
				// Handles open and message event
				case 3:
					if (xhr.status !== 200) {
						return;
					}
					
					handler.response(xhr.responseText);
					
					// For Opera
					if ($.browser.opera && !polling) {
						polling = true;
						
						stop = iterate(function() {
							if (xhr.readyState === 4) {
								return false;
							}
							
							if (xhr.responseText.length > message.index) {
								handler.response(xhr.responseText);
							}
						}, stream.options.operaInterval);
					}
					break;
				// Handles error or close event
				case 4:
					// HTTP status 0 could mean that the request is terminated by abort method
					// but it's not error in Stream object
					handler.close(xhr.status !== 200 && preStatus !== 200);
					break;
				}
			};
			
			return {
				open: function() {
					xhr.open("GET", prepareURL(stream.url, stream.options.openData));
					xhr.send();
				},
				close: function() {
					if (stop) {
						stop();
					}
					
					// Saves status
					try {
						preStatus = xhr.status;
					} catch (e) {}
					xhr.abort();
				}
			};
		},

		// Hidden iframe: Internet Explorer
		iframe: function(stream, handler, message) {
			var stop,
				closed,
				onload = function() {
					if (!closed) {
						closed = true;
						handler.close();
					}
				},
				doc = new window.ActiveXObject("htmlfile");
			
			doc.open();
			doc.close();
			
			return {
				open: function() {
					var iframe = doc.createElement("iframe");
					iframe.src = prepareURL(stream.url, stream.options.openData);
					
					doc.body.appendChild(iframe);
					
					// For the server to respond in a consistent format regardless of user agent, we polls response text
					var cdoc = iframe.contentDocument || iframe.contentWindow.document;

					stop = iterate(function() {
						if (!cdoc.documentElement) {
							return;
						}
						
						// Detects connection failure
						if (cdoc.readyState === "complete") {
							try {
								$.noop(cdoc.fileSize);
							} catch(e) {
								handler.close(true);
								return false;
							}
						}
						
						var response = cdoc.body.lastChild,
							readResponse = function() {
								// Clones the element not to disturb the original one
								var clone = response.cloneNode(true);
								
								// If the last character is a carriage return or a line feed, IE ignores it in the innerText property 
								// therefore, we add another non-newline character to preserve it
								clone.appendChild(cdoc.createTextNode("."));
								
								var text = clone.innerText;
								return text.substring(0, text.length - 1);
							};
						
						// To support text/html content type
						if (!$.nodeName(response, "pre")) {
							// Injects a plaintext element which renders text without interpreting the HTML and cannot be stopped
							// it is deprecated in HTML5, but still works
							var head = cdoc.head || cdoc.getElementsByTagName("head")[0] || cdoc.documentElement,
								script = cdoc.createElement("script");
							
							script.text = "document.write('<plaintext>')";
							
							head.insertBefore(script, head.firstChild);
							head.removeChild(script);
							
							// The plaintext element will be the response container
							response = cdoc.body.lastChild;
						}
						
						// Handles open event
						handler.response(readResponse());
						
						// Handles message and close event
						stop = iterate(function() {
							var text = readResponse();
							if (text.length > message.index) {
								handler.response(text);
								
								// Empties response every time that it is handled
								response.innerText = "";
								message.index = 0;
							}

							if (cdoc.readyState === "complete") {
								onload();
								return false;
							}
						}, stream.options.iframeInterval);
						
						return false;
					});
				},
				close: function() {
					if (stop) {
						stop();
					}
					
					doc.execCommand("Stop");
					onload();
				}
			};
		},

		// XDomainRequest: Optionally Internet Explorer 8+
		xdr: function(stream, handler) {
			var xdr = new window.XDomainRequest(),
				rewriteURL = stream.options.rewriteURL || function(url) {
					// Maintaining session by rewriting URL
					// http://stackoverflow.com/questions/6453779/maintaining-session-by-rewriting-url
					var rewriters = {
						JSESSIONID: function(sid) {
							return url.replace(/;jsessionid=[^\?]*|(\?)|$/, ";jsessionid=" + sid + "$1");
						},
						PHPSESSID: function(sid) {
							return url.replace(/\?PHPSESSID=[^&]*&?|\?|$/, "?PHPSESSID=" + sid + "&").replace(/&$/, "");
						}
					};
					
					for (var name in rewriters) {
						// Finds session id from cookie
						var matcher = new RegExp("(?:^|;\\s*)" + encodeURIComponent(name) + "=([^;]*)").exec(document.cookie);
						if (matcher) {
							return rewriters[name](matcher[1]);
						}
					}
					
					return url;
				};
			
			// Handles open and message event
			xdr.onprogress = function() {
				handler.response(xdr.responseText);
			};
			// Handles error event
			xdr.onerror = function() {
				handler.close(true);
			};
			// Handles close event
			var onload = xdr.onload = function() {
				handler.close();
			};
			
			return {
				open: function() {
					xdr.open("GET", prepareURL(rewriteURL(stream.url), stream.options.openData));
					xdr.send();
				},
				close: function() {
					xdr.abort();
					onload();
				}
			};
		}
		
	});
		
	// Closes all stream when the document is unloaded 
	// this works right only in IE
	$(window).bind("unload.stream", function() {
		for (var url in instances) {
			instances[url].close();
			delete instances[url];
		}
	});
	
	$.each("streamOpen streamMessage streamError streamClose".split(" "), function(i, o) {
		$.fn[o] = function(f) {
			return this.bind(o, f);
		};
	});
	
	// Works even in IE6
	function getAbsoluteURL(url) {
		var div = document.createElement("div");
		div.innerHTML = "<a href='" + url + "'/>";

		return div.firstChild.href;
	}
	
	function trigger(stream, event, props) {
		event = event.type ? 
			event : 
			$.extend($.Event(event), {bubbles: false, cancelable: false}, props);
		
		var handlers = stream.options[event.type],
			applyArgs = [event, stream];
		
		// Triggers local event handlers
		for (var i = 0, length = handlers.length; i < length; i++) {
			handlers[i].apply(stream.options.context, applyArgs);
		}

		if (stream.options.global) {
			// Triggers global event handlers
			$.event.trigger("stream" + event.type.substring(0, 1).toUpperCase() + event.type.substring(1), applyArgs);
		}
	}
	
	function prepareURL(url, data) {
		// Converts data into a query string
		if (data && typeof data !== "string") {
			data = param(data);
		}
		
		// Attaches a time stamp to prevent caching
		var ts = $.now(),
			ret = url.replace(/([?&])_=[^&]*/, "$1_=" + ts);

		return ret + (ret === url ? (/\?/.test(url) ? "&" : "?") + "_=" + ts : "") + (data ? ("&" + data) : "");
	}
	
	function param(data) {
		return $.param(data, $.ajaxSettings.traditional);
	}
	
	function iterate(fn, interval) {
		var timeoutId;
		
		// Though the interval is 0 for real-time application, there is a delay between setTimeout calls
		// For detail, see https://developer.mozilla.org/en/window.setTimeout#Minimum_delay_and_timeout_nesting
		interval = interval || 0;
		
		(function loop() {
			timeoutId = setTimeout(function() {
				if (fn() === false) {
					return;
				}
				
				loop();
			}, interval);
		})();
		
		return function() {
			clearTimeout(timeoutId);
		};
	}
	
})(jQuery);