/*
Direct DOM element setting:
===========================
Form:
-----
// First need elem:
var elem = document.getElementById('elemid');

Examples:

elemText.value = 'xyz';
s = elemSelect.options[elemSelect.selectedIndex].text;


*/
//var CHAN_NAME_CLIENTCONTROL = 'clientcontrol';
//var CHAN_ROLE_CLICTL_WEB = 'web';
var HTTP_STATUS_OK = 200;
var HTTP_STATUS_CREATED = 201;
var HTTP_STATUS_NODATA = 204;
var HTTP_STATUS_NOTFOUND = 404;
var HTTP_STATUS_NEEDMORE = 405;
var HTTP_STATUS_INVALIDDATA = 406;

var LISTDIRS = "LISTDIRS";
var LISTFILES = "LISTFILES";
var LISTDATABASES = "LISTDATABASES";

var CHAN_ROLE_RPC_CLIENT = 'rpcc';
var CHAN_ROLE_RPC_SERVER = 'rpcs';
var RPC_OK = 200;
var RPC_IDLE = 210;
var RPC_NORESPONSE = 220;
var RPC_TIMEOUT = 290;
var RPC_AUTHFAIL = 401;
var RPC_ERROR = 500;

var C_ERROR = '#e00000';
var C_SUCCESS = '#008000';
var C_WARN = '#D07000';
var C_INFO = '#000090';
var C_PROMPT = '#0000D0';

var JSON_OBJECT = 1;
var JSON_ARRAY = 2;
var JSON_STRING = 3;
var JSON_ERROR = -1;

var sHostDocRoot = null;
var saQueryStringParams = new Array();
var sUser = "", sDatabase = "", sClient = "", sLoginLevel = "";
//var saLocation = new Array();
var bClientOnline = false;
var sClientChannelId = null;
var iSrvrTimeDif = 0;
var panDebug = null;
var logring = null;
var panStatus = null;
var sNextLogringSeq = '-1';
var iRpcSeq = 0;

//var bIE, bOldIE;
//
//if (navigator.appName.indexOf('Explorer') == -1)
//{
//	bOldIE = bIE = false;
//}
//else
//{
//	bIE = true;
//	var ua = navigator.userAgent;
//	var re = /MSIE\s+([^\);]+)(\)|;)/;
//
//	re.test(ua);
//	var version = Number(RegExp.$1);
//	if (version < 8)
//		bOldIE = true;
//}

String.prototype.isDigit = function() { return ( this >= '0' && this <= '9' ); };
String.prototype.isAlpha = function () { return (this >= 'a' && this <= 'z\uffff') || 
	(this >= 'A' && this <= 'Z\uffff'); };

// OLD WAY:
// String.prototype.trim = function() { return this.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); };
// From Steve Levithan and author of:
// http://yesudeep.wordpress.com/2009/07/31/even-faster-string-prototype-trim-implementation-in-javascript/
String.whiteSpace = [];
String.whiteSpace[0x0009] = true;
String.whiteSpace[0x000a] = true;
String.whiteSpace[0x000b] = true;
String.whiteSpace[0x000c] = true;
String.whiteSpace[0x000d] = true;
String.whiteSpace[0x0020] = true;
String.whiteSpace[0x0085] = true;
String.whiteSpace[0x00a0] = true;
String.whiteSpace[0x1680] = true;
String.whiteSpace[0x180e] = true;
String.whiteSpace[0x2000] = true;
String.whiteSpace[0x2001] = true;
String.whiteSpace[0x2002] = true;
String.whiteSpace[0x2003] = true;
String.whiteSpace[0x2004] = true;
String.whiteSpace[0x2005] = true;
String.whiteSpace[0x2006] = true;
String.whiteSpace[0x2007] = true;
String.whiteSpace[0x2008] = true;
String.whiteSpace[0x2009] = true;
String.whiteSpace[0x200a] = true;
String.whiteSpace[0x200b] = true;
String.whiteSpace[0x2028] = true;
String.whiteSpace[0x2029] = true;
String.whiteSpace[0x202f] = true;
String.whiteSpace[0x205f] = true;
String.whiteSpace[0x3000] = true;

String.prototype.trim = function()
{
	str = this;
	var len = str.length;
	if (len)
	{
		var whiteSpace = String.whiteSpace, i = 0;
		while (whiteSpace[str.charCodeAt(--len)]);
		if (++len)
		{
			while (whiteSpace[str.charCodeAt(i)])
			{
				++i;
			}
		}
		str = str.substring(i, len);
	}
	return str;
}


function debug(sText)
{
	var bKeepScrollbarAtBottom = false;

	// Escape '<' and '>' so we see any html in sText in its raw form
	var sEscapedText = hhmmss() + ' ' + sText.replace(/</g, '&lt;');
	sEscapedText = sEscapedText.replace(/>/g, '&gt;');
//	if (panDebug.scrollTop == (panDebug.scrollHeight - panDebug.offsetHeight))
//		bKeepScrollbarAtBottom = true;
	addContent(panDebug, sEscapedText + '<br>');
//	if (bKeepScrollbarAtBottom)
//		panDebug.scrollTop = panDebug.scrollHeight;
}


function debugObj(prefix, obj)
{
	var props = '';
	for (var prop in obj)
		props += prop + ': ' + obj [prop] + ', ';
	debug(prefix + ': ' + props);
}


function setStatus(sMsg, sStyle, elem)
{
	if (elem)
	{
		if (typeof(elem) == 'string')
			elem = document.getElementById(elem);
	}
	if (! elem)
		elem = panStatus;
	if (! sStyle || sStyle.charAt(0) != '#')
		sStyle = C_INFO;
	elem.style.color = sStyle;
	setContent(elem, sMsg);
	if (sStyle == C_PROMPT)
		blink(elem, 5000);
}


function log(sText)
{
//	var bKeepScrollbarAtBottom = false;

//	if (logring.scrollTop == (logring.scrollHeight - logring.offsetHeight))
//		bKeepScrollbarAtBottom = true;
	addContent(logring, sText);
//	if (bKeepScrollbarAtBottom)
		logring.scrollTop = logring.scrollHeight;
}


function logopenclk()
{
	logring.style.height = '10em';
	replaceClass('lropen', 'openinr', 'shut');
	replaceClass('lrclose', 'shut', 'openinr');
}


function logcloseclk()
{
	logring.style.height = '1.4em';
	replaceClass('lrclose', 'openinr', 'shut');
	replaceClass('lropen', 'shut', 'openinr');
}


function set(variable)
{
	return (typeof(variable) != 'undefined' && variable != null);
}


function def(variable)
{
	return (typeof(variable) != 'undefined');
}


function stackTrace()
{
	// Mozilla only
	var sFullStackTrace, sStackTrace = "", i = 0;
/*	try { qaz.wsx(); } catch (e) { sFullStackTrace = e.stack; }
	// @http://ispan.biz/script/main.js:31 rpcGetRetVal(
	while ((i = sFullStackTrace.indexOf('@', i)) != -1)
	{
		var j;
		if	(	((i = sFullStackTrace.indexOf(':', i)) == -1) ||
				((i = sFullStackTrace.indexOf('\n', i)) == -1) ||
				((j = sFullStackTrace.indexOf('(', i)) == -1)
			)
			break;
		if (sStackTrace.length > 0)
			sStackTrace += '<-';
		sStackTrace += sFullStackTrace.substring(i + 1, j);
	}
*/

	return sStackTrace;
}


function serverDate()
{
	var webDate = new Date();

	return new Date(webDate.getTime() + iSrvrTimeDif);
}


function isLower(c)
{
	if ('abcdefghijklmnopqrstuvwxyz'.indexOf(c) != -1)
		return true;
	else
		return false;
}


function isUpper(c)
{
	if ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'.indexOf(c) != -1)
		return true;
	else
		return false;
}


function equalsIgnoreCase(s1, s2)
{
	if (s1 && s2)
	{
		s1 = s1.toLowerCase();
		s2 = s2.toLowerCase();
		return s1 == s2;
	}
	return false;
}


function hhmmss()
{
	var date = serverDate();
	
	var	sHour = "" + date.getHours();
	var	sMin = "" + date.getMinutes();
	var	sSec = "" + date.getSeconds();

	if (sHour.length == 1)
		sHour = "0" + sHour;
	if (sMin.length == 1)
		sMin = "0" + sMin;
	if (sSec.length == 1)
		sSec = "0" + sSec;
	return sHour + ":" + sMin + ":" + sSec;
}


function elemxSetHeader(iContentLen, bLeafElem, iElemType)
{
	var sHeader = iContentLen + ',' + iElemType + (bLeafElem ? '{' : '[');
	return sHeader;
}


function elemxGetHeader(xElem, iOffset, header)
{
	var	i, j, L = '';

	i = xElem.indexOf(",", iOffset);
	if (i != -1)
	{
		j = i + 1;
		while (j < xElem.length && (L = xElem.charAt(j)) >= '0' && L <= '9')
			++ j;
	}
	if (i == -1 || j == xElem.length)
		return false;

	header.iHeaderLen = (j + 1) - iOffset;
	header.iContentLen = Number(xElem.substring(iOffset, i));
	header.iType = Number(xElem.substring(i + 1, j));
	if (L == '{')
		header.bLeaf = true;
	else if (L == '[')
		header.bLeaf = false;
	else
		return false;
	return true;
}


function elemxCreate(sContent, bLeafElem, iElemType)
{
	var sElem;
	var iContentLen = 0;

	if (set(sContent))
		iContentLen = sContent.length;
	sElem = elemxSetHeader(iContentLen, bLeafElem, iElemType);
	if (iContentLen)
		sElem += sContent;
	return sElem;
}


function elemxAdd(xParent, sChild, iChildType)
{
	// If iChildType == -1, sChild is an elem. Add it to xParent.
	// If iChildType != -1, sChild is a string. Create a new elem and add it to xParent.
	// Adjust xParent's header's iContentLen by the total length of sChild.
	var xChild, iParentContentLen, iParentContLenLen;

	if (iChildType != -1)
		xChild = elemxCreate(sChild, true, iChildType)
	else
		xChild = sChild;
	iParentContLenLen = xParent.indexOf(',');
	iParentContentLen = Number(xParent.substr(0, iParentContLenLen));
	iParentContentLen += xChild.length;
	xParent = String(iParentContentLen) + xParent.substring(iParentContLenLen) + xChild;
	return xParent;
}


function elemxGet(xParent, iOffset, header)
{
	// Elements have the format: CCC,TTTLDDDD... where CCC is the Content length,
	// TTT is the Type, L is '[' for Compound or '{' for Leaf, and DDDD... is the data.
	argsParentHdr = { };

	if (! elemxGetHeader(xParent, 0, argsParentHdr))
		return 0;

	// If iOffset == 0 and parent is not a Leaf Element, move past parent header to 1st child.
	if (! iOffset)
	{
		if (! argsParentHdr.bLeaf)
			iOffset = argsParentHdr.iHeaderLen; 
	}
	// Check for end of Parent or empty eParent or exceeding xParent length
	if	(	iOffset >= argsParentHdr.iHeaderLen + argsParentHdr.iContentLen || 
			! argsParentHdr.iContentLen || iOffset >= xParent.length
		)
		return 0;

	if (! elemxGetHeader(xParent, iOffset, header))
		return 0;

	if (header.bLeaf)
		header.xChild = xParent.substr(iOffset + header.iHeaderLen, header.iContentLen);
	else
		header.xChild = xParent.substr(iOffset, header.iHeaderLen + header.iContentLen);
	// Return offset of next child
	return iOffset + header.iHeaderLen + header.iContentLen;
}


function elemxGetArray(xElem)
{
	// An Element contains nested Compound Elements and Leaf elements.
	// This structure corresponds to a multi-dimensional Array whose
	// elements are either Arrays or String objects.
	var aArray = new Array();
	var iOffset = 0;
	var header = { };


	while (iOffset = elemxGet(xElem, iOffset, header))
	{
		var arrayElem;

		if (header.bLeaf)
			arrayElem = header.xChild;
		else
			arrayElem = elemxGetArray(header.xChild);
		aArray.push(arrayElem);
	}

	return aArray;
}


function elemxMakeObjFromElem(xElem, iLevel, oElemId)
{
	// Make an Element Object from a Text Element
	//
	// 1) An Element contains nested Compound elements and Leaf elements. Both Compound and 
	// Leaf elements contain an element Type and element Content. Compound elements Content
	// is one or more Elements. Leaf elements Content is raw data.
	//
	// This structure corresponds to a Javascript Object. An Element Object is a nested structure of
	// Element properties whose values are Compound Element Objects and Leaf Element Objects.
	//
	// 2) The Element property's name may be any name that is unique within the root Element 
	// Object. The Element property's name, the ElemId, is the only way of uniquely identifying 
	// elements within an Object, for example to link them with their counterparts in another 
	// structure such as a Table cell. This function uses a default property name of 'e' plus 
	// a sequentially increasing number.
	//
	// 3) The Element property's value is either a Compound Element Object or a Leaf Element Object.
	//
	// 4) Both Compound and Leaf Element Objects contain a Type property whose property name is 'T' 
	// and whose value is a numeric Type. In addition to the Type property, Element Objects contain
	// Content properties as follows:
	// 
	// 5) A Compound Element Object contains zero or more Content properties whose name is a unique
	// ElemId, as described in (2) above, and whose value is one or more Element Objects.
	// 
	// 6) A Leaf Element Object contains a Content property whose name is 'L' and whose value 
	// is a String, which is the actual data.
	//
	// 7) An empty Leaf Element (one with zero content length) has a null value. 
	// 
	// Example Element Object of Type 14 containing 1 Leaf Elem of Type 21, 1 untyped Leaf Elem
	// and 1 Compound Elem of Type 15 containing 2 untyped Leaf Elems and 1 untyped compound Elem 
	// containg 1 Leaf Elem of Type 22. The property names are the default sequentially increasing 
	// level/number, p1_1, p1_2, p1_3, p2_1, p2_2, p2_3, ...
	// 
	//	{
	//		"T": 14,
	//		"e1": { "T": 21, "L": "abc" },
	//		"e2": { "T": 0, "L": "def" },
	//		"e3": { "T": 15,
	//				"e4": { "T": 0, "L": "ghi" },
	//				"e5": { "T": 0, "L": "jkl" },
	//				"e6": { "T": 0,
	//						"e7": { "T":22, "C": "mno" }
	//					  }
	//			  }
	//	}
	//
	var iOffset = 0;
	var obj = { };
	var header = { };
	var args = { };

	if (! set(iLevel) || ! iLevel)
	{
		iLevel = 0;
		oElemId = { "iElemId": 0 };
	}
	++ iLevel;
	if (! elemxGetHeader(xElem, 0, header))
		return {};

	obj ["T"] = header.iType;

	while (iOffset = elemxGet(xElem, iOffset, args))
	{
		var id = 'e' + (++ (oElemId.iElemId));
		if (args.bLeaf)
			obj [id] = { "T": args.iType, "L": args.xChild };
		else
			obj [id] = elemxMakeObjFromElem(args.xChild, iLevel, oElemId);
	}

	if (iLevel == 1)
		obj.elemid = oElemId.iElemId;

	return obj;
}


function elemxGetChildFromPropVal(oPropVal)
{
	var oChild = { "bElem": true, "bLeaf": false };

	if (typeof(oPropVal) != 'object' || ! set(oChild.iType = oPropVal ["T"]))
		oChild.bElem = false;
	else if (set(oPropVal ["L"]))
	{
		if (set(oPropVal.domnode))
			oChild.content = oPropVal.domnode.value;
		else
			oChild.content = oPropVal ["L"];
		oChild.bLeaf = true;
	}
	else
		oChild.content = oPropVal;
	return oChild;
}


function elemxMakeElemFromObj(oElem)
{
	// Make a Text Element from an Element Object which has been created
	// by elemxMakeObjFromElem.
	//
	var	iParentType = 0;

	iParentType = oElem ["T"];
	var xParent = elemxCreate(null, false, iParentType);

	for (var prop in oElem)
	{
		if (prop == "T")
			// Already set in iParentType above
			continue;
		var oChild = elemxGetChildFromPropVal(oElem [prop]);
		// Ignore non-element properties
		if (! oChild.bElem)
			continue;
		if (oChild.bLeaf)
		{
			xParent = elemxAdd(xParent, oChild.content, oChild.iType)
		}
		else
		{
			sChild = elemxMakeElemFromObj(oElem [prop]);
			xParent = elemxAdd(xParent, sChild, -1)
		}
	}

	return xParent;
}


function jsonFromObj(obj, replacer, space)
{
	// Convert this Formset Template object to JSON.
	// See json2.js for args.
	var jObj;

	try
	{
		jObj = JSON.stringify(obj, replacer, space);
	}
	catch (e)
	{
		fcStatus('jsonFromObj failed: ' + e, C_ERROR);
		jObj = null;
	}
	return jObj;
}


function jsonToObj(jObj, reviver)
{
	var obj

	try
	{
		obj = JSON.parse(jObj, reviver);
	}
	catch (e)
	{
		setStatus('jsonToObj failed: ' + e, C_ERROR);
		obj = null;
	}
	return obj;
}


function TypeOf(value)
{
	// More precise 'typeof'
	// Possible types are object, string, number, boolean, function, undefined 
	// Javascript returns Array and null as object
	var sType = (typeof(value));

	if (value == null)
		sType = 'null';
	else if (sType == 'object')
	{
		if (value instanceof Array)
			sType = 'array';
	}
	return sType;
}


function funcNameToFunc(sFuncName, domNode)
{
	// window[sFuncName] will not work with a namespace'd function:
	// window["My.Namespace.functionName"](arguments); // fails
	// window["My"]["Namespace"]["functionName"](arguments); // succeeds

	if (! sFuncName)
		return null;
	if (sFuncName.length > 2 && sFuncName.substring(sFuncName.length - 2) == '()')
		sFuncName = sFuncName.substring(0, sFuncName.length - 2);

	if (! domNode)
		domNode = window;
	var saNamespaces = sFuncName.split(".");
	// Descend saNamespaces to the last node which must be a function
	for (var i = 0; i < saNamespaces.length && TypeOf(domNode) == 'object'; i++)
		domNode = domNode [saNamespaces [i]];

	if (typeof(domNode) != 'function' && typeof(domNode) != 'object')
		return null;
	return domNode;
}


//function funcNameToString(func)
//{
//	if (typeof(func) != 'function')
//		return null;
//	var sFuncName = func.toString();
//	var aRet = sFuncName.match(/\W*function\W*(\w+)\(/);
//	if (aRet) 
//		sFuncName = aRet [1];
//	else
//		sFuncName = null;
//	return sFuncName;
//}


function funcNameToString(func)
{
	var sName;

	if (typeof(func) != 'function' && typeof(func) != 'object')
		return null;
	if (func.toFuncName)
		sName = func.toFuncName();
	else if (typeof(func) == 'object')
		// Object doesn't have 'toFuncName()' which returns the object constructor func name
		return null;
	else
	{
		sName = func.toString();
		var aRet = sName.match(/\W*function\W*(\w+)\(/);
		if (aRet) 
			sName = aRet [1];
		else
			sName = null;
	}
	if (! sName)
		return null;
	return sName;
}


function Hash()
{
	this.length = 0;
	this.items = new Array();

	for (var i = 0; i < arguments.length; i += 2)
	{
		if (typeof(arguments [i + 1]) != 'undefined')
		{
			this.items [arguments [i]] = arguments [i + 1];
			++ this.length;
		}
	}
   
	this.del = function(key)
	{
		var tmp_value;

		if (typeof(this.items [key]) != 'undefined')
		{
			-- this.length;
			tmp_value = this.items [key];
			delete this.items [key];
		}
	   
		return tmp_value;
	}

	this.get = function(key)
	{
		return this.items [key];
	}

	this.set = function(key, value)
	{
		if (typeof(value) != 'undefined')
		{
			if (typeof(this.items [key]) == 'undefined')
				++ this.length;

			this.items[key] = value;
		}
	   
		return value;
	}

	this.has = function(key)
	{
		return typeof(this.items [key]) != 'undefined';
	}
}

/***************** EVENT HANDLING **********************************/

/*
NB Convert to use document.createEvent()/element.dispatchEvent() _IF_ warranted

...OR....

What about the Object.watch() function? Surely this is tailor-made for what we want.
E.g: Event.watch('SyncLog', function(..) {...}) or
E.g: Event.watch('SyncLog', csSetStatus)

http://developer.mozilla.org/en/docs/DOM:dispatchEvent_example

DOM:dispatchEvent example
« Gecko DOM Reference

This example demonstrates simulating a click on a checkbox using DOM methods. 

You can view the example in action here: 
http://developer.mozilla.org/samples/domref/dispatchEvent.html

Implementation:
--------------

<input type="checkbox" id="checkbox"/><label for="checkbox">Checkbox</label>
<input type="button" onclick="simulateClick();" value="Simulate click"/>
<input type="button" onclick="addHandler();" value="Add a click handler that calls preventDefault"/>
<input type="button" onclick="removeHandler();" value="Remove the click handler that calls preventDefault"/>

function preventDef(event) {
  event.preventDefault();
}

function addHandler() {
  document.getElementById("checkbox").addEventListener("click", 
    preventDef, false);
}

function removeHandler() {
  document.getElementById("checkbox").removeEventListener("click",
    preventDef, false);
}

function simulateClick() {
  var evt = document.createEvent("MouseEvents");
  evt.initMouseEvent("click", true, true, window,
    0, 0, 0, 0, 0, false, false, false, false, 0, null);
  var cb = document.getElementById("checkbox"); 
  var canceled = !cb.dispatchEvent(evt);
  if(canceled) {
    // A handler called preventDefault
    alert("canceled");
  } else {
    // None of the handlers called preventDefault
    alert("not canceled");
  }
}

*/


// ********* 'My' event handling **************

var aaEventHandler = new Array();


function addEventHandler(sEvent, eventHandlerFunction)
{
	var i;

	for (i = 0; i < aaEventHandler.length; i ++)
	{
		if (aaEventHandler [i] == [ sEvent, eventHandlerFunction ])
			// Event, Handler combination already in array
			return;
	}
	aaEventHandler [i] = [ sEvent, eventHandlerFunction ];
//debug('addEventHandler sEvent [' + sEvent + '] eventHandlerFunction: ' + eventHandlerFunction + ' aaEventHandler [i] ' + aaEventHandler [i]);
}


function delEventHandler(sEvent, eventHandlerFunction)
{
	var i;

	for (i = 0; i < aaEventHandler.length; i ++)
	{
		if (aaEventHandler [i] == [ sEvent, eventHandlerFunction ])
		{
			// Event, Handler combination found
			aaEventHandler.splice(i, 1);
			break;
		}
	}
}


function callEventHandler(sEvent)
{
	// Call all functions registered for this event
	var i;

	for (i = 0; i < aaEventHandler.length; i ++)
	{
		if (aaEventHandler [i][0] == sEvent)
			// Call Handler function
			aaEventHandler [i][1](sEvent);
	}
}


// ********* 'DOM' event handling **************

function getEventProps(ev, currentTarget)
{
	// Cope with cross-browser issues
	var e = {};

	if (ev)
	{
		// W3C
		e = ev;
	}
	else
	{
		// IE, or ev just null
		ev = window.event;
		if (ev)
		{
			e.type = ev.type;
			e.target = ev.srcElement;
		}
		else
		{
			e.type = null;
			e.target = null;
		}

		// Ref www.quirksmode.org/js/events_order.html: 
		// "In IE you cannot know which HTML element currently handles the event".
		//
		// The currentTarget arg may be set by the caller if the listener is only
		// set on one element.
		e.currentTarget = currentTarget;
		e.stopPropagation = function() { e.cancelBubble = true; };
	}

	if (bIE)
	{
		if (currentTarget)
			clickButton(currentTarget, true);
		else if (e.target && e.target.srcForm && e.target.srcForm instanceof Button)
			clickButton(e.target, true);
	}

	return e;
}


function setEventListener(elem, sEvent, func)
{
	// Use the 'Traditional event registration model' until W3C fuuly cross-browser
	// (http://www.quirksmode.org/js/events_tradmod.html)

	if (elem)
	{
		elem ['on' + sEvent] = func;
	}

	// W3C (http://www.quirksmode.org/js/events_advanced.html)
	//if (window.addEventListener)
	//	// Standard
	//	elem.addEventListener(sEvent, func, false);
	//else
	//{
	//	// IE
	//	try
	//	{
	//		//elem.attachEvent(sEvent, func);
	//
	//		// Can't get attachEvent() to work. Debugging shows attachEvent
	//		// doesn't throw an error, but the target func doesn't get called
	//		// when the action is taken that triggers the event. It does get called
	//		// with addEventListener in Firefox and with the old method in IE.
	//
	//		// Use old method.
	//		switch (sEvent)
	//		{
	//			case 'change': elem.onchange = func; break;
	//			case 'click': elem.onclick = func; break;
	//			default: debug('setEventListener invalid event [' + sEvent + ']');
	//		}
	//	}
	//	catch (e)
	//	{
	//		debug('setEventListener, attachEvent e = ' + e);
	//	}
	//}
}


function loadQueryStringParams(saDefaultParams)
{
	if (saDefaultParams)
		saQueryStringParams = saDefaultParams;
	else
		saQueryStringParams = new Array();

	var sQuery = window.location.search.substring(1);
	var saParams = sQuery.split('&');
	for (var i = 0; i < saParams.length; i++)
	{
		var iEqual = saParams[i].indexOf('=');
		if (iEqual > 0)
		{
			var sName = saParams[i].substring(0, iEqual);
			var sValue = saParams[i].substring(iEqual + 1);
			saQueryStringParams[sName] = sValue;
		}
	}
}


//function setLocation(sPlace)
//{
//	// Replace the saLocation array or add a place to it or remove the last place from it
//	var sLocation = "";
//
//	if (sPlace)
//	{
//		if (sPlace instanceof Array)
//			saLocation = sPlace;
//		else
//			saLocation.push(sPlace);
//	}
//	else
//		saLocation.pop();
//	for (i = 0; i < saLocation.length; i ++)
//	{
//		if (i > 0)
//			sLocation += ' > ';
//		sLocation += saLocation [i];
//	}
//	setContent('panlocation', sLocation);
//}


function setClientOnline(bOnline)
{
	// Open or close the ONLINE/OFFLINE label. If there is no Client
	// logged in, close it, else show it as on or off line.
	if (bOnline)
	{
		setContent('loginonline', ' ONLINE ');
		setStyle('loginonline', 'color: #3DC13D; font-weight: bold;');
		elemOpen('loginonline', true);
	}
	else
	{
		if (! sClient || sClient == '')
			elemOpen('loginonline', false);
		else
		{
			setContent('loginonline', ' OFFLINE ');
			setStyle('loginonline', 'color: #FF0000;');
			elemOpen('loginonline', true);
		}
	}

//debug('setClientOnline typeof bClientOnline: ' + typeof bClientOnline + ' bOnline: ' + bOnline);
	var bClientWasOnline = bClientOnline;

	if (bOnline) 
	{
		bClientOnline = true;
		if (! bClientWasOnline)
			callEventHandler(EV_CHAN_CLIENTONLINE);
	}
	else
	{
		bClientOnline = false;
		if (bClientWasOnline)
			callEventHandler(EV_CHAN_CLIENTOFFLINE);
	}
}


/**************** HTML DOM Elements start *****************************************/


function elemCreate()
{
	var elem = document.createElement(sElemTag);
//>>>
/*
id			elem2
className	fld2 brd clrfld
nodeName	DIV
innerHTML	CCCCCC
style.*		""
*/
}


function elemAdd(parent, child, beforeElem)
{
	if (beforeElem)
		parent.insertBefore(child, beforeElem);
	else
		parent.appendChild(child);
}


/**************** HTML DOM Elements end *******************************************/


function elemDisplay(elem, sValue)
{
	if (typeof(elem) == 'string')
		elem = document.getElementById(elem);
	if (elem && elem.style)
		elem.style.display = sValue;
}


function hasClass(elem, sClassname)
{
	if (typeof(elem) == 'string')
		elem = document.getElementById(elem);
	if (elem && elem.className)
	{
		var re = new RegExp("(^|\\s)" + sClassname + "(\\s|$)");
		return re.test(elem.className);
	}
	return false;
}


function addClass(elem, sClassname)
{
	if (typeof(elem) == 'string')
		elem = document.getElementById(elem);
	if (elem)
	{
		if (elem.className == null || elem.className == '')
		{
			elem.className = sClassname;
			return;
		}
		if (hasClass(elem, sClassname))
			return;
		elem.className = elem.className + " " + sClassname;
	}
}


function delClass(elem, sClassname)
{
	if (typeof(elem) == 'string')
		elem = document.getElementById(elem);
	if (elem && elem.className)
	{
		var re = new RegExp("(^|\\s+)" + sClassname + "(\\s+|$)");
		elem.className = elem.className.replace(re,' ');
	}
}


function replaceClass(elem, sOldClassname, sNewClassname)
{
	if (typeof(elem) == 'string')
		elem = document.getElementById(elem);
	if (elem)
	{
		delClass(elem, sOldClassname);
		addClass(elem, sNewClassname);
	}
}


function elemOpen(elem, bTrueFalse)
{
	var openclass = 'openblk';

	if (typeof(elem) == 'string')
		elem = document.getElementById(elem);
	if (elem && elem.tagName)
	{
		if (elem.tagName.toUpperCase() == 'SPAN')
			openclass = 'openinl';
		var bOpen = hasClass(elem, openclass);
		if (bTrueFalse === undefined)
			return(bOpen);
		if (bTrueFalse)
			replaceClass(elem, 'shut', openclass);
		else
			replaceClass(elem, openclass, 'shut');
	}
}


function elemVisible(elem, bTrueFalse)
{
	if (typeof(elem) == 'string')
		elem = document.getElementById(elem);
	if (elem)
	{
		var bVisible = hasClass(elem, 'visible');
		if (bTrueFalse === undefined)
			return(bVisible);
		if (bTrueFalse)
			replaceClass(elem, 'hidden', 'visible');
		else
			replaceClass(elem, 'visible', 'hidden');
	}
}

/* This function also probably works, but needs to distinguish between IE & non-IE stuff
function elemVisible(elem, bTrueFalse)
{
	if (typeof(elem) == 'string')
		elem = document.getElementById(elem);
	if (elem)
	{
		if (bTrueFalse === undefined)
		{
			if (elem.style)
				return(elem.style.visibility == 'visible');
			else
				return(elem.visibility == 'show');
		}
		if (elem.style)
			elem.style.visibility = (bTrueFalse ? 'visible' : 'hidden');
		else
			elem.visibility = (bTrueFalse ? 'show' : 'hide');
	}
}
*/

function elemMove(elem, destParent)
{
	if (typeof(elem) == 'string')
		elem = document.getElementById(elem);
	if (elem)
	{
		if (typeof(destParent) == 'string')
			destParent = document.getElementById(destParent);
		if (destParent)
		{
			// Save any attribs, styles, etc that are lost by broken appendChild()
			// here >>> ...

			destParent.appendChild(elem);

			// ... and restore them here >>>
		}
	}
}


function elemHasDescendantId(elem, sChildId)
{
	if (typeof(elem) == 'string')
		elem = document.getElementById(elem);
	if (elem && elem.childNodes)
	{
		for (var i = 0; i < elem.childNodes.length; i ++)
		{
			if (elem.childNodes [i].id && elem.childNodes [i].id == sChildId)
				return true;
			if (elemHasDescendantId(elem.childNodes [i], sChildId))
				return true;
		}
	}
	return false;
}


function getContent(elem)
{
	if (typeof(elem) == 'string')
		elem = document.getElementById(elem);
	if (elem && typeof(elem.innerHTML) != 'undefined')
		return elem.innerHTML;
	return '';
}


function addContent(elem, sContent)
{
	if (typeof(elem) == 'string')
		elem = document.getElementById(elem);
	if (elem)
		setContent(elem, getContent(elem) + sContent);
}


function setContent(elem, sContent)
{
	if (typeof(elem) == 'string')
		elem = document.getElementById(elem);
	if (elem && typeof(elem.innerHTML) != 'undefined')
		elem.innerHTML = sContent;
}


function getElemValue(elem)
{
	if (typeof(elem) == 'string')
		elem = document.getElementById(elem);
	if ((! elem) || elem.value == null || elem.value == undefined)
		return '';
	return elem.value;
}


function setElemValue(elem, sValue)
{
	if (typeof(elem) == 'string')
		elem = document.getElementById(elem);
	if (elem)
		elem.value = sValue;
}


function getAttValue(elem, sAttName)
{
	var sValue = null;

	if (typeof(elem) == 'string')
		elem = document.getElementById(elem);
	if (elem && elem.getAttribute)
		sValue = elem.getAttribute(sAttName);
	return sValue;
}


function setAttValue(elem, sAttName, sAttValue)
{
	// For use by funcs that haven't declared and set vars to the elem.
	// If the elem is available already as a var, x, then simply write 'x.<att> = <value>',
	// eg "x.type = 'password'" rather than "setAttValue('Password', 'type', 'password')".
	if (typeof(elem) == 'string')
		elem = document.getElementById(elem);
	if (elem)
	{
		if (bOldIE)
		{
			switch (sAttName)
			{
				//Not allowed: case 'type': elem.type = sAttValue; return;
				case 'value': elem.value = sAttValue; return;
			}
		}
		if (elem.setAttribute)
			elem.setAttribute(sAttName, sAttValue);
	}
}


function delAtt(elem, sAttName)
{
	if (typeof(elem) == 'string')
		elem = document.getElementById(elem);
	if (elem && elem.removeAttribute)
		elem.removeAttribute(sAttName);
}


function setStyle(elem, sDecl)
{
	if (elem)
		setAttValue(elem, 'style', sDecl);
}


function clickButton(but, bStart)
{
	// For IE, where style class .frmbutton:active border-style inset/outset 
	// has problems.
	if (bStart)
	{
		but.style.borderColor = '#606060 #FFFFFF #FFFFFF #606060';
		setTimeout(function(){clickButton(but, false);}, 100);
	}
	else
		but.style.borderColor = '#FFFFFF #606060 #606060 #FFFFFF';
}


function stopblink(elem)
{
	if (elem)
		delClass(elem, 'blinking');
}


function blink(elem, iMillis)
{
	if (elem)
	{
		if (iMillis)
		{
			addClass(elem, 'blinking');
			//setTimeout("stopblink('" + elem + "')", iMillis);
			setTimeout(function(){stopblink(elem);}, iMillis);
			//setTimeout("delClass('" + elem + "', 'blinking')", iMillis);
			setTimeout(function(){delClass(elem, 'blinking');}, iMillis);
		}
		else
			delClass(elem, 'blinking');
	}
}


function setChecked(elem, onoff)
{
	if (typeof(elem) == 'string')
		elem = document.getElementById(elem);
	if (elem)
		elem.checked = onoff;
}


function enableButton(elem)
{
	if (typeof(elem) == 'string')
		elem = document.getElementById(elem);
	if (elem)
	{
		delClass(elem, 'greyed');
		delAtt(elem, 'disabled');
	}
}


function disableButton(elem)
{
	if (typeof(elem) == 'string')
		elem = document.getElementById(elem);
	if (elem)
	{
		addClass(elem, 'greyed');
		setAttValue(elem, 'disabled', 'disabled');
	}
}


function clearSelect(elemSelect, iSkipLines)
{
	if (typeof(elemSelect) == 'string')
		elemSelect = document.getElementById(elemSelect);
	if (elemSelect && elemSelect.remove)
	{
		if (! set(iSkipLines))
			iSkipLines = 0;

		while (elemSelect.length > iSkipLines)
			elemSelect.remove(elemSelect.length - 1);

//what is this??		elemSelect.value = '';
	}
}


function addSelectOption(elemSelect, sText)
{
	if (typeof(elemSelect) == 'string')
		elemSelect = document.getElementById(elemSelect);
	if (elemSelect && elemSelect.add)
	{
		var opt = document.createElement('option');
		opt.text = sText;
		try
		{
			elemSelect.add(opt, null); // standards compliant
		}
		catch(ex)
		{
			elemSelect.add(opt); // IE only
		}
	}
}


function clearSelectElement(elemSelect)
{
	if (typeof(elemSelect) == 'string')
		elemSelect = document.getElementById(elemSelect);
	//while (elemSelect.firstChild)
	//	elemSelect.removeChild(elemSelect.firstChild);
	if (elemSelect.options)
		elemSelect.options.length = 0;
}


function loadSelectElement(elemSelect, saaOptions, sSelected)
{
//>>>										VVVVVVVVVVVVVVVVVVVVVVVVVV
	// Load saaOptions into elemSelect. saaOptions may be an array of strings 
	// or an array of pairs of strings, each in a 2-elem array.
	// In pairs of strings, the 1st is the text and the second is the value.
	// In a single string the string is both the text and the value.
	//
	// If sSelected is not null or undefined, the option with the same value as
	// sSelected is selected. If the value of sSelected is blank, an extra, blank
	// option is added as the first option.
	var iOption = 0, iIndex = 0, iSelectedIndex = -1;

	if (typeof(elemSelect) == 'string')
		elemSelect = document.getElementById(elemSelect);
	if (! elemSelect.options)
		return;

	clearSelectElement(elemSelect);

	if (set(sSelected) && typeof(sSelected) == 'string' && ! sSelected.length)
	{
		option = new Option('');
		iSelectedIndex = 0;
		elemSelect.options[iIndex ++] = option;
	}

	while (iOption < saaOptions.length)
	{
		var option;

		option = new Option(saaOptions [iOption ++]);
		if (set(sSelected) && option.value == sSelected)
			iSelectedIndex = iIndex;
		elemSelect.options[iIndex ++] = option;
	}

	if (iSelectedIndex != -1)
		elemSelect.selectedIndex = iSelectedIndex;
}


function setSelectedOption(elemSelect, sValue)
{
	var iIndex = 0;

	if (typeof(elemSelect) == 'string')
		elemSelect = document.getElementById(elemSelect);

	if (elemSelect.options)
	{
		while (iIndex < elemSelect.options.length)
		{
			if (equalsIgnoreCase(elemSelect.options[iIndex].value, sValue))
			{
				elemSelect.selectedIndex = iIndex;
				break;
			}
			++ iIndex;
		}
	}
}


function getSelectedOption(elemSelect)
{
	if (typeof(elemSelect) == 'string')
		elemSelect = document.getElementById(elemSelect);

	if (elemSelect.options && elemSelect.selectedIndex >= 0)
	{
		return elemSelect.options[elemSelect.selectedIndex].text
	}
	return null;
}


function setFocus(elem)
{
	if (typeof(elem) == 'string')
		elem = document.getElementById(elem);
	if (elem && typeof(elem.focus) != 'undefined')
		elem.focus();
}


function cap(s)
{
	var sCapitalised = "";
	if (s && s.length != 0)
	{
		var sFirstChar = s.substring(0,1).toUpperCase();
		sCapitalised = sFirstChar + s.substring(1);
	}
	return sCapitalised;
}


function clone(obj)
{
	var jObj = jsonFromObj(obj, cloneReplacer);
	var objClone = jsonToObj(jObj, cloneReviver);
	return objClone;
}


function cloneReplacer(key, value)
{
	if (typeof(value) == 'function' || typeof(value) == 'object')
	{
		var sName = funcNameToString(value);
		if (sName)
			return sName;
		if (typeof(value) == 'function')
			return null;
	}
	return value;
}


function cloneReviver(key, value)
{
	var func = null;

	if (typeof(value) == 'string')
		func = funcNameToFunc(value);
	if (func)
		return func;
	else
		return value;
}

//function clone(o)
//{
//	var type = typeof(o);
//	if (o == null || type != 'object')
//		return o;
//	var c = new o.constructor();
//	try
//	{
//		for (var prop in o)
//			c [prop] = clone(o [prop]);
//	}
//	catch(e)
//	{
//		return o;
//	}
//	return c;
//}


function popup(sContent, sStyle)
{
	setStatus(sContent, sStyle);
}


function runWhenReady(sFunc, sObject)
{
	// runWhenReady() calls setTimeout() recursively until no object in sObject is 
	// undefined and sFunc is verified as a function. It then calls sFunc, passing 
	// sFunc all args that are passed to runWhenReady() after sObject. 
	//
	// If sObject is null, sFunc is called immediately. There may still be args following 
	// the null sObject.
	//
	// ALL args to this function must be strings.
	var bNotReady = false;
	var saObjects = [];
	var sArgStrings = '';
	var saArgs = [];
	var sUndefined = null;
	var func;

//debug('runWhenReady( sFunc [ ' + sFunc + '] sObject [' + sObject + '] )');
	if (sObject)
	{
		if (sObject instanceof Array)
			saObjects = sObject;
		else
			saObjects = [sObject];
	}

	for (var i = 0; i < saObjects.length; i ++)
	{
		if (window [saObjects [i]] == undefined)
		{
			sUndefined = saObjects [i];
//debug('runWhenReady sUndefined: ' + sUndefined);
			break;
		}
//debug('runWhenReady saObjects [i]: ' + saObjects [i]);
	}

	for (var i = 2; i < arguments.length; i ++)
	{
		var sComma = (i > 2) ? ', ' : '';
		sArgStrings += ', "' + arguments[i] + '"';
		saArgs.push(arguments[i]);
	}
//debug('runWhenReady(' + sFunc + ', [' + saObjects + ']' + saArgs + ')');

	if (! sUndefined)
	{
		func = window [sFunc];
		if (typeof(func) != 'function')
			sUndefined = sFunc;
//debug('runWhenReady func: ' + sFunc + '(' + typeof(func) + ') sUndefined: ' + sUndefined);
	}

	if (sUndefined)
	{
//debug('runWhenReady setTimeout sUndefined: ' + sUndefined + ' setTimeout(runWhenReady("' + sFunc + '", "' + sObject + '"' + sArgStrings + ')');
		setTimeout(function(){runWhenReady(sFunc, sObject);}, 1000);
	}
	else
	{
//debug('runWhenReady func: ' + sFunc + '(' + typeof(func) + ') saArgs: (' + saArgs + ')');
		try
		{
			func(saArgs);
		}
		catch (e)
		{
			debug('runWhenReady catch e: ' + e + ' [' + e.description + ']  sFunc: ' + sFunc + ' saArgs: (' + saArgs + ')');
		}
	}
}


function loadResource(sUrl, sElemTag, sTypeParam, sFunc, sObject, retLoadResource)
{
	// Establish ID to identify resource in DOM, to avoid unnecessary reloading
	var id = sUrl;
	var iSlash = id.lastIndexOf('/');
	if (iSlash != -1)
		id = id.substr(iSlash + 1);
	var sElem = document.getElementById(id);
	if (sElem && sFunc)
	{
		// Resource is already loaded. Call the target function in it.
		runWhenReady(sFunc, sObject);
		return;
	}

	var headElem = document.getElementsByTagName("head")[0];         
	var childElem = document.createElement(sElemTag);
	childElem.id = id;
	childElem.type = sTypeParam;
	switch (sElemTag)
	{
		case 'script': 
			childElem.src = sUrl; 
			break;
		case 'link': 
			childElem.href = sUrl; 
			childElem.rel = 'stylesheet';
			childElem.media = 'screen';
			break;
	}
	headElem.appendChild(childElem);

	if (retLoadResource)
	{
		// If retLoadResource is not null, it means that sUrl must be retrieved by
		// callServer(..., retLoadResource) -> XmlHttpRequest, and then retLoadResource()
		// will complete the creation of the resource element and call any associated function.

		callServer(sHostDocRoot + sUrl, 'GET', null, retLoadResource);
	}
	else
	{
		// If sFunc is not null, run sFunc when sFunc and all objects in sObject
		// have been loaded and are in the DOM.
		if (sFunc)
			runWhenReady(sFunc, sObject);
	}
}


/* test this as an alternative to the function above...
function retLoadScript(responseText, responseStatus)
{
	if (responseStatus == 200)
	{
		var html_doc = document.getElementsByTagName('head')[0];
		var js = document.createElement("script");
		js.type="text/javascript";
		js.text = oHttp.responseText;
		if (!document.all) {
			js.innerHTML = oHttp.responseText;
		}
		html_doc.appendChild(js);
		sScriptFunction();
	}
	else
	{
		popup(loadScript returned status: ' + responseStatus + ' : ' + responseText);
		return false;
	}
}


function loadResource(sUrl, sElemTag, sTypeParam, sScriptFunction)
{
	// What advantage would this have over the version above if that loads the
	// js/css asynchronously?
	sScriptFunctionLoaded = sScriptFunction;
	var headElem = document.getElementsByTagName("head")[0];         
	var childElem = document.createElement(sElemTag);
	childElem.type = sTypeParam;
	switch (sElemTag)
	{
		case 'script': 
			break;
		case 'link': 
			childElem.rel = 'stylesheet';
			childElem.media = 'screen';
			break;
	}
	headElem.appendChild(childElem);

	>>>check the dom of an existing css and see what else has to be created here in the
	childElem, or what can only be created in retLoadScript.


	callServer(sHostDocRoot + sUrl, 'GET', null, retLoadScript);
}
*/


function setScriptText(sScriptId, sScriptText)
{
	// Insert the javascript in sScriptText into the element whose id is sScriptId.
	var elemScript = document.getElementById(sScriptId);
	elemScript.text = sScriptText;
	if (! document.all)
		elemScript.innerHTML = sScriptText;
}

/*
function createScriptElement(sScriptId, sScriptText)
{
	// Insert the javascript in sScriptText into the DOM in the HEAD element as 
	// an element whose id is sScriptId.

	var headElem = document.getElementsByTagName('head')[0];
	var childElem = document.createElement("script");
	childElem.id = sScriptId;
	childElem.type = "text/javascript";
	childElem.text = sScriptText;
	if (! document.all) {
		childElem.innerHTML = sScriptText;
	}
	headElem.appendChild(childElem);
}


function loadScript(sUrl, retLoadScript)
{
	// The calling function that needs the script calls this function which simply uses
	// callServer() to GET the js file from sUrl after first checking that the script 
	// is not already loaded and in the DOM. callServer() then calls retLoadScript.
	// The retLoadScript() function should call createScriptElement() above to insert 
	// the script into the DOM, and then call the required function in the js file.
	//
	// This AJAX method of loading a script dynamically achieves the same result as 
	// loadResource() above, but it has the drawback that Firebug does not include it in its 
	// list of scripts, so you can't debug with it, whereas the loadResource() method, which 
	// uses elem.src="Url...", causes Firebug to include the script after it has been loaded.
	
	// Establish ID to identify resource in DOM, to avoid unnecessary reloading
	var id = sUrl;
	var iSlash = id.lastIndexOf('/');
	if (iSlash != -1)
		id = id.substr(iSlash + 1);
	if (document.getElementById(id))
	{
		// Resource is already loaded. Call the target function in it.
		//eval(retLoadScript);
		retLoadScript(null);
		return;
	}

	callServer('sHostDocRoot + sUrl, 'GET', null, retLoadScript);
}
*/


/************************ RPC ************************************/


function rpcGetRetVal(sRetVal, bJson, reviver)
{
	// RPC return values consist of a series of length-prefixed strings.
	//
	// sRetVal consists of a header section of 3 optional logging args and two standard args
	// followed by zero or more optional rpc server return args:
	// [LOGRING, LogringSeq, Logring-data] (1) Argc, (2) Function [, (3...) Argc arg strings].
	//
	// A length-prefix is the length of the following value, and is a numeric string 
	// terminated by a 0x01 byte, eg 123 is 0x31323301. A length-prefix of zero has no
	// following value, ie the value is null or empty, and is immediately followed by
	// the next length-prefix. A length-prefix of -1 has a following value of unknown length
	// which is the last value and extends to the end of the data.
	//
	// The string 'XY' as a length-prefix string in hex: 0x32015859.
	//
	// Example return value where ~ represents the 0x01 byte:
	// 1~312~somefunction15~This is Arg one7~Arg two0~-1~Variable length data goes on till eof...

	// RPC call arg values and return values may contain binary data 0x00-0xff. The reason that the
	// length-prefixes are ascii, and not a short, is that those RPC calls made from a web client
	// are currently constrained to ascii with no embedded nulls to avoid problems with Javascript,
	// Internet Explorer, XMLhttpRequest, etc.
	//
	// Return a string array of the values returned in sRetVal by the server of an rpc call.
	// sRetVal consists of three or more args:
	// (1) Argc, (2) Function, Args: (3) 1st arg PidTid, [4...] any remaining arg strings.
	// The returned array consists of just the PidTid and the arg strings. Its length is Argc.
	// The Function string is the name of the function that was called. It may
	// be used to distinguish between returned values from different concurrent 
	// calls that use the same callback.
	// The PidTid is the ProcessId.ThreadId of the function that processes the rpc call
	// and provides the return value. It is always passed as the 1st arg. It is used by 
	// the calling action to retrieve action-related log lines from the channel log.

	var MAXLENLEN = 8;
	var iLenPfxStart = 0, iIndex = 0;
	var saRetVal = [ '' ];
	var	iLoggingArgs = 0, iString = 0, iArgc = 0, sFunc;

	if (! set(sRetVal))
		sRetVal = "";

	// If the caller expects the returned value to be a JSON string, call jsonToObj()
	// which will return the Object.
	if (set(bJson))
		return jsonToObj(sRetVal, reviver);

	while ((iString < iArgc + 2 || iLoggingArgs) && iLenPfxStart < sRetVal.length) 
	{
		var	i = iLenPfxStart, c, iLenPfxEnd, iLenPrefix, bLenPfxErr = false;

		if ((iLenPfxEnd = sRetVal.indexOf('\x01', iLenPfxStart)) == -1 || iLenPfxEnd - iLenPfxStart > MAXLENLEN + 1)
			bLenPfxErr = true;
		else			
		{
			while ((((c = sRetVal.charAt(i)) >= '0' && c <= '9') || c == '-' || c == '+') && i < iLenPfxEnd)
				++ i;
			if (i < iLenPfxEnd)
				bLenPfxErr = true;
		}
		if (bLenPfxErr)
		{
			// No LenPrefix Terminator (0x01) found in remainder (or all) of string. Assume that 
			// the rest (or all) of the string is a simple, non-RetVal error message string, and 
			// return a single element array with string as the last (or only) element in saRetVal.
			saRetVal [iIndex] = sRetVal.substring(iLenPfxStart);
			break;
		}
		iLenPrefix = +(sRetVal.substring(iLenPfxStart, iLenPfxEnd));
		if (iLenPrefix > 0)
		{
			var sValue, iValueEnd;

			iValueEnd = iLenPfxEnd + 1 + iLenPrefix;
			if (iValueEnd > sRetVal.length)
			{
				debug('rpcGetRetVal: Value end (' + iValueEnd + ') > RetVal length in [' + sRetVal + ']');
				return null;
			}
			sValue = sRetVal.substring(iLenPfxEnd + 1, iValueEnd);

			if (sValue == 'LOGRING')
			{
				iLoggingArgs = 3;
			}
			else if (iLoggingArgs)
			{
				if (iLoggingArgs == 2)
					sNextLogringSeq = sValue;
				else if (iLoggingArgs == 1)
					log(sValue);
			}
			else if (iString == 0)
				iArgc = sValue;
			else if (iString == 1)
				sFunc = sValue;
			else saRetVal [iIndex ++] = sValue;
			iLenPfxStart = iValueEnd;
		}
		else if (iLenPrefix == -1 && ! iLoggingArgs)
		{
			saRetVal [iIndex ++] = sRetVal.substring(iLenPfxEnd + 1);
			iLenPfxStart = sRetVal.length;
		}
		else if (! iLoggingArgs)
		{
			saRetVal [iIndex ++] = '';
			iLenPfxStart = iLenPfxEnd + 1;
		}
		else
			iLenPfxStart = iLenPfxEnd + 1;
		if (iLoggingArgs)
			-- iLoggingArgs;
		else
			++ iString;
	}
	return saRetVal;
}


function rpcStringList(sListElem)
{
	// sListElem is a String List Element - a Compound Element of level 1 strings, 
	// ie each terminated by 0x01. return a string array from this String List Element.
	var	saStrings = new Array();
	var iStart = 0, iEnd, iIndex = 0;

	while (iStart < sListElem.length) 
	{
		if ((iEnd = sListElem.indexOf('\x01', iStart)) == -1)
			iEnd = sListElem.length;
		saStrings [iIndex ++] = sListElem.substring(iStart, iEnd);
		iStart = iEnd + 1;
	}
	return saStrings;
}


function rpcArgs(sFunction, saFuncArgs, bUrlEncode)
{
	var	i, iArgs = 0;
	var saRpcArgs = new Array();

	// Standard args
	saRpcArgs [iArgs ++] = 'logincode';
	saRpcArgs [iArgs ++] = sLoginCode;

	// caActionClient, use those. If they are not available then the rpc function must be a User
	// or Database level function which does not require one or both of them.
	if	(sLoginLevel == LOGINNODE_USER && sDatabase.length != 0)
	{
		saRpcArgs [iArgs ++] = 'rpcdatabase';
		saRpcArgs [iArgs ++] = sDatabase;
	}
	if	(	(sLoginLevel == LOGINNODE_USER || sLoginLevel == LOGINNODE_DATABASE) && 
			sClient.length != 0
		)
	{
		saRpcArgs [iArgs ++] = 'rpcclient';
		saRpcArgs [iArgs ++] = sClient;
	}
	// The logring request, for any new log records
	saRpcArgs [iArgs ++] = 'logring';
	saRpcArgs [iArgs ++] = sNextLogringSeq;
	// The actual rpc function
	saRpcArgs [iArgs ++] = 'function';
	saRpcArgs [iArgs ++] = sFunction;

	// The rpc function arguments - argc and argv
	saRpcArgs [iArgs ++] = 'argc';
	if (saFuncArgs != undefined && saFuncArgs != null)
	{
		saRpcArgs [iArgs ++] = '' + saFuncArgs.length;
		for (i = 0; i < saFuncArgs.length; i ++)
		{
			saRpcArgs [iArgs ++] = 'arg' + i;
			saRpcArgs [iArgs ++] = saFuncArgs [i];
		}
	}
	else
		saRpcArgs [iArgs ++] = '0';

	var sRpcArgs;

	if (bUrlEncode)
	{
		var i = 0;
		sRpcArgs = '';
		while (i < saRpcArgs.length)
		{
			if (i > 0)
				sRpcArgs += '&';
			sRpcArgs += saRpcArgs [i ++] + '=';
			sRpcArgs += encodeURIComponent(saRpcArgs [i ++]);
		}
	}
	else
		sRpcArgs = saRpcArgs;

	return sRpcArgs;
}


function rpc(sFunction, saFuncArgs, callback)
{
	var sRpcArgs = rpcArgs(sFunction, saFuncArgs);
	if (iRpcSeq > 99)
		iRpcSeq = 0;
	callServer(sHostDocRoot + '/bin/rpc.cgi?W' + (iRpcSeq ++) + '.' + sFunction, 'POST', sRpcArgs, callback);
}


/************************** AJAX ************************************/

/*******************************************************
From http://developer.mozilla.org/en/docs/AJAX/Getting Started
Some versions of some Mozilla browsers won't work properly if the response from the server 
doesn't have an XML mime-type header. To satisfy this, you can use an extra method call to 
override the header sent by the server, just in case it's not text/xml.
http_request = new XMLHttpRequest();
http_request.overrideMimeType('text/xml');
********************************************************/

function onProgress(e) {
  var percentComplete = (e.position / e.totalSize)*100;
  //...
}

function onError(e) {
	try
	{
		alert("Error " + e.target.status + " occurred while receiving the document.");
	}
	catch(e)
	{
	}
}

function onLoad(e) {
  // ...
}


function httpRequest(url, callbackFunction)
{

// TODO: See Wikipedia article on XMLHttpRequest and change to that, after rechecking
// http://www.hunlock.com/blogs/The_Ultimate_Ajax_Object. What advantage does this confer over
// the Wiki one. See also the para re using ajax.open() before setting onreadystatechange, which
// this func doesn't do.


	var that=this;      
	this.updating = false;

	//debug('httpRequest(' + url + ', ' + (' ' + callbackFunction).substring(0,40) + ')');
	this.abort = function()
	{
		if (that.updating)
		{
			that.updating=false;
			that.AJAX.abort();
			that.AJAX=null;
		}
	}

	this.update = function(passData, postMethod)
	{ 
		if (that.updating) 
			return false;

		that.AJAX = null;                          
		if (window.XMLHttpRequest)
			that.AJAX = new XMLHttpRequest();              
		else                                  
		{
//			that.AJAX = new ActiveXObject("Microsoft.XMLHTTP");
			try { that.AJAX = new ActiveXObject("Msxml2.XMLHTTP.6.0") } catch(e) {
			try { that.AJAX = new ActiveXObject("Msxml2.XMLHTTP.3.0") } catch(e) {
			try { that.AJAX = new ActiveXObject("Msxml2.XMLHTTP") } catch (e) {
			try { that.AJAX = new ActiveXObject("Microsoft.XMLHTTP") } catch (e) { }}}}
		}
		if (that.AJAX==null)
			return false;                               
		else
		{


//			that.AJAX.onprogress = onProgress;
//			that.AJAX.onload = onLoad;
//			that.AJAX.onerror = onError;

			that.AJAX.onreadystatechange = function()
			{  
				try
				{
					if (that.AJAX.readyState==4)
					{             
						that.updating=false;                
						try
						{
							//var sRH = that.AJAX.getAllResponseHeaders();
							//debug('ResponseHeaders: ' + sRH);
							var iContentLength = that.AJAX.getResponseHeader("Content-Length");
							that.callback(that.AJAX.responseText,that.AJAX.status, iContentLength,
								that.AJAX.responseXML);        
						}
						catch(e)
						{
							var sCallbackId = that.callback.id;
							debug('' + stackTrace() + 'httpRequest() callback threw error.' + 
								' URL: ' + that.sUrl + ' CALLBACK: ' + 
								that.callback.toString().substring(0,40) + '.... ERROR: ' + e);
						}
						that.AJAX=null;                                         
					}
				}
				catch(e)
				{
					that.updating=false;                
					//that.callback('Connection error: ' + stackTrace() + ': ' + e.description, '-1');        
					that.callback('Connection error: ' + e.description, '-1');        
					that.AJAX=null;                                         
				}
			}                                                        

			that.updating = new Date();                              
			if (/post/i.test(postMethod))
			{
				var uri;
				if (urlCall.indexOf('?') == -1)
				{
					if (iRpcSeq > 99)
						iRpcSeq = 0;
					uri = urlCall + '?' + iRpcSeq ++;
				}
				else
					var uri=urlCall;
				that.AJAX.open("POST", uri, true);
				that.AJAX.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
				that.AJAX.setRequestHeader("Content-Length", passData.length);
				that.AJAX.send(passData);
			}
			else
			{
				var uri=urlCall+'?'+passData+'&timestamp='+(that.updating.getTime()); 
				that.AJAX.open("GET", uri, true);                             

// CHANGE THIS to pages/scripts/css etc that are under development
that.AJAX.setRequestHeader("Cache-Control", "no-cache, no-store");

				that.AJAX.send(null);                                         
			}              
			return true;                                             
		}                                                                           
	}

	var urlCall = url;        
	this.callback = callbackFunction || function () { };
	this.sUrl = url;
}


function callServer(sUrl, sMethod, saPostData, callback)
{
	var request;

	if (callback != null)
		request = new httpRequest(sUrl, callback);
	else
		request = new httpRequest(sUrl);

	// saPostData is an array of name and value pairs. 
	// Even-numbered elems are names, odd-numbered are values.
	// E.g. 'surname=Smith&age=35' : 0=[surname] 1=[Smith] 2=[age] 3=[35]
	// Form-encode saPostData into sPostData
	var sPostData = '';
	var i = 0;
	if (saPostData != null)
	{
		while (i < saPostData.length)
		{
			if (i > 0)
				sPostData += '&';
			sPostData += saPostData [i ++] + '=';
			sPostData += encodeURIComponent(saPostData [i ++]);
		}
	}

	// Make the XMLHttpRequest call. 
	// The response is handled by the 2nd arg 'callback' function.
	request.update(sPostData, sMethod);
}

/************************** END AJAX ************************************/

