// ********************************************************************************************
// JavaScript debugging routines © Ben Johnson, 2004
// 	- REQUIRES (depending on func):	[core], [dom.core], [dom.events], [js.strings]
// 	- designed to be an optional module that can be disabled when debugging not required, without breaking other modules
// ********************************************************************************************
SetModStatus("dbg", "loaded");


// PUBLIC GLOBAL VARS *************************************************************************
var _winDbg, _winDbgDoc;

// ROUTINE CONFIG CONSTANTS *******************************************************************
DbgMsg._DEFAULT_MSG_LVL = 2;
InitDbgMsgArea.DBG_WIN_PARAMS = "top=140,left=0,width=300,height=400";
ThrowErr.DEFAULT_ERROR_CODE = -8;
GetDebugLevel.DEFAULT_DBG_LEVEL = 1;
WarnMsg.DEFAULT_MSG_LVL = 1;


// INITIALISATION *****************************************************************************
if(!window.MANUAL_MODULE_INITIALISE) InitDebugModule();
function InitDebugModule() {
	window.DBG_LEVEL = GetDebugLevel();
	// DbgMsg("DBG_LEVEL = " + DBG_LEVEL);
	if(DBG_LEVEL>0) window.onerror = null;							// if debugging enabled, remove window.onerror message suppression handler
	WatchMod("dom.events", AttachDbgWinEvents);		// attach debug win events
	SetModStatus("dbg", "ready");
}


// INIT ROUTINES ******************************************************************************

// init debug level, from query param, existing global var, or default level (in that order)
function GetDebugLevel() {
	var aReMatch, sUrlParams;
	
	// try to get top frame URL params; if error (due to frameset), use current page URL params
	try { sUrlParams = top.location.search; } 
	catch(oErr) { sUrlParams = location.search; }
	
	// if debug level specified as query param, return it
	if(( (aReMatch = sUrlParams.match(/dbg=(\d)/)) != null) && is.Num(Number(aReMatch[1])) )
		return Number(aReMatch[1]);
	else
		// otherwise return window.DBG_LEVEL, if defined, or else _DEFAULT_DBG_LEVEL
		return (is.Num(window.DBG_LEVEL)? window.DBG_LEVEL : GetDebugLevel.DEFAULT_DBG_LEVEL);
}


// DEBUG MESSAGING ****************************************************************************

// write specified debug message, if debug level above or equal to specified level (default to message debug level 3)
function DbgMsg(sHtml, iMsgLvl) {
	// alert("iMsgLvl="+ iMsgLvl +", sHtml="+sHtml+", DBG_LEVEL="+ DBG_LEVEL);
	if(typeof iMsgLvl!="number") iMsgLvl = DbgMsg._DEFAULT_MSG_LVL;
	if(DBG_LEVEL >= iMsgLvl)
		return DbgRw("<ul title=\"Posted by "+ DbgMsg.CallerName() +"()\"><li>"+ sHtml +"</li></ul>");
	else
		return false;
}

// write specified HTML to debug message area, if debugging enabled. if a debug level specified, will only be written if debug level equal or above
// 	returns true if wrote message successfully, else false
function DbgRw(sHtml, iMsgLvl) { 
	if(InitDbgMsgArea())
		if((typeof iMsgLvl != "number") || (DBG_LEVEL >= iMsgLvl))
			if(_winDbg.writeTarget != null) { 
				AppendHTML(_winDbg.writeTarget, sHtml); 
				if(document.all) _winDbgDoc.body.lastChild.scrollIntoView();
				return true;
			} 
			else WarnMsg("DbgRw() error - _writeTarget is null",1);
	return false;
}

// create a UL child of current dbg write target and redirect message to it, until DbgEndSub called
function DbgBeginSub() {
	try {
		if(!(window._winDbg && document.createElement)) return false; 
		var ul = _winDbgDoc.createElement("ul");
		_winDbg.writeTarget.appendChild(ul); 
		_winDbg.writeTarget = ul;
	}
	catch(oErr) { WarnMsg("error starting new sub-section"); return false; }
	return true;
}
// redirect dbg messages to parent of current write target (+ prevent stepping higher than BODY element)
function DbgEndSub() { 
	try {
		if(!(window._winDbg && document.createElement)) return false; 
		if(_winDbg.writeTarget != _winDbgDoc.body) {
			var elParent = _winDbg.writeTarget.parentElement;
			// remove the sub-section, if empty
			//if(_winDbg.writeTarget.childNodes.length==0) _winDbg.writeTarget.removeNode(true);
			_winDbg.writeTarget = elParent;
		}
	}
	catch(oErr) { WarnMsg("error ending sub-section"); return false; }
	return true;
}
// redirect dbg messages back to debug win body, regardless of current target
function DbgEndAllSubs() { 
	try { 
		if(!(window._winDbg && document.createElement)) return false; 
		_winDbg.writeTarget = _winDbgDoc.body;
	}
	catch(oErr) { WarnMsg("error ending all sub-sections"); return false; }
	return true;
}


// InitDbgMsgArea() - if debugging enabled, creates/brings into foreground the _winDbg window and returns true, else returns false
function InitDbgMsgArea() {
	if(DBG_LEVEL<1) return false;
	if(!window._winDbg) {
		_winDbg = window.open(null, "_winDbg", InitDbgMsgArea.DBG_WIN_PARAMS + ",resizable=yes,scrollbars=yes");
		_winDbgDoc = _winDbg.document;							// global shortcut pointer to debug win document object
		with(_winDbgDoc) {
			open();
			// write simple debug page + stylesheet link. 
			write(
				'<!DOCTYPE html public "-//w3c//dtd html 4.01//en"><html><head><title>Debug Messages</title>' +
				"<link href='/af/css/dbg_win.css' rel=stylesheet /></head><body scroll=auto><h1>DEBUG MESSAGES</h1></body></html>"
				);
			close();
		}
		AttachDbgWinEvents();
	
		// point to debug win as DbgRw write target
		_winDbg.writeTarget = _winDbgDoc.body;
		
		// DISABLED: (prevent error if debug win closed + parent tries to access. note that needs [dom.events])	 AttachEvent(_winDbg, "beforeunload", function() { window.onerror = function() {return false} } );
		_winDbg.focus(); window.focus();

		DbgMsg("DBG_LEVEL = " + DBG_LEVEL);			// write DBG_LEVEL as first client-side debug message
	}
	// if window has been closed by user, _winDbg.document will be "unknown", so return false (disables debug messages)
	// 		note that, strangely, _winDbgDoc still points to an object, even after window has closed!
	return (typeof _winDbg.document=="object");
}

function AttachDbgWinEvents() {
	if(Mod("dom.events") && (window._winDbg)) {
		// DISABLED (weird repeated reloading bug) refresh main window, if debug window refreshed (but not if closed, hence use of timeout)
		// AttachEvent(_winDbg, "unload", function() { setTimeout("if(!_winDbg.closed) location.reload()",100) } );
		// clear debug win content if double-clicked
		AttachEvent(_winDbgDoc.body, "dblclick", function() { _winDbgDoc.body.innerHTML = "<h1>DEBUG MESSAGES</h1>"; } );
	}
}


// ERROR HANDLING *****************************************************************************

function ThrowErr(sDescr, fncErrSource) {
	var fncSource = (typeof fncErrSource == "function")? fncErrSource : ThrowErr.caller;
	// create new Error object with specified description/message
	var oErr = new Error(ThrowErr.DEFAULT_ERROR_CODE, fncSource.Name() +"() error: "+ sDescr);
	// set source property to point to func that threw error (use caller if not specified)
	oErr.source = fncSource;
	throw oErr;
}

// write warning message to debug message area (default to debug message level 1)
function WarnMsg(sText, iMsgLvl) {
	if(DBG_LEVEL >= (is.Num(iMsgLvl)?iMsgLvl:WarnMsg.DEFAULT_MSG_LVL))
		DbgRw("<LI class='warn'><SPAN class='warnTitle'><B>"+ WarnMsg.CallerName() +"</B>() warning:</SPAN> "+ sText +"</LI>");
}


// FUNCTION META INFORMATION ******************************************************************

// returns bold formatted name of specified function, if available, else "unknown func" (italic formatted)
function GetFuncLabel(fnc) {
	var sFuncName = fnc.Name? fnc.Name(): "";
	return (sFuncName!=""? sFuncName.bold()+"()" : "<I>unknown func</I>");
}

// returns array of specified Function's arguments
function GetArgs(fnc) {
	var i, oArgs=fnc.arguments, aArgs=[];
	for(i=0; i<oArgs.length; i++) {aArgs[i]=oArgs[i];}
	return aArgs;
}


// OBJECT META INFORMATION ********************************************************************

// dump attributes of specified object/element in multi-sectioned table (can dblclick to remove)
function ListAttribs(oElem) {
	var x, oErr;		// general counter for object loops

	try {
		if((typeof oElem != "object") || (oElem == null)) { WarnMsg("invalid object specified"); return false; }
		if(!Mod("js.strings")) { WarnMsg("Aborting - required module [js.strings] not loaded"); return false; }
	
		// matches "", "<I>""</I>", "null", "object", "unknown", "undefined"
		var reNoUsefulValue = RegExp().compile("/^$|^<I>\"\"<\/I>$|^null$|^\[object\]$|^unknown|^undefined$/");
	
		InitDbgMsgArea();
		var d = _winDbgDoc;
		var divPlaceHolder = d.createElement("DIV");
		divPlaceHolder.innerHTML = "<UL><LI>"+ (oElem.tagName? "&lt;<B>"+ oElem.tagName +"</B>&gt; element attributes" : "Object properties") +" : <I>(building, please wait ..)</I></LI></UL>";
		d.body.appendChild(divPlaceHolder);
	
		// increment IDs used in this attrib list table, to avoid naming conflicts
		if(typeof _winDbg.attribListCount != "number") _winDbg.attribListCount = -1;		// intialise attribListCount to -1 (so starts at 0)
		var i = ++_winDbg.attribListCount;																							// increment attribListCount
	
		// if object has an attributes collection, list that. Otherwise list all object properties
		var attribTerm, bHasAttribsCol = is.Def(oElem.attributes);
		// create terminology dictionary according to listing type
		if(bHasAttribsCol) { 
			attribTerm = {toString:function(){return "Attribute"}, lc:"attribute", plural:"attributes", shPlural:"attribs"}; }
		else {
			attribTerm = {toString:function(){return "Property"}, lc:"property", plural:"properties", shPlural:"props"}; }
		
		var sb = new StringBuilder();
		sb.Add("<table class=tblElemAttribs onclick='this.parentNode.removeNode(true); event.cancelBubble=true;'><col align=right /><col align=center />");
		sb.Add("<thead><tr><th>"+ attribTerm +" Name</th><th>Value</th></tr></thead>");
		sb.Add("<tr class=tbdyHead><td colspan=2>Explicitly specified "+ attribTerm.shPlural +":</td></tr><tbody id=tbdySpecified"+i+"></tbody>");
		sb.Add("<tr class=tbdyHead><td colspan=2>Default "+ attribTerm.shPlural +" with non-blank values:</td></tr><tbody id=tbdyWithVal"+i+"></tbody>");
		sb.Add("<tr class=tbdyHead style='cursor:pointer;'><td colspan=2 onclick='tbdyNoVal"+i+".style.display=\"block\"; event.cancelBubble=true;'>Empty / undisplayable "+ attribTerm.shPlural +" (click to display):</td></tr><tbody id=tbdyNoVal"+i+" style='display:none'></tbody>");
		sb.Add("<tr class=tbdyHead style='cursor:pointer;'><td colspan=2 onclick='tbdyEvents"+i+".style.display=\"block\"; event.cancelBubble=true;'>Empty event handlers (click to list):</td></tr><tbody id=tbdyEvents"+i+" style='display:none'><tr><td id=tdEvents"+i+" class=tdEvents colspan=2 class=wrap></td></tr></tbody></table>");
		AppendHTML(divPlaceHolder, sb);
		DelObj(sb);
		
		// create pointers to the various table elements
		with(_winDbgDoc) {
			var tbdySpecified = getElementById("tbdySpecified"+i);
			var tbdyWithVal = getElementById("tbdyWithVal"+i);
			var tbdyNoVal = getElementById("tbdyNoVal"+i);
			var tbdyEvents = getElementById("tbdyEvents"+i);
			var tdEvents = getElementById("tdEvents"+i);
			var tblElemAttribs = tbdySpecified.parentNode;
		}
	
		// point oAttribsCol to either the object's attributes collection, or the object itself
		var oAttribsCol = bHasAttribsCol? oElem.attributes : oElem;
		var lstEvents = new ListBuilder(", ");
		
		// iterate through the element attributes
		for(x in oAttribsCol) {
			try {
				if(oAttribsCol[x]) {
					var sAttribName = bHasAttribsCol? oAttribsCol[x].nodeName : x;
					if(typeof sAttribName == "string") {
						var attribVal = bHasAttribsCol? oAttribsCol[x].nodeValue : oAttribsCol[x];
						try {
							var sAttribVal = String(attribVal); }
						catch(oErr) {
							sAttribVal = "unknown ("+ typeof attribVal+")";
						}
						if(typeof attribVal == "string") sAttribVal = "<I>\""+ sAttribVal +"\"</I>";
						
						// add attribute information row under appropriate TBODY section
						if(bHasAttribsCol && oAttribsCol[x].specified)
							AddTR(tbdySpecified, [sAttribName, sAttribVal] );												// explicitly specified
						else if( !reNoUsefulValue.test(sAttribVal) )
							AddTR(tbdyWithVal, [sAttribName, sAttribVal] );													// non-blank default oAttribsCol
						else if( (sAttribName.substr(0,2) == "on") && (sAttribVal == "null"))
							lstEvents.Add(sAttribName);																							// empty events (add to ListBuilder string, then write later)
						else
							AddTR(tbdyNoVal, [sAttribName, sAttribVal] );														// other blank oAttribsCol
					}
				}
			}
			catch(oErr) {
				WarnMsg("error displaying an element "+ attribTerm.lc +" &nbsp;(<I>\""+ oErr.description +"\"</I>)", 5);
			}
		}
	
		if(lstEvents.len>0)
			tdEvents.innerHTML = lstEvents;						// if empty events found, insert list and show section
		else
			tbdyEvents.previousSibling.style.display = "none";
	
		// hide empty TBODY elements
		var aTbdies = [tbdySpecified, tbdyWithVal, tbdyNoVal, tbdyEvents];
		for(x in aTbdies) if(aTbdies[x].childNodes.length==0) aTbdies[x].previousSibling.style.display = "none";
	
		// update listing title to include element ID and remove "building.." message
		divPlaceHolder.childNodes[0].childNodes[0].innerHTML = (oElem.tagName? "&lt;<B>"+ oElem.tagName +"</B>&gt; element " + (typeof oElem.id == "string"? "(id=\""+ oElem.id +"\") " :"") : "Object ") + attribTerm.plural +" :";
		tblElemAttribs.style.display = "block";			// unhide the table
	}
	catch(oErr) {
		WarnMsg("error while building attributes table &nbsp;(<I>\""+ oErr.message +"\"</I>)");
		return false; 
	}
	return true; 
}


// MODULE TEST ROUTINES ***********************************************************************

// StringBuilder/ListBuilder
function _SBTest() {
	sb = new StringBuilder("dog"); sb2 = new StringBuilder("xyz"); lst = new ListBuilder("-"); lst2 = new ListBuilder("=");
	sb.Add(" cat"); lst.Add("ben"); sb2.Add(" dfd"); sb.Add(" rabbit"); lst2.Add("WOW"); sb.Add(" pig"); lst.Add("luke"); 
	sb.Add(" donkey"); lst2.Add("WHOOSH"); lst.Append("_sucks"); sb2.Add(" blrg"); lst2.Append("BANG"); lst.Add("tom");
	alert(lst +"#"+ sb +"#"+ sb2 +"#"+ lst2);
}


// EXTENDED DEBUG INFORMATION ROUTINES ********************************************************

// Centralised function for producing various debug output, to avoid clutter and unnecessary processing within main code
// 	- Obtains data in two ways: either in aData array (parameter), or automatically by accessing caller's 'arguments' collection
function ShowDbgInfo(sInfoId, aData, iMsgLvl) {
	var oErr;
	if(DBG_LEVEL > 0) {
		try {
			var s={}; 	// general purpose object for collecting together temporary vars (since required local vars varies considerably)
			// shortcut pointer to caller Function object and its arguments collection
			var fncCaller = ShowDbgInfo.caller, aCallerArgs = ShowDbgInfo.caller.arguments;

			switch(sInfoId) {
				case "LoadModules.begin" :
					s.aModuleNames = aCallerArgs[0];
					DbgMsg("LoadModules: [<B>"+ s.aModuleNames.join("</B>], [<B>") +"</B>]", (is.Num(iMsgLvl)?iMsgLvl:2));
					break;
				case "TmpWrapper.result" :
					s.tmpFuncName = aData[0];
					s.funcName = (aCallerArgs.length>=1)? GetFuncLabel(aCallerArgs[0]) : "";
					s.arg = (aCallerArgs.length>=2)? aCallerArgs[1] : null;
					s.argText = s.arg? "with arg: "+ (typeof s.arg == "string"? "\""+s.arg.italics()+"\"" : s.arg) : " with no arguments";
					DbgMsg("wrapper func <I>\""+ s.tmpFuncName +"\"</I> setup to call "+ s.funcName.italics() + s.argText, (is.Num(iMsgLvl)?iMsgLvl:5));
					break;
				case "ModWatcher.callBack" :
					s.cbFnc = aData[0];
					DbgMsg("firing call-back func "+ GetFuncLabel(s.cbFnc), 3);
					break;
				default :
					WarnMsg("unknown sInfoId", 2);
			}
		}
		catch(oErr) {
			WarnMsg("error generating debug info (\""+ oErr.message +"\")");
		}
	}
}

// displays name of specified function (or caller, if unspecified) and a breakdown of its arguments
function FuncDbgInfo(iMsgLvl, fnc) {
	if(!is.Def(fnc)) fnc = FuncDbgInfo.caller;
	var sCallerLabel = GetFuncLabel(fnc);
	var aArgs = GetArgs(fnc);
	var sArgCount = aArgs.length;
	DbgMsg(sCallerLabel +" called with "+ sArgCount +" argument(s)" + ((sArgCount>0)&&LabelArray? ": "+LabelArray(aArgs).join(", ") :""), (is.Num(iMsgLvl)?iMsgLvl:5));
}


// HELPER ROUTINES ****************************************************************************

// temp placeholder for [js.strings] version
function GetLabel(v) { return (typeof v == "function")? v.Name().italics() : v+""; }

// Formats elements of specified array according to their type (via GetLabel)
// 	- modifies specified array directly (by reference) and also returns pointer
function LabelArray(array) {
	var i;
	// OLD: if(!window.GetLabel) WarnMsg("GetLabel not available; falling back on plain text formatting", 5);
	for(i=0; i<array.length; i++)	array[i] = window.GetLabel? GetLabel(array[i]) : array[i]+"";
	return array;
}
