// common.js
// Copyright (C) 2006 Notefish. All rights reserved.
// commonly used functionality

// debugging support
var DebugConsoleHeight = 20;

var EnableDebug = false;

// extending the stadard window object
window.getCurWidth=
	function()
	{
		if (self.innerWidth)
			return self.innerWidth;
		else if (document.body.clientWidth)
			return document.body.clientWidth;
	};

window.getCurHeight=
	function()
	{
		if (self.innerHeight)
			return self.innerHeight;
		else if (document.body.clientHeight)
			return document.body.clientHeight;
	};

// extending the standard string object
String.prototype.trim=
	function()
	{
		var res = this;
		res = res.replace(/^\s*(.*)/, "$1");
		res = res.replace(/(.*?)\s*$/, "$1");
		return res;
	};

// for firefox, adding selectSingleNode and selectNodes methods
if (document.implementation.hasFeature("XPath", "3.0")){
	XMLDocument.prototype._selectNodes=
		function(path, node, maxCount)
		{
			var nsres = this.createNSResolver(this.documentElement);
			var items = this.evaluate(path, node, nsres, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
			var itemCnt = items.snapshotLength;
			var res = new Array();
			for (var i=0; i<itemCnt; ++i){
				if (maxCount && i>=maxCount) break;
				res.push(items.snapshotItem(i));
			}

			return res;
		};

	XMLDocument.prototype.selectNodes=
		function(path) { return this._selectNodes(path, this); };

	XMLDocument.prototype.selectSingleNode=
		function(path)
		{
			var coll = this._selectNodes(path, this, 1);
			if (coll.length==0) return null;
			return coll[0];
		};

	Element.prototype.selectNodes=
		function(path) { return this.ownerDocument._selectNodes(path, this); }

	Element.prototype.selectSingleNode=
		function(path)
		{
			var coll = this.ownerDocument._selectNodes(path, this, 1);
			if (coll.length==0) return null;
			return coll[0];
		};
}

// helper functions
function GetHttp()
{
	try{
		if (window.ActiveXObject)
			return new ActiveXObject("Microsoft.XMLHTTP");
		else
			return new XMLHttpRequest();
	}
	catch (a){}
	return null;
}

function GetXmlElemValue(xmlElem)
{
	if (xmlElem.ownerDocument.createRange){
		var r=xmlElem.ownerDocument.createRange();
		r.selectNode(xmlElem);
		return r.toString();
	}
	else return xmlElem.text;
}

function Round(x) { return Math.round(x); }

function GetElem(id) { return document.getElementById(id); }

function IsPlural(n) { return (n-1)%10!=0 || (n-11)%100==0; }

function FixEvent(e)
{
	if (!e) e=window.event;
	return e;
}

function KillEvent(e)
{
	e = FixEvent(e);
	e.cancelBubble = true;
	return false;
}

// a browser independent way to read element's currrent background color
function GetElemCurBkgColor(elem)
{
	if (elem.currentStyle)
		return elem.currentStyle.backgroundColor;
	else if (document.defaultView && document.defaultView.getComputedStyle){
		return document.defaultView.getComputedStyle(elem, null).getPropertyValue("background-color");
	}
}

// a browser independed way to read element's current positioning
function GetElemCurPosition(elem)
{
	if (elem.currentStyle)
		return elem.currentStyle.position;
	else if (document.defaultView && document.defaultView.getComputedStyle){
		return document.defaultView.getComputedStyle(elem, null).getPropertyValue("position");
	}
}

// get a left position of element relative document body
function GetAbsLeft(elem)
{
 	var cur = elem;
 	var left = 0;
 	var body = document.body;
 	while (cur != body && cur){
 		left += cur.offsetLeft;
 		cur = cur.offsetParent;
 	}

 	return left;
}

// get a top position of element relative document body
function GetAbsTop(elem)
{
 	var cur = elem;
 	var top = 0;
 	var body = document.body;
 	while (cur != body && cur){
 		top += cur.offsetTop;
 		cur = cur.offsetParent;
 	}

 	return top;
}

// replace unsafe html characters
function SafeHtml(s)
{
	var safe = s.replace(/&/g, "&amp;");
	safe = safe.replace(/>/g, "&gt;");
	safe = safe.replace(/</g, "&lt;");
	return safe;
}

// reload the current page
function ReloadPage()
{
	alert("Sorry! A server error occured.\nPlease retry after the page is reloaded");
	document.location = document.location.toString();
}

// a simple queue
function Queue()
{
	this.q = new Array();
}

// put an element into the end of the queue
Queue.prototype.push = function(e)
{
	this.q[this.q.length] = e;
};

// remove an element from the head of the queue and return it
Queue.prototype.pop = function()
{
	return this.q.splice(0, 1)[0];
};

// clear the queue
Queue.prototype.clear = function()
{
	this.q = new Array();
};

// check if a queue is empty
Queue.prototype.isEmpty = function()
{
	return this.q.length == 0;
};

// enumrator that works for both firefox and ie
function Enum(col)
{
	this.col=col;
	if (typeof(Enumerator)!="undefined")
		this.sysEnum=new Enumerator(col);
	else this.idx=0;
}

Enum.prototype.col=null;
Enum.prototype.sysEnum=null;
Enum.prototype.idx=-1;

Enum.prototype.atEnd=
	function()
	{
		if (this.sysEnum) return this.sysEnum.atEnd();
		else return this.col.length<=this.idx;
	};

Enum.prototype.moveNext=
	function()
	{
		if (this.sysEnum) this.sysEnum.moveNext();
		else this.idx++;
	};

Enum.prototype.item=
	function()
	{
		if (this.sysEnum) return this.sysEnum.item();
		else return this.col[this.idx];
	};

// http request object
function HttpReq() {}

HttpReq.prototype.url="";
HttpReq.prototype.req=null;
HttpReq.prototype.id="";   // text id of request, format is free
HttpReq.prototype.type=""; // a text description of request, e.g. "get-square"

// session timeout support
HttpReq.prototype.ignoreInvalSession = false; // set this to true if you don't want your
                                              // request to redirect the page to the timeout page
HttpReq.prototype.invalSession = false; // when this is true, the page is about to be
                                        // redirected, return immediately from your script

// load the resource specified, either asynchronously (default) or synchronously
// two methods supported: GET and POST
// if POST is used, the encoding for data is set to "application/x-www-form-urlencoded"
// parameters:
//   method:   HTTP method, can either be GET or POST
//   url:      a URL to load
//   data:     optional, data to post if POST is used
//   callback: optional, user-provided function called when request is successfully complete
//   async:    optional, default is true, specifies whether to perform asynchronously
//   queued:   optional, default is false, specifies whether the request is coming for a queue
//   nolog:    optional, default is false, indicates whether to log the request
HttpReq.prototype.load=
	function(method, url, data, callback, async, queued, nolog)
	{
		if (typeof data     == "undefined") data     = null;
		if (typeof callback == "undefined") callback = null;
		if (typeof async    == "undefined") async    = true;

		if (!nolog) debug.log("Accomplishing '"+method+"' request: "+url);
		try{
			this.queued = queued;
			this.nolog = nolog;
			this.url = url;
			this.onLoadComplete = callback;
			this.req = GetHttp();
			this.req.open(method, url, async);

			// setting form encoding if posting
			if (method.toUpperCase()=="POST" && data!=null)
				this.req.setRequestHeader("Content-Type",
					"application/x-www-form-urlencoded");

			var tmp = this;
			this.req.onreadystatechange =
				function()
				{
					if (tmp.req.readyState==4){
						var xml = tmp.req.responseXML.documentElement;
						if (xml && xml.tagName=="error"){
							var errType = xml.getAttribute("type");
							debug.log("Error in response for "+tmp.url+": "+errType);
							if (tmp.queued){
								// all queued requests must finish successfully
								ReloadPage();
							}
							else if (errType=="unauthorized"){
								// most likely, it's session timeout
								tmp.invalSession = true;
								if (!tmp.ignoreInvalSession)
									document.location = "index.php?timeout";
							}
							else if (errType=="old data"){
								// the request failed because
								// the client had obsolete data
								// simply refresh the page
								ReloadPage();
							}
							tmp._uninit();
							return;
						}
						if (tmp.onLoadComplete)
							tmp.onLoadComplete(tmp);
						tmp._uninit();
					}
				};
			this.req.send(data);
		}
		catch (a) {}
	};

HttpReq.prototype.cancel=
	function()
	{
		if (!this.req) return;
		this.req.onreadystatechange=function(){};
		this.req.abort();
	};

HttpReq.prototype._uninit =
	function()
	{
		this.req.onreadystatechange = function() {};
		this.onLoadComplete = null;
	};

// dragging support
// owner is an object that receives notifications about dragging
// handleId is just 1-based number for owner to know which handle
// is being dragged
function Drag(owner, handleId)
{
	this.owner=owner;
	this.handleId=handleId;
}

Drag.prototype.owner=null;
Drag.prototype.handleId=-1;
Drag.prototype.oldX=0;
Drag.prototype.oldY=0;

Drag.prototype.start=
	function(e)
	{
		e=FixEvent(e);
		this.oldX=e.clientX;
		this.oldY=e.clientY;

		var curDrag=this;
		document.onmousemove=function(e) { curDrag.drag(e); }
		document.onmouseup=function(e) { curDrag.end(e); }
		if (typeof document.body.setCapture!="undefined")
			document.body.setCapture();

		// disabling selection during dragging
		document.onselectstart=function(e) { return false; } // this will work for IE
		document.body.style.MozUserSelect = "none";          // this will work for Mozilla
	};

Drag.prototype.end=
	function(e)
	{
		document.onmousemove=null;
		document.onmouseup=null;
		document.onselectstart=null;
		document.body.style.MozUserSelect = "";
		if (typeof document.releaseCapture!="undefined")
			document.releaseCapture();
		this.owner.onDragEnd();
	};

Drag.prototype.drag=
	function(e)
	{
		e=FixEvent(e);

		var dx=e.clientX-this.oldX;
		var dy=e.clientY-this.oldY;

		this.oldX=e.clientX;
		this.oldY=e.clientY;

		this.owner.onDrag(e.clientX, e.clientY, dx, dy);
	};

// event multiplexer
// allows to connect multiple event listeners to a single source
function MultiEvent()
{
	this.handlers = new Array();
}

// array of event handers
MultiEvent.prototype.handlers = null;

// attach an event handler
// save the cookie returned to detach the handler later
MultiEvent.prototype.attachHandler =
	function(handler)
	{
		var hdl = this.handlers;
		var len = hdl.length;

		var i=0;
		for (; i<len; ++i)
			if (!hdl[i]) break;
		hdl[i] = handler;

		return i;
	};

MultiEvent.prototype.detachHandler =
	function(cookie)
	{
		this.handlers[cookie] = null;
	};

// pass this function as a handler
// to the desired event
// use closure
MultiEvent.prototype.handler =
	function(e)
	{
		var hdl = this.handlers;
		var len = hdl.length;
		for (var i=0; i<len; ++i)
			if (hdl[i]) hdl[i](e);
	};

// returns number of currently attached handlers
MultiEvent.prototype.getCount =
	function()
	{
		var res = 0;
		var hdl = this.handlers;
		var len = hdl.length;
		for (var i=0; i<len; ++i)
			if (hdl[i]) ++res;
		return res;
	};

// a collection of dialogs
// attaches to a multiplexer on the onscroll event
// also owns the document onscroll event
function Dialogs()
{
	this.dlgs = new Array();
	var d = this; // to create closure
	document.onkeydown = function(e) { d._onKeyDown(e); };
	this.scrollCookie = window.onScrollHandler.attachHandler(
		function(e){ d._onScroll(e); }
	);
}

Dialogs.prototype.dlgs = null;       // array of dialogs
Dialogs.prototype.curDlg = null;     // the currently active dialog
Dialogs.prototype.scrollCookie = -1; // cookie returned by scrolling multiplexer

// uninit the object
// make sure you call it to avoid IE memory leaks
Dialogs.prototype.uninit =
	function()
	{
		var dlgs = this.dlgs;
		if (dlgs){
			var len = dlgs.length;
			for (var i=0; i<len; ++i){
				dlgs[i].uninit();
				dlgs[i] = null;
			}
			this.dlgs = null;
		}
		if (this.scrollCookie!=-1){
			window.onScrollHandler.detachHandler(this.scrollCookie);
			this.scrollCookie = -1;
		}

		this.curDlg = null;
	};

// add a dialog object
// a dialog object must provide the following methods
//   * open:   // open the dialog
//   * cancel: // close the dialog without saving changes
//   * ok:     // ok was clicked, returns true on dialog close
//   * uninit: // uninitialize the dialog
// a dialog object may profive the following property
//   * okOnEnter: // when true hitting Enter will OK the current dialog
// dialog objects are identified by the 'id' attribute,
// the dialog html must also bear the same id
Dialogs.prototype.addDialog =
	function(dlg)
	{
		this.dlgs.push(dlg);
		var html = GetElem(dlg.id);
		dlg.width = html.offsetWidth;
		dlg.height = html.offsetHeight;
		html.style.display = "none";
		html.style.visibility = "";
	};

Dialogs.prototype.openDialog =
	function(id)
	{
		var dlg = this.getDialog(id);
		if (!dlg) throw "Dialog not found: "+id;
		if (dlg==this.curDlg) return;

		if (this.curDlg) this.cancelCurDlg();

		this._posDlg(dlg);
		dlg.open();
		this.curDlg = dlg;
	};

Dialogs.prototype.cancelCurDialog =
	function()
	{
		if (!this.curDlg) return;
		this.curDlg.cancel();
		this._hideCurDlg();
	};

Dialogs.prototype.okCurDialog =
	function()
	{
		if (!this.curDlg) throw "No dialog open";
		if (this.curDlg.ok()) this._hideCurDlg();
	};

Dialogs.prototype.reposCurDialog =
	function()
	{
		var dlg = this.curDlg;
		if (!dlg) throw "No dialog open";

		var hdlg   = GetElem(dlg.id);
		dlg.width  = hdlg.offsetWidth;
		dlg.height = hdlg.offsetHeight;
		this._posDlg(dlg);
	};

Dialogs.prototype.getDialog =
	function(id)
	{
		var dlgs = this.dlgs;
		var len = dlgs.length;
		for (var i=0; i<len; ++i)
			if (dlgs[i].id==id) return dlgs[i];
		return null;
	};

Dialogs.prototype._posDlg = 
	function(dlg)
	{
		var html = GetElem(dlg.id);
		var w = dlg.width;
		var h = dlg.height;
		var ww = window.getCurWidth();
		var wh = window.getCurHeight();
		var sl = document.body.scrollLeft;
		var st = document.body.scrollTop;
		html.style.left = ww/2-w/2+sl;
		html.style.top  = wh/2-h/2+st;
		html.style.display = "";
		document.body.scrollTop = st; // this is to fix the FF bug
	};

Dialogs.prototype._hideCurDlg =
	function()
	{
		var html = GetElem(this.curDlg.id);
		html.style.display = "none";
		this.curDlg = null;
	};

Dialogs.prototype._onKeyDown =
	function(e)
	{
		if (!this.curDlg) return;
		e = FixEvent(e);
		switch (e.keyCode){
		case 27: // escape
			this.cancelCurDialog();
			break;
		case 13: // enter
			if (this.curDlg.okOnEnter) this.okCurDialog();
			break;
		};
	};

Dialogs.prototype._onScroll =
	function(e)
	{
		if (this.curDlg) this.reposCurDialog();
	};

// debug object
// provides a debugging console
function Debug() {}

Debug.prototype.console = null;

Debug.prototype.init=
	function()
	{
		var console = document.createElement("div");
		console.id = "debug-console";

		console.style.height = DebugConsoleHeight+"em";

		console.style.border = "solid 1px #808080";
		console.style.fontFamily = "Courier New";
		console.style.fontSize = "10pt";

		console.style.overflow = "scroll";

		this.console = console;
		document.body.appendChild(console);

		this.log("Debug console is ready");
		this.log("Script version 0.5");
	};

Debug.prototype.uninit=
	function()
	{
		if (this.console) document.removeChild(this.console);
		this.console = null;
	};

Debug.prototype.log=
	function(msg)
	{
		var console = this.console;
		if (!console) return;

		var now = new Date();
		var hrs = now.getHours().toString();
		if (hrs.length==1) hrs = "0"+hrs;
		var min = now.getMinutes().toString();
		if (min.length==1) min = "0"+min;
		var sec = now.getSeconds().toString();
		if (sec.length==1) sec = "0"+sec;
		var time = hrs+":"+min+":"+sec+" ";

		console.innerHTML += time;
		console.innerHTML += msg;
		console.innerHTML += "<br>";

		console.scrollTop = console.scrollHeight; // scroll to the end
	};

var debug=new Debug();

// simple time interval
// for performance evaluation
function TimeInterval()
{
	this.start = new Date();
}

TimeInterval.prototype.start = null;
TimeInterval.prototype.duration = 0;

TimeInterval.prototype.cut =
	function()
	{
		this.duration = new Date() - this.start;
	};
