// ********************************************************************************************
// Core Javascript language extension routines © Ben Johnson, 2004
// ********************************************************************************************

// PUBLIC GLOBAL VARS *************************************************************************
if(!window.DBG_LEVEL) window.DBG_LEVEL = 0;		// default to debugging disabled

// ROUTINE CONFIG CONSTANTS *******************************************************************
StatusBarMsg.DEFAULT_MSG_DBG_LEVEL = 3;				// default debug level for status bar debug messages

// AF INITIALISE ******************************************************************************
// TO DO: merge! we have two registry types now!
window.AF = { classes:{}, cfg:{}, val:{}, dbgLvl:{} }; 


// MODULE INITIALISE **************************************************************************
//
// Module initialise notes: initialisation routines should avoid dependency on other modules as
// 	much as possible, since this constrains the required load order. If a module 
//
InitCoreModule();
function InitCoreModule() {
	Function.prototype.Name = GetFuncName;						// add Name() method to all Function objects
	Function.prototype.CallerName = GetCallerName;		// add CallerName() method to all Function objects
	window.D = document;															// D = shortcut to Document object
	if(DBG_LEVEL<1) window.onerror = function(sMsg, sUrl, sLine) { if(sMsg) window.status = "ERROR: \""+ sMsg +"\""; return true; }
	window.is = new TypeChecker();
	window.to = new TypeConverter();

	// create modules registry and add self into it
	InitModulesReg();	SetModStatus("core", "ready");
}


// ROUTINES ***********************************************************************************

function TypeChecker() {
	this.Def = function IsDef(v) { return (typeof v!="undefined") && (v!=null); }			// true if not undefined/null
	this.Obj = function IsObj(v) { return (typeof v=="object") && (v!=null); }				// true if obj and not null
	this.Num = function IsNum(v) { return (typeof v=="number") && !isNaN(v); }
	this.Int = function IsInt(v) { return (typeof v=="number") && !isNaN(v) && (Math.floor(v) != v); }
	this.Str = function IsStr(v) { return (typeof v=="string"); }
	this.Array = function IsArray(v) { return IsObj(v) && (v instanceof Array); }			// UNTESTED
	this.NBStr = function IsNBStr(v) { return IsStr(v) && (v!=""); }									// true if is Non-Blank STRing
	this.True =  function IsTrue(v) { return Boolean(v); }														// true unless false, 0, null, NaN or ""
}
function TypeConverter() {
	// returns null (not NaN) if can't convert to number
	this.Num = function ToNum(v) { v=parseFloat(v); return isNaN(v)? null : v; }
	// returns null (not NaN) if can't convert to number
	this.Int = function ToNum(v) { v=parseInt(v, 10); return isNaN(v)? null : v; }
	// returns blank string if null or undefined
	this.Str = function ToStr(v) { return ((typeof v!="undefined") && (v!=null))? String(v) : ""; }
	// identical to IsBool()!
	this.Bool = function ToBool(v) { return Boolean(v); }
	// returns specified string converted to Date object (e.g., "10 June 2005"), or null if invalid
	this.Date = function ToDate(v) { v= new Date(v); return isNaN(v)? null : v; }
}

// to make work, must call in form:  myVar = DelObj(myVar), coz simple indirect assignment doesn't work
function DelObj(o) { delete o; o = null; return o; }


// ERROR HANDLING ***************************************************************************

// return Error object with specified description, number, etc
// 		Usage:  throw CustomErr("bad bad", 249);
function CustomErr(sDescription, iNumber) {
	var oErr = new Error(iNumber, sDescription);
	oErr.source = CustomErr.CallerName();
	return oErr;
}
// returns specified Error object, with specified comment annotated to the Error's description
// 		Usage:  throw AnnotatedErr(oErr, "problem accessing config file");
function AnnotatedErr(oErr, sComment) {
	oErr.description += " >> " + sComment;
	return oErr;
}



// Write SCRIPT element to page to load specified AF script module
function AFModInc(sModName, sCustomAttribs) {
	DbgMsg("Including script module \""+ sModName +"\"");
	document.write("<script src=\""+ AF.cfg.ROOT_URL +"scripts/"+ sModName +".js\" type='text/javascript'"+ (sCustomAttribs?" "+sCustomAttribs:"") +"></script"+">");
}


// MODULES REGISTRY ***************************************************************************

function InitModulesReg() {
	window._mods = [];						// modules public array (associative array of module IDs to status codes)

	// module status public enum constants (for most purposes, just use "if(Mod("dom.core")) ..")
	window._modStatus = ["registered", "loading", "loaded", "ready"];

	// status code look-up by name (reverse of above code-to-status-name)
	_modStatus["registered"] = 0;		// module is registered but not known to be loading yet
	_modStatus["loading"] = 1;			// module registered as loading, but not known to be finished yet
	_modStatus["loaded"] = 2;				// module loaded but not known to be initialised yet
	_modStatus["ready"] = 3; 				// module loaded and initialised
}

// returns true if specified module has specified status ("ready" if none specified), otherwise false
function Mod(sModId, sStatus) { return window._mods[sModId] == _modStatus[(sStatus? sStatus:"ready")]; }
// sets specified module's status code according to the specified status name
function SetModStatus(sModId, sStatus) {
	if(!is.Num(_modStatus[sStatus])) ThrowErr("Invalid module status");
	_mods[sModId] = _modStatus[sStatus];
	if(window._ModWatch) window._ModWatch.OnStatusChange(sModId, sStatus);
}

function WatchMod(sModId, fncCallBack) {
	if(!window._ModWatch) window._ModWatch = new ModWatcher();
	_ModWatch.Add(sModId, fncCallBack);
}
function ModWatcher() {
	this._callBacks = [];
	// add a call-back func to be called when specified module reports status "ready"
	this.Add = function AddModWatcher(sModId, fncCallBack) {
		FuncDbgInfo();
		if(Mod(sModId))
			fncCallBack();					// if module already loaded, call-back immediately
		else {
			// otherwise add call-back func to call-backs list for specified module
			if(!this._callBacks[sModId])
				this._callBacks[sModId] = [fncCallBack];
			else
				this._callBacks[sModId].push(fncCallBack);
		}
	}
	// receive notification that a module's status has changed and fire any call-backs that are waiting
	this.OnStatusChange = function OnModStatusChange(sModId, sStatus) {
		DbgMsg("[<B>"+ sModId +"</B>] module status = <I>\""+ sStatus +"\"</I>", (sStatus=="ready"?4:5) );
		if(sStatus=="ready") {
			var aCallBacks = this._callBacks[sModId];
			if(aCallBacks) {
				DbgBeginSub();
				for(var i=0;i<aCallBacks.length;i++) {
					ShowDbgInfo("ModWatcher.callBack", [aCallBacks[i]]);
					aCallBacks[i]();
				}
				DbgEndSub();
			}
		}
	}
}


// FUNCTION META INFO *************************************************************************

// when called as method of Function object, returns function name as string
function GetFuncName() {
	var sFuncName = this.name;							// get cached function name
	
	if(!is.Def(sFuncName)) {									// if name property undefined, get from function source
		sFuncName = String(this.toString().match(GetFuncName._reFUNC_NAME)[1]);
		// OLD:		GetFuncName._reFUNC_NAME.exec(sFuncCode);	sFuncName = String(RegExp.$1);
		this.name = sFuncName;								// cache function name (but not if came back as blank)
	}
	return sFuncName;
}
// regular expression for extracting function name from function declaration source code
GetFuncName._reFUNC_NAME = /^function\s*(.*?)\(/i;

// when called as method of Function object, returns name of caller function as string
function GetCallerName() {
	var oCallerFunc = this.caller;
	if(oCallerFunc == null)
		return "root code";
	else
		return oCallerFunc.Name();
}


function Request(sParamName) {
	// lazy-load array of page params first time Request() accessed
	if(!AF.aRequest) AF.aRequest = GetPageParamsArray();
	return AF.aRequest[sParamName] || "";
}

function GetPageParamsArray() {
	var x, aParts, aPair, aParams = [];
	if(to.Str(location.search).length > 1) {
		aParts = location.search.substr(1).split("&");
		for(x in aParts) {
			aPair = aParts[x].split("=");
			if(aPair.length==2)  aParams[aPair[0]] = aPair[1];
		}
	}
	return aParams;
}
// alternative (one-off params):
function PageParam(sName) { 
	var aMatch = (new RegExp("[^\\w]"+sName+"=(\\w*)")).exec(location.search);
	return aMatch? aMatch[1] : null;
}


// PLACEHOLDER ROUTINES ***********************************************************************
// 	- scaled down/dummy versions of common module routines (placeholder until full version loads/in case isn't used)
// 	- replace with module checks instead?

function ThrowErr(sDescr) { throw new Error(0, sDescr); }

// simple status bar messaging (gradually scrolls if messages exceed width, HTML is stripped)
var DbgMsg, WarnMsg, DbgRw;
DbgMsg = WarnMsg = DbgRw = function StatusBarDbgMsg(sMsg, iMsgLvl) {
	// use default message debug level, if unspecified
	if(typeof iMsgLvl!="number") iMsgLvl = StatusBarDbgMsg.DEFAULT_MSG_DBG_LEVEL;
	// only write if window.DBG_LEVEL defined and is higher or equal to the message debug level
	if( (typeof window.DBG_LEVEL=="number"? window.DBG_LEVEL : 0) >=iMsgLvl) StatusBarMsg(sMsg)
}
// pseudo-static private constants
StatusBarMsg._reHtmlTags = /<.*?>/gm;		// reg exp to match all HTML tags
function StatusBarMsg(sMsg) { window.status += sMsg.replace(StatusBarMsg._reHtmlTags,"") +" ¶ "; ScrollIfStatusBarOverflow(); }
function ScrollIfStatusBarOverflow() { if(window.status.length >= 129) { window.status = window.status.substr(1); setTimeout("ScrollIfStatusBarOverflow()",150); } }

// simplifed: appends specified HTML to end of specified element, but only if support insertAdjacentHTML()
function AppendHTML(oElem, sHtml) { if(oElem.insertAdjacentHTML) oElem.insertAdjacentHTML("beforeEnd", sHtml); }

// dummy functions and assignments
function _NOP() {}; function _BnkStr() { return ""; }
var DbgBeginSub, DbgEndSub, ShowDbgInfo;  DbgBeginSub = DbgEndSub = ShowDbgInfo = _NOP;
