/*********************
  form.js

  Form validation and interactivity routines

  Form validation routines
    Elements are marked for validation by adding special css classes.
    Validation happens automatically onblur.  If there is an element
    with the id controlname_error, an error message will appear there.

  Form convenience features
    Simpler ways of getting/setting values of SELECT and radiobutton elements

Dependencies (YUI):
  yahoo-dom-event
  yahoo-element-beta

**********************/

var form = new Object; // namespace

form.TEXT_PAUSE = 400; // msec after last key pressed before a validator fires (200-1000 typical)

var yui = YAHOO.util; // alias
yui.log = YAHOO.log;

/*

  CSS Classes for validation (may be used in combination):

  formValReq - the control must have a non-blank value
  formValNum - the control must have a numeric value
  formValInt - the control must have a integer value
  formValAlpha - the control must have a alphabetical value
  formValName - alpha, spaces, apostrophes, periods, and ampersands
  formValEmail - foo@bar.com
  formValDate - parseable as a Date
  formValZip  - zip code, 5 or 5-4
  formValUrl  - looks like a URL
  formValPassword - at least 6 characters, all letters & numbers
  formValChecked - must be a checked checkbox

  // Only allowed for radio buttons:
  formValRadioReq - one of the radio buttons in the group must be checked

*/

form.initters = new Array();
form.initted = false;

form.safeInit = function (fn) {
    form.initted = false;
    form.initters.push(fn);
}

form.fireInit = function() {
    form.debug(16, 'fireInit-01');
    if (form.initted) { return 1; }
    form.debug(16, '-02');
    for (var i =0; i < form.initters.length; i++) {
        form.debug(16, '-03');
        form.initters[i].call();
        form.debug(16, '-04');
    }
    form.debug(16, '-05');
    form.initted = true;
};
yui.Event.onDOMReady(form.fireInit);
yui.Event.addListener(window, 'load', form.fireInit);


form.debug = function(n, t) {
    if (!form.debugLevel) { return; }
    var e = document.getElementById('sso_debug_' + n);
    if (!e) { return; }
    e.value = (e.value || '') + t;
};


/*********************************/
/*         Setup Validation      */
/*********************************/
form.injectValidators = function () {
    // Perl-style: accumulate elems of interest using their IDs as keys to this hash.
    var elems = new Object;

    // Loop over known validator classes and find any elems that have at least one.
    var forms = document.forms;
    for (var valClass in form.valsByClass) {
        for (var f = 0; f < forms.length; f++) {
            var frm = forms[f];
            var ctls = yui.Dom.getElementsByClassName(valClass, null, frm);
            for (var c=0; c < ctls.length; c++) {
                elems[ctls[c].id] = 1;
            }
        }
    }

    // Now that we have a unique collection, loop over it.
    for (var ctlId in elems) {
        form.attachValidation(ctlId);
    }
};
form.safeInit(form.injectValidators);


/***
  Validator static data
***/
form.errorsByClass = {
    'formValReq'   : 'This field is required.',
    //'formValNum'   : '',
    'formValPhone' : 'This field should be a phone number consisting of 10 to 18 digits.',
    'formValEmail' : 'This field must be a valid email address, like "jane@example.edu".',
    'formValInt'   : 'This field must be a whole number, like 5 or 27.',
    'formValAlpha' : 'This field should only have letters in it.',
    'formValName'  : 'This field should only have letters, spaces, periods, apostrophes, and ampersands.',
    'formValDate'  : 'This field should be a valid date in MM/DD/YYYY format.',
    'formValMatch' : 'The fields do not match.',
    'formValZip'   : 'This field should have either 5 or 9 digits with an optional dash.',
    'formValUrl'   : 'This field should look like a web address, like "http://www.yahoo.com"',
    'formValPassword' : 'This field should be at least 6 characters long and can contain only letters and numbers.',
    'formValChecked' : 'You must check this box to continue.',
    'formValRadioReq'  : 'Please choose one of the options.',
    'formValCCNumber' : 'The number entered is not a valid credit card number.  Please check for typos.'
};


form.regexByClass = {
    //'formValNum'   : '',
    'formValEmail'    : /^[^\s\@\"\']+[\@]([\w\-]+\.)+(\w+)$/,
    'formValPhone'    : /^\s*(\d{10,18})\s*$/,
    'formValInt'      : /^\d+$/,
    'formValZip'      : /^\d{5}(\-?)(\d{4})?$/,
    'formValUrl'      : /^http(s?):\/\/[\d\w]+(\.[\d\w]+)+(\.((com)|(org)|(edu)|(gov)|(us)|(net)|(info)))(\/.*)?$/,
    'formValAlpha'    : /^[A-Za-z]+$/,
    'formValName'     : /^[\w\s\.\'\&]+$/,
    'formValDate'     : /^\d\d?\/\d\d?\/\d{4}$/,
    'formValPassword' : /^[a-zA-Z0-9]{6,}$/
    //'formValPassword' : /^[a-zA-Z0-9 '"$%@_~+=]{6,}$/ // '
};


//                   x  J  F  M  A  M  J  J  A  S  O  N  D
form.daysPerMonth = [0, 31,29,31,30,31,30,31,31,30,31,30,31];

// General validator for regex-based vaildations.
form.validateRegex = function(elem, valClass) {
    //alert('In validateRegex, have elem ' + elem);
    if (! elem.get('value')) { return true; } // hmmmm....???
    //alert('In validatebyRegex, have regex for vC ' + valClass + ' as  ' + form.regexByClass[ valClass ]);
    return form.regexByClass[ valClass ].test(elem.get('value'));
};


form.validateRequired = function(elem) { return (elem.get('value') && elem.get('value').length != 0); };

form.validateAlpha    = function(elem) { return form.validateRegex(elem, 'formValAlpha'    ); };
form.validateName     = function(elem) { return form.validateRegex(elem, 'formValName'     ); };
form.validateInteger  = function(elem) { return form.validateRegex(elem, 'formValInt'      ); };
form.validateEmail    = function(elem) { return form.validateRegex(elem, 'formValEmail'    ); };
form.validatePhone    = function(elem) { return form.validateRegex(elem, 'formValPhone'    ); };
form.validateZip      = function(elem) { return form.validateRegex(elem, 'formValZip'      ); };
form.validateUrl      = function(elem) { return form.validateRegex(elem, 'formValUrl'      ); };
form.validatePassword = function(elem) { return form.validateRegex(elem, 'formValPassword' ); };


// Implements formValDate
form.validateDate = function(elem) {
    var val = elem.get('value')
    if (! val) { return true; }

    // First see if it passes regex
    if (! form.regexByClass[ 'formValDate' ].test(val)) {
        return false;
    }

    // Date contructor will happily parse a bogus date.... so will Date.parse.
    var parts = val.match(/^(\d\d?)\/(\d\d?)\/(\d{4})$/);
    if (0 < parts[1] && parts[1] < 13 && // Month between 1 and 12 inclusive
        0 < parts[2] && parts[2] <= form.daysPerMonth[Number(parts[1])] &&
        1900 < parts[3] && parts[3] < 2100) { // Year between 1901 and 2099 inclusive
        return true;
    } else {
        return false;
    }
};




form.validateChecked = function(elem) {
    return elem.get('checked');
};


// Checks that this control matches another control.
// Match pairs are set by adding a class of the form 'match-XXXX'.
form.validateMatch = function(elem, showError) {

    // Find the match class tag
    var re = /match.+/;
    var classes = elem.get('className').split(/ /);
    var tag = ''
    for (var c = 0; c < classes.length; c++) {
        if (re.test(classes[c])) { tag = classes[c]; }
    }

    // Find all controls in this match ring, and confirm they all match
    var allmatch = true;
    var value = elem.get('value');
    var frm = elem.get('form');
    var matches =  yui.Dom.getElementsByClassName(tag, 'input', frm);
    for (var m=0; m < matches.length; m++) {
        allmatch = allmatch && (matches[m].value == value);
    }

    if (showError) {
        for (var m=0; m < matches.length; m++) {
            elem = new yui.Element(matches[m].id);
            if (allmatch) {
                form.clearError(elem);
            } else {
                form.showError(elem, 'formValMatch');
            }
        }
    }

    return allmatch;
};

form.validateRadioRequired = function(elem) {
    // Get radio group name
    var groupname = elem.get('name');
    // Get form
    var frm = elem.get('form');

    // Loop over radio buttons in the group, and if one is checked, return true
    var radios = frm.elements[groupname];
    for (var i=0; i < radios.length; i++) {
        if (radios[i].checked) { return true; }
    }
    // Else return false (none selected)
    return false;
};

form.validateCreditCard = function(elem) {
    var number = elem.get('value');

    // Algorithm adapted from Business::CreditCard
    var sum = 0;
    var weight = 0;


    // Numbers & spaces only
    if (!number.match(/^[\d\s]+$/)) { return false; }
    number = number.replace(/\D/g, '');

    var len = number.length;
    if (len < 13) { return false; }

    for (var i=0; i < len - 1; i++) {
        weight = Number(number.substr(len - (i + 2), 1)) * (2 - (i % 2));
        sum = sum + ((weight < 10) ? weight : (weight - 9));
    }

    return (number.substr(len - 1, 1) == ((10 - sum % 10) % 10));
};



// Callbacks by class name
form.valsByClass = {
    'formValReq'      : form.validateRequired, // ok
    //'formValNum'      : form.validateNumeric,
    'formValEmail'    : form.validateEmail,// ok
    'formValPhone'    : form.validatePhone, // ok
    'formValAlpha'    : form.validateAlpha, // ok
    'formValName'     : form.validateName, // ok
    'formValInt'      : form.validateInteger, // ok
    'formValZip'      : form.validateZip,  // ok
    'formValUrl'      : form.validateUrl,  // ok
    'formValDate'     : form.validateDate, // ok
    'formValPassword' : form.validatePassword, // ok
    'formValMatch'    : form.validateMatch,
    'formValChecked'  : form.validateChecked,
    'formValRadioReq' : form.validateRadioRequired,
    'formValCCNumber' : form.validateCreditCard
};


// Add a custom validator
form.addCustomValidator = function (className, errorMsg, validator) {
    form.valsByClass[className]  = validator;
    form.errorsByClass[className] = errorMsg;
};


// Given a control ID, wire up the validators that the control requests in its CSS classes.
form.attachValidation = function(id) {
    var elem = new yui.Element(id);
    var type = elem.get('type') || elem.get('nodeName');
    type = type.toLowerCase();

    elem.addListener('blur', form.validateControlOnEvent);

    // If it's a SELECT, also grab change
    if (type == 'select') {
        elem.addListener('change', form.validateControlOnEvent);
        //elem.Listener('keyup', form.validateControlOnEvent);
    }
    if (type == 'radio' || type == 'checkbox') {
        elem.addListener('click', form.validateControlOnEvent);
    }

    // If it's text, also attach to onkeypress
    if (type == 'text' || type == 'password' || type == 'textarea') {
        elem.addListener('keypress', form.validateControlOnEventWithDelay);
    }
};


// Stop validating a control (presumably because you're about to destroy it).
form.detachValidation = function(id) {
    var elem = new yui.Element(id);
    var type = elem.get('type');
    type = type.toLowerCase();

    elem.removeListener('blur', form.validateControlOnEvent);

    // If it's text, also detach from onkeypress
    if (type == 'text' || type == 'password' || type == 'textarea') {
        elem.removeListener('keypress', form.validateControlOnEventWithDelay);
    }

    // If it's a SELECT, also grab keyup and change
    if (type == 'select') {
        elem.removeListener('change', form.validateControlOnEvent);
        //elem.removeListener('keyup', form.validateControlOnEvent);
    }
};

// Validate the control, and show the error message in response to user events.
form.validateControlOnEvent = function(evt) {
    var elem = yui.Event.getTarget(evt);
    form.validateControl(elem.id, true);
};

// Validate the control, and show the error message in response to user events,
// but only if this event hasn't happened in an a short period.
form.validationIntID = undefined;

form.validateControlOnEventWithDelay = function(evt) {
    var elem = yui.Event.getTarget(evt);
    if (form.validationIntID) {
        clearTimeout(form.validationIntID);
    }
    form.validationIntID = setTimeout('form.delayedValidation("' + elem.id + '",true);', form.TEXT_PAUSE);
};

form.delayedValidation = function(id, showError) {
    clearTimeout(form.validationIntID);
    form.validationIntID = undefined;
    form.validateControl(id, showError);
};

// Validate one control, optionally showing the error.
form.validateControl = function(id, showError) {

    if (form.validationIntID) {
        clearTimeout(form.validationIntID);
        form.validationIntID = undefined;
    }

    if (showError == undefined) { showError = true; }

    // Loop over CSS classes, looking for known validation classes
    // if it fails validation, remember the class.
    var valid = true;
    var failClass = '';
    var elem = new yui.Element(id);
    var classes = elem.get('className').split(/ /);
    for (var c =0; c < classes.length; c++) {
        var cls = classes[c];
        var validator = form.valsByClass[cls];
        if (valid && validator) {
            valid = validator(elem, showError); // Second param only used by valMatch
            if (! valid) { failClass = cls; }
        }
    }

    // Show/clear error message if requested.
    if (showError) {
        if (valid) {
            form.clearError(elem);
        } else {
            form.showError(elem, failClass);
        }
    }

    return valid;
};

// Given an array of field IDs, validate them all.
form.validateArray = function(field_ids, showError) {
    var allvalid = true;
    for (var f = 0; f < field_ids.length; f++) {
        //alert('About to validate ' + field);
        allvalid = allvalid && form.validateControl(field_ids[f], showError);
    }
    return allvalid;
};

// Cry foul.
form.showError = function(ctlElem, valClass) {

    var errElem = form.getErrorElemForField(ctlElem.get('id'));
    //alert('In showError with error tag id ' + errElem.get('id'));
    var msg = form.errorsByClass[valClass];
    if (errElem) {
        if (msg) {
            errElem.innerHTML = msg;
        }
        yui.Dom.setStyle(errElem, 'visibility', 'visible');
    } else {
        alert(msg);
    }
};
// Un-cry foul.
form.clearError = function(ctlElem) {
    //alert('In clearError');
    var errElem = form.getErrorElemForField(ctlElem.get('id'));
    if (errElem) {
        yui.Dom.setStyle(errElem, 'visibility', 'hidden');
    }
};

/*******************************************
     Handy-Dandy Global Button Disablement
********************************************/
form.field2buttons = new Object; // Map field IDs to arrays of button IDs they should affect.
form.button2fields = new Object; // Map button IDs to arrays of field IDs that affect them.
form.button2callbacks = new Object;  // Callbacks whenever the button is revalidated


// To set up automatic disabling of submit buttons based on validation,
// call this function after onDOMReady
form.disableToo = new Object();
form.monitorButton = function(buttonID, fieldIDs, immediateValidation, disableToo) {

    if (immediateValidation == undefined) { immediateValidation = true; }

    form.disableToo[buttonID] = disableToo;

    // Add the info to the lists.
    form.button2fields[buttonID] = [];
    for (var f = 0; f < fieldIDs.length; f++) {
        //if (fieldIDs[f] == 'birthdate') { form.debug(6, 'birthdate is associated with submit button '); } // DEBUG
        //if (fieldIDs[f] == 'visualcode') { form.debug(7, 'visualcode is associated with submit button '); } // DEBUG
        var fieldID = fieldIDs[f];
        if (!yui.Dom.get(fieldID)) { continue; }
        var list = form.field2buttons[fieldID];
        if (list) {
            list[list.length] = buttonID;
        } else {
            list = [ buttonID ];
        }
        form.field2buttons[fieldID] = list;
        form.button2fields[buttonID].push(fieldID);
    }

    // If the button is a Submit button, trigger its validation on form submit
    var btn = yui.Dom.get(buttonID);

    form.debug(12, 'have button ID ' + buttonID + ' and have button object ' + btn); //DEBUG
    try {
        if ((btn.nodeName == 'INPUT' || btn.nodeName == 'BUTTON')&& btn.type == 'submit') {
            form.debug(8, 'attaching form validation to button ID ' + buttonID); // DEBUG
            yui.Event.addListener(btn.form, 'submit', form.validateByButtonOnSubmit, btn);
        } else {
            yui.Event.addListener(btn, 'click', form.validateByButtonOnSubmit, btn);
        }
    } catch (e) {
        form.debug(13, 'monitorButton exception:  ' + e);
    }

    // Go ahead and validate now
    if (immediateValidation) {
        form.validateByButton(buttonID, false);
    }
};

form.addButtonMonitorCallback = function(buttonId, callback) {
    form.button2callbacks[buttonId] = form.button2callbacks[buttonId] || new Array();
    form.button2callbacks[buttonId].push(callback);
}

form.validateByButtonOnSubmit = function(evt, btn) {
    form.debug(9, 'ByButtonOnSubmit fired with button id ' + btn.id); // DEBUG

    if (form.skipSubmitButtonValidation(btn.id)) { return true; }

    var valid = form.validateByButton(btn.id, true);
    if (!valid) {
        yui.Event.preventDefault(evt);
        return false;
    }
    return true;
}

// Override this when combining forms
form.skipSubmitButtonValidation = function(buttonID) {
    return false;
}

form.validateByButton = function(buttonID, showError) {
    if (showError == undefined) { showError = true; }

    var offender = form.whichFieldIsInvalidForButton(buttonID, showError);
    form.notifyButtonMonitorCallbacks(buttonID, offender);

    if (!offender) {
        form.hideSecondaryError(buttonID);
        if (form.disableToo[buttonID]) { yui.Dom.get(buttonID).disabled = false; }
        return true;
    }
    if (form.disableToo[buttonID]) {  yui.Dom.get(buttonID).disabled = true; }
    if (!showError) { return false; }

    form.showSecondaryError(offender, buttonID);

}

form.notifyButtonMonitorCallbacks = function(buttonID, offender) {
    var cbs = form.button2callbacks[buttonID];
    if (!cbs) { return; }
    for (var i=0; i < cbs.length; i++) {
        cbs[i].call(null, buttonID, offender);
    }
};


form.updateButtonEnable = function(buttonID) {
    var offender = form.whichFieldIsInvalidForButton(buttonID, false);
    if (!offender) {
        form.hideSecondaryError(buttonID);
        if (form.disableToo[buttonID]) { yui.Dom.get(buttonID).disabled = false; }
    } else {
        form.showSecondaryError(offender, buttonID);
        if (form.disableToo[buttonID]) {  yui.Dom.get(buttonID).disabled = true; }
    }
};

form.updateButtonsForField = function(fieldID) {
    var buttonIDs = form.field2buttons[fieldID];
    if (buttonIDs) {
        for (var b = 0; b < buttonIDs.length; b++) {
            form.updateButtonEnable(buttonIDs[b]);
        }
    }
};

form.whichFieldIsInvalidForButton = function(buttonID, showError) {
    var fieldIDs = form.button2fields[buttonID];
    if (!fieldIDs) { return null; }

    for (var f=0; f < fieldIDs.length; f++) {
        if (!form.validateControl(fieldIDs[f], showError, false)) {
            return fieldIDs[f];
        }
    }
    return null;
}

form.showSecondaryError = function(field, buttonID) {

    // First use the field ID to find the associated label
    var label = form.getLabelForField(field);
    if (!label) { return; }

    var error = form.getErrorForField(field);
    if ((!error) || (error == '&nbsp;')) { return; }

    var msg = 'The "' + label + '" field has the following error: ' + error;

    var err_elem = yui.Dom.get(buttonID + '_error');
    if (!err_elem) {
        //alert(msg);
    } else {
        err_elem.innerHTML = msg;
        yui.Dom.setStyle(err_elem,'visibility','visible');
    }
}

form.getErrorElemForField = function(field) {
    var field_elem = yui.Dom.get(field);
    if (field_elem.type == 'radio') {
        field = field_elem.name;
    }
    var errorElem = yui.Dom.get(field + '_error');
    return errorElem;
}

form.getErrorForField = function(field) {
    var error = form.getErrorElemForField(field);
    if (!error) { return null; }
    error = error.innerHTML;

    error = error.replace(/^\s+/g, '');  // Remove leading space
    error = error.replace(/\s+$/g, '');  // Remove trailing space
    error = error == '&nbsp;' ? '' : error; // Ignore &nbsp;

    return error;
}

form.getLabelForField = function(field) {
    var field_elem = yui.Dom.get(field);
    if (field_elem.type == 'radio') {
        field = field_elem.name;
    }
    var label = yui.Dom.get(field + '_label');
    if (!label) { return null; }
    label = label.innerHTML;

    label = label.replace(/\*/g, '');  // Remove '*'
    label = label.replace(/\:/g, '');  // Remove ':'
    label = label.replace(/^\s+/g, '');  // Remove leading space
    label = label.replace(/\s+$/g, '');  // Remove trailing space

    return label;
}

form.hideSecondaryError = function(buttonID) {
    yui.Dom.setStyle(buttonID + '_error','visibility','hidden');
}

/*******************************************
         SELECT element helpers
********************************************/

// Selects the first option with the given value.
// All other options are deselected.
form.setSelect = function(select_id, value) {
    var saw_it = false;
    var sel_elem = document.getElementById(select_id);
    var opts = sel_elem.options;
    for (var o = 0; o < opts.length; o++) {
        if (!saw_it && opts[o].value == value) {
            opts[o].selected = true;
            saw_it = true;
        } else {
            opts[o].selected = false;
        }
    }

}

form.deselectAll = function(select_id) {
    var sel_elem = document.getElementById(select_id);
    var opts = sel_elem.options;
    for (var o = 0; o < opts.length; o++) {
        opts[o].selected = false;
    }
}


