// This file uses JSDoc-friendly comments [ http://jsdoc.sourceforge.net/ ]

/**
 * @file         grvUtils.js
 * @fileoverview Collection of general utility routines
 * @author       Bruce Wallace  (PolyGlotInc.com)
 * @version      2.5
 */

//////////////////////////////////////////////////////////////////
// GRAVEY LEXICAL CODING CONVENTIONS:
// (*) All private variables or functions start with "_"
// (*) All variables and functions start with a lowercase letter
// (*) All Classes start with an uppercase letter
// (*) All Class methods and instance variables start with lowercase
// (*) All Class "static" methods and variables start with uppercase
// (*) All constants start with "k"
// (*) All global variables start with "g"
// (*) All event handler functions start with "on"
//
// (*) All Gravey utility global variables start with "gGrv"
// (*) All Gravey MVC     global variables start with "gMVC"
// (*) All Gravey EDO     global variables start with "gEDO"
// (*) All Gravey utility functions start with "grv"
// (*) All Gravey MVC     functions start with "mvc"
// (*) All Gravey MVC event handler functions start with "onMVC"
// (*) All Gravey EDO event handler functions start with "onEDO"
// (*) All Gravey MVC classes start with "MVC"
// (*) All Gravey EDO classes start with "EDO"
//////////////////////////////////////////////////////////////////

// (unless user already has) init each global event tracing flag
if (typeof gGrvTraceObj === 'undefined') gGrvTraceObj = false;	//OO simulation tracing
if (typeof gGrvTraceMVC === 'undefined') gGrvTraceMVC = false;	//MVC setup tracing
if (typeof gGrvTraceEvt === 'undefined') gGrvTraceEvt = false;	//Event processing tracing
if (typeof gGrvTraceMsg === 'undefined') gGrvTraceMsg = false;	//broadcast message tracing
if (typeof gGrvTraceCmp === 'undefined') gGrvTraceCmp = false;	//compile message tracing
if (typeof gGrvTraceTODO=== 'undefined') gGrvTraceTODO= false;	//"not implemented yet" msgs and other hacks

function grvIs_IE_Pre7(){ if (typeof(gGrvIE_Pre7_Flag)=="undefined") return false; else return gGrvIE_Pre7_Flag; }
function grvIs_IE     (){ return '\v'=='v'; }

//----------------------------------------------------------------------------
// Utility routines to support debugging
//----------------------------------------------------------------------------

/**
* open debug window with contents in scrollable area
* @param {String} contents what to put into this debug window
*/
function grvDebugWindow( contents )
{
	//TODO use jQuery to create/use DebugWindow
	window.open().document.write("<textarea cols=150 rows=40>"+ contents + "</textarea>" );
}

/** cause the debugger to stop (ala a breakpoint) */
function grvBreakpoint()
{
	//TODO use jQuery to get at DebugWindow
	//open window with current dynamic HTML displayed
	grvDebugWindow( document.body.parentNode );
	//cause Javascript debugger to kick in on undefined object reference
	//OR, comment out the next line and pre-set a breakpoint here via debugger
	grvUNDEFINED();
}

/** output the given message and ask user if they wish to break
* @return true iff not "break"ing
*/
function grvBreak(msg)
{
	if (msg.length>200) { grvDebugWindow( msg ); msg="(see window)"; }
	if (confirm(msg+"\nBreak here?")) { grvBreakpoint(); return false; }
	return true;
}

//----------------------------------------------------------------------------
// Utility routines for Trace Messages
//----------------------------------------------------------------------------

function grvTrace(flag,msg){ if (flag) return grvBreak(msg); else return true; }

function grvTraceMVC(msg){ return grvTrace(gGrvTraceMVC,msg); }
function grvTraceObj(msg){ return grvTrace(gGrvTraceObj,msg); }
function grvTraceEvt(msg){ return grvTrace(gGrvTraceEvt,msg); }
function grvTraceMsg(msg){ return grvTrace(gGrvTraceMsg,msg); }
function grvTraceCmp(msg){ return grvTrace(gGrvTraceCmp,msg); }

function grvTraceTODO(msg){ return grvTrace(gGrvTraceTODO,"TO DO:"+msg); }

function grvTimestamp(){ return (new Date()).getTime(); }

grvTraceCmp("grvUtils.js: Trace API compiled");

// ----------------------------------------------------------------------------
// Utility routines for Formatting Data
// ----------------------------------------------------------------------------

/** return the HTML to draw a bold right arrow @type String */
function grvRightArrow(){ return grvIs_IE() ? '<font face="Symbol">&#222;</font>' : '&rArr;'; }

/** return the HTML to draw a biggish dot @type String */
//function grvDot       (){ return '<font face="Symbol">&#183;</font>'; }
//function grvDot       (){ return '<font face="Symbol">&middot;</font>'; }
function grvDot       (){ return '<font face="Symbol">&bull;</font>'; }

/** return the HTML to draw a smallish "E" @type String */
//function grvEpsilon   (){ return '<font face="Symbol">&#101;</font>'; }
function grvEpsilon   (){ return '&epsilon;'; }


/** return the HTML to draw a diamond
 * @param {boolean} isBlank if true, says generate blanks of same size as 1 diamond, otherwise a diamond
 * @type String
 */
function grvDiamond( isBlank ){ return isBlank ? "&loz;&nbsp;" : "&diams;&nbsp;"; }

/** return the HTML to draw a subitem bullet @type String */
function grvSubitem( isInValid ){ return isInValid ? grvEpsilon() : grvRightArrow() }

/** Is the given keyCode in the given list of allowed characters?
 * @param {int} keyCode the charCode of the character to validate
 * @param {String} allowedChars the list of characters that are allowed
 * @return true iff keyCode is legal
 * @type boolean
 */
function grvKeyFilter( keyCode, allowedChars ){
	return allowedChars.indexOf(String.fromCharCode(keyCode)) >= 0;
}

/** Does the given string consist of only characters in the given
 * list of allowed characters?
 * @param {int} keyCode the charCode of the character to validate
 * @param {String} allowedChars the list of characters that are allowed
 * @return true iff keyCode is legal
 * @type boolean
 */
function grvStrFilter( s, allowedChars ){
	var q = "";
	for (var i=0; i<s.length; ++i)
	{
	  var c = s.charAt(i);
	  if (allowedChars.indexOf(c) >= 0) q += c;
	}
	return q;
}

/** Is there no more than one occurance of each
 * character in the given character list in the given string?
 * @param {String} s string to check
 * @param {String} charList list of characters that should occur only once
 * @return true if there are no duplicates
 * @type boolean
 */
function grvNoDuplicates( s, charList )
{
	for (var i=0; i<charList.length; ++i)
	{
	  var c = charList.charAt(i);
	  var first = s.indexOf(c);
	  if (first!=-1 && first!=s.lastIndexOf(c)) return false;
	}
	return true;
}

var kDigitChars = "0123456789";
var kFloatChars = "+-.";

/** convert the given character to uppercase
 * @param {int} keyCode the proposed new character
 * @param {String} valueSoFar the value so far
 * @return updated char iff character is legal else null
 * @type boolean
 */
function grvUpperKeyFilter( keyCode, valueSoFar )
{
	if ( keyCode>96 && keyCode<123 ) keyCode -= 32;
	return keyCode;
}

/** is the given character a legal addition to the given dollar string
 * @param {int} keyCode the proposed new character
 * @param {String} valueSoFar the value so far
 * @return updated char iff character is legal else null
 * @type boolean
 */
function grvDateKeyFilter( keyCode, valueSoFar )
{
	if ( grvKeyFilter( keyCode, kDigitChars+"/-" ) ) return keyCode;
	return null;
}

/** is the given character a legal addition to the given dollar string
 * @param {int} keyCode the proposed new character
 * @param {String} valueSoFar the value so far
 * @return updated char iff character is legal else null
 * @type boolean
 */
function grvDollarKeyFilter( keyCode, valueSoFar )
{
	if ( grvKeyFilter( keyCode, kDigitChars+kFloatChars+"$," ) )
//	&& grvNoDuplicates( valueSoFar+String.fromCharCode(keyCode), "$"+kFloatChars );
	  return keyCode;
	return null;
}
function grvDollarStrFilter(s){ return grvStrFilter(s,kDigitChars+kFloatChars); }
function grvIsZeroDollars(v){ return Math.abs(parseFloat(v))<0.01; }


/** format given value as -<$>#<,>###.00<%>
 * @type String
 * @param {anyType} v value to format
 * @param {String} currencyPreSymbol   prefix "currency symbol" to add
 * @param {String} currencyPostSymbol postfix "currency symbol" to add
 * @param {String} sepSymbol thousands "separator" to add
 * @param {boolean} optZeroAsBlankFlag optional flag to make zero format as blank
 * @author Bruce Wallace (PolyGlotInc.com)
 * @version 2.0
 */
function grvFormatDecimal( v, currencyPreSymbol, currencyPostSymbol, sepSymbol, optZeroAsBlankFlag )
{
	if ( v==null || v=="null" ) return grvDot();
	if ( grvIsZeroDollars(v) ) v = 0; //get rid of microcents and negative zeroes
	if ( optZeroAsBlankFlag && v==0 ) return "&nbsp;";

	// Do the equivalent to XSL::format-number($v,'$#,###.00')
	var x = parseFloat(v).toFixed(2);
	var sign = '';
	if (x < 0){ sign = '-'; x = x.substr(1); }
    var C    = x.split("");
    var n    = C.length-4;
    var s    = x.substring(n+1,n+4);

	for (var i=n; i>=0; --i)
	    s = (((C.length-i)%3==0 && i!=0) ? sepSymbol : "") + C[i] + s;

	return sign + currencyPreSymbol + s + currencyPostSymbol;
}

/** format given value as $#,###.00
 * @type String
 * @param {anyType} v value to format
 * @param {boolean} optZeroAsBlankFlag optional flag to make zero format as blank
 * @author Bruce Wallace (PolyGlotInc.com)
 * @version 2.0
 */
function grvFormatDollar ( v, optZeroAsBlankFlag ) {
  return grvFormatDecimal( v, "$", "", ",", optZeroAsBlankFlag );
}

// a global month names array
var kMonthNames = new Array(
		'January',
		'February',
		'March',
		'April',
		'May',
		'June',
		'July',
		'August',
		'September',
		'October',
		'November',
		'December'
	);

// a global day names array
var kDayNames = new Array(
		'Sunday',
		'Monday',
		'Tuesday',
		'Wednesday',
		'Thursday',
		'Friday',
		'Saturday'
	);

var kMonthNums = new Array(
		"01","02","03","04","05","06","07",
		"08","09","10","11","12");

function grvGetTodayAsXXDDYYYY(monthNames)
{
var now = new Date();
return monthNames[ now.getMonth() ]
 + "/" + now.getDate()
 + "/" + now.getFullYear();
}
function grvGetTodayAsMMDDYYYY(){
return grvGetTodayAsXXDDYYYY( kMonthNums );
}

function grvFormatDate( d, f )
{
    if (!d) return '';
    return f.replace(/(yyyy|mmmm|mmm|mm|dddd|ddd|dd|hh|nn|ss|a\/p)/gi,
        function($1)
        {
            switch ($1.toLowerCase())
            {
            case 'yyyy': return d.getFullYear();
            case 'mmmm': return kMonthNames[d.getMonth()];
            case 'mmm':  return kMonthNames[d.getMonth()].substr(0, 3);
            case 'mm':   return grvFormatLpadded( (d.getMonth()+1), 2, "0" );
            case 'dddd': return kDayNames[d.getDay()];
            case 'ddd':  return kDayNames[d.getDay()].substr(0, 3);
            case 'dd':   return grvFormatLpadded( d.getDate(), 2, "0" );
            case 'hh':   return grvFormatLpadded( ((h=d.getHours()%12)?h:12), 2, "0" );
            case 'nn':   return grvFormatLpadded( d.getMinutes(), 2, "0" );
            case 'ss':   return grvFormatLpadded( d.getSeconds(), 2, "0" );
            case 'a/p':  return d.getHours() < 12 ? 'a' : 'p';
            }
        }
    );
}


/** format given value as ####.00%
 * @type String
 * @param {anyType} v value to format
 * @param {boolean} optZeroAsBlankFlag optional flag to make zero format as blank
 * @author Bruce Wallace (PolyGlotInc.com)
 * @version 2.0
 */
function grvFormatPercent( v, optZeroAsBlankFlag ) {
  return grvFormatDecimal( v, "", "%", "", optZeroAsBlankFlag );
}

/** like format_dollar except empty string returned for zero @type String */
function grvFormatDollarNot( v ){ return grvFormatDollar( v, true ); }


/** pad or truncate to be exactly given length @type String */
function grvFormatRpadded( s,len,padChar ){
	if ( s==null || s=="null" ) return grvDot();
	for (var i=0; i<len; ++i) s += padChar;
	return s.substr(0,len);
}

/** pad or truncate to be exactly given length @type String */
function grvFormatLpadded( s,len,padChar ){
	if ( s==null || s=="null" ) return grvDot();
	for (var i=0; i<len; ++i) s = padChar+s;
	return s.substring(s.length-len);
}

/** blankpad or truncate to be exactly given length @type String */
function grvFormatPaddedStr( s,len ){
//	return grvFormatRpadded( s,len,"&nbsp;");
	return grvFormatRpadded( s,len," ");
}


/** convert CRs to <br> tags @type String */
function grvFormatMultilineStr( s,len )
{
	if (s==null || s=="null") return grvDot();
	return s.replace( new RegExp( "\\n", "g" ), "<br>" );
}

/** format Server Error Messages @type String */
function grvFormatErrorStr( msg ){
	if (grvIsEmpty(msg)) return "";
	msg = msg.replace( new RegExp( "\\n", "g" ), "<br>" );
	var HTML  = new Array();
		HTML.push( '<table class="grvErrpanel" cellpadding="10" cellspacing="0" width="100%">' );
		HTML.push( '<tr><td>SERVER ERROR: '+msg+'</td></tr>' );
		HTML.push( '</table>' );
 return HTML.join('');
}

/** generate a piece of HTML, identified by the given ID,
 * that can safely have its innerHTML replaced at runtime.
 * @param {String} hookID name of the HTML to be generated
 * @param {String} optInnerHTML optional HTML to be inserted
 * into the generated HTML
 * @return generated HTML
 * @type String
 * @see #grvGetHook
 * @author Bruce Wallace (PolyGlotInc.com)
 * @version 2.0
 */
function grvGenHook( hookID, optInnerHTML ){
	return '<span id="'+hookID+'">'
	     + (optInnerHTML?optInnerHTML:"")
	     + '</span>';
}
//function grv$GetHook( hookID ){
//	function jq(myid) { return '#' + myid.replace(/(:|\.)/g,'\\$1'); }
//	return $( jq(hookID) );
//}
function grvGetHook( hookID ){
	//TODO decide whether there are any remaining portability problems now that we use ".id" instead of ".name"
	//and if jQuery solves them
//	var jE = grv$GetHook( hookID )[0];

	//special case: document.body
	if (hookID=="document.body"){
	 var e = document.body;
	 grvTraceMVC("document body is:["+e+"]");
	 return e;
	}

	var e = document.getElementById( hookID );
	if (e==null) grvTraceMVC("cant find hook["+hookID+"]");
//	else  if (jE==null) alert("jQuery failed finding["+grvObjectToString(e)+"]");
	return e;
}

function grvSetElemBackgroundColor( e, color ){
    if (!e) {grvBreak("null setElemBackgroundColor!"); return;}
	e.style.backgroundColor = color;
}
function grvSetBackgroundColor( ID, color ){
	grvSetElemBackgroundColor( document.getElementById(ID), color );
}
function grvSetElemText( e, str ){
    if (!e) {grvBreak("null setElemText!"); return;}
	e.innerHTML = str;
}
function grvSetText( ID, str ){
	grvSetElemText( grvGetHook(ID), str );
}
/** @see http://www.permadi.com/tutorial/jsInnerHTMLDOM/ */
function grvSetButtonElemText( button, str )
{
	if (!button) {grvBreak("null setButtonText!"); return;}
	if (button.childNodes[0]) button.childNodes[0].nodeValue = str; else
	if (button.value)         button.value = str;                   else
  /*if (button.innerHTML)*/   button.innerHTML = str;
}
function grvSetButtonText( buttonId, str ){
	grvSetButtonElemText( grvGetHook(buttonId), str );
}


//TODO implement these via CSS instead
var kColorTeal       = '#6eaba1';
var kColorTealLight  = '#99CCCC';
var kColorCornstarch = '#FFFAF0';
var kColorWhitePepper= '#F0F0CC';
var kColorDaffodil   = '#FFFACA';
var kColorNavy       = '#000066';
var kColorDarkSilver = '#COCOCO';
var kColorLime       = '#66FF66';
var kColorLightMoney = '#CCFFCC';
var kColorMatteSilver= '#CCCCCC';
var kColorLemonCream = '#FFFFCC';
var kColorPeriwinkle = '#9CA9C5';
var kColorBanana     = '#FFFF66';
var kColorTurqoise   = '#66CCCC';
var kColorPumpkin    = '#FFCC33';
var kColorPumpkinGrey= '#DDBB08';
var kColorDustyRose  = '#FFE0E0';
var kColorDustierRose= '#EEAAAA';
var kColorRoseGrey   = '#DAABAB';
var kColorLipstick   = '#FF6666';
var kColorYellow     = '#FFFF00';


var      kInValidColor = '#FF9900';
var       kEditedColor = kColorTealLight;//'#FFCC99';
var     kSelectedColor = kColorLemonCream;
var   kUnSelectedColor = kColorCornstarch;//kColorDaffodil;
var kROUnselectedColor = kColorMatteSilver;

function grvHighlightElem( e, makeSelected, selectColor, optUnselectColor ){
	if ( grvIsUndefined(optUnselectColor) ) optUnselectColor = kUnSelectedColor;
	grvSetElemBackgroundColor( e, makeSelected?selectColor:optUnselectColor );
}
/*ALTERNATE COLOR SCHEME...
//set the background color of the given element based on
//whether it is selected, dirty, and valid.
function grvEditElem( e, makeSelected, makeEdited, makeInvalid ){
	var useColor = makeSelected || makeEdited || makeInvalid;
	grvHighlightElem( e, useColor, makeInvalid ? kInValidColor :
	                               makeEdited  ? kEditedColor  :
	                                             kSelectedColor );
}
*/
//set the background color of the given element based on
//whether it is selected, dirty, and valid.
function grvEditElem( e, makeSelected, makeEdited, makeInvalid, optUnselectColor ){
	var useColor = makeSelected || makeEdited;
	grvHighlightElem( e, useColor, makeEdited ? kEditedColor  :
	                                          kSelectedColor, optUnselectColor );
}
function grvEditElemID( ID, makeSelected, makeEdited, makeInvalid ){
	grvEditElem( grvGetHook(ID), makeSelected, makeEdited, makeInvalid );
}
function grvSelectElem( e, makeSelected, optUnselectColor ){
	grvHighlightElem( e, makeSelected, kSelectedColor, optUnselectColor );
}
function grvSelectElemID( ID, makeSelected, optUnselectColor ){
	grvSelectElem( grvGetHook(ID), makeSelected, optUnselectColor );
}

function grvSetElemVisibility( e, visibleFlag )
{
    if (!e) {if (grvTraceMVC("null setElemVisibility!")) return;}
	e.style.visibility = visibleFlag ? "visible"  : "hidden";
//	e.style.position   = visibleFlag ? "relative" : "absolute";
}
function grvGetElemVisibility( e )
{
    if (!e) {if (grvTraceMVC("null getElemVisibility!")) return false;}
	return e.style.visibility == "visible";
}

function grvHideElem( e )
{
	if ( e.style.display === "none" ) return;
	e.grvOldDisplay = e.style.display;
	e.style.display = "none";
}
function grvShowElem( e )
{
	if ( e.grvOldDisplay ) e.style.display = e.grvOldDisplay;
	else if ( e.style.display==="none") e.style.display = "";
}

function grvSetElemDisplay( e, visibleFlag )
{
    if (!e) {if (grvTraceMVC("null setElemDisplay!")) return;}
	if (visibleFlag) grvShowElem(e); else grvHideElem(e);
// // //	e.style.display = visibleFlag ? "inline" : "none";
// //	e.style.display = visibleFlag ? "block" : "none";
//	if (visibleFlag) $(e).show(); else $(e).hide();
}
function grvSetVisibility( ID, visibleFlag ){
	grvSetElemVisibility( grvGetHook(ID), visibleFlag );
}

function grvGetFormInt( ID ){
	return parseInt( grvGetFormValue(ID) );
}
function grvGetFormValue( ID ){
	return document.getElementById(ID).value;
}
function grvSetFormValue( ID, theValue ){
	document.getElementById(ID).value = theValue;
}


/**
 * Determine whether a given datestr is explicitly before the given date.
 * @param {String} datestr string rendition of date in Date.parse format
 * @param {Date} date date object to compare to
 * @return true if given date is not empty and before given date
 * @type boolean
 * @author Bruce Wallace (PolyGlotInc.com)
 * @version 2.0
 */
function grvBefore( datestr, date ){
	return datestr && datestr.length>0 && ( date > Date.parse(datestr) );
}

/**
 * Determine whether given date is at or before a given datestr (where empty means "future").
 * @param {String} datestr string rendition of date in Date.parse format
 * @param {Date} date date object to compare to
 * @return true if given date is empty or after today's date
 * @type boolean
 * @author Bruce Wallace (PolyGlotInc.com)
 * @version 2.0
 */
function grvAfter( datestr, date ){
	return( (datestr && datestr.length>0) ? (date < Date.parse(datestr)) : true	);
}

/**
 * Determine whether a given date is explicitly before today.
 * @param {String} datestr string rendition of date in Date.parse format
 * @return true if given date is not empty and before today's date
 * @type boolean
 * @author Bruce Wallace (PolyGlotInc.com)
 * @version 2.0
 */
function grvBeforeNow( datestr ){ return grvBefore( datestr, new Date() ); }

/**
 * Determine whether today is at or before a given date (where empty means "future").
 * @param {String} datestr string rendition of date in Date.parse format
 * @return true if given date is empty or after today's date
 * @type boolean
 * @author Bruce Wallace (PolyGlotInc.com)
 * @version 2.0
 */
function grvAfterNow( datestr ){ return grvAfter( datestr, new Date() ); }

// ----------------------------------------------------------------------------
// Utility routines for "please wait" indicators
// ----------------------------------------------------------------------------

function grvAppName(appId){
	return appId ? appId : (kAppName?kAppName:"");
}
function grvWaitWindowName(appId){
	return "_gGrvWaitWindow" + grvAppName(appId);
}
/** Create an idempotent "wait a minute" window, if one doesn't exist,
 * and squirrel away a reference to it that survives window reloads
 */
function grvWAIT(appId)
{
	// HACK! windows get goofed up if we dont suppress
	// the "please wait" window on launch...
	if (_gGrvHackStartupInhibitWait) return;

	var winName = grvWaitWindowName(appId);
	if (navigator[ winName ]==null)
		navigator[ winName ]= grvPleaseWait( true, "Loading data.", appId );
}
/** close the "please wait" window if it exists otherwise no effect */
function grvUNWAIT(appId) {
	var winName = grvWaitWindowName(appId);
	if (navigator[ winName ]==null) return;
	    navigator[ winName ].close();
	    navigator[ winName ] = null;
}

/** are we in "please wait" mode? @type boolean */
function grvWAITING(appId){ return navigator[ grvWaitWindowName(appId) ]; }

/** create and open a window (if enabled) with the specified message
 * @param {boolean} enableFlag iff true then create window
 * @param {String} msg message to place in window
 * @return the created window or null if not enabled
 * @type Object
 */
function grvPleaseWait( enableFlag, msg, appId )
{
	var theWindow = null;
	if ( enableFlag )
	{
	 var xMax = screen.width, yMax = screen.height;
     var xOffset = (xMax - 220)/2, yOffset = (yMax - 100)/2;
     theWindow = window.open("", 'grvPleaseWait', 'width=250 height=100 toolbar=no scrollbars=no menubar=no resizable=yes top='+yOffset+' left='+xOffset+'');
     theWindow.document.write("<BODY BGCOLOR="+kColorTeal+">");
     theWindow.document.write("<TITLE>" +grvAppName(appId)+ " Processing...</TITLE>");
     theWindow.document.write('<FONT FACE="Arial" SIZE=2><layer id="c"><left><B>'+msg+'<br/>Please Wait.</left></layer></font><br>');
	}
	return theWindow;
}

/** set the cursor to the hourglass icon */
function grvBusy() {
  document.body.style.cursor='wait';
}
/** set the cursor to the default icon */
function grvNotBusy() {
  document.body.style.cursor='default';
}

/**
 * generic utility function to queue up for future execution the specified
 * function with the specified arguments after a specified amount of delay.
 * THIS function returns immediately, and the specified function will execute
 * later (in potentially a different thread). If this is called with the same
 * function specified as was specified earlier, and that function has not
 * executed yet, then the scheduled execute time is updated, rather than the
 * function being executed twice.
 * @param {String} funcname the name of the function to call
 * @param {String} funcargs a string image of the parameters to be passed
 * @param {int} optDelayMilliSecs optional delay amount (default 5 milliSeconds)
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function grvBusyDo( funcname, funcargs, optDelayMilliSecs )
{
	var timedelay = optDelayMilliSecs || 5;
//	grvBusy();
	var globalVarName = "_gGrvTimeout"+funcname;
	if (navigator[ globalVarName ]==null) navigator[globalVarName] = null;
	if (navigator[ globalVarName ]!=null) clearTimeout( navigator[globalVarName] );
	    navigator[ globalVarName ] = setTimeout( funcname+funcargs, timedelay );
}

// ----------------------------------------------------------------------------
// Utility routines for URL Handling
// ----------------------------------------------------------------------------

/** This decodes URL-encoded strings because the Javascript
 * function "unescape" only does part of the job; adapted from
 * <a target="_blank" href="http://www.albionresearch.com/misc/urlencode.php">here.</a>
 * @param {String} encoded the URL-encoded string to translate
 * @return decoded version of given encoded string
 * @type String
 * @author Bruce Wallace (PolyGlotInc.com)
 * @version 2.0
 */
function grvURLDecode( encoded )
{
   // Replace + with space
   // Replace %xx with equivalent character
   // Put [ERROR] in output if %xx is invalid.
   var HEXCHARS = "0123456789ABCDEFabcdef"; 
   var plaintext = "";
   var i = 0;
   while (i < encoded.length) {
       var ch = encoded.charAt(i);
	   if (ch == "+") {
	       plaintext += " ";
		   i++;
	   } else if (ch == "%") {
			if (i < (encoded.length-2) 
					&& HEXCHARS.indexOf(encoded.charAt(i+1)) != -1 
					&& HEXCHARS.indexOf(encoded.charAt(i+2)) != -1 )
			{
				plaintext += unescape( encoded.substr(i,3) );
				i += 3;
			} else {
				throw 'Bad escape combination near ...' + encoded.substr(i);
				i++;
			}
		} else {
		   plaintext += ch;
		   i++;
		}
	}
   return plaintext;
}

/**
 * @class This function parses ampersand-separated name=value
 * argument pairs from the query string of the URL; It stores the
 * name=value pairs in a static list; adapted from
 * <a target="_blank" href="http://www.oreilly.com/catalog/jscript3/chapter/ch13.html#ch13_08.htm">here.</a>
 * @author Bruce Wallace (PolyGlotInc.com)
 * @version 2.0
 */
function grvArgs( anArgName )
{
	if ( grvIsUndefined(grvArgs.ARGS) )
	{
		grvArgs.ARGS = new Object(); //private singleton

	    var query = location.search.substring(1);  // Get query string.
	    var pairs = query.split("&");              // Break at ampersand.

	    for (var i=0; i<pairs.length; ++i)
	    {
		  var pos = pairs[i].indexOf('=');            // Look for "name=value".
		  if (pos == -1) continue;                    // If not found, skip.
		  var argname   = pairs[i].substring(0,pos);  // Extract the name.
		  var value     = pairs[i].substring(pos+1);  // Extract the value.
		  grvArgs.ARGS[argname] = grvURLDecode(value);// Store in LIST.
	    }
	}

    return grvArgs.ARGS[ anArgName ];	// Return the specified argument
}

function grvGetArg( argname ) {
	return grvArgs( argname );
}

function grvIsChecked( ID )
{
	alert("grvIsChecked is deprecated");
  var checkbox = document.getElementById( ID );
  return checkbox.checked;
}

/** send a message to the browser status bar
 * @param {String} msg the message to send.
 */
function grvSendStatusMessage( msg )
{
    window.status = msg; grvTraceMsg(msg);
    window.status = "";
}

// ----------------------------------------------------------------------------
// Utility routines to convert entities into strings
// ----------------------------------------------------------------------------

function grvObjectToString( o )
{
	var s = "";
	for (var property in o)// iterate over all properties  
    	s += "Property [" + property + "] is [" +  o[property] +"]\n";
    return s;
}
function grvObjectToInitializer( o )
{
	var s = "{\n";
	for (var property in o)// iterate over all properties
	{
	  var q = (o[property] instanceof Function) ? '' : '"';
	  s += property + ':'+q + o[property] +q+',\n';
    }
    return s.substring(0,s.length-2)+"\n}";//get rid of dangling comma
}
function grvObjectToShortInitializer( o )
{
	var s = "{\n";
	for (var property in o)// iterate over all properties
	{
	  var P = o[ property ];
	  if (!(P instanceof Function))
	       s += property + ':"' + P + '",\n';
//	  else s += property + ': function,\n';
    }
    return s.substring(0,s.length-2)+"\n}";//get rid of dangling comma
}
function grvArrayToString( a )
{
	var s = "";
	for (var i in a)// iterate over all array items  
    	s += "["+ i +"] is ["+  a[i] +"]\n";
    return s;
}
function grvArgsToString( args )
{
     var s = "";
     for (var i=0; i<args.length; ++i)// iterate over all function args
    	s += "["+ i +"] is ["+  args[i] +"]\n";
     return s;
}

///// GLOBAL-PERSISTENCE VARIABLES ROUTINES /////

function grvGlobalVar()
{
	grvBreak("deprecated global var 1");
	if (window._gGrvGlobalMap==null) window._gGrvGlobalMap = new Object();
	return window._gGrvGlobalMap;
}

/** static routine to dynamically define/update a global (to window/page
 * but reloaded with page) variable with the given name and value.
 * @param {String} varname name of "persistent" variable
 * @param {Object} value value to set variable to
 */
function grvSetGlobalVar( varname, value )
{
	grvBreak("deprecated global var 2");
	grvGlobalVar()[ varname ] = value;
}

/** static routine to return the value of persistent variable with given name
 * @param {String} varname name of "global" i.e. "static" variable
 * @return {Object} value of variable (empty object if not previously defined)
 * @type Object
 */
function grvGetGlobalVar( varname )
{
	grvBreak("deprecated global var 3");
	return grvGlobalVar()[ varname ];
}

/** static routine to dynamically undefine/delete ALL "...GlobalVars" variables */
function grvClearGlobalVars(){
	grvBreak("deprecated global var 4");
	window._gGrvGlobalMap = null;
}

/** static routine to dynamically undefine/delete a global variable
 * @param {String} varname name of "persistent" variable
 */
function grvClearGlobalVar( varname ){
	grvBreak("deprecated global var 5");
	delete grvGlobalVar()[ varname ];
}

grvTraceCmp("grvUtils.js: End");