// List of pointers to error messages that have been inserted into the page by validateForm
var _valFormErrorMessages = new Array();
// Hash mapping an element's name to its display text
var _valFormDisplayNames;

/* Validate a form, returning true if no errors are found, false otherwise
 * f - form object to validate
 * e - optional element object to validate, otherwise does all elements in form.  If e is specified, no errors
 *     are reported to the user.  This is to allow on the fly validation.
 */
function validateForm2(f, e) {

  // Check validation hasn't been specifically turned off.
  if (f.validate === false) {
    return true;
  }

  var reportErrors = (e == null);

  var errorsFound = false;
  // Store the display names of fields with errors
  var errorFields = [];
  var errorsFound;

  // Return the text within a parent element, optionally stopping at a specified child element that is directly
  // contained in the parent
  function getLabelText(parent, child) {
    var text = "";
    for (var i = 0; i < parent.childNodes.length; i++) {
      var node = parent.childNodes[i];
      if (child && node == child) {
        break;
      }
      if (node.textContent) {
        text += node.textContent; // DOM
      } else if (node.innerText) {
        text += node.innerText; // IE
      } else if (node.nodeType == 3 /* Node.TEXT_NODE */) {
        text += node.data; // IE again
      }
    }

    text = text.replace(/\n/g, ' '); // Remove newlines
    text = text.replace(/^\s*/, ''); // Trim leading whitespace
    text = text.replace(/:?\s*$/, ''); // Trim trailing whitespace
    return text;
  }

  // Highlight element as having an error and display msg after it.  Also add the elements
  // displayName to the list of fields with errors
  function reportError(e, msg) {
    if (reportErrors) {
      var displayName;

      if (e.displayName) {
        // displayName has been explicitly set on this element
        displayName = e.displayName;
      } else if (_valFormDisplayNames[e.name] && e.type != "radio" && e.type != "checkbox") {
        // displayName has been read from a label that targets this element with the for attribute
        displayName = _valFormDisplayNames[e.name];
      } else if (e.parentNode.nodeName == 'LABEL' && e.type != "radio" && e.type != "checkbox") {
        // read display name from the element's parent, which is a label
        displayName = getLabelText(e.parentNode, e);
      } else {
        // use the elements's name, capitalising the first letter
        displayName = e.name.substr(0,1).toUpperCase() + e.name.substr(1);
      }

      // Create a span containing the error message and place it after the form element
      var span = document.createElement('span');
      span.innerHTML = '&nbsp;<span style="font-style: italic; color: red">' + msg + '</span>';

      // A container for error messages can be provided by giving an element the appropriate id.
      var errorContainer = document.getElementById(e.name + 'ValErrors');

      if (errorContainer) {
        errorContainer.insertBefore(span, errorContainer.firstChild);
        _valFormErrorMessages.push(span);
      } else if (e.type != "radio" && e.type != "checkbox") {
        // You can't put the message after a radio button or checkbox, as it needs to go after the group of buttons
        e.parentNode.insertBefore(span, e.nextSibling);
        _valFormErrorMessages.push(span);
      }

      if (e.type != "radio" && e.type != "checkbox") {
        // These must be set separately, or you completely lose the border when resetting the style in IE
        e.style.borderColor = 'red';
        e.style.borderStyle = 'solid';
        e.style.borderWidth = '1px';
      }

      errorFields.push(displayName);
    }

    errorsFound = true;
  }


  if (reportErrors) {
    // Clear any existing error messages from previous attempts
    errorFields = [];
    while (_valFormErrorMessages.length) {
      var errorMessage = _valFormErrorMessages.pop();
      if (errorMessage.previousSibling) {
        // Assume previousSibling is the form element and reset its style
        errorMessage.previousSibling.style.borderColor = '';
        errorMessage.previousSibling.style.borderStyle = '';
        errorMessage.previousSibling.style.borderWidth = '';
      }
      if(errorMessage.parentNode){
        errorMessage.parentNode.removeChild(errorMessage);
      }
    }

    // Build a hash that maps an element's name to its display text.  This is only done
    // for elements that are labelled by labels that have a for attribute
    // Only do this the first time validateForm is called for this page.  This could cause problems
    // if people are dynamically manipulating their labels!
    if (_valFormDisplayNames == null) {
      var labels = document.getElementsByTagName('label');
      _valFormDisplayNames = new Object();
      for (var i = 0; i < labels.length; i++) {
        l = labels[i];
        if (l.htmlFor && document.getElementById(l.htmlFor)) {
          _valFormDisplayNames[document.getElementById(l.htmlFor).name] = getLabelText(l);
        }
      }
    }
  }

  // Get the elements to check
  var elements;
  if (e == null) {
    elements = f.elements;
  } else {
    elements = [e];
  }

  // Check the form fields
  for (var i = 0; i < elements.length; i++) {
    var msg = []; // Array of error messages for this element
    var e = elements[i];
    var optional = e.optional || f.allOptional; // every element in the form can be made optional by setting form.allOptional = true

    // Some conditions imply others
    e.numeric = e.numeric || (e.min != null && e.min != '') || (e.max != null && e.max != '') || e.integer != null || e.positive != null || e.natural != null;
    e.date = e.date || e.minDate || e.maxDate;
    e.time = e.time || e.minTime || e.maxTime;

    if (! e.disabled) {
      // Text fields
      if (e.type == 'text' || e.type == "password" || e.type == "textarea" || e.type == "file") {
        var empty = ! e.value.search(/^\s*$/);

        // Numeric elements
        if (e.numeric) {
          if (! (empty && optional)) {
            var v = new Number(e.value);
            if (empty
                || isNaN(v)
                || ((e.min != null && e.min != '') && v < e.min)
                || ((e.max != null && e.max != '') && v > e.max)
                || (e.exmin != null && v <= e.exmin)
                || (e.exmax != null && v >= e.exmax)
                || ((e.positive || e.natural) && v < 0)
                || ((e.integer || e.natural) && e.value.search(/\./) != -1)) {

              msg[0] = "must be a";

              if (e.positive || e.natural) {
                msg[0] += " positive";
              }

              if (e.integer || e.natural) {
                msg[0] += " whole";
              }

              msg[0] += " number";

              if ((e.min != null && e.min != '') && (e.max != null && e.max != '')) {
                msg[0] += " between " + e.min + " and " + e.max + ", inclusive";
              } else if ((e.min != null && e.min != '')) {
                msg[0] += " that is greater than or equal to " + e.min;
              } else if ((e.max != null && e.max != '')) {
                msg[0] += " that is less than or equal to " + e.max;
              }

              if (e.exmin != null && e.exmax != null) {
                msg[0] += " between " + e.exmin + " and " + e.exmax + ", exclusive";
              } else if (e.exmin != null) {
                msg[0] += " that is greater than " + e.exmin;
              } else if (e.exmax != null) {
                msg[0] += " that is less than " + e.exmax;
              }

            }
          }

        // Date elements
        } else if (e.date) {
          if (! (empty && optional)) {
            if (e.minDate == "today" || e.maxDate == "today") {
              // minDate & maxDate can be set to the special string "today"
              var today = new Date();
              // Throw away the hours, mins secs etc
              today = new Date(today.getFullYear(), today.getMonth(), today.getDate());
              var todayStr = today.getDate() + '/' + (today.getMonth() + 1) + '/' + today.getFullYear();
            }

            if (e.maxDate) {
              if (e.maxDate == 'today') {
                var maxDate = today;
              } else {
                maxDate = e.maxDate.split("/");
                maxDate = new Date(maxDate[2], maxDate[1] - 1, maxDate[0]);
              }
            }
            if (e.minDate) {
              if (e.minDate == 'today') {
                var minDate = today;
              } else {
                var minDate = e.minDate.split("/");
                minDate = new Date(minDate[2], minDate[1] - 1, minDate[0]);
              }
            }
            var dateArray = e.value.split("/");
            if (e.value.search('[^/-9]') != -1
                || dateArray.length != 3
                || dateArray[2].length != 4
                || dateArray[0] > 31 || dateArray[0] < 1
                || dateArray[1] > 12 || dateArray[1] < 1
                || isNaN(date = new Date(dateArray[2], dateArray[1] - 1, dateArray[0]))
                || date.getDate() != dateArray[0] // Check the actual date is valid e.g. reject 31/09/2002
                || e.maxDate && ((maxDate.getTime() - date.getTime()) / 86400000) < 0 // 1000*60*60*24
                || e.minDate && ((minDate.getTime() - date.getTime()) / 86400000) > 0) {
              msg[0] = "must be a valid date in d/m/yyyy format";
              if (e.minDate) {
                msg[0] += " that cannot be before " + e.minDate;
                if (e.minDate == 'today') {
                  msg[0] += " (" + todayStr + ")";
                }
                if (e.maxDate) {
                  msg[0] += " and cannot be after " + e.maxDate;
                  if (e.maxDate == 'today') {
                    msg[0] += " (" + todayStr + ")";
                  }
                }
              } else if (e.maxDate) {
                msg[0] += " that cannot be after " + e.maxDate;
                if (e.maxDate == 'today') {
                  msg[0] += " (" + todayStr + ")";
                }
              }
            }
          }

        // Time elements
        } else if (e.time) {
          if (! (empty && optional)) {
            if (e.minTime == "now" || e.maxTime == "now") {
              // minTime & maxTime can be set to the special string "now"
              var now = new Date();
              // Throw away the hours, mins secs etc
              now = new Date(0, 0, 0, now.getHours(), now.getMinutes());
              nowStr = now.getHours() + ":";
              if (now.getMinutes < 10) {
                nowStr += "0";
              }
              nowStr += now.getMinutes();
            }

            if (e.maxTime) {
              if (e.maxTime == "now") {
                var maxTime = now;
                var maxTimeStr = "the current time (" + nowStr + ")";
              } else {
                maxTime = e.maxTime.split(":");
                maxTime = new Date(0, 0, 0, maxTime[0], maxTime[1]);
                maxTimeStr = e.maxTime;
              }
            }
            if (e.minTime) {
              if (e.minTime == "now") {
                var minTime = now;
                var minTimeStr = "the current time (" + nowStr + ")";
              } else {
                minTime = e.minTime.split(":");
                minTime = new Date(0, 0, 0, minTime[0], minTime[1]);
                minTimeStr = e.minTime;
              }
            }
            var timeArray = e.value.split(":");
            if (timeArray.length != 2
                || timeArray[1].length != 2
                || timeArray[0] < 0 || timeArray[0] > 23
                || timeArray[1] < 0 || timeArray[1] > 59
                || isNaN(time = new Date(0, 0, 0, timeArray[0], timeArray[1]))
                || e.maxTime && (maxTime.getTime() - time.getTime()) < 0
                || e.minTime && (minTime.getTime() - time.getTime()) > 0) {
              msg[0] = "must be a valid time in hh:mm format"
              if (e.minTime) {
                msg[0] += " that cannot be before " + minTimeStr;
                if (e.maxTime) {
                  msg[0] += " and cannot be after " + maxTimeStr;
                }
              } else if (e.maxTime) {
                msg[0] += " that cannot be after " + maxTimeStr;
              }
            }
          }

        // Email addresses (only checks that it at least looks like an email address!)
        } else if (e.email) {
          if (! (empty && optional) && e.value.search(/^[^@]+@[A-Za-z0-9-.]+\.[A-Za-z0-9-]+$/) == -1) {
            msg[0] = "must be an email address";
          }

        // validation against a supplied javascript regular expression
        } else if (typeof e.regex == 'object') {
          // e.regex should be an object two properties:
          // regex - a RegExp object
          // msg - a string to display if the regex isn't found
          if (! (empty && optional) && e.value.search(e.regex.regex) == -1) {
            msg[0] = e.regex.msg;
          }

        // All other elements
        } else if (empty && ! optional) {
          msg[0] = "must not be left empty";
        }


        // Do checks that can apply on top of the previous ones

        // Text/numeric elements with a minimum length.
        if (! (empty && optional)) {
          if (e.minLength && (msg.length || e.value.length < e.minLength)) {
            var pos = msg.length;
            msg[pos] = "must be at least " + e.minLength;
            if (e.numeric) {
              msg[pos] += " digits long";
            } else {
              msg[pos] += " characters long";
            }
          }

          // Text/numeric elements with a maximum length (IE defaults this to 2147483647, Mozilla to -1, Chrome/Safari defaults this value to 524288)
          if (e.maxLength > 0 && e.maxLength < 2147483647 && e.maxLength != 524288 && (msg.length || e.value.length > e.maxLength)) {
            var pos = msg.length;
            msg[pos] = "cannot be longer than " + e.maxLength;
            if (e.numeric) {
              msg[pos] += " digits";
            } else {
              msg[pos] += " characters";
            }
          }
        }

      // Radio buttons
      } else if (e.type == "radio") {
        // This is one of a group of radio buttons, so get the group.  Attributes for radio
        // buttons are set on the first member of the group
        var radioGroup = eval('f.' + e.name);

        // Only run checks if this is the first radio in the group (this will check the whole group)
        if (radioGroup[0] == e && ! optional) {
          var radioChecked = false;

          for (var j=0; j < radioGroup.length; j++) {
            if (radioGroup[j].checked) {
              radioChecked = true;
              break;
            }
          }

          if (! radioChecked) {
            msg[0] = "please tick one";
          }
        }

      // Checkboxes
      } else if (e.type == "checkbox") {
        // As with radio buttons, this could be one of a group of checkboxes.  If it's only one, we don't care about it
        var checkGroup = eval('f.' + e.name);

        if (checkGroup[0] == e && (! optional || e.min || e.max)) {
          if (! optional && ! e.min) {
            e.min = 1;
          }

          var numChecked = 0;

          for (var j=0; j < checkGroup.length; j++) {
            if (checkGroup[j].checked) {
              numChecked += 1;
            }
          }

          if (! f.allOptional || numChecked > 0) {
            if ((e.min && numChecked < e.min) || (e.max && numChecked > e.max)) {
              if (e.min == e.max) {
                msg[0] = "exactly " + e.min;
              } else {
                msg[0] = "";
                if (e.min) {
                  msg[0] += "at least " + e.min;
                  if (e.max) {
                    msg[0] += ", and ";
                  }
                }
                if (e.max) {
                  msg[0] += "no more than " + e.max;
                }
              }
              if (e.min > 1 || e.max > 1) {
                msg[0] += " boxes ";
              } else {
                msg[0] += " box ";
              }
              msg[0] += "must be ticked";
            }
          }
        }

      // Selects (only when multiple selection is not allowed)
      } else if (e.type == "select-one") {
        // Assume that the first entry in the select box is something like "-- Please Select --"
        if (! optional && e.selectedIndex == 0) {
          msg[0] = "please select one";
        }
      }


      // Call custom validation function (on top of other validation!).  To disable the standard validation checks
      // before this, the element should be marked as optional
      if (e.valFunction) {
        var newMsg = e.valFunction(e, msg, optional, f.allOptional);
        msg = []; // valFunction can choose to discard all previous messages
        if (typeof newMsg == 'object') {
          msg = newMsg;
        }
      }


      // Concatenate error messages in a language natural way and display them
      if (msg.length) {
        var errorMsg = msg[0];
          if (msg.length > 1) {
          for (var j = 1; j < msg.length - 1; j++) {
            errorMsg += ", " + msg[j];
          }
          errorMsg += " and " + msg[msg.length - 1];
        }

        reportError(e, errorMsg);
      }
    }
  }

  if (reportErrors && errorsFound) {
    var errorText = 'This form could not be submitted as the following fields had errors:\n';
    for (var i = 0; i < errorFields.length; i++) {
      errorText += '\n' + errorFields[i];
    }
    errorText += '\n\nPlease correct the errors and try again.';
    alert(errorText);
  }
  return ! errorsFound;
}
