function gcbos_engine()
{
	this.console = null;
	this.input = null;
	this.notice = null;
	
	this.typingInterval = -1;
	this.typingTime = new Date();
	this.typingSpeedMin = 5;
	this.typingSpeedMax = 50;
	this.typingSpeed = 1;
	
	this.checkInterval = -1;
	this.checkTime = new Date(0);
	this.checkSpeedMin = 1000;
	this.checkSpeedMax = 50000;
	this.checkSpeed = this.checkSpeedMin;
	
	this.typingLeft = '';
	this.start = -1;
	this.request = null;
	this.autoScrollOffset = 50;
	this.tags = [];
	this.tagStack = [];
	this.lastElement = null;
	this.lastTextNode = null;
	this.messages = [];
	
	this.enabled = true;
	
	this.init = function()
	{
		var handle = this;
		
		this.console = fetch_object("gcbos_console");
		this.input = fetch_object("gcbos_input");
		this.notice = fetch_object("gcbos_notice");
		
		if (fetch_cookie("gcbos_speed") !== null)
		{
			this.typingSpeed = fetch_cookie("gcbos_speed");
		}
		if (fetch_cookie("gcbos_height") !== null)
		{
			this.setHeight(fetch_cookie("gcbos_height"));
		}
		
		this.enter = new YAHOO.util.KeyListener(this.input, {keys: YAHOO.util.KeyListener.KEY.ENTER}, {fn: this.sendInput, scope: handle, correctScope: handle});
		this.speeddown = new YAHOO.util.KeyListener(this.input, {keys: YAHOO.util.KeyListener.KEY.UP}, {fn: this.speedDown, scope: handle, correctScope: handle});
		this.speedup = new YAHOO.util.KeyListener(this.input, {keys: YAHOO.util.KeyListener.KEY.DOWN}, {fn: this.speedUp, scope: handle, correctScope: handle});
		
		this.tags["instant"] = 
		{
			"open": function(tag, option, closed)
			{
				if ((!!option) || (closed)) return false;
				handle.instant = true;
				return true;
			},
			"close": function()
			{
				handle.instant = false;
			}
		};
		this.tags["notice"] =
		{
			"open": function(tag, option, closed)
			{
				handle.clearNotice();
				if ((!!option) || (closed)) return false;
				handle.lastElement = handle.notice;
				handle.lastElement.appendChild(handle.lastTextNode = document.createTextNode(''));
				return true;
			},
			"close": function()
			{
				handle.lastElement = handle.console;
				handle.lastElement.appendChild(handle.lastTextNode = document.createTextNode(''));
			}
		};
		this.tags["message"] =
		{
			"open": function(tag, option, closed)
			{
				if ((!option) || (closed)) return false;
				option = option.split('|');
				
				var id = parseInt(option[0], 10);
				var message = option[1];
				
				if ((typeof(handle.messages[id]) == 'undefined') || (id <= 0))
				{
					handle.console.appendChild(handle.lastElement = document.createElement('div'));
					handle.lastElement.appendChild(handle.lastTextNode = document.createTextNode(''));
					
					handle.messages[id] = handle.lastElement;
					handle.lastElement.gcbosid = id;
					handle.lastElement.message = message
				}
				else
				{
					var match = handle.typingLeft.match(/^(\w+)(|=(["|']?)([\s\S]+?)\3)\s*(\/|)\](.*?\[\/\1\])/i);
					handle.typingLeft = handle.typingLeft.substr(match[6].length);
				}
				
				return true;
			},
			"close": function()
			{
				handle.lastElement = handle.console;
				handle.lastElement.appendChild(handle.lastTextNode = document.createTextNode(''));
			}
		};
		this.tags["edit"] =
		{
			"open": function(tag, option, closed)
			{
				if ((!option) || (closed)) return false;
					handle.lastElement.appendChild(handle.lastElement = document.createElement('span'));
					handle.lastElement.appendChild(handle.lastTextNode = document.createTextNode(''));
					YAHOO.util.Event.addListener(handle.lastElement, 'dblclick', handle.editMessage, handle, handle);
					return true;
			},
			"close": function()
			{
				handle.lastElement = handle.lastElement.parentNode;
				handle.lastElement.appendChild(handle.lastTextNode = document.createTextNode(''));
			}
		};
		this.tags["delmessage"] =
		{
			"open": function(tag, option, closed)
			{
				if ((!option) || (closed)) return false;
				var title = document.createAttribute('title');
				title.value = '#' + option;
				
				handle.lastElement.appendChild(handle.lastElement = document.createElement('span'));
				handle.lastElement.appendChild(handle.lastTextNode = document.createTextNode(''));
				handle.lastElement.setAttributeNode(title);
				
				YAHOO.util.Event.addListener(handle.lastElement, 'click', handle.deleteMessage, handle, handle);
				return true;
			},
			"close": function()
			{
				handle.lastElement = handle.lastElement.parentNode;
				handle.lastElement.appendChild(handle.lastTextNode = document.createTextNode(''));
			}
		};
		this.tags["modify"] =
		{
			"open": function(tag, option, closed)
			{
				if ((!option) || (closed)) return false;
				option = option.split('|');
				var id = option[0];
				var del = option[1];
				var message = option[2];
				if (handle.messages[id])
				{
					if (del == '1')
					{
						handle.messages[id].parentNode.removeChild(handle.messages[id]);
						handle.messages[id] = null;
					}
					else
					{
						handle.messages[id].message = message;
						handle.lastElement = handle.messages[id];
						while (handle.lastElement.firstChild)
						{
							handle.lastElement.removeChild(handle.lastElement.firstChild);
						}
						handle.lastElement.appendChild(handle.lastTextNode = document.createTextNode(''));
					}
				}
				else
				{
					var match = handle.typingLeft.match(/^(\w+)(|=(["|']?)([\s\S]+?)\3)\s*(\/|)\](.*?\[\/\1\])/i);
					handle.typingLeft = handle.typingLeft.substr(match[6].length);
				}
				return true;
			},
			"close": function()
			{
				handle.lastElement = handle.console;
				handle.lastElement.appendChild(handle.lastTextNode = document.createTextNode(''));
			}
		};
		this.tags["delete"] = 
		{
			"open": function(tag, option, closed)
			{
				if ((!!option) || (closed)) return false;
				handle.buildingDelete = true;
				handle.deleting = false;
				handle.forDeletion = 0;
				return true;
			},
			"close": function()
			{
				handle.buildingDelete = false;
				handle.deleting = true;
			}
		};
		this.tags["pm"] = 
		{
			"open": function(tag, option, closed)
			{
				if ((!option) || (closed)) return false;
				handle.lastElement.appendChild(handle.lastElement = document.createElement('span'));
				handle.lastElement.appendChild(handle.lastTextNode = document.createTextNode(''));
				handle.lastElement.username = option;
				YAHOO.util.Event.addListener(handle.lastElement, 'click', handle.sendPm, handle, handle);
				return true;
			},
			"close": function()
			{
				handle.lastElement = handle.lastElement.parentNode;
				handle.lastElement.appendChild(handle.lastTextNode = document.createTextNode(''));
			}
		};
		this.tags["clear"] = 
		{
			"open": function(tag, option, closed)
			{
				if ((option) || (!closed)) return false;
				handle.clearConsole();
				return true;
			}
		};
		this.dummyElements = this.elementList("area", "base", "basefont", "br", "col", "frame", "hr", "img", "input", "isindex", "link", "meta", "param", "embed");
		
		this.enter.enable();
		this.speedup.enable();
		this.speeddown.enable();
		
		this.clearConsole();
		
		this.lastElement = this.console;
		this.console.appendChild(this.lastTextNode = document.createTextNode(''));
		
		this.checkMessages();
		
		this.typingInterval = window.setInterval(function(){handle.continueTyping();}, this.typingSpeedMax);
		this.checkInterval = window.setInterval(function(){handle.checkMessages();}, this.checkSpeed);
	}
	
	this.sendPm = function(e)
	{
		var message = e.target || e.srcElement;
		var text = this.input.value;
		var match = text.match(/^\/pm (("|').*?\2|\w+?)\s+/i);
		
		while (typeof(message.username) == "undefined") message = message.parentNode;
		
		if (match)
		{
			text = text.substr(match[0].length);
		}
		
		//this.input.value = '/pm "' + message.username.replace('"', '\\"') + '" ' + text;
	}
	
	this.editMessage = function(e)
	{
		var message = e.target || e.srcElement;
		
		while (typeof(message.gcbosid) == "undefined") message = message.parentNode;
		
		this.input.value = '/edit ' + message.gcbosid + ' ' + message.message;
		this.input.focus();
	}
	
	this.deleteMessage = function(e)
	{
		var message = e.target || e.srcElement;
		
		while (typeof(message.gcbosid) == "undefined") message = message.parentNode;
		
		this.input.value = '/delete ' + message.gcbosid;
		this.input.focus();
	}
	
	this.htmlEntitiesDecode = function(input)
	{
		var parser = document.createElement('textarea');	// I'm sure there's a law against this.
		parser.innerHTML = input;
		return parser.value;								// Damn.
	}
	
	this.elementList = function()
	{
		var result = new Object();
		for (var i = 0; i < arguments.length; i++)
		{
			result[arguments[i]] = true;
		}
		return result;
	}
	
	this.speedUp = function()
	{
		var expires = new Date();
		expires.setDate(expires.getDate()+7);
		this.typingSpeed++;
		if (this.typingSpeed > this.typingSpeedMax) this.typingSpeed = this.typingSpeedMax;
		set_cookie('gcbos_speed', this.typingSpeed, expires);
	}
	
	this.speedDown = function()
	{
		var expires = new Date();
		expires.setDate(expires.getDate()+7);
		this.typingSpeed--;
		if (this.typingSpeed < this.typingSpeedMin) this.typingSpeed = this.typingSpeedMin;
		set_cookie('gcbos_speed', this.typingSpeed, expires);
	}
	
	this.buildQuery = function(data)
	{
		var result = "";
		
		for (var key in data)
		{
			if (result.length > 0) result += "&";
			result += encodeURIComponent(key) + "=" + encodeURIComponent(data[key]);
		}
		
		return result;
	}

	this.sendInput = function()
	{
		if (this.input.value.length > 0)
		{
			var query =
			{
				action: 'send_message',
				ajax: true,
				start: this.start,
				message: this.input.value
			};
			var callback =
			{
				success: this.processMessages,
				argument: query,
				handle: this
			}
			
			if ((this.request) && (YAHOO.util.Connect.isCallInProgress(this.request))) YAHOO.util.Connect.abort(this.request);
			
			this.request = YAHOO.util.Connect.asyncRequest('POST', 'gcbos.php', callback, this.buildQuery(query));
		}
		
		this.input.value = '';
	}
	
	this.clearConsole = function()
	{
		while (this.console.firstChild)
		{
			this.console.removeChild(this.console.firstChild);
		}
		
		this.lastElement = this.console;
		this.lastElement.appendChild(this.lastTextNode = document.createTextNode(''));
	}
	
	this.clearNotice = function()
	{
		while (this.notice.firstChild)
		{
			this.notice.removeChild(this.notice.firstChild);
		}
	}
	
	this.checkMessages = function(handle)
	{
		if (handle == null) handle = this;
		if (!handle.enabled) return;
		
		var now = new Date();
		var mil = now.getTime();
		var last = handle.checkTime.getTime();
		
		if (mil - last < handle.checkSpeed)
		{
			return;
		}
		
		handle.checkTime = now;
		
		var query =
		{
			action: 'list_messages',
			ajax: true,
			start: handle.start
		};
		var callback =
		{
			success: handle.processMessages,
			argument: query,
			handle: handle
		};
		
		if ((handle.request) && (YAHOO.util.Connect.isCallInProgress(handle.request))) return;
		
		handle.request = YAHOO.util.Connect.asyncRequest('POST', 'gcbos.php', callback, handle.buildQuery(query));
	}
	
	this.processMessages = function(r)
	{
		var response = r.responseText;
		var last = r.getResponseHeader['GCBOS-Start'] || r.getResponseHeader['Gcbos-Start']; // Wtf safari camel cases headers?!
		last = parseInt(last, 10);
		
		if ((last < this.handle.start) || (response.length < 1))
		{
			this.handle.checkSpeed += this.handle.checkSpeedMin / 2;
			if (this.handle.checkSpeed > this.handle.checkSpeedMax) this.handle.checkSpeed = this.handle.checkSpeedMax;
			return;
		}
		else
		{
			this.handle.checkSpeed = this.handle.checkSpeedMin;
		}
		
		this.handle.start = last;
		this.handle.request = null;
		
		if (response == null) response = '';
		
		if (response.length > 0)
		{
			if (this.handle.checkInterval <= 0)
			{
				clearInterval(this.handle.checkInterval);
				this.handle.checkInterval = -1;
			}
			
			this.handle.typingLeft += response;
			this.handle.checkMessages();
		}
		else
		{
			if (this.handle.checkInterval < 0) this.handle.checkInterval = setInterval(this.handle.checkMessages, this.handle.checkSpeed, this.handle);
		}
	}
	
	this.continueTyping = function(handle)
	{
		if ((this.continueTyping == null) && (handle != null)) return handle.continueTyping(handle);
		if (!this.enabled) return;
		
		var now = new Date();
		var mil = now.getTime();
		var last = this.typingTime.getTime();
		var autoscroll = !(this.console.scrollTop < (this.console.scrollHeight - this.console.clientHeight) - this.autoScrollOffset);
		var typed = false;
		
		this.typingTime = now;
		
		while (((mil - last >= this.typingSpeed) || (this.instant)) && (this.typingLeft.length > 0))
		{
			mil -= this.typingSpeed;
			typed = true;
			
			if (this.deleting)
			{
				this.forDeletion--;
				
				if (this.forDeletion <= 0)
				{
					this.forDeletion = 0;
					this.deleting = false;
					this.buildingDelete = false;
				}
				
				if ((this.lastElement.lastChild != null) && (this.lastElement.lastChild.nodeType == 3) && (this.lastElement.lastChild.data.length > 0))
				{
					this.lastElement.lastChild.deleteData(this.lastElement.lastChild.data.length - 1, 1);
				}
				else if (this.lastElement.lastChild != null)
				{
					this.lastElement.removeChild(this.lastElement.lastChild);
					this.forDeletion++;
				}
			}
			else
			{
				var letter = this.typingLeft.substr(0, 1);
				this.typingLeft = this.typingLeft.substr(1);
				
				if (!this.lastTextNode) this.lastElement.appendChild(this.lastTextNode = document.createTextNode(''));
				
				if (letter == '[')
				{
					if (this.typingLeft.substr(0, 1) == '!')
					{
						this.typingLeft = this.typingLeft.substr(1);
					}
					else if (this.processCode())
					{
						continue;
					}
				}
				else if (letter == '<')
				{
					if (this.processHtml()) continue;
				}
				else if (letter == '&')
				{
					var match = this.typingLeft.match(/^#?(\w+);/);
					
					if (match)
					{
						this.typingLeft = this.typingLeft.substr(match[0].length);
						letter = this.htmlEntitiesDecode('&' + match[0]);
					}
				}
				else
				{
					if (this.instant)
					{
						var match = this.typingLeft.match(/^(\w+)/);
						
						if (match)
						{
							letter += this.typingLeft.substr(0, match[0].length);
							this.typingLeft = this.typingLeft.substr(match[0].length);
						}
					}
				}
				
				if (this.buildingDelete) this.forDeletion += letter.length;
				
				this.appendText(letter);
			}
		}
		
		if ((autoscroll) && (typed)) this.console.scrollTop = this.console.scrollHeight;
	}
	
	this.processCode = function()
	{
		if (this.typingLeft.substr(0, 1) == '/') return this.closeCode();
		
		var match = this.typingLeft.match(/^(\w+)(|=(["|']?)([\s\S]+?)\3)\s*(\/|)\]/);

		if (!match) return false;
		
		var tag = match[1];
		var option = match[4];
		var closed = !!match[5];
		var laterClosed = false;
		var result = false;
		
		if ((match[3] != '') && (option)) option = option.replace(new RegExp('\\\\' + match[3], 'gi'), match[3]);
		if (option) option = option.replace(/\\([\[\]])/gi, '$1');
		if (!this.tags[tag]) return false;
		
		var restOfMessage = this.typingLeft.substr(0, this.typingLeft.match(/^([\s\S]*?)(\[message|$)/)[0].length);
		laterClosed = restOfMessage.match(new RegExp("\\[\\/" + tag + "\\]", 'i'));
		
		if ((!closed) && (!laterClosed)) return false;
		
		if (!closed) this.tagStack.push(tag);
		
		if (this.tags[tag]["open"](tag, option, closed))
		{
			this.typingLeft = this.typingLeft.substr(match[0].length);
			return true;
		}
		else
		{
			return false;
		}
	}
	
	this.processHtml = function()
	{
		if (this.typingLeft.substr(0, 1) == '/') return this.closeHtml();
		
		var match = this.typingLeft.match(/^(\w+)((?:\s+\w+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/);
		var attr = [];
		
		if (!match) return false;
		
		var tag = match[0];
		var name = match[1];
		var attributes = match[2];
		var closed = !!match[3] || this.dummyElements[name];
		var handle = this;
		
		if ((!closed) && (!this.typingLeft.substr(0, this.typingLeft.match(/^(.*?)(\[reset \/\])?$/m)[0].length).match(new RegExp("<\\/" + name + ">", 'i')))) return false;
		
		var element = document.createElement(name);
		
		attributes.replace(
			/(\w+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g,
			function()
			{
				var name = arguments[1];
				var value = arguments[2] ? arguments[2] :
							arguments[3] ? arguments[3] :
							arguments[4] ? arguments[4] :
							'';
				
				value = value.replace(/&#?(\w+);/g, handle.htmlEntitiesDecode);
				
				if (name == "style")
				{
					element.style.cssText = value;
				}
				else
				{
					var attr = document.createAttribute(name);
					attr.value = value;
					element.setAttributeNode(attr);
				}
			}
		);
		
		this.lastElement.appendChild(element);
		
		if (!closed) this.lastElement = element;
		
		this.lastElement.appendChild(this.lastTextNode = document.createTextNode(''));
		
		this.typingLeft = this.typingLeft.substr(match[0].length);
		
		return true;
	}
	
	this.closeCode = function()
	{
		if (this.tagStack.length < 1) return false;
		
		var match = this.typingLeft.match(/^\/(\w+)\]/);
		var found = false;
		var position = -1;
		
		if (!match) return false;
		
		for (var i in this.tagStack)
		{
			if (this.tagStack[i].toLowerCase() == match[1].toLowerCase())
			{
				this.tags[this.tagStack[i]]["close"]();
				
				this.typingLeft = this.typingLeft.substr(match[0].length);
				
				found = true;
				position = i;
				
				break;
			}
		}
		
		if (found)
		{
			var stack = [];
			
			for (var i in this.tagStack)
			{
				if (i != position)
				{
					stack.push(this.tagStack[i]);
				}
			}
			
			this.tagStack = stack;
			
			return true;
		}
		else
		{
			return false;
		}
	}
	
	this.closeHtml = function()
	{
		var match = this.typingLeft.match(new RegExp("^\\/" + this.lastElement.nodeName + ">", 'i'));
		if (!match) return false;
		
		this.lastElement = this.lastElement.parentNode;
		this.lastElement.appendChild(this.lastTextNode = document.createTextNode(''));
		
		this.typingLeft = this.typingLeft.substr(match[0].length);
		return true;
	}
	
	this.appendText = function(text)
	{
		var scrollAfter = true;
		var hasNewline = false;
		
		if (this.console.scrollTop < (this.console.scrollHeight - this.console.clientHeight) - this.autoScrollOffset) scrollAfter = false;
		
		if (text.substr(text.length - 1) == "\n")
		{
			text = text.substr(0, text.length - 1);
			hasNewline = true;
		}
		
		this.lastTextNode.appendData(text);
		
		if (hasNewline)
		{
			this.lastElement.appendChild(document.createElement("br"));
			this.lastElement.appendChild(this.lastTextNode = document.createTextNode("\n"));
		}
		
		if (scrollAfter) this.console.scrollTop = this.console.scrollHeight;
	}
	
	this.modifyHeight = function(height)
	{
		this.setHeight(parseInt(this.console.style.height) + height);
	}
	
	this.setHeight = function(height)
	{
		if (height <= 0) height = 0;
		
		this.console.style.height = height + "px";
		
		var expires = new Date();
		expires.setDate(expires.getDate()+7);
		set_cookie('gcbos_height', height, expires);
		
		this.setEnabled(height > 0);
	}
	
	this.setEnabled = function(enabled)
	{
		if (enabled == this.enabled) return;
		
		if (enabled)
		{
			this.console.style.display = 'block';
			this.input.style.display = 'block';
		}
		else
		{
			this.console.style.display = 'none';
			this.input.style.display = 'none';
		}
		
		this.enabled = enabled;
	}
}