// Character classification functions implemented in JavaScript.
// Simple [prototype] port of ctype.h from the STDC Library.

function isspace(ch) {
   switch (ch) {
      case ' ':
      case '\t':
      case '\n':
      case '\r':
      case '\f':
      case '\v':
         return true;
   }
   return false;
}

function isNameChars(str)  {
   var string = new String(str);
   if (!isalpha(string.charAt(0)))
      return false;
   for (i = 0; i < string.length; i++)
      if (!isalpha(string.charAt(i)) && !isnamepunct(string.charAt(i)))
	  return false;
      return true;
}

function isAlpha(str)  {
   var string = new String(str);
   for (i = 0; i < string.length; i++)
      if (!isalpha(string.charAt(i)))
         return false;
   return true;
}

function isNum(str)  {
   var string = new String(str);
   for (i = 0; i < string.length; i++)
      if (!isdigit(string.charAt(i)))
         return false;
   return true;
}

function isAlphaNum(str)  {
   var string = new String(str);
   for (i = 0; i < string.length; i++)
      if (!isalpha(string.charAt(i)) && !isdigit(string.charAt(i)))
         return false;
   return true;
}

function isInt(str)  {
   var string = new String(str);
   var i = 0;
   if (0 == string.length || isEmpty(string)) return false;
   if (string.charAt(0) == '-' || string.charAt(0) == '+') 
   {   i = 1;
       if (1 == string.length)
           return false;
   }

   for (; i < string.length; i++)
      if (!isdigit(string.charAt(i)))
         return false;
   return true;
}

function isFloat(str)  {
   var string = new String(str);
   var i = 0;
   var dot = false;
   if (0 == string.length || isEmpty(string)) return false;
   if (string.charAt(0) == '-' || string.charAt(0) == '+') 
   {   i = 1;
       if (1 == string.length)
           return false;
   }
   for (; i < string.length; i++)
   {  if (string.charAt(i) == '.')
      {  if (dot == false)
            dot = true;
	  else
	    return false;
      }
      else if (!isdigit(string.charAt(i))) 
         return false;
   }
   return true;
}

function isNaN(str)  {
   string = new String(str);
   if (string.search("NaN") != -1) return true;
   else return false;
}

function isdigit(ch) { 
   return (ch >= '0' && ch <= '9'); 
}

function isxdigit(ch) {
   if (isdigit(ch)) return true;
   if (ch >= 'a' && ch <= 'f') return true;
   return (ch >= 'A' && ch <= 'F');
}

function isalpha(ch) {
   if (islower(ch)) return true;
   return (isupper(ch));
}

function isalnum(ch) {
   return (isalpha(ch) || isdigit(ch));
}

function islower(ch) {
   return (ch >= 'a' && ch <= 'z');
}

function isupper(ch) {
   return (ch >= 'A' && ch <= 'Z');
}

function ispunct(ch) {
   var CHARS = "!\"#%&\'();<=>?[\\]*+,-./:^~";
   return (CHARS.indexOf(ch) != -1);
}

function isnamepunct(ch) {
   var CHARS = "\'-.~ ";
   return (CHARS.indexOf(ch) != -1);
}

function toupper(ch) {
   if (!isalpha(ch)) return ch;
   if (isupper(ch)) return ch;
   return (ch.toUpperCase());
}

function tolower(ch) {
   if (!isalpha(ch)) return ch;
   if (islower(ch)) return ch;
   return (ch.toLowerCase());
}
/**
 * @see http://www.usps.gov/ncsc/lookups/abbr_state.txt
 *
 * STATES and STATE_NAMES are "parallel" arrays...
 */
var STATES = new Array("AL","AK","AS","AZ","AR","CA","CO","CT",
   "DE","DC","FM","FL","GA","GU","HI","ID","IL","IN","IA","KS",
   "KY","LA","ME","MH","MD","MA","MI","MN","MS","MO","MT","NE",
   "NV","NH","NJ","NM","NY","NC","ND","MP","OH","OK","OR","PW",
   "PA","PR","RI","SC","SD","TN","TX","UT","VT","VI","VA","WA",
   "WV","WI","WY","AE","AA","AE","AE","AE","AP");

var STATE_NAMES = new Array(
   "Alabama", "Alaska", "American Samoa", "Arizona", "Arkansas",
   "California", "Colorado", "Connecticut", "Delaware",
   "District of Columbia", "Federated States of Micronesia",
   "Florida", "Georgia", "Guam", "Hawaii", "Idaho", "Illinois",
   "Indiana", "Iowa", "Kansas", "Kentucky", "Louisiana", "Maine",
   "Marshall Islands", "Maryland", "Massachusetts", "Michigan",
   "Minnesota", "Mississippi", "Missouri", "Montana", "Nebraska",
   "Nevada", "New Hampshire", "New Jersey", "New Mexico", "New York",
   "North Carolina", "North Dakota", "Northern Mariana Islands", 
   "Ohio", "Oklahoma", "Oregon", "Palau", "Pennsylvania", "Puerto Rico",
   "Rhode Island", "South Carolina", "South Dakota", "Tennessee",
   "Texas", "Utah", "Vermont", "Virgin Islands", "Virginia",
   "Washington", "West Virginia", "Wisconsin", "Wyoming",
   "Armed Forces Africa", "Armed Forces Americas",
   "Armed Forces Canada", "Armed Forces Europe",
   "Armed Forces Middle east", "Armed Forces Pacific");

function validateState(value) {
   if (null == value || "" == value) return false;
   string = new String(value);
   var st = string.toUpperCase();
   for (i = 0; i < STATES.length; i++)
      if (STATES[i] == st) return true;
   return false;
}

function setStateOptions(options, withName) {
   var j =  options.length;
   var s;
   for (var i = 0; i < STATES.length; i++, j++) {
      s = STATES[i];
      if (withName) s += " -- " + STATE_NAMES[i];
      options[j] = new Option(s);
   }
}

var DBUG = false;

// <howto topic='adding a new type'>
// to add a new XHTML Form type:
// write a function named  check_foo(form, name)
// where  foo  is the name of your type
// parameter  form  is a Form object
// parameter  name  is the name of the field being validated
// </howto>


// hidden field suffixes 
//
var TYPE = "type", REQ = "req", NAME = "name",
    MIN = "min", MAX = "max", TRIM = "trim", MINLEN = "minlength",
    INDEX = "index", REL = "rel";

// string trim options (left only, right only, both)...
//
var TRIM_LEFT = "left", TRIM_RIGHT = "right", TRIM_BOTH = "both";

// FormValidator hidden field names prefixed with PREFIX...
// SEPARATOR used to separate FormValidator field name with suffix...
// (e.g. FVfoo_int  FV is the prefix,  foo  is a field name,  
//  underscore is the separator, and  int  is the suffix)...
//
var SEPARATOR = "_"; 
var PREFIX = "FV";

// type of input elements that the client doesn't see
// e.g.  <input type="hidden" name="foo" value="bar" />
//
var HIDDEN = "hidden", TEXT = "text",  PASSWORD = "password", SELECT1="select-one", SELECT2="select-multiple",
    TEXTAREA = "textarea", FILE="file", RADIO = "radio", CHECKBOX = "checkbox"; 

// global error number value; named in the C tradition...
// [globals are generally bad -- use them with caution]
//
var errno = E_OKAY;

// values that can be assigned to the  errno  global...
var E_OKAY = 0, E_EMPTY = 1, E_REQUIRED = 2, E_RANGE = 3, 
    E_SYNTAX = 5, E_NAN = 6, E_NO_DIGITS = 7,
    E_RANGE_MIN = 8, E_RANGE_MAX = 9, E_TOO_SHORT = 10, E_LENGTH_PROBLEM = 11;

function setDebug(state) {
   if (state) DBUG = state;
   else state = false;
}

// checkForm() is called when a submit event is fired
// [e.g. user clicks on a <input type="submit" ... /> button]
//
// The function loops though the array of  form  objects that
// is contained within the  document  object.  If the form type
// of a form object is not hidden, then the value for that form
// object is checked for validity.
// 
function checkForm(form) {
   for (var i = 0; i < form.length; i++) {
      if (HIDDEN == form[i].type) continue;
      if (isEmpty(form[i].name)) continue;
      if (check(form, form[i].name) == false)
          return false;
   }
   return true;
}

// receives:  a form object and a field name
//
function check(form, name) {
   errno = E_OKAY;
   if (form[name] == null)  return true;
     //single select list - Required checking only
   if (SELECT1 == form[name].type)
   {   n = getName(form, name, REQ);
       var value = "";
       if (form[name].selectedIndex >= 0)
          value = form[name].options[form[name].selectedIndex].value;
       if (n && isYes(n.value) && isEmpty(value))
       {  n = getName(form, name, NAME);
          if (n)
             return error("Required field ( " + n.value + " ) is missing ... please enter", E_EMPTY);
          else
             return error("Required field ( " + name + " ) is missing ... please enter", E_EMPTY);
       }
       return true;
   }
     //multiple-select list
   if (SELECT2 == form[name].type)
   {   n = getName(form, name, REQ);
       if (n)
       {  for (var i = 0; i < form[name].options.length; i++)
              if (form[name].options[i].selected)
                 return true;
          n = getName(form, name, NAME);
          if (n)
             return error("Required field ( " + n.value + " ) is missing ... please enter", E_EMPTY);
          else
             return error("Required field ( " + name + " ) is missing ... please enter", E_EMPTY);
       }
       return true;
   }
       //radio buttons - Required checking only -- see if any are "checked"
   if (form[name].length > 0)
   {   n = getName(form, name, REQ);
       if (n && isYes(n.value))
       {   for (i=0; i<form[name].length; i++) 
           {    if (form[name][i].checked) 
                   return true;
           }
           n = getName(form, name, NAME);
           if (n)
              error("Required field ( " + n.value + " ) is missing ... please enter", E_EMPTY);
           else
              error("Required field ( " + name + " ) is missing ... please enter", E_EMPTY);
           form[name][0].focus();
           return false;
       }
       return true;
   }
      //Checkbox groups - see if any are checked - Required checking only
   if (CHECKBOX == form[name].type)
   {   n = getName(form, name, REQ);
       if (n && isYes(n.value))
       {   if (form[name].checked)
              return true;
             //**Look at "associated checkboxes -- same basename with an appended "-#"
           for (i=0; i<100; i++)
           {  n = name + "-" + i;
              if (form[n] != null && form[n].checked)
                 return true;
           }
           n = getName(form, name, NAME);
           if (n)
              error("Required field ( " + n.value + " ) is missing ... please enter", E_EMPTY);
           else
              error("Required field ( " + name + " ) is missing ... please enter", E_EMPTY);
           setFocus(form, name, false);
           return false;
       }
       return true;
   }
   if (!canValidate(form, name)) return true;
   // text/text area input fields only... 
   var value = form[name].value;
   n = getName(form, name, REQ);
   var ok = true;
   if (isEmpty(value))
   {  if (n && isYes(n.value))
      {    n = getName(form, name, NAME);
           if (n)
              ok = error("Required field ( " + n.value + " ) is missing ... please enter", E_EMPTY);
           else
              ok = error("Required field ( " + name + " ) is missing ... please enter", E_EMPTY);
      }
      else
         return true;
   }
   if (ok) {
      if ((n = getName(form, name, TYPE)) == null) return true;
      var func = "check_" + n.value + "(form, name)";
      ok = eval(func);
   }
   if (!ok) setFocus(form, name, false);
   return ok;
}

function setFocus(form, name, rv) {
   if (rv) return rv;
   if (DBUG) error("field named " + name + " is invalid", errno);
   form[name].focus();
   form[name].select();
   return rv;
}

function getName(form, name, suffix) {
   return form[PREFIX + name + SEPARATOR + suffix];
}

function isEmpty(value) {
   if (null == value || 0 == value.length) return true;
   for (var i = 0; i < value.length; i++)
      if (!isspace(value.charAt(i)))
         return false;
   return true;
}

function isYes(value) {
   var s = value.toLowerCase();
   return "y" == s || "yes"  == s;
}

function isNo(value) {
   return !isYes(value);
}

function error(msg, value) {
   errno = value;
   if (!DBUG)
      alert(msg);
   else
      alert(msg + "[errno=" + errno + "]");
   return false;
}

function trim(value, which) {
   var i;
   if (TRIM_LEFT == which || TRIM_BOTH == which) {
      for (i = 0; i < value.length; i++)
         if (!isspace(value.charAt(i))) break;
      if (i > 0)
         value = value.substring(i, value.length);
   }
   if (TRIM_RIGHT == which || TRIM_BOTH == which) {
      for (i = value.length-1; i >= 0; i--)
         if (!isspace(value.charAt(i))) break;
      if (i != value.length-1)
         value = value.substring(0, i+1);
   }
   return value;
}

function canValidate(form, name) {
   var value = form[name].value;
   return (null == value || (TEXT != form[name].type && PASSWORD != form[name].type && TEXTAREA != form[name].type && FILE != form[name].type)) 
          ? false : true;
}


// begin implementations for the  check_type()  functions...
//
function check_int(form, name) {
   var value = form[name].value;
   var n = getName(form, name, TRIM);
   if (n) {
      value = trim(value, n.value);
      if (0 == value.length || isEmpty(value)) return false;
   }
   var i = 0;
   if (value.charAt(i) == '-' || value.charAt(i) == '+') {
      if (1 == value.length) 
         return error("no digits entered", E_NO_DIGITS);
      i++;
   }
   for (; i < value.length; i++) {
      if (!isdigit(value.charAt(i))) 
         return error("only digits can be entered", E_NAN);
   }
   var nbr = parseInt(value, 10);
   if (n = getName(form, name, MIN)) {
      if (nbr < parseInt(n.value, 10))
         return error("number out of range ( >= " + n.value + " )", E_RANGE_MIN);
   }
   if (n = getName(form, name, MAX)) {
      if (nbr > parseInt(n.value, 10))
         return error("number out of range ( <= " + n.value + " )", E_RANGE_MAX);
   }
   return check_string(form, name);
}

function check_float(form, name) {
   var value = form[name].value;
   var n = getName(form, name, TRIM);
   if (n) value = trim(value, n.value);
   var n = parseFloat(value);
   if (!isFloat(value) || isNaN(n))
      return error("value must be a real number", E_NAN);
   return true;
}

function check_hex(form, name)
{  var i;
   var value = form[name].value;
   for (i=0;  i < value.length; i++)
   if (!isxdigit(value.charAt(i)))
	return error("must be hex digits (0-f)", E_SYNTAX);
   return check_string(form, name);
}

function check_phone(form, name) {
   var i;
   var value = form[name].value;
   if (value.length == 4 || value.length == 5 || value.length == 6)
   {   if (value.charAt(0) != 'x' && value.charAt(0) != 'X')   
             return error("phone number format for extension is incorrect (x12345)", E_SYNTAX);
       for (i = 1; i < value.length; i++)
          if (!isdigit(value.charAt(i)))
             return error("phone number format for extension is incorrect (x12345)", E_SYNTAX);
       return true;
   }
   if (value.length < 7)
      return error("phone number is too short", E_TOO_SHORT);
   if (value.length > 12)
      return error("phone number is too long", E_LENGTH_PROBLEM);
   if (value.length != 10 && value.length != 12 && value.length != 7 && value.length != 8)
      return error("Format incorrect(nnn nnn nnnn)", E_LENGTH_PROBLEM);
   if (value.length == 12)
   {   for (i = 0; i < 3; i++)
          if (!isdigit(value.charAt(i)))
             return error("phone number format is incorrect (nnn nnn nnnn)", E_SYNTAX);
       for (i = 4; i < 7; i++)
          if (!isdigit(value.charAt(i)))
             return error("phone number format is incorrect (nnn nnn nnnn)", E_SYNTAX);
       for (i = 8; i < 12; i++)
          if (!isdigit(value.charAt(i)))
             return error("phone number format is incorrect (nnn nnn nnnn)", E_SYNTAX);
       if (isspace(value.charAt(3)) && isspace(value.charAt(7)))
          return true;
       if (value.charAt(3) == '-' && value.charAt(7) == '-')
          return true;
       return error("phone number format is incorrect(nnn nnn nnnn)", E_SYNTAX);
   }
   if (value.length == 10)
   {   for (i = 0; i < 10; i++)
          if (!isdigit(value.charAt(i)))
             return error("phone number format is incorrect", E_SYNTAX);
       return true;
   }
   if (value.length == 8)
   {   for (i = 0; i < 3; i++)
          if (!isdigit(value.charAt(i)))
             return error("phone number format is incorrect (nnn nnnn)", E_SYNTAX);
       for (i = 4; i < 8; i++)
          if (!isdigit(value.charAt(i)))
             return error("phone number format is incorrect (nnn nnnn)", E_SYNTAX);
       if (isspace(value.charAt(3)))
          return true;
       if (value.charAt(3) == '-')
          return true;
       return error("phone number format is incorrect(nnn nnnn)", E_SYNTAX);
   }
   if (value.length == 7)
   {   for (i = 0; i < 7; i++)
          if (!isdigit(value.charAt(i)))
             return error("phone number format is incorrect", E_SYNTAX);
       return true;
   }
   return error("phone number format is incorrect(nnn nnn nnnn)", E_SYNTAX);
}

function check_zip(form, name) {
   var i;
   var value = form[name].value;
   if (value.length < 5)
      return error("zipcode is too short", E_TOO_SHORT);
   for (i = 0; i < 5; i++) 
      if (!isdigit(value.charAt(i)))
         return error("zipcode contains digits only", E_SYNTAX);
   if (value.length == i) return true;
   if (value.length < 9)
      return error("invalid number of zipcode digits", E_TOO_SHORT);
   if (10 == value.length) {
      if (value.charAt(i) != ' ' && value.charAt(i) != '-')
         return error("zipcode format is wrong", E_SYNTAX);
      ++i;
   }
   for (; i < value.length; i++)
      if (!isdigit(value.charAt(i)))
         return error("zipcode format is wrong", E_SYNTAX);
   return true;
}

function check_string(form, name) {
   var n = getName(form, name, MINLEN);
   if (null == n) return true;
   var min = parseInt(n.value, 10);  //assume correct XHTML code
   var len = parseInt(form[name].value.length, 10); 
   if (len < min)
      return error("field value is too short ( < " + min + " )", E_TOO_SHORT);
   return true;
}

function check_alpha(form, name) {
   var value = form[name].value;
   if (!isAlpha(value))
      return error("Field can only include letters", E_SYNTAX);
   else
      return check_string(form, name);
}

function check_alphaNumeric(form, name) {
   var value = form[name].value;
   if (!isAlphaNum(value))
      return error("Field can only include letters or numbers", E_SYNTAX);
   else
      return check_string(form, name);
}


function check_name(form, name) {
   var value = form[name].value;
   if (!isNameChars(value))
      return error("Names can only include letters and limited punctuation; they must start with a letter", E_SYNTAX);
   else
      return check_string(form, name);
}

function check_email(form, name) {
   var EMAIL_FORMAT = "x@y.z";
   var EMAIL_MIN_LEN = EMAIL_FORMAT.length;
   var EMAIL_EMSG = "email address: ";
   var value = form[name].value;
   value = trim(value, TRIM_BOTH);
   if (value.length <= EMAIL_MIN_LEN) 
      return error(EMAIL_EMSG + "format must be: " + EMAIL_FORMAT, E_SYNTAX);
   // switch code to use a regular expression (shell:  ?*@?*.?*)
   var index = value.indexOf("@");
   if (-1 == index)
      return error(EMAIL_EMSG + "no @ used", E_SYNTAX);
   if (0 == index)
      return error(EMAIL_EMSG + "cannot start with @", E_SYNTAX);
   var index2 = value.indexOf("@", index+1);
   if (index2 != -1)
      return error(EMAIL_EMSG + " only 1 email address needed", E_SYNTAX);
   index = value.indexOf(".", index);
   if (-1 == index)
      return error(EMAIL_EMSG + "no . used after @", E_SYNTAX);
   if ((value.length - 1) == index)
      return error(EMAIL_EMSG + "cannot end in . (dot)", E_SYNTAX);

   return true;
}

//first.last@cgcmail.maricopa.edu
function check_cgcmail(form, name) {
   var EMAIL_EMSG = "CGCC email address: ";
   if (!check_email(form, name))
     return false;
   var value = form[name].value;
   var index = value.indexOf(".");
   index = value.indexOf(".", index+1);
   index = value.indexOf(".", index+1);
   if (-1 == index)
      return error(EMAIL_EMSG + "Must be of the form first.last@cgcmail.maricopa.edu", E_SYNTAX);
   index = value.indexOf("cgcmail.maricopa.edu");
   if (-1 == index)
      return error(EMAIL_EMSG + "must end in cgcmail.maricopa.edu", E_SYNTAX);
   return true;
}

function check_state(form, name) {
   var value = form[name].value;
   if (2 != value.length || !validateState(value))
      return error("use 2-character state symbol", E_SYNTAX);
   return true;
}

function check_month(form, name) {
   var value = form[name].value;
   input = parseInt(value, 10)
   if (!isNum(value) || isNaN(input))
      return error("month must be a number", E_NAN);
   if (input < 1 || input > 12)
      return error("invalid month entered (1 - 12)", E_RANGE);
   return true;
}

function check_day(form, name) {
   var value = form[name].value;
   day  = parseInt(value, 10)
   if (!isNum(value) || isNaN(day))
      return error("day must be a number", E_SYNTAX);
   if (day <= 0 || day > 31) 
       return error("day is out of bounds", E_SYNTAX);
   return true;
}

function check_year(form, name) {
   var YEAR_LEN = 4, MIN_YEAR = 1900, MAX_YEAR = 2038;
   var value = form[name].value;
   if (YEAR_LEN != value.length) 
      return error("year must be " + YEAR_LEN + " digits long", E_SYNTAX);
   var year = parseInt(value, 10);
   if (!isNum(value) || isNaN(year))
      return error("year must be all digits", E_SYNTAX);
   if (year < MIN_YEAR || year > MAX_YEAR)
      return error("year must be between " + MIN_YEAR +
                   " and " + MAX_YEAR, E_SYNTAX);
   return true;
}

function check_date(form, name) 
{  var YEAR_LEN = 4, MIN_YEAR = 1900, MAX_YEAR = 2038;
   var value = form[name].value;
   var i = 0;
   var sep1="";
   var sep2="";
   for (i = 0; i < value.length; i++)
      if (!isdigit(value.charAt(i))) 
      {   d1=i;
          sep1=value.charAt(d1);
          month=value.substring(0, d1);
	  break;
      }
   d2 = ++i;
   for ( ; i < value.length; i++)
      if (!isdigit(value.charAt(i))) 
      {   d3=i;
          sep2=value.charAt(d3);
          day=value.substring(d2, d3);
          year=value.substring(d3+1, value.length);
	  break;
      }
   if (sep1 != '/' && sep1 != '-')
      return error("invalid separator:  mm/dd/yyyy", E_SYNTAX);
   if (sep2 != '/' && sep2 != '-')
      return error("invalid separator:  mm/dd/yyyy", E_SYNTAX);
   m = parseInt(month, 10)
   if (!isNum(month) || isNaN(m))
       return error("month must be a number (mm/dd/yyyy)", E_NAN);
   if (m < 1 || m > 12)
      return error("invalid month entered (1 - 12) - mm/dd/yyyy", E_RANGE);
   var d  = parseInt(day, 10)
   if (!isNum(day) || isNaN(d))
      return error("day must be a number - mm/dd/yyyy" + day, E_SYNTAX);
   if (d <= 0 || d > 31) 
       return error("day is out of bounds - mm/dd/yyyy", E_SYNTAX);
   var y = parseInt(year, 10);
   if (!isNum(year) || isNaN(y))
      return error("year must be all digits - mm/dd/yyyy", E_SYNTAX);
   if (YEAR_LEN != year.length) 
      return error("year must be " + YEAR_LEN + " digits long - mm/dd/yyyy", E_SYNTAX);
   if (y < MIN_YEAR || y > MAX_YEAR)
      return error("year must be between " + MIN_YEAR + " and " + MAX_YEAR + " - mm/dd/yyyy", E_SYNTAX);

   return true;
}
