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

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

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

function Breakpoint()
{
	//open window with current dynamic HTML displayed
	DebugWindow( document.body.parentNode.innerHTML );
	//cause Javascript debugger to kick in on undefined object reference
	//OR, comment out the next line and pre-set a breakpoint here via debugger
 //   UNDEFINED();
}

function Break(msg)
{
	if (msg.length>200) { DebugWindow( msg ); msg="(see window)"; }
	if (confirm(msg+"\nBreak here?")) Breakpoint(); else return true;
}

function Error(msg){
 if (Break("ERROR:"+msg))
 	alert("It is recommended that you close this window and try again.");
}

//global debug flags to enable verbose event tracing
var gTraceMVC = false;	//MVC setup tracing
var gTraceEvt = false;	//Event processing tracing
var gTraceMsg = false;	//message broadcast tracing

function Trace(flag,msg){ if (flag) return Break(msg); else return true; }

function TraceMVC(msg){ return Trace(gTraceMVC,msg); }
function TraceEvt(msg){ return Trace(gTraceEvt,msg); }
function TraceMsg(msg){ return Trace(gTraceMsg,msg); }

/** return the current time in milliseconds since start of epoc */
function timestamp(){ return (new Date()).getTime(); }

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

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

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

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

/** return the HTML to draw a subitem bullet @type String */
function subitem( isInValid ){ return isInValid ? "&epsilon;" : "&rArr;" }

/** 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 keyFilter( 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 strFilter( 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 noDuplicates( 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 = "+-.";

/** 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 true iff character is legal
 * @type boolean
 */
function dollarKeyFilter( keyCode, valueSoFar )
{
	return keyFilter( keyCode, kDigitChars+kFloatChars+"$," )
//	    && noDuplicates( valueSoFar+String.fromCharCode(keyCode), "$"+kFloatChars );
}
function dollarStrFilter(s){ return strFilter(s,kDigitChars+kFloatChars); }
function isZeroDollars(v){ return Math.abs(parseFloat(v))<0.01; }

/** 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 1.0
 */
function format_dollar( v, optZeroAsBlankFlag )
{
	if ( v==null || v=="null" ) return dot();
	if ( isZeroDollars(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) ? "," : "") + C[i] + s;

	return sign + "$" + s;
}

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

/** strip leading and trailing whitespace @type String */
function trimStr(sInString) {
  sInString = sInString.replace( /^\s+/g, "" );// strip leading
  return sInString.replace( /\s+$/g, "" );// strip trailing
}

/** 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 #getHook
 * @author Bruce Wallace (PolyGlotInc.com)
 * @version 1.0
 */
function genHook( hookID, optInnerHTML ){
	return '<span id="'+hookID+'">'
	     + (optInnerHTML?optInnerHTML:"")
	     + '</span>';
}
function getHook( hookID ){
	var e = document.getElementById( hookID );
	if (e==null) TraceMVC("cant find hook["+hookID+"]");
	return e;
}

function setElemBackgroundColor( e, color ){
    if (!e) {Break("null setElemBackgroundColor!"); return;}
	e.style.backgroundColor = color;
}
function setBackgroundColor( ID, color ){
	setElemBackgroundColor( document.getElementById(ID), color );
}
function setElemText( e, str ){
    if (!e) {Break("null setElemText!"); return;}
	e.innerHTML = str;
}
function setText( ID, str ){
	setElemText( getHook(ID), str );
}


//TODO implement these via CSS instead
var    kInValidColor = '#FF9900';
var     kEditedColor = '#99CCCC';//'#FFCC99';
var   kSelectedColor = '#FFFFCC';
var kUnSelectedColor = '#FFFAF0';

function highlightElem( e, makeSelected, selectColor ){
	setElemBackgroundColor( e, makeSelected?selectColor:kUnSelectedColor );
}
/*ALTERNATE COLOR SCHEME...
//set the background color of the given element based on
//whether it is selected, dirty, and valid.
function editElem( e, makeSelected, makeEdited, makeInvalid ){
	var useColor = makeSelected || makeEdited || makeInvalid;
	highlightElem( 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 editElem( e, makeSelected, makeEdited, makeInvalid ){
	var useColor = makeSelected || makeEdited;
	highlightElem( e, useColor, makeEdited  ? kEditedColor  :
	                                          kSelectedColor );
}
function editElemID( ID, makeSelected, makeEdited, makeInvalid ){
	editElem( getHook(ID), makeSelected, makeEdited, makeInvalid );
}
function selectElem( e, makeSelected ){
	highlightElem( e, makeSelected, kSelectedColor );
}
function selectElemID( ID, makeSelected ){
	selectElem( getHook(ID), makeSelected );
}

function setElemVisibility( e, visibleFlag )
{
    if (!e) {if (TraceMVC("null setElemVisibility!")) return;}
	e.style.visibility = visibleFlag ? "visible"  : "hidden";
	e.style.position   = visibleFlag ? "relative" : "absolute";
}
function setVisibility( ID, visibleFlag ){
  setElemVisibility( getHook(ID), visibleFlag );
}


var kMonthNames = new Array(
				"January","February","March","April","May","June","July",
				"August","September","October","November","December");
var kMonthNums = new Array(
				"01","02","03","04","05","06","07",
				"08","09","10","11","12");

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

// ----------------------------------------------------------------------------
// Utility routines for Exception Handling
// ----------------------------------------------------------------------------

function LoudThrow( exceptionObj )
{
	Break(exceptionObj);
	throw exceptionObj;
}

function MissingArgException( argDesc, funcName )
{
	return "Error: "+argDesc+" wasnt passed to "+funcName;
}

function NullArgException( argDesc, funcName )
{
	return "Illegal null "+argDesc+" passed to "+funcName;
}

function TooFewArgsException( funcName, required, passed )
{
	return funcName + " requires "+ required +" arguments, but was passed " + passed;
}

/**
 * This function returns the name of a given function; It does this by
 * converting the function to a string, then using a regular expression
 * to extract the function name from the resulting code.
 * @param {Function} f reference to a function
 * @return name of given function
 * @type String
 * @author Bruce Wallace (PolyGlotInc.com)
 * @version 1.0
 */
function FuncName(f)
{
    var s = f.toString().split( new RegExp("[ \(]"), 2)[1];
    if ((s == null) || (s.length == 0)) return "anonymous";
    return s;
}

/**
 * This utility function returns the name of the function that
 * called this routine.
 * @param {Arguments} argumentsObj the Javascript arguments object
 * @return the caller's function name
 * @type String
 * @see #FuncName
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 1.0
 */
function CallerName( argumentsObj ){
	return FuncName( argumentsObj.caller.callee )
}

/**
 * This utility function takes (via "THIS") a "class" and
 * (re)sets its superClass.
 * @param {Function} superClass the "superclass constructor" function
 * @return a reference to the "class object"
 * @type Function
 * @see #Class
 * @see OObject
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 1.0
 */
function Extends( superClass )
{
	this.baseClass.prototype  = superClass ? superClass.prototype : null;
	this.prototype            = new this.baseClass();
	this.prototype.name       = this.name;//default value
	this.prototype[this.name] = this.prototype.konstructor;//so people can call super
	return this;
}

/**
 * Declare a class and create the glue objects and code
 * to allow "pretty" source; A mechanism is set up to insure
 * that required constructor parameters each have a defined value;
 * This function also defines the "name" attribute of the specified
 * class and initializes it with the class name.
 * @param {Function} theClass the "class constructor" function
 * @param {StringArray} optConstructorArgDescArray optional array
 * of strings describing required parameters for this class'
 * "constructor"; Note: Optional constructor parameters should
 * not be included in this array.
 * @return the "class object" (an extended Function object)
 * @type Function
 * @see #Extends
 * @see #ValidateArgs
 * @see OObject
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 1.0
 */
function Class( theClass, optConstructorArgDescArray )
{
	// The "class" as passed in will really be an abstract base class. We
	// will squirrel that away and replace theClass with a wrapper function
	// so that method source code is not copied into each object instance
	// as would otherwise be the case.
	var baseClass = theClass;

	// create the new wrapper function to replace theClass
	var  className  = FuncName( theClass );
	self[className] = theClass = function(){
		ValidateArgs( theClass.required );
		theClass.prototype.konstructor.apply( this, arguments );
	}
	theClass.required = optConstructorArgDescArray;
	theClass.name     = className;
	theClass.baseClass= baseClass;
	theClass.Extends  = Extends;//so we can call Extends as a method
	return theClass.Extends();//needed, even tho no superclass specified
}

/**
 * @class the "abstract base class" OObject has no code, only
 * a design pattern followed by convention; Two utility
 * functions (Class,Extends) replace the "constructor"
 * Javascript function with a wrapper function and also
 * set up a proper inheritence mechanism; This mechanism
 * allows the pretty source whereby "methods" are declared
 * inline in the Javascript function, but the source code
 * of those methods are not copied into every object instance
 * as would otherwise be the case with simple Javascript;<p>
 * The wrapper function also sets up the "constructor"
 * to use {@link GLOBALS#ValidateArgs} to insure that the required
 * parameters are passed to the constructor and throw an
 * exception if not.<p>
 * The design pattern implemented by Class/Extends
 * corrects several problems that occur with naive
 * Class/Superclass mechanisms often used with Javascript;
 *<pre>
 *  The required "interface" for "OObject":
 *  (1) the Object has its "class" declared via the utility
 *      function {@link GLOBALS#Class}
 *  (2) the "superclass" (if any) is declared via the
 *      utility function {@link GLOBALS#Extends}
 *  (3) the "constructor" logic for the class is placed
 *      in a "method" named "konstructor"
 *  (4) the konstructor method invokes the "super" constructor
 *      via its class name [rather than super()]
 *</pre>
 * @throws MissingArgException thrown by constructor wrapper
 * @see GLOBALS#Class
 * @see GLOBALS#Extends
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 1.0
 */
function OObject(){/* this function exists only for JsDoc purposes.*/}


/**
 * generic utility function to verify that required args
 * (of the caller of this function) have been passed; The given
 * array of descriptions defines how many arguments should be
 * found with a defined value;  Therefore, the caller of this
 * routine should place its optional arguments at the end of its
 * parameter list with no description specified in "optArgDescArray".
 * @param {StringArray} optArgDescArray optional array of strings
 * describing required parameters; Note: Optional constructor parameters should
 * not be included in this array.
 * @throws MissingArgException
 * @see #Class
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 1.0
 */
function ValidateArgs( optArgDescArray )
{
	if (optArgDescArray==null || optArgDescArray.length==0) return;

    // arguments.caller.callee is the Function object that called us.
    // Its arity property is the number of arguments that were expected.
    //var expected = arguments.caller.callee.arity;

    // arguments.caller is the arguments object of the function that
    // called us. Its length property is the number of actual args passed.
    var passed = arguments.caller.length;
    
    // argDescArray should be an array of arg names of args that musnt be null
    var required = optArgDescArray==null ? 0 : optArgDescArray.length;
    if (passed < required)
	  LoudThrow( TooFewArgsException( CallerName(arguments), required, passed ) );

    for (var i=0; i<required; ++i)
	 //robust test for "undefined"
	 //see http://pt.withy.org/ptalk/archives/2005/06/dont_assume_undefined_is_undefined.html
	 if (arguments.caller[i] === void 0)
	  LoudThrow( MissingArgException( optArgDescArray[i], CallerName(arguments) ) );
}

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

function WaitWindowName(appId){
	return "gWaitWindow" + 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 WAIT(appId) {
	var winName = WaitWindowName(appId);
	if (navigator[ winName ]==null)
		navigator[ winName ]= pleaseWait( true, "Loading data.", appId );
}
/** close the "please wait" window if it exists otherwise no effect */
function UNWAIT(appId) {
	var winName = WaitWindowName(appId);
	if (navigator[ winName ]==null) return;
	    navigator[ winName ].close();
	    navigator[ winName ] = null;
}
/** 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 pleaseWait( 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("", 'pleaseWait', 'width=250 height=100 toolbar=no scrollbars=no menubar=no resizable=yes top='+yOffset+' left='+xOffset+'');
     theWindow.document.write("<BODY BGCOLOR=#6eaba1>");
     theWindow.document.write("<TITLE>" +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 busy() {
  document.body.style.cursor='wait';
}
/** set the cursor to the default icon */
function notbusy() {
  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 1.0
 */
function BusyDo( funcname, funcargs, optDelayMilliSecs )
{
	var timedelay = optDelayMilliSecs || 5;
//	busy();
	var globalVarName = "gTimeout"+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 1.0
 */
function URLDecode( 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;
}

var gArgs = null;	//global singleton object reference
/**
 * @class This singleton class parses ampersand-separated name=value
 * argument pairs from the query string of the URL; It stores the
 * name=value pairs as properties of "this" object; 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 1.0
 * @todo refactor this into a proper object now that I know how to
 */
function Args()	//lazy singleton factory
{
	if (gArgs!=null) return gArgs; //use singleton

	gArgs = new Object; //create singleton
	gArgs.get = function(argname){ return eval("this."+argname); };
	// todo someday see if above eval can be replaced with "this[argname]"

    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.
	  gArgs[argname] = URLDecode(value);            // Store as a property.
    }
    return gArgs;                               // Return the singleton.
}
function GetArg( argname ) {
	return Args().get( argname );
}

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

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

function ObjectToString( o )
{
	var s = "";
	for (var property in o)// iterate over all properties  
    	s += "Property [" + property + "] is [" +  o[property] +"]\n";
    return s;
}
function ObjectToInitializer( 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 ObjectToShortInitializer( 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 ArrayToString( a )
{
	var s = "";
	for (var i in a)// iterate over all array items  
    	s += "["+ i +"] is ["+  a[i] +"]\n";
    return s;
}
function ArgsToString( 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 /////

/** static routine to dynamically define/update a persistent (across page loads)
 * variable with the given name and value.
 * @param {String} varname name of "persistent" variable
 * @param {Object} value value to set variable to
 */
function SetPersistentVar( varname, value ) //persisted across page loads
{
	if (navigator.gGlobalMap==null) navigator.gGlobalMap = new Object();
	navigator.gGlobalMap[varname] = value;
}

/** static routine to return the value of the persistent variable with given name
 * @param {String} varname name of "persistent" variable
 * @return {Object} value of variable (empty object if not previously defined)
 * @type Object
 */
function GetPersistentVar( varname )
{
	if (navigator.gGlobalMap==null) navigator.gGlobalMap = new Object();
	return navigator.gGlobalMap[varname];
}

/** static routine to dynamically undefine/delete ALL "...PersistentVars" variables  */
function ClearPersistentVars(){ navigator.gGlobalMap = null; }

/** 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 SetGlobalVar( varname, value )
{
	if (window.gGlobalMap==null) window.gGlobalMap = new Object();
	window.gGlobalMap[ 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 GetGlobalVar( varname )
{
	if (window.gGlobalMap==null) window.gGlobalMap = new Object();
	return window.gGlobalMap[ varname ];
}

/** static routine to dynamically undefine/delete ALL "...GlobalVars" variables */
function ClearGlobalVars(){ window.gGlobalMap = null; }

/** static routine to dynamically undefine/delete a global variable
 * @param {String} varname name of "persistent" variable
 */
function ClearGlobalVar( varname ){ delete window.gGlobalMap[ varname ]; }

///// BASIC DATA VALIDATION ROUTINES /////

/** Check whether string s is "empty". */
function isEmpty(s)
{   return ((s == null) || (s.length == 0))
}

/** Returns true if character c is an English letter (A .. Z, a..z). */
function isLetter(c)
{   return ( ((c >= "a") && (c <= "z")) || ((c >= "A") && (c <= "Z")) )
}

/** Returns true if character c is a digit (0 .. 9). */
function isDigit(c)
{
 return (c >= "0") && (c <= "9");
}

/**
 * Returns true if all characters in string s are numbers.
 * Accepts non-signed integers only. Does not accept floating 
 * point, exponential notation, etc.
 * We don't use parseInt() because that would accept a string
 * with trailing non-numeric characters.
 */
function isInteger(s)
{
	if (isEmpty(s)) return true;
    for (i = 0; i < s.length; i++)   
        if (!isDigit(s.charAt(i))) return false;

    return true;
}

/**
 * Returns true if string s is an unsigned floating point (real) number. 
 * Does not accept exponential notation.
 */
function isFloat(s)
{
   if (isEmpty(s)) return true;
    var seenDecimalPoint = false;
    var decimalPointDelimiter = ".";

    if (s == decimalPointDelimiter) return false;

    for (i = 0; i < s.length; i++)
    {   
        var c = s.charAt(i);
        if ((c == decimalPointDelimiter) && !seenDecimalPoint) seenDecimalPoint = true;
        else if (!isDigit(c)) return false;
    }

    return true;
}


/** 
 * Returns true if string s is a signed or unsigned floating point 
 * (real) number. First character is allowed to be + or -.
 * Does not accept exponential notation.
 */
function isSignedFloat(s)
{
   if (isEmpty(s)) return true;
   var startPos = 0;
   // skip leading + or -
   if ( (s.charAt(0) == "-") || (s.charAt(0) == "+") ) startPos = 1;    
   return (isFloat(s.substring(startPos, s.length)));
}

/**
 * Notify user that contents of field theField are invalid.
 * String s describes expected contents of theField.value.
 */
function warnInvalid(theField, s)
{
	if (isEmpty(s)) return;
	theField.focus();
    theField.select();
    alert(s);
}

/** return error message on invalid date or null if valid */
function isDate(dateStr)
{
  if (isEmpty(dateStr)) return null;

  var datePat    = /^(\d{1,2})(\/|-)(\d{1,2})(\/|-)(\d{4})$/;
  var matchArray = dateStr.match(datePat); // is format OK?

  if (matchArray == null)
    return "Please enter date as either mm/dd/yyyy or mm-dd-yyyy.";

  // parse date into variables
  month = matchArray[1];
  day   = matchArray[3];
  year  = matchArray[5];

  if (month < 1 || month > 12)
    return("Month must be between 1 and 12.");

  if (day < 1 || day > 31)
    return("Day must be between 1 and 31.");

  if ((month==4 || month==6 || month==9 || month==11) && day==31)
    return("Month " + month + " doesn't have 31 days!");

  if (month == 2) { // check for february 29th
    var isleap = (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
    if (day > 29 || (day==29 && !isleap))
      return("February " + year + " doesn't have " + day + " days!");
  }
  return null;  // date is valid
}


function filterNum( str ) {
	var re = /\$|,/g; // remove "$" and ","
	return str.replace(re, "");
}

function validateDollar( hookID )
{
	var field = getHook( hookID );
    field.value = filterNum( field.value );
    if ( ! isSignedFloat(field.value) )
	  warnInvalid( field, "Please enter a valid dollar amount" );
}
function validateInteger( hookID )
{
	var field = getHook( hookID );
    field.value = filterNum(field.value);
    if (!isInteger(field.value))
	  warnInvalid( field, "Please enter a valid integer" );
}
function validateFloat( hookID )
{
	var field = getHook( hookID );
    //field.value = filterNum(field.value);
    if (!isSignedFloat(field.value))
	  warnInvalid( field, "Please enter a valid number" );
}
function validateDate( hookID )
{
	var field = getHook( hookID );
    warnInvalid( field, isDate(field.value) );
}
