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

/**
 * @file         grvValidate.js
 * @fileoverview Collection of routines to validate data form fields
 * @author       Bruce Wallace  (PolyGlotInc.com)
 * @version      2.0
 */

//////////////////////////////////////////////////////////////////
// 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"
//////////////////////////////////////////////////////////////////

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

/**
 * Perform a robust test for JavaScript "undefined" using the wisdom
 * at <a target="_blank" 
 * href="http://pt.withy.org/ptalk/archives/2005/06/dont_assume_undefined_is_undefined.html">
 * Dont Assume Undefined is Undefined</a>.
 * @param {anyType} x variable to check for undefinedness
 * @return true iff x is undefined (as opposed to merely null)
 * @type boolean
 */
function grvIsUndefined(x) {
	 return x === void 0;
}

/** Verify that given string is "empty". @type boolean */
function grvIsEmpty(x)
{
	if (x==null) return true;
	var s = ""+x; //force to string
	if (s.length==0) return true;
	s = grvTrimStr(s);
	return (s.length==0);
}

/** Evaluate boolean string value
 * @return boolean value or null if not boolean
 * @type boolean
 */
function grvBoolValue(s)
{
	s = ""+s;//force to string
	if (grvIsTrue (s)) return true;
	if (grvIsFalse(s)) return false;
	return null;
}

/** Generate string representation of given boolean value
 * @return boolean value or null if not boolean
 * @type String
 */ 
function grvAsBoolStr(flag)
{
	if (flag==true ) return "true";
	if (flag==false) return "false";
	return null;
}

/** Verify that given string explicitly equates to "true". @type boolean */
function grvIsSelected(s)
{
	return grvIsEmpty(s) ? false : grvIsTrue(s);
}

/** Verify that given string equates to "true". @type boolean */
function grvIsTrue(s)
{
	var x = s.toLowerCase();
	return x=="y" || x=="yes" || x=="true" || x=="1";
}

/** Verify that given string equates to "false". @type boolean */
function grvIsFalse(s)
{
	var x = s.toLowerCase();
	return x=="n" || x=="no" || x=="false" || x=="0";
}

/** Verify that given character is an English letter (A .. Z, a..z). @type boolean */
function grvIsLetter(c)
{   return ( ((c >= "a") && (c <= "z")) || ((c >= "A") && (c <= "Z")) ); }

/** Verify that given character is a digit (0 .. 9). @type boolean */
function grvIsDigit(c)
{	return (c >= "0") && (c <= "9");	}

/** Verify that all characters in given string are alpha chars. @type boolean */
function grvIsAlpha(s)
{
	if (grvIsEmpty(s)) return true;
    for (var i=0; i<s.length; ++i) 
        if (!grvIsLetter(s.charAt(i))) return false;

    return true;
}

/** Verify that all characters in given string are alpha or blank chars. @type boolean */
function grvIsAlphaBlank(s)
{
	if (grvIsEmpty(s)) return true;
	var c;
    for (var i=0; i<s.length; ++i){
    	c = s.charAt(i);
        if (c!=' ' && !grvIsLetter(c)) return false;
    }

    return true;
}


/** Verify that all characters in given string are alphanumeric chars. @type boolean */
function grvIsAlphaNum(s)
{
	if (grvIsEmpty(s)) return true;
	var c = '';
    for (var i=0; i<s.length; ++i)
    {
    	c = s.charAt(i);
        if (!grvIsLetter(c) && !grvIsDigit(c)) return false;
    }

    return true;
}


/** Verify that given string contains an integer.
 * 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.
 * @return true if all characters in string s are numbers 
 * @type boolean
 */
function grvIsInteger(s)
{
	if (grvIsEmpty(s)) return true;
    for (var i=0; i<s.length; ++i)  
        if (!grvIsDigit(s.charAt(i))) return false;

    return true;
}

/** Verify that given string is a delimited integer
 * @return True iff string s is <int><delim><int>
 * @type boolean
 */
function grvIsIntDelimInt(s,delim)
{
	s = ""+s;//force to string
	if (grvIsEmpty(s)) return true;
    var seenDelim = false;
    if (s == delim) return false;
	var c = '';
    for (var i=0; i<s.length; ++i)
    {
        c = s.charAt(i);
        if ((c == delim) && !seenDelim) seenDelim = true;
        else if (!grvIsDigit(c)) return false;
    }
    return true;
}

/** Verify that the given string is a zip code
 * @return True iff string s is <int><optional-dash-then-int>.
 * @type boolean
 */
function grvIsZipCode(s){ return grvIsIntDelimInt(s,"-"); }

/** 
 * Verify given string represents a float. Does not accept exponential notation.
 * @return True if string s is an unsigned floating point (real) number. 
 * @type boolean
 */
function grvIsFloat(s){ return grvIsIntDelimInt(s,"."); }

/** Verify given string is a phone number.
 * @return True if string s only has <digits><space>x()-# chars.
 * @type boolean
 */
function grvIsPhoneNum(s)
{
	if (grvIsEmpty(s)) return true;
	var c = '';
    for (var i=0; i<s.length; ++i)
    {
    	c = s.charAt(i);
        if (!grvIsDigit(c)
         && c!='x' && c!=' ' && c!='#' && c!='-' && c!='(' && c!=')')
          return false;
    }
    return true;
}

/** Verify that given string contains a signed or unsigned
 * floating point (real) number. First character is allowed
 * to be + or -. Does not accept exponential notation.
 * @return true iff string contains a "real" number. 
 * @type boolean
 */
function grvIsSignedFloat(s)
{
	s = ""+s;//force to string
	if (grvIsEmpty(s)) return true;
	var startPos = 0;
	// skip leading + or -
	if ( (s.charAt(0) == "-") || (s.charAt(0) == "+") ) startPos = 1;    
	return (grvIsFloat(s.substring(startPos, s.length)));
}

/** Verify that given string contains a date.
 * @return error message on invalid date or null if valid
 * @type String
 */
function grvDateParseErrorMsg(dateStr)
{
  if (grvIsEmpty(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
}

/** remove dollar formatting characters from given string
 * @return filtered version of numeric string
 * @type String
 */
function grvFilterNum( str ) {
	var re = /\$|,/g; // remove "$" and ","
	return str.replace(re, "");
}

/** Notify user that contents of field theField are invalid.
 * @param {element} theField HTML element containing form field
 * @param {String} s describes expected contents of theField.value.
 */
function grvWarnInvalid(theField, s)
{
	if (grvIsEmpty(s)) return;
	theField.focus();
    theField.select();
    alert(s);
}

/** validate dollar value of form field with given element ID */
function grvValidateDollar( fieldName )
{
	var field = document.getElementById( fieldName );
    field.value = filterNum(field.value);
    if (!grvIsSignedFloat(field.value))
	  grvWarnInvalid( field, "Please enter a valid dollar amount" );
}

/** validate integer value of form field with given element ID */
function grvValidateInteger( fieldName )
{
	var field = document.getElementById( fieldName );
    field.value = filterNum(field.value);
    if (!grvIsInteger(field.value))
	  grvWarnInvalid( field, "Please enter a valid integer" );
}

/** validate float value of form field with given element ID */
function grvValidateFloat( fieldName )
{
	var field = document.getElementById( fieldName );
    //field.value = filterNum(field.value);
    if (!grvIsSignedFloat(field.value))
	  grvWarnInvalid( field, "Please enter a valid number" );
}

/** validate date value of form field with given element ID */
function grvValidateDate( fieldName )
{
	var field = document.getElementById( fieldName );
    grvWarnInvalid( field, grvDateParseErrorMsg(field.value) );
}

// ----------------------------------------------------------------------------
// Utility routines to support param validations, exceptions, and debugging
// ----------------------------------------------------------------------------

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

/** cause the debugger to stop (ala a breakpoint) */
function grvBreakpoint()
{
	//open window with current dynamic HTML displayed
	grvDebugWindow( 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();
}

/** 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(); else return true;
}

/** output the given error message and ask user if they wish to break */
function grvError(msg){
 if (grvBreak("ERROR:"+msg))
 	alert("It is recommended that you close this window and try again.");
}

/** throw an exception if given condition is not true */
function grvASSERT(condition,msg){
	if (condition) return;
	grvError(msg);
	throw msg;
}

/** throw an exception but first give user a chance to "break" */
function grvLoudThrow( exceptionObj ) {
	grvBreak(exceptionObj);
	throw exceptionObj;
}
/** return the formatted message for a missing-argument exception @String */
function grvMissingArgException( argDesc, funcName ) {
	return "Error: "+argDesc+" wasnt passed to "+funcName;
}
/** return the formatted message for a too-few-arguments exception @String */
function grvTooFewArgsException( funcName, required, passed ) {
	return funcName + " requires "+ required +" arguments, but was passed " + passed;
}
/** return the formatted message for a calling-abstract-method exception @String */
function grvAbstractException( funcName ) {
	return "Abstract "+ funcName + " was called but never overridden!";
}
/** return the formatted message for a not-implemented exception @String */
function grvNotImplementedException( what ) {
	return what + " is not implemented yet!";
}
/** throw an exception because given function is abstract */
function grvMustOverride( funcName ) {
	grvLoudThrow( grvAbstractException( funcName ) );
}
/** throw an exception because given functionality is unimplemented */
function grvNotImplemented( what ) {
	grvLoudThrow( grvNotImplementedException( what ) );
}

/**
 * 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} argDescArray array of strings describing
 * required parameters; Note: Optional constructor parameters should
 * not be included in this array.
 * @throws grvMissingArgException
 * @see #Class
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function grvValidateArgs( callerName, argDescArray, callersArgs )
{
	if (argDescArray==null || argDescArray.length==0) grvError("Bad call to grvValidateArgs");

    // callersArgs is the arguments object of the function that
    // called us. Its length property is the number of actual args passed.
    var passed = callersArgs.length;

    // argDescArray should be an array of arg names of args that musnt be null
    var required = argDescArray.length;
    if (passed < required)
	  grvLoudThrow( grvTooFewArgsException( callerName, required, passed ) );

    for (var i=0; i<required; ++i)
	 if ( grvIsUndefined( callersArgs[i] ) )
	  grvLoudThrow( grvMissingArgException( argDescArray[i], callerName ) );
}

