// Validate a string
function validate_string(theForm, elemName, minVal, maxVal)
{
   // Check that string is within specified bounds
   var val = value(theForm.elements[elemName]);
   return (
      ((minVal.length == 0) || (val.length >= minVal)) &&
      ((maxVal.length == 0) || (val.length <= maxVal))
   );
   // NOTREACHED
}

// Validate an alphanumeric string
function validate_alnum(theForm, elemName, minVal, maxVal)
{
   // Check that all the characters in the string are alphanumeric
   var val = value(theForm.elements[elemName]);
   return (
      (val.match(/^[a-z0-9]*$/i)) &&
      validate_string(theForm, elemName, minVal, maxVal)
   );
   // NOTREACHED
}

// Validate a submitted URL
function validate_url(theForm, elemName, minVal, maxVal)
{
   // Special case - if no minimum length specified, empty value is allowed
   var val = value(theForm.elements[elemName]);
   if ((!minVal) && (!(val.length))) {
      return(true);
      // NOTREACHED
   }

   // Very simple validation: check that the hostname has at least two
   // elements, separated by a '.' character, and check that the last
   // element contains only alphanumeric characters. It's not a catchall
   // but it'll do for now.
   var hostname = val.replace(/^[a-z]+:\/*/i, "");
   hostname = hostname.replace(/\/.*$/, "");
   return (
      (hostname.match(/^[a-z0-9_-]+\.[a-z0-9_-]+/i)) &&
      (hostname.match(/[a-z0-9_-]+\.[a-z]{2,4}$/i)) &&
      validate_string(theForm, elemName, minVal, maxVal)
   );
   // NOTREACHED
}

// Validate a submitted email address
function validate_email(theForm, elemName, minVal, maxVal)
{
   // Special case - if no minimum length specified, empty value is allowed
   var val = value(theForm.elements[elemName]);
   if ((!minVal) && (!(val.length))) {
      return(true);
      // NOTREACHED
   }

   // Very simple validation: check the hostname as for URL checking above.
   // Check that user names don't contain invalid characters.
   var username = val.replace(/@[^@]+$/, "");
   var hostname = val.replace(/^[^@]+@/, "");
   return (
      (hostname.match(/^[a-z0-9_-]+\.[a-z0-9_-]+/i)) &&
      (hostname.match(/[a-z0-9_-]+\.[a-z]{2,4}$/i)) &&
      (username.match(/^[^@<>()"',;\[\]\\ ]+$/i)) &&
      validate_string(theForm, elemName, minVal, maxVal)
   );
   // NOTREACHED
}

// Validate a date field generated by the input_date custom tag
function validate_date(theForm, elemName, minVal, maxVal)
{
   // If any of the required sub-elements are undefined, form is invalid
   if (
      (typeof(theForm.elements[elemName + "_day"]) != "object") ||
      (typeof(theForm.elements[elemName + "_month"]) != "object") ||
      (typeof(theForm.elements[elemName + "_year"]) != "object")
   ) {
      return("Missing date elements for '" + elemName + "'");
      // NOTREACHED
   }

   // if the minVal and the maxVal are passed through as 0 then blank dates are allowed 
   if (minVal == 0 && maxVal ==0 && theForm.elements[elemName + "_day"].value == ""){
      // the date string is empty, but this is allowed in this case so return true
      return (true);
   } else if ( minVal == 0 && maxVal ==0) {
      // if the date is not empty then remove minValue and maxVal as both being zero will result in a date that does not make sense
      minVal = "";
      maxVal = "";
   }
      
   // Convert to a date.
   var day = value(theForm.elements[elemName + "_day"]);
   var month = value(theForm.elements[elemName + "_month"]);
   var year = value(theForm.elements[elemName + "_year"]);
   var mydate = new Date(year, month - 1, day);
   var month_check = mydate.getMonth() + 1;
   if (
      (month_check == month) &&
      ((minVal.length == 0) || (year >= minVal)) &&
      ((maxVal.length == 0) || (year <= maxVal))
   ) {
      set(theForm.elements[elemName], (String(year) +
        (month < 10 ? "0" : "") + String(month) +
        (day < 10 ? "0" : "") + String(day)));
      return(true);
      // NOTREACHED
   }

   // Date is incorrect - change day to number of days in the month
   newDay = (
      28 + (
         (month == 2) ? (
            (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0)) ? 1 : 0
         ) : (
            (month == 4 || month == 6 || month == 9 || month == 11) ? 2 : 3
         )
      )
   );
   set(theForm.elements[elemName + "_day"], newDay);
   return(false);
   // NOTREACHED
}

// Validate a checkbox group (where at least one must be checked)
function validate_checkbox_group(theForm, elemName)
{
   // Loop through each of the checkboxes
   var is_any_checkbox_checked = 0;
   for (j = 0; j < theForm.elements[elemName].length; j++) {
      if (theForm.elements[elemName][j].checked) {
         is_any_checkbox_checked = 1;
      }
   }
   return(is_any_checkbox_checked);
   // NOTREACHED
}

// Validate an option group (where a non-empty value is to be selected)
function validate_option_group(theForm, elemName)
{
   // Loop through each of the checkboxes
   var val=get_select_option(theForm.elements[elemName]);
   return ((val != null) && val.length);
   // NOTREACHED
}

// Validate a numeric field
function validate_numeric(theForm, elemName, minVal, maxVal,
                          minPrecision, maxPrecision, optional)
{
   // If field contains an empty string, return valid if
   // field is 'optional', invalid otherwise.
   var val = value(theForm.elements[elemName]);
   if (!(val.length)) {
      return (optional.length > 0);
   }

   // Generate pattern to match numeric field
   var temp_re = "^([-]?)([0-9]*)";
   if (maxPrecision != 0) {
      temp_re += "(\\.[0-9]{" +
       ((minPrecision > 0) ? minPrecision : 1) + "," + maxPrecision +
       "})" + ((minPrecision > 0) ? "" : "?");
   }
   temp_re += "$";
   var regex = new RegExp(temp_re);

   // Determine whether field matches our criteria
   return(
      (val.length > 0)
   && (val != '-')
   && (val.match(regex))
   && (val.match(regex).length != 0)
   && ((minVal.length == 0) || (val - minVal >= 0))
   && ((maxVal.length == 0) || (maxVal - val >= 0))
   );
   // NOTREACHED
}

// Validate a list; valid if field value is contained in list
// if 'rev' is true, reverses the sense of the validation
function validate_list(theForm, elemName, list, delim, rev)
{
   // Empty values can't appear in lists
   var val = value(theForm.elements[elemName]);
   if (!(val.length)) {
      return(rev);
      // NOTREACHED
   }

   if (typeof(theForm.elements[list]) != "object") {
      // RETURN WARNING
      return("Missing field value list '" + list +
            "' for field '" + elemName + "' of type " +
	    (rev ? "NOT_IN" : "IN"));
      // NOTREACHED
   }
   var newlist = value(theForm.elements[list]).split(delim);
   var i, valid = rev;

   // Search through list for submitted value
   for (j = 0; j < newlist.length; j++) {
       if (newlist[j].toLowerCase() == val.toLowerCase()) { valid = (!rev); }
   }
   return(valid);
}

// Validate a field that must match another field in the same form
// (e.g. password change verification)
function validate_equal(theForm, elemName, elemNameToo)
{
   if (typeof(theForm.elements[elemNameToo]) != "object") {
      // RETURN WARNING
      return("Missing field '" + elemNameToo +
            "' for field '" + elemName + "' of type EQUAL");
      // NOTREACHED
   }
   return(value(theForm.elements[elemName]) == value(theForm.elements[elemNameToo]));
}

// Validate a field that must be greater in value
// than another field in the same form
function validate_greater_than(theForm, elemName, elemNameToo)
{
   if (typeof(theForm.elements[elemNameToo]) != "object") {
      // RETURN WARNING
      return("Missing field '" + elemNameToo +
            "' for field '" + elemName + "' of type GT");
      // NOTREACHED
   }
   return(value(theForm.elements[elemName]) > value(theForm.elements[elemNameToo]));
}

// Validate a field that must be smaller in value
// than another field in the same form
function validate_smaller_than(theForm, elemName, elemNameToo)
{
   if (typeof(theForm.elements[elemNameToo]) != "object") {
      // RETURN WARNING
      return("Missing field '" + elemNameToo +
            "' for field '" + elemName + "' of type LT");
      // NOTREACHED
   }
   return(value(theForm.elements[elemName]) < value(theForm.elements[elemNameToo]));
}

// Validate a field that must be greater than or equal to
// another field in the same form
function validate_greater_or_equal(theForm, elemName, elemNameToo)
{
   if (typeof(theForm.elements[elemNameToo]) != "object") {
      // RETURN WARNING
      return("Missing field '" + elemNameToo +
            "' for field '" + elemName + "' of type GE");
      // NOTREACHED
   }
   return(value(theForm.elements[elemName]) >= value(theForm.elements[elemNameToo]));
}

// Validate a field that must be smaller than or equal to
// another field in the same form
function validate_smaller_or_equal(theForm, elemName, elemNameToo)
{
   if (typeof(theForm.elements[elemNameToo]) != "object") {
      // RETURN WARNING
      return("Missing field '" + elemNameToo +
            "' for field '" + elemName + "' of type LE");
      // NOTREACHED
   }
   return(value(theForm.elements[elemName]) <= value(theForm.elements[elemNameToo]));
}


// Validate all required fields in a form
function validate_form(theForm)
{
   var i;
   var required_fields = new Array;
   var bad_fields = new Array;
   var warnings = new Array;
   

   // If the form element 'required' exists, use the value of this element
   // to determine which fields are to be validated. The 'required' element
   // should be a comma-separated list of field definitions. If the element
   // does not exist, no validation is performed.
   //
   // Each field definition takes the form:
   //    field name:description:data type[:optional additional definitions]
   // Valid data types are: 'url', 'email', 'alnum', 'date', 'checkbox_group',
   //                       'option_group', 'numeric', 'integer',
   //                       'in', 'not_in', 'equal', 'eq', 'gt', 'lt',
   //			    'lte', 'le' ,'gte', 'ge'
   //
   // 
   // A field may have more than one definition, though why you would want
   // to do this is anybody's guess.
   //
   // For all string fields (including unrecognised data types), you may also
   // specify the minimum and maximum string length (in that order) after the
   // data type.
   //
   // e.g. A password which must be at least 6 characters in length
   //        password:Password:string:6:
   // 
   // String fields with an undefined (or zero) minimum length denotes a
   // field that may be left blank. For string fields which must contain
   // a value, some minimum length must be defined in the 'required' field.
   //
   // For Date, Numeric and Integer fields, you may also specify the minimum
   // and maximum allowed values (in that order) after the data type. For
   // dates, the minimum and maximum values are treated as the earliest and
   // latest years that are allowed. If a date field has a minimum and maximum 
   // value of 0 then empty values are allowed (sometimes a date may be optional but 
   // still be required to be valid if present)
   // Numeric fields may include the minimum and maximum number of digits
   // allowed after the decimal point.
   //
   // e.g. A table tell width, which must be an integer between 320 and 800
   //        cellWidth:Table cell width:integer:320:800
   //
   // e.g. A currency field, which must be positive and have two digits after
   //      the decimal point
   //        itemPrice:Item price:numeric:0::2:2
   //
   // e.g. An embargo date, which must be a year between 2001 and 2010
   //      the decimal point
   //        embargoDate:Embargo date:date:2001:2010
   // 
   // The "In"/"Not_In" data types take as parameters: (1) the fieldname of
   // the list of elements which must not appear in the submitted field, and
   // (2) the character delimiter of the above list (optional; default comma).
   // The supplied fieldname should be the fieldname of a hidden input item
   // in the same form, containing the list of values delimited by the
   // (default or supplied) delimiter.
   //
   // e.g. A numeric ID, which must be either 4, 5 or 6
   //        someID:Some numeric ID:in:id_list
   //
   // The hidden field will then read as follows:
   //        <INPUT TYPE="HIDDEN" NAME="id_list" VALUE="4,5,6">
   //
   // e.g. A site ID, which must be three characters long and must NOT be
   //      'opo' or 'fse'
   //        siteID:Site ID::3:3,siteID:Site ID:not_in:site_id_list
   //
   // The hidden field will then read as follows:
   //        <INPUT TYPE="HIDDEN" NAME="site_id_list" VALUE="opo,fse">
   // 
   // The "Equal" data type takes as its parameter the fieldname of the field
   // whose value it should match, e.g. password fields:
   //
   //        password:Password:equal:password_verify
   //
   // The HTML fields would look something like:
   //        Enter password: <INPUT TYPE="PASSWORD" NAME="password"><br>
   //        Verify password: <INPUT TYPE="PASSWORD" NAME="password_verify"><br>
   //
   // Any unrecognised data type is assumed to be a normal string.

   // Parse the 'required' field element
   // Generate a warning if it does not exist
   if (typeof(theForm.elements["required"]) == "object") {
      if (typeof(theForm.elements["required"].length) == "number") {
         for (i = 0; i < theForm.elements["required"].length; i++) {
            if (typeof(theForm.elements["required"][i].value) != "undefined") {
               required_fields[i] = theForm.elements["required"][i].value;
            }
            else {
               // SHOULD NEVER HAPPEN
               required_fields[i] = 'Parser Error!:-:ERROR'; // dummy field
               warnings[warnings.length] = "Error parsing required field 'required'!";
            }
         }
      }
      else if (typeof(theForm.elements["required"].value) != "undefined") {
         required_fields = theForm.elements["required"].value.split(',');
      }
      else {
         // SHOULD NEVER HAPPEN
         warnings[warnings.length] = "Error parsing required field 'required'!";
      }
   }
   else {
      // WARNING
      warnings[warnings.length] = "Missing required field 'required'!";
   }

   // Split element into individual field definitions
   for (i = 0; i < required_fields.length; i++) {
      // Get the definition of this form element
      var field_def = required_fields[i].split(':');

      // Get the field name and label
      for (j = 0; j < 8; j++) {
         if (typeof(field_def[j]) == "undefined") { field_def[j] = ''; }
      }
      var field_name = field_def[0];
      var field_desc = field_def[1];
      var field_type = field_def[2].toLowerCase();
      var is_valid = 0;

      // If the field exists, validate it
      if(typeof(theForm.elements[field_name]) == "object") {
         // Get the rest of the field definition
         if (
(field_type == 'not_in') ||
(field_type == 'in') ||
(field_type == 'equal') ||
(field_type == 'eq') ||
(field_type == 'gt') ||
(field_type == 'gte') ||
(field_type == 'ge') ||
(field_type == 'lt') ||
(field_type == 'lte') ||
(field_type =='le')) {
            var list = field_def[3];
            var delim = field_def[4];
         }
         else {
            // Determine minimum/maximum field values (length for strings)
            var minval = field_def[3];
            var maxval = field_def[4];
            if (isNaN(minval) || (minval < 0)) { minval = ""; }
            if (isNaN(maxval) || (maxval < 0)) { maxval = ""; }
            if (field_type == 'numeric') {
               // Determine minimum and maximum digits after decimal point
               var minprec = field_def[5];
               var maxprec = field_def[6];
               var optional = field_def[7];
               if (isNaN(minprec) || (minprec < 0)) { minprec = 0; }
               if (isNaN(maxprec) || (maxprec < 0)) { maxprec = ""; }
            }
	    else {
	       var minprec = 0;
	       var maxprec = 0;
               var optional = field_def[5];
	    }
         }

	 // Determine reason code if field validation fails
	 if ((field_type == 'numeric') || (field_type == 'integer')) {
	    reason = 'must ';
	    if ((minval) || (maxval)) {
		reason += 'have a value of ';
		if (minval == maxval) { reason += minval; }
		else if ((minval) && (maxval)) { reason += 'between ' + minval + ' and ' + maxval; }
		else if ((!minval) && (maxval)) { reason += maxval + ' or less'; }
		else if ((minval) && (!maxval)) { reason += minval + ' or greater'; }
	    }

            if (((minval) || (maxval))
            && ((minprec) || (maxprec))) { reason += ' and '; }

            if ((minprec) || (maxprec)) {
		reason += 'have ';
		if (!minprec) { minprec = 0; }
		if (minprec == maxprec) { reason += minprec; }
		else if ((minprec > 0) && (maxprec)) { reason += 'between ' + minprec + ' and ' + maxprec; }
		else if ((minprec <= 0) && (maxprec)) { reason += 'up to ' + maxprec; }
		else if ((minprec > 0) && (!maxprec)) { reason += minprec + ' or more'; }

		reason += ' digits after the decimal point';
	    }
	 }
	 else if (field_type == 'not_in') {
	    reason = 'supplied is reserved or in use';
	 }
	 else if (field_type == 'in') {
	    reason = 'supplied is invalid or not available';
	 }
	 else if ((field_type == 'equal') || (field_type == 'eq')) {
	    reason = 'fields must match';
	 }
	 else if ((field_type == 'lt') || (field_type == 'gt')
	 || (field_type == 'gte') || (field_type == 'ge')
	 || (field_type == 'lte') || (field_type == 'le')) {
	    reason = delim;
	 }
	 else if (field_type == 'checkbox_group') {
	    reason = '(please select at least one)';
	 }
	 else if (field_type == 'option_group') {
	    reason = '(please select one)';
	 }
	 else if (field_type == 'date') {
	    reason = 'does not make sense to me';
	 }
	 else {
	    reason = 'must be';
	    if (field_type == 'url') {
		reason += ' a valid URL';
	    }
	    else if (field_type == 'alnum') {
		reason += ' an alphanumeric string';
	    }
	    else if (field_type == 'email') {
		reason += ' a valid email address';
	    }
	    if ((minval) || (maxval)) {
		if (reason.length > 8) { reason += ' and'; }
		if (minval == maxval) { reason += minval; }
		else if ((minval > 0) && (maxval)) { reason += ' between ' + minval
+ ' and ' + maxval + ' characters in length'; }
		else if ((minval == 0) && (maxval)) { reason += ' up to ' + maxval +
' characters in length'; }
		else if ((minval == 1) && (!maxval)) { reason += ' non-empty'; }
		else if ((minval) && (!maxval)) { reason += ' at least ' + minval + ' characters in length'; }
	    }
	 }

         // Do the validation
         if (field_type == 'url') {
            is_valid = validate_url(theForm, field_name, minval, maxval);
         }
         else if (field_type == 'alnum') {
            is_valid = validate_alnum(theForm, field_name, minval, maxval);
         }
         else if (field_type == 'email') {
            is_valid = validate_email(theForm, field_name, minval, maxval);
         }
         else if (field_type == 'date') {
            is_valid = validate_date(theForm, field_name, minval, maxval);
         }
         else if (field_type == 'checkbox_group') {
            is_valid = validate_checkbox_group(theForm, field_name);
         }
         else if (field_type == 'option_group') {
            is_valid = validate_option_group(theForm, field_name);
         }
         else if (field_type == 'numeric') {
            is_valid = validate_numeric(theForm, field_name,
               minval, maxval, minprec, maxprec, optional);
         }
         else if (field_type == 'integer') {
            is_valid = validate_numeric(theForm, field_name,
               minval, maxval, 0, 0, optional);
         }
         else if ((field_type == 'not_in') || (field_type == 'in')) {
            var rev = (field_type == "not_in");
            if (delim.length < 1) { delim = ","; }
            is_valid = validate_list(theForm, field_name, list, delim, rev);
         }
         else if ((field_type == 'equal') || (field_type == 'eq')) {
            is_valid = validate_equal(theForm, field_name, list);
         }
         else if (field_type == 'gt') {
            is_valid = validate_greater_than(theForm, field_name, list);
         }
         else if (field_type == 'lt') {
            is_valid = validate_smaller_than(theForm, field_name, list);
         }
         else if ((field_type == 'gte') || (field_type == 'ge')) {
            is_valid = validate_greater_or_equal(theForm, field_name, list);
         }
         else if ((field_type == 'lte') || (field_type == 'le')) {
            is_valid = validate_smaller_or_equal(theForm, field_name, list);
         }
         else {
	    // DEFAULT: normal string
            is_valid = validate_string(theForm, field_name, minval, maxval);
         }

         // If warning was returned, push onto array of warnings
         if (typeof(is_valid) == "string") {
            warnings[warnings.length] = is_valid;
         }
         // If element is invalid, push onto the array of bad fields   
         else if (!is_valid) {
            if (!(bad_fields.length)) {
               var focus_field = field_name;
            }
	    var badfield = field_desc + " " + reason;
	    bad_fields[bad_fields.length] = badfield;
         }
      }
      // N.B. If we can't find the form element, generate warning
      else {
         // WARNING
         warnings[warnings.length] = "Missing required field '" + field_name +
                       "' of type " + field_type.toUpperCase();
      }

   }

   // Determine action to take
   if (warnings.length) {
      alert("The form could not be validated because:\n\n  * " +
            warnings.join("\n  * ") + "\n\nPlease check your code.");
      return(false);
   }
   else if (bad_fields.length) {
// XXX THIS DOES NOT WORK FOR RADIOS/CHECKBOXES. NEEDS INVESTIGATION!
// XXX kmj, 24.09.2001
//      theForm.elements[focus_field].focus();
//      window.scrollBy(-40, -30);
      alert("The following fields have not been entered correctly:\n\n  * " +
            bad_fields.join("\n  * ") + "\n\nPlease check your entries and try again.");
      return(false);
   }
   else {
      return(true);
   }
}
