/* 
Class: Jel.FormValidator
    Provides unobtrusive client-side validation behaviour to an HTML form, via recognition of special CSS class strings (validation classes) attached to each field. Includes customisation options for how to display the validation errors (results), how to change the display of "culprit" fields and their labels (through css classes)
    and also the option to display these errors adjacent to each field in "inline" containers (on field blur). Error messages are automatically generated through use of internationalizable error template strings, but can also be entirely customized if the automatic messages are insufficient.
    See below for terminology used within this documentation.
    
Terminology:
    culprit - A *culprit* field is a field that has been deemed not valid when validating the form it belongs to.
    results - The *results* of the form validation refers to the information gathered about the culprits during that validation (if any, that is, if validation fails). *Displaying the results* usually refers to displaying the error messages that describe what is wrong with each culprit field to the person filling in the form. 
    validation class - the special CSS classes that are applied to form elements which describe rules for them to be valid. Many pretermined classes are available to use, or you can define your own custom validation classes (functions) and associated *error templates* (see below).
    error template - a string template resource linked to each validation class that defines the error message to show when that validation rule fails. For custom validation classes, an equivalent *error template* needs to be defined also.
    error message - the message for each culprit field describing why validation has failed. (this is the "filled-in" version of the *error template* for the *validation class* rule that has not been satisifed). 
    inline validation - if activated, individual fields will be validated on blur. If this validation fails, an *inline error message* (a variation of the standard *error message*, without the field label) will be placed inside the associated *inline* (see below)
    inline - an optional container element defined for each field (typically adjacent to it in the DOM) that will be filled with any *inline error messages* generated during *inline validation*.

Validation Classes:
    The validation classes recognised by Jel.FormValidator are specified in the class attribute of each field in the form being validated.  
    The range of supported classes is quite extensive and automate validation of many conditions that you previously would have written custom code for. 
    Not only that, but all of the validation classes are able to build readable error messages that describe what went wrong to the user. They can be be placed into 4 major categories:

    required - the special case of a field simply being required
    length based - checks the character length of a field against a specified value, or lower and upper values
    value based - checks the value of a field against a specified comparison value, or upper and lower value, or even another field value
    pattern based - special cases such as email addresses 

    It's also important to note that most of these validation classes are derived from the function names in the <Jel.Validator> class, and they call out to those functions to check fields. 
    
The "required" validation class:
    To mark a form field as being required (that is, a value must be provided to be able to submit the form), simply add the class "required" to the class attribute of the field. 

Length-based validation classes:
    Length based classes check that a field in the form matches a certain character length condition. These follow the format length-OPERATOR-COMPARE where:
        
        OPERATOR - is the comparison operator being performed (see below) 
        COMPARE - the comparison length value, or lower and upper value if OPERATOR is "range" 
    
    Values for OPERATOR:    
        eq - equality comparison (==)
        neq - inequality comparison (!=)
        lt - less than (<)
        gt - greater than (>)
        le - less than or equal to (<=)
        ge - greater than or equal to (>=)
        range - between lower and upper bound (in a...b)

    Values for COMPARE:
        a - where a is an integer, if OPERATOR is not "range"
        a:b - where a and b are integers representing lower and upper bounds if OPERATOR is range, and you want the bounds to be non-inclusive
        a::b - as above if OPERATOR is range, and you want the bounds to be inclusive
    
    Examples of length-based validation classes:
        > length-gt-5 - field length must be greater than (gt) 5 characters
        > length-eq-5 - field length must be equal to (eq) 5 characters
        > length-range-5:10 - field length must be between 5 and 10 characters, non-inclusive (:)
        > length-range-5::10 - field length must be between 5 and 10 characters, inclusive (::)
    
Value-based classes:
    Value-based classes check a field's data type and value compared to another value, or lower and upper values if the operator is a range. 
    These values can either be constant, or can specified as the values of other fields in the form. 
    These follow the format [TYPE-]OPERATOR-COMPARE-FORMAT where:
        
        TYPE - is the data type that is expected in this field. 
        OPERATOR - is the comparison operator being performed.
        COMPARE - is the value, upper and lower value, or even another field to be compared
        FORMAT - is the date format when TYPE is "date" or "time"
    
    Refer to the reference below for possible values of each of these attributes.
    
    Values for TYPE:
        empty (not specified) - for strings, that is, no type check really occurs at all 
        int - for integers
        float - for floating point numbers, that is, they have decimals
        numeric - for int or float
        date - for date strings. When specifying a date, the FORMAT must also be specified
        time - synonym for "date". This is more readable when you're using a time format
    
    Values for OPERATOR:
        The same as for length-based classes, with the extra exceptions:
        
        (int|float|numeric)-positive - a positive number 
        (int|float|numeric)-negative - a negative number 
        date-future-FORMAT - a date string in the future  in FORMAT
        date-past-FORMAT - a date in the past in FORMAT
        time-earlier-FORMAT - a time earlier than the current time
        time-later-FORMAT- a time later than the current time

    Values for COMPARE (comparing constants):
        The syntax for COMPARE follows the same rules as for length-based comparisons, but for different types, that is:
        
        a - where a is an appropriate type (see below), for a single value comparison
        a:b - where a, b are an appropriate type (see below), for "range" lower and upper bounds (non-inclusive)
        a::b - where a, b are an appropriate type (see below), for "range" lower and upper bounds (inclusive)
        
        Types are represented like so:
        
        TYPE is empty (string) - whatever string you want to compare
        TYPE is int - an integer
        TYPE is float - an integer, followed by lowercase "p" (for point), then another integer for decimal part. E.g. 3p14159
        TYPE is date - a representation of a date in the format yyyymmdd (e.g. 20070403) for just a date, and yyyymmddThhmmss for date AND time. (e.g. 20070403T133000)
        TYPE is time - a representation of a time in the format hhmmss (e.g. 133000)
        
    Values for COMPARE (when comparing other fields):
        If you want to compare the value of the field with another one in the same form, simply substitute the constant in the class string with the ID of the field(s) to compare, followed by the suffix in the "suffixCompareField" in Options hash of the <constructor> (by default this is "-field").
        
    Values for FORMAT (only appropriate for "date" and "time" types):
        a dash-delimited version of a property identifier for the formatting constants in <Jel.Date.FORMAT>. E.g. date-future-uk (Jel.Date.FORMAT.UK), date-future-uk-12 (Jel.Date.FORMAT.UK_12)

    Examples of value-based validation classes:
        
        > int-ge-4                             - integer greater than or equal to 4
        > int-range-4:8                         - integer between 4 and 8, non-inclusive
        > float-range-4p5:20p1                  - floating point, between 4.5 and 20.1
        > neq-password                          - not equal to "password" case-sensitive
        > eq-password-field                     - equal to the value of the field with ID "password" (useful for confirm password) 
        > gt-ci-a                               - greater than "a", case-insensitive
        > lt-b                                  - less than "b", case-sensitive
        > date-future-uk                        - UK formatted date (dd/mm/yyyy) in the future
        > date-ge-20000101-us                   - US formatted date (mm/dd/yyyy) sometime after the turn of the millenium (1st Jan 2000)
        > date-range-uk-20050201::20070301      - UK formatted date between 1st Feb 2005 and 1st Mar 2007, inclusive

    Hopefully it's clear from the rules above that they are a lot easier to construct than they are to explain!
    
*/

/* 
Property: form
    The HTML form associated with this validator.

Property: fieldLabels
    A hash of field labels (the text, not the label elements themselves) indexed by the ID of the related control. 
*/


Jel.FormValidator = Base.extend
({
    /*
    Method: constructor
        Class constructor.
    
    Parameters:
        form - HTML Form Element, the form to apply form validation behaviour to
        options - hash, a hash of options to configure the validator (see below)
        
    Options available (specified in *options* hash): 
        callbacks - hash, a collection of optional callbacks to customise the behaviour of the form validator (see below)
        validators            - Hash, a collection of custom validators to use for this form, indexed by the camelized class names that they match (for example, a "user-name" class would be indexed "userName" in this hash)
        errorTemplates        - Hash, a collection of errorTemplates to use for any custom validators in validators (each validator requires an error template), indexed by the uppercase-underscore-delimited version of the class name (for example, a "user-name" class would be indexed "USER_NAME" in this hash)   
        culpritFieldClassName - String (default "culprit"), the class name to apply to fields that are responsible for the form not validating (termed 'culprits') 
        culpritLabelClassName - String (default "culprit"), the class name to apply to the labels of culprit fields
        selectEmptyValue      - String (default ""), the value of a select field that is regarded as empty (that is, not yet chosen)
        validateInline        - Boolean (default true), indicates whether single field validation should occur on each field's onblur event, and be displayed in associated "inline" elements
        suffixCompareField    - String (default "-field"), the suffix used to describe field comparisons in validation classes
        suffixInline          - String (default "-inline"), the suffix added to a field ID to determine the ID of an "inline" container, used to display inline error messages for that field 
        resultsContainer      - HTML Element, if specified, the form validator will build an XHTML error message inside this container, which consists of an intro paragraph, followed by an unordered list of all error messages)
        resultsAlert          - Boolean, if true the form validator will display a nicely formatted JavaScript alert box showing your errors. This will also occur by default automatically if no other display has been specified, that is if both options.callbacks.onShowResults and options.resultsContainer are not specified. 
        
    Callbacks available (specified in *options.callbacks* hash):
       formatFieldLabel(label, field) - If specified, the string this function returns will be used as the field label when it is shown in error messages
       formatDateFormat(format) - If specified, the string this function returns will be used as a date format when they are shown in error messages
       onShowResults(results, validator) - If specified, will be called when displaying the results of a validation (only occurs is there are problems). Refer to <showResultsAlert> for information on the results parameter. 
       onValidate(validator) - If specified, this will be called after regular field validation has occurred, to allow you to perform any custom validation (return false if not the form is still not valid, true otherwise) 
       onSubmit(validator) - If specified, this function will be called once all validation is successful, but before the form is submitted. Return false to prevent form submission, and true to let it through. Preventing form submission (returning false) could be an approach for AJAX applications that hijack a standard form submit. 
       
    Example:
        > var validator = new Jel.FormValidator
        > (
        >     $('contact-form'),
        >     {
        >          callbacks:
        >          {
		>   			formatFieldLabel: function(label) 
		>                                 { 
		>                                     return '<em>' + label.replace(/[:\*]/g, '') + '</em>'; 
		>                                 }, 
        >               
		>   			onSubmit: function(validator) 
		>                         { 
		>                             this.sendAjaxRequest(Form.serialize(validator.form)); 
		>                             return false; 
		>                         }		
        >          }
        >     }    
        > );
        
    
    */
    
    constructor: function(form, options)
    {
        
        this.form = form;

        this.options = Object.extend
        (
           {
               culpritFieldClassName:   'culprit',
               culpritLabelClassName:   'culprit',
               selectEmptyValue:        '',
               suffixCompareField:      '-field',
               suffixInline:     '-inline',
               validateInline:          true
           },
           options || {}
        );
        
        this.validators = Object.extend
        (
            Jel.Validator,
            this.options.validators || {}
        );

        //console.log(this.validators);
        
        this.errorTemplates = Object.extend
        (
            Jel.Lang.FormValidator.ERRORS,
            this.options.errorTemplates || {}
        );
        
        this.callbacks = Object.extend
        (
            {
                formatFieldLabel: this.formatFieldLabel,
                formatDateFormat: this.formatDateFormat
            },
            this.options.callbacks || {}
        );
        
        
        this.observers = 
        {
            _formOnSubmit:               this._formOnSubmit.bindAsEventListener(this),
            _fieldOnBlur:                this._fieldOnBlur.bindAsEventListener(this)
        };
        
        // build a regular expression prefix list of supported validators
        
        var keys = $H(this.validators).inject
        (
            [],
            function(array, validator)
            {
                array.push(Jel.String.decamelize(this._replaceReserved(validator.key)));
                return array;
            }
            .bind(this)
        );
        
        // sort the keys in reverse order, so that the most specific validators come first in the pattern. this is vital
        keys.sort(function(a, b) { return a < b ? 1 : -1; });
        
        this.validatorPattern = "(?:^|\\b)(" + keys.join("|") + ")(?:\-([a-zA-Z0-9\:\-]*))?";
        
        // build a regular expression date format pattern
        
        keys = $H(Jel.Date.FORMAT).inject
        (
            [],
            function(array, format)
            {
                array.push(format.key.replace(new RegExp("_", "gi"), "-").toLowerCase());
                return array;
            }
            .bind(this)
        );
        
        // sort the keys in reverse order, so that the most specific validators come first in the pattern. this is vital
        keys.sort(function(a, b) { return a < b ? 1 : -1; });

        this.dateFormatPattern = "(" + keys.join("|") + ")";
        
        Event.observe(this.form, "submit", this.observers._formOnSubmit, false);
        
        this._init();

    },

    _replaceReserved: function(validatorName)
    {
        switch (validatorName)
        {
            case "dateType":
            case "numericType":
            case "intType":
            case "floatType":
            {
                return validatorName.replace("Type", "");
            }
            default:
            {
                return validatorName;
            }
        }
    },

    _getReserved: function(validatorName)
    {
        switch (validatorName)
        {
            case "date":
            case "numeric":
            case "int":
            case "float":
            {
                return validatorName + "Type";
            }
            default:
            {
                return validatorName;
            }
        }
    },
    
    /*
    Method: registerErrorMessage
        *Registers* a custom *error message* and custom *inline error message* for a *given fieldId, and className* (validation class). Note
        that this is *not* an error template, rather, it is the *final* error message that will be displayed.
    
    Parameters:
        fieldId - String, the ID of the field
        className - String, the validation class to attach error messages to
        errorMessage - String, the error message to display when validation on the field fails for the validation rule described by className.
        errorMessage - String, the inline error message to display when validation fails.
    
    Example:
        > validator.registerErrorMessage(
        >                                   "username", 
        >                                   "length-ge-6", 
        >                                   "Username must be at least 6 characters long", 
        >                                   "must be at least 6 characters long"
        >                               ); 

    */
    
    registerErrorMessage: function(fieldId, className, errorMessage, errorInlineMessage)
    {
        var nClassName = Jel.String.normalize(className);
        
        if (!this.errorMessages[fieldId])
            this.errorMessages[fieldId] = {};

        if (!this.errorInlineMessages[fieldId])
            this.errorInlineMessages[fieldId] = {};
            
        this.errorMessages[fieldId][nClassName] = errorMessage;
        this.errorInlineMessages[fieldId][nClassName] = errorInlineMessage ? errorInlineMessage : errorMessage; 
    },
    
    /*
    Method: disableFields
        *Prevents the specified fields from being regarded in form validation*. This would be essential if you have elements that are hidden or disabled based on the values
        of other elements, since you wouldn't want to display error messages for fields the user can't see or edit.

    Parameters:
        fieldId (variable) - String(s), A variable amount of element IDs for fields you want disregarded in validation, or the element "name" attribute for radio buttons and checkboxes
        
    Example:
        > validator.disableFields("billing-address", "billing-state", "billing-post-code", "billing-city", "billing-country"); 
        > // suppose you had a checkbox labelled "use shipping address" which disabled billing address fields
        > // which you wanted to disregard in validation 
        
    See also: <enableFields>
    */
    
    disableFields: function()
    {
        this._setDisabledFields(arguments, true);
    },

    /*
    Method: enableFields
        *Causes the specified fields to be regarded in form validation once again*. This would be generally called for fields tat had been previously disabled by <disableFields>.
        *IMPORTANT*: Don't call this method if you are adding new fields to the form and want them regarded in validation. Please use <addFields> instead for this purpose.

    Parameters:
        fieldId (variable) - String(s), A variable amount of element IDs for fields you want disregarded in validation, or the element "name" attribute for radio buttons and checkboxes
        
    Example:
        > validator.disableFields("billing-address", "billing-state", "billing-post-code", "billing-city", "billing-country"); 
        > // suppose you had a checkbox labelled "use shipping address" which disabled billing address fields
        > // which you wanted to disregard in validation 
        
    See also: <disableFields>
    */
    
    enableFields: function()
    {
        this._setDisabledFields(arguments, false);
    },
    
    /*
    Method: disableFieldByName
        *Prevents the fields with the specified name from being regarded in form validation*. This should only be used for radio buttons and checkboxes. 

    Parameters:
        name - String, the "name" attribute for radio buttons and checkboxes you want disabled (that is, to be ignored in validation).
        
    Example:
        > validator.disableFieldByName("interests"); 
        
    See also: <disableFields>, <enableFieldByName>
    */
    
    disableFieldByName: function(name)
    {
        this._setDisabledFieldByName(name, true);
    },

    /*
    Method: enableFieldByName
        *Causes fields with the specified name to be regarded in form validation once again*. This should only be used for radio buttons and checkboxes. 

    Parameters:
        name - String, the "name" attribute for radio buttons and checkboxes you want enabled.
        
    Example:
        > validator.disableFieldByName("interests"); 
        
    See also: <disableFieldByName>, <enableFields>
    */
    
    enableFieldByName: function(name)
    {
        this._setDisabledFieldByName(name, false);
    },
    
    /*
    Method: addFields
        *Registers the specified fields to be validated* by the form validator. This call is ESSENTIAL for fields that are added as the user interacts with the page, and it
        should be called *after* fields have been added inside the <form> tag for this validator. See below for why this is not handled automatically.

    Parameters:
        element (variable) - Element(s), a variable number of fields to add to the validator
        
    Reason for this method:
        For performance reasons, the form validator analyses the fields inside its associated form only when the class is constructed, where it builds quickly accessible information it can
        use for subsequent validations. It does not check for new fields in the form each time the form is validated, so you need to call this method if any new fields are added to the form.
        
    Example:
        > new Insertion.Top($("extra-credentials"), "<label for="alternate-username">Alternate Username:</label>" + 
        > "<input id="alternate-username" anme="alternate-username" class="length-ge-7" />" + 
        > "<div id="alternate-username-inline" class="inline"></div>");
        >
        > validator.addFields($("alternate-username")); 
        
    See also: <removeFields>
    */
    
    addFields: function()
    {
        this.elements.update();
        $A(arguments).each(this._setupField.bind(this));
    },

    /*
    Method: removeFields
        *Removes the specified fields* from the form validator. This call is ESSENTIAL for fields that are removed as the user interacts with the page, and it
        should be called *before* fields have been removed from the form. See <addFields> for a discussion of why this is not handled automatically.

    Parameters:
        element (variable) - Element(s), a variable number of fields to remove from the validator
 
    Example:
        > validator.removeFields($('alternate-username'));
        > $("extra-credentials").innerHTML = ""; // assuming "extra-credentials" had "alternate-username" control inside it.

    */
        
    removeFields: function()
    {
        this.elements.update();
        $A(arguments).each(this._dropField.bind(this));
    },
    
    /*
    Method: displayCulprit
        *displays* a *given field* as a *validation culprit*. While this method is best utilised internally by this class, it can be overridden to provide more sophisticated behaviour if so desired.
        
    Parameters:
        field - Element, the field to display as a culprit
        isInline - when called by this class, indicates whether this culprit display is occurring for inline validation (on field blur)
    
    Example:
        > validator.displayCulprit($('username'), true); 

    See also: <releaseCulprit>, <displayInline>
    */
    
    displayCulprit: function(field, isInline)
    {
        // displays a field that is in error
        Element.addClassName(field, this.options.culpritFieldClassName);
        Element.addClassName(this.labels[field.id], this.options.culpritLabelClassName);
    },

    /*
    Method: releaseCulprit
        *displays* a *given field* as being *valid*, that is, *NOT a validation culprit*. While this method is best utilised internally by this class, it can be overridden to provide more sophisticated behaviour if so desired.
        
    Parameters:
        field - Element, the field to display as a non-culprit
        isInline - when called by this class, indicates whether this non-culprit display is occurring for inline validation (on field blur)
    
    Example:
        > validator.releaseCulprit($('username'), true); 

    See also: <displayCulprit>, <releaseInline>
    */
    
    releaseCulprit: function(field)
    {
        // removes culprit status from a field (that is, it is now valid)
        Element.removeClassName(field, this.options.culpritFieldClassName);
        Element.removeClassName(this.labels[field.id], this.options.culpritLabelClassName);
    },

    /*
    Method: displayInline
        *displays* an *inline* element for a *validation culprit*. While this method is usually best utilised internally by this class, it can also be overridden to provide more sophisticated behaviour if so desired.
        
    Parameters:
        inline - Element, the inline element to display, for a culprit field
        message - String, the inline error message being displayed
         
    Example:
        > validator.displayInline($('username-inline'), "username already in use"); 

    See also: <displayCulprit>, <releaseInline>
    */

    displayInline: function(inline, message)
    {
        Element.show(inline);
        Element.update(inline, message);
    },

    /*
    Method: releaseInline
        *hides* an *inline* element for a field that is likely no longer a *validation culprit*. While this method is usually best utilised internally by this class, it can also be overridden to provide more sophisticated behaviour if so desired.
        
    Parameters:
        inline - Element, the inline element to hide, for a non-culprit field
         
    Example:
        > validator.releaseInline($('username-inline')); 

    See also: <displayInline>, <releaseCulprit>
    */
    
    releaseInline: function(inline)
    {
        Element.hide(inline);
        inline.innerHTML = "";
    },

    /*
    Method: classedCulprit
        *checks* if a given field is *currently classed as a culprit* (via CSS)
        
    Parameters:
        field - Element, the field to check
    */
    
    classedCulprit: function(field)
    {
        return Element.hasClassName(field, this.options.culpritFieldClassName);
    },

    /*
    Method: addCulprit
        *Marks* the given field as *a culprit field in the validation results*. This would be most useful in the onValidate callback (see options for <constructor>) to add a field as a culprit before results are displayed.
        This would generally be based on it failing some complex condition that can't be described simply by validation classes on the field. 
        Note that this does not display the fields as a culprit, it just registers it in the validation results.
        
    Parameters:
        field - Element, the field to register as a culprit
        errorMessage - String, the standard error message to associate with the culprit field in the results
        errorInlineMessage - String, the inline error message to associate with the culprit field in the results

    Example:
        > if (parseInt($('percentage-male') + parseInt($('percentage-female'))) != 100)
        >     validator.addCulprit($('percentage-male'), "Percentage Male and Female must total to 100 percent", "Must total to 100 percent"); 
        
    */
    
    addCulprit: function(field, errorMessage, errorInlineMessage)
    {
        if (!this._isCulprit(field))
        {
            this.results.errors.push(errorMessage);
            this.results.errorsById[field.id] = errorMessage;
            this.results.inlines.push(errorInlineMessage);
            this.results.inlinesById[field.id] = errorInlineMessage;
        
            this.results.culprits.push(field);
            
            if (Form.Element.isInputRadio(field) || Form.Element.isInputCheckbox(field))
            {
                this.nameErrors[field.name] = true;            
            }
        }
    },

    /*
    Method: submit
        *Attempts to submit the form associated with this validator*, by running all validation, and submitting if successful. Note that you
        *would only need to use this if you have not provided a proper submit* button, or input type="image" button, which is not recommended, since it may be inaccessible.
        If you have provided a submit or image, the validation will be automatically occur during the onsubmit event for the form.  
        
    Example:
        > validator.submit();
    */
    
    submit: function()
    {
        this.releaseCulprits();

        if (!this.validate())
        {
            this.displayCulprits();
            this.showResults(this.results);
            
            return false;
        }
        else
        {
            this.hideResults(this.results);

            var doSubmit = true;
            
            if (this.callbacks.onSubmit)
            {
                doSubmit = this.callbacks.onSubmit(this);
            }
            
            if (doSubmit)
            {
                this.form.submit();
            }
        }
    },

    /* 
    Method: validate
        *Validates the form associated with this validator*, and sets up validation results. Returns a boolean indicating if the validation as successful or not. 
        Generally, you won't need to call this method directly. 

    */

    validate: function()
    {
        // validates the entire form
        
        if (this._valid)
            return this._valid;
        
        this._prepareResults();
        
        // here, we need to cache the result of ths validation
        this._valid = true;
          
        $H(this.fields).each
        (
            function(pair)
            {
                this.checkField(pair.value);
            }
            .bind(this)
        );
        
        // run a custom onValidate callback, if provided
        
        if (this.callbacks.onValidate)
        {
            this._valid = this.callbacks.onValidate(this) && this._valid;
        }
        
        this.justValidated = true;
        
        return this._valid;
    },
    
    /* 
    Method: displayCulprits
        *Displays all of the current culprit fields* for the latest validation. This is *called automatically* after validation occurs if any *culprits are present*. 
        
    */
    
    displayCulprits: function()
    {
        this._valid = null;
        
        this.results.culprits.each
        (
            function(field)
            {
                this.displayCulprit(field, false);
                
                var inline = this.getInline(field);

                if (inline)
                    this.displayInline(inline, this.results.inlinesById[field.id]);
            }
            .bind(this)
        );
         
        if (this.results.culprits.length > 0)
        {
            // focus the first culprit
            if (this.results.culprits[0].focus)
                this.results.culprits[0].focus();

            // select the first culprit, if applicable
            if (this.results.culprits[0].select)
                this.results.culprits[0].select();
                
            this.justValidated = false;
        }
    },

    /* 
    Method: releaseCulprits
        *Shows all of the form fields as non-culprit*. This is *called automatically before validation* to clear any previous validation state.  
    
    */

    releaseCulprits: function()
    {
        $A(this.form.elements).each
        (
            function(field)
            {
                this.releaseCulprit(field);

                var inline = this.getInline(field);

                if (inline)
                    this.releaseInline(inline);
            }
           .bind(this)
        );
    },

    /* 
    Method: showResults
        *Displays the results of the validation*, *based on options* specified in the <constructor> method. This is *called automatically on form submit*.

    Parameters:
        results - Hash, a hash containing a number of properties that describe the results of validation. See below for more details.
        
    Results Hash:
        culprits - Array, an array of the culprit fields, that is those that have caused the validation to fail
        errors - Array, an array of error messages. This is useful if you want to "split" the error messages without needing to know what fields they belong to
        errorsById - Hash, an object hash of error messages, indexed by their corresponding culprit field ID.
        inlines - Array, an array of the inline error messages. The inline messages are generally the same as the standard error messages, but without reference to the field itself, since they are intended to be displayed adjacent to the field.
        inlinesById - Hash, an object hash of the inline error messages, indexed by their corresponding culprit field ID.
        
    See also: <hideResults>, <showResultsAlert>, <showResultsList>

    */

    showResults: function(results)
    {
        if (this.options.resultsContainer)
        {
            this.showResultsList(results, this.options.resultsContainer);
        }
        
        if ( (!this.options.resultsContainer && !this.callbacks.onShowResults) || this.options.resultsAlert)
        {
            // either no error handling has been provided (so default to showing alert), or the options have explicitly requested an alert
            this.showResultsAlert(results);
        }
        
        if (this.callbacks.onShowResults)
        {
            this.callbacks.onShowResults(results, this);
        }
    },

    /* 
    Method: hideResults
        *Hides the results of the validation*, and also any inlines associated with previous validations (if visible). This is *called automatically just before form submit* if the form is valid.

    Parameters:
        results - Hash, a hash containing a number of properties that describe the results of validation. See the reference under <showResults> for details.
    
    See also: <showResults>, <hideResultsList>
    */
    
    hideResults: function()
    {
        // hide the error container if it's being used
        
        if (this.results)
        {        
            if (this.options.resultsContainer)
            {
                this.hideResultsList(this.results, this.options.resultsContainer);
            }
        
            // hide any inlines that may be showing
        
            if (this.options.suffixInline)
            {
                this.results.culprits.each
                (
                    function(field)
                    {
                        var inline = this.getInline(field);

                        if (inline)
                            this.releaseInline(inline);
                    }
                    .bind(this)
                );
            }
        }
    },

    /*
    Method: showResultsAlert
        The method called to *display the validation results as a JavaScript alert*, if the options specify this (see <constructor> for more). This could be overridden to provide more specific functionality. 

    Parameters:
        results - Hash, a hash containing a number of properties that describe the results of validation. See the reference under <showResults> for details.
    
    See also: <showResults>, <showResultsList>
    */
    
    showResultsAlert: function(results)
    {
        // the default way to display errors is a simple alert box
        var errorLines = [];
        
        $A(results.errors).each
        (
            function(error)
            {
                errorLines.push(Jel.String.wrapToLines(error, Jel.FormValidator.ALERT_WRAP_LENGTH).join("\n    "));
            }
        );
    
        var errorMessage = Jel.Lang.FormValidator.ERRORS_TITLE + "\n\n - " + errorLines.join("\n\n - ");
        alert(errorMessage);
    },

    /*
    Method: showResultsList
        The method called to *display the validation results inside a DOM container* (controlled via the *resultsContainer option* in <constructor>). The default behaviour is to display an intro paragraph, which is in the language string Jel.Lang.FormValidator.ERRORS_TITLE, followed by an unordered list.
        This could be overridden to provide more specific functionality. 

    Parameters:
        results - Hash, a hash containing a number of properties that describe the results of validation. See the reference under <showResults> for details.
        container - Element, the container to show results in 
    
    See also: <showResults>, <showResultsAlert>, <hideResultsList>
    */

    showResultsList: function(results, container)
    {
        container.style.display = 'block';
        Element.update(container, '<p>' + Jel.Lang.FormValidator.ERRORS_TITLE + '</p>' + '<ul><li>' + results.errors.join('</li><li>') + '</li></ul>');
    },

    /*
    Method: hideResultsList
        The method called to *hide the DOM container that validation results are displayed in* (controlled via the *resultsContainer option* in <constructor>). 

    Parameters:
        results - Hash, a hash containing a number of properties that describe the results of validation. See the reference under <showResults> for details.
        container - Element, the container to hide 
        
    See also: <showResults>, <showResultsAlert>, <showResultsList>
    */

    hideResultsList: function(results, container)
    {
        container.style.display = 'none';
        container.innerHTML = '';
    },

    /*
    Method: formatFieldLabel
        The default method for formatting a field label when displaying them in validation error messages or inline error messages. This can be overridden by specifying the
        formatFieldLabel callback in <constructor> options, or you may wish to override this default method for a custom class derived from FormValidator.  
    
    Parameters:
        text - String, the text for the field label (usually the innerHTML of its associated <label> element)
        field - Element, the field being displayed
    */
    
    formatFieldLabel: function(text, field)
    {
        return Jel.String.trim(text.replace(/[:\*]/gi, ''));
    },

    /*
    Method: formatDateFormat
        The default method for formatting date format strings when displaying them in validation error messages or inline error messages. This can be overridden by specifying the
        formatDateFormat callback in <constructor> options, or you may wish to override this default method for a custom class derived from FormValidator.  
    
    Parameters:
        format - String, the date format string. These are usually one of the properties in the <Jel.Date.HUMAN_FORMAT> hash, that is, a date format string as understood by humans.
    */
    
    formatDateFormat: function(format)
    {
        return format;
    },
    
    /*
    Method: validateField
        *Validates a single given field* in the form associated with this validator, and sets up the results for just this field. This is called by the on blur event for the field if inline validation is setup
        
    Parameters:
        field - Element, the field to validate
    
    Returns:
        true - if the field is valid
        false - otherwise
    
    Example:
        > validator.validateField($('username'));
    */
    
    validateField: function(field)
    {
        this._prepareResults();
        return this.checkField(field);
    },

    
    /*
    Method: checkField
        *Checks if a given field* is valid within the form associated with this validator. This is called for each field in the form when attempting to submit.
        
    Parameters:
        field - Element, the field to validate
        compareOnly - Boolean, whether the check is simply for comparison which doesn't generate errors, and is mainly used for comparing two fields with each other.
    
    Returns:
        true - if the field is valid
        false - otherwise
    
    Example:
        > validator.checkField($('username'));
    */

    checkField: function(field, compareOnly)
    {
        var valid = true;

        if (!this.disabled[field.id] || compareOnly)
        {
            
            if (!Element.hasClassName(field, "required") && this.isFieldEmpty(field))
            {
                // first, if the field is not required, and empty, then validation always passes
                return true;
            }
            else if (Element.hasClassName(field, "required") && this.isFieldEmpty(field))
            {
                // check if this field has been provided first, since other validators depend on this being checked first
            
                var validatorName = "required";
            
                if (Form.Element.isInputCheckbox(field))
                    validatorName = "required_checkbox";
                else if (Form.Element.isInputRadio(field))
                    validatorName = "required_radio";
                else if (Form.Element.isSelect(field))
                    validatorName = "required_select";
                
                this._prepareErrorMessage(field.id, this.fieldLabels[field.id], "required", validatorName, {});
            
                var errorMessage = this.errorMessages[field.id]["required"];
                var errorInlineMessage = this.errorInlineMessages[field.id]["required"];

                if (!compareOnly)
                {
                    this.addCulprit(field, errorMessage, errorInlineMessage);
                    this._setValid(false);
                }
                    
                return false; // don't perform any more validation
            }
        
        
            field.className.split(" ").each
            (
                function(className)
                {
                    var nClassName = Jel.String.normalize(className);
        
                    if (valid)
                    {
                        if (nClassName != 'required')
                        {
                            var matches = nClassName.match(new RegExp(this.validatorPattern, "i"));
                
                            if (matches)
                            {
                                var validatorName = matches[1];
                                
                                var info = this._parseValidationClass(nClassName, validatorName);
                                

                                if (info)
                                {
                                    if (this.validators[this._getReserved(validatorName.camelize())])
                                    {
                                        valid = this.validators[this._getReserved(validatorName.camelize())](field.value, info);
                                        
                                        if (!compareOnly)
                                        {
                                            // we should not validate this field if it is being compared to a field that is not yet valid itself
                                            if 
                                            ( 
                                                 (info.compareField != null && $(info.compareField) != null  && !this.checkField($(info.compareField), true)) ||
                                                 (info.lowerField != null   && $(info.lowerField) != null    && !this.checkField($(info.lowerField), true)) ||
                                                 (info.upperField != null   && $(info.upperField) != null    && !this.checkField($(info.upperField), true)) 
                                            )
                                            {
                                                this._addValue(field, valid);

                                                return;
                                            }   
                                        }
                                
                                        this._prepareErrorMessage(field.id, this.fieldLabels[field.id], nClassName, validatorName.camelize(), info);
                                
                                        var errorMessage = this.errorMessages[field.id][nClassName];
                                        var errorInlineMessage = this.errorInlineMessages[field.id][nClassName];
                                    }
                            
                                    if (!valid && !compareOnly)
                                    {
                                        this.addCulprit(field, errorMessage, errorInlineMessage);
                                    }
                            
                                    if (!compareOnly)
                                        this._setValid(valid);
                                }
                            }
                        }
                    }
                
                    this._addValue(field, valid);
                }    
                .bind(this)
            );
        
        }
        return valid;
    },

    /*
    Method: isFieldEmpty
        *Checks if a given field is "empty" within the form.* For textareas and text inputs, this is when the field value is the empty string. 
        For checkboxes and radios, a field is empty if all of the fields with the same name are unchecked. A select is empty if its value is equal to options.selectEmptyValue (see <constructor>).
        This is generally the first check done when a field has the validation class "required".
        
    Parameters:
        field - Element, the field to check for emptiness
    
    */
    
    isFieldEmpty: function(field)
    {
        if (Form.Element.isInputRadio(field) || Form.Element.isInputCheckbox(field))
        {
            // check to see if any of the other fields with this name are empty
            var checked = !$A(this.nameFields[field.name]).any( function(field) { return field.checked; } );
            
            return checked;
        }
        else if (Form.Element.isSelect(field))
        {
            return Jel.String.trim(field.value) == this.options.selectEmptyValue;
        }
        else
        {
            return Jel.String.trim(field.value) == ''; 
        }
    },

    /*
    Method: getInline
        *Gets the inline error message container* for a given field.
        
    Parameters:
        field - Element, the field to get an inline container for
    
    */

    getInline: function(field)
    {
        if (this.options.suffixInline)
        {
            var inline = $(field.id + this.options.suffixInline) || $(field.name + this.options.suffixInline);
                        
            return inline;
        } 
    },
    
    /* Remaining properties are private and should not be called */
     
    _fieldOnBlur: function(event)
    {
        if (!this.justValidated)
        {
            var field = Event.element(event);
            var inline = this.getInline(field);
            
            // inline validation should NEVER remove errors set by the non-inline validation, as this would be bad behaviour
        
            if (!this.isFieldEmpty(field))
            {
                // inline validation is more annoying than useful on empty fields, even if they are required
            
                var valid = this.validateField(field);
            
                if (!valid)
                {
                    this.inlineFields[field.id] = true;
                    this.displayCulprit(field, true);
                    
                    if (inline)
                        this.displayInline(inline, this.results.inlinesById[field.id]);
                }
                else
                {
                    this.inlineFields[field.id] = false;
                    this.releaseCulprit(field, true);
                    
                    if (inline)
                        this.releaseInline(inline);
                }
            }
            else
            {
                if (this.inlineFields[field.id])
                {
                    this.inlineFields[field.id] = false;
                    this.releaseCulprit(field, true);
                    
                    if (inline)
                        this.releaseInline(inline);
                }
            }
        }
        else
            this.justValidated = false;
    },

    _formOnSubmit: function(event)
    {
        this.releaseCulprits();

        if (!this.validate())
        {
            this.displayCulprits();
            this.showResults(this.results);
            
            Event.stop(event);
        }
        else
        {
            this.hideResults();
            
            var doSubmit = true;
        
            // check for the onSubmit callback
        
            if (this.callbacks.onSubmit)
            {
                doSubmit = this.callbacks.onSubmit(this);
            }
            
            if (!doSubmit)
            {
                // stop the event if onSubmit returned false (hijacking a form would be a valid approach for AJAX applications)
                Event.stop(event);
            }
        }
        
        this._valid = null;
    },
    
    _prepareResults: function()
    {
        this.fieldErrors = {};
        this.fieldInlineErrors = {};

        this.results =
        {
            culprits: [],

            errors: [],
            errorsById: {},

            inlines: [],
            inlinesById: {}
        };
        
        this.nameErrors = {};
    },
    
    _addValue: function(field, valid)
    {
        if (valid && valid.value)
            this.values[field.id] = valid.value;
    },
    
    _setValid: function(valid)
    {
        if (this._valid && !valid)
            this._valid = false;
    },
    
    _isCulprit: function(field)
    {
        return this.results.culprits.indexOf(field) != -1 || this.nameErrors[field.name];
    },
    
    _prepareErrorMessage: function(fieldId, fieldLabel, className, validatorName, info)
    {        
        if (this.errorMessages && this.errorMessages[fieldId] && this.errorMessages[fieldId][className])
            return this.errorMessages[fieldId][className];
        
        var errorTemplate;
        var errorInlineTemplate;
        var errorMessage;
        var errorInlineMessage;
        
        errorTemplate = this.errorTemplates[Jel.String.decamelize(validatorName, "_").toUpperCase()];

        
        if (info.formatKey)
        {
            errorTemplate = errorTemplate.replace("[format]", this.formatDateFormat(Jel.Date.HUMAN_FORMAT[this._getFormatKey(info.formatKey)]));
        }
        
        if (info.compare != null || info.compareField != null)
            errorTemplate = errorTemplate.replace("[compare]", info.compareField ? this.callbacks.formatFieldLabel(this.fieldLabels[info.compareField], info.compareField) : this._getCompareDisplay(info.compare, info.format));

        if (info.lower != null)
            errorTemplate = errorTemplate.replace("[lower]", info.lowerField ? this.callbacks.formatFieldLabel(this.fieldLabels[info.lowerField], info.lowerField) : this._getCompareDisplay(info.lower, info.format));
        
        if (info.upper != null)
            errorTemplate = errorTemplate.replace("[upper]", info.upperField ? this.callbacks.formatFieldLabel(this.fieldLabels[info.upperField], info.upperField) : this._getCompareDisplay(info.upper, info.format));
        
        errorTemplate = errorTemplate.replace("[inclusive]", info.inclusive ? " " + Jel.Lang.FormValidator.TERM_INCLUSIVE : "");
        
        errorInlineTemplate = errorTemplate;

        errorMessage = errorTemplate.replace("[field_label]", this.callbacks.formatFieldLabel(fieldLabel, this.fields[fieldId]) + " ");
        errorInlineMessage = errorInlineTemplate.replace("[field_label]", "");

        // cache the error message
        this.registerErrorMessage(fieldId, className, errorMessage, errorInlineMessage);
    },
    
    _getFormatKey: function(key)
    {
        // transforms a lowercase key into the appropriate key for the Date extensions
        return Jel.String.decamelize(key.camelize(), "_").toUpperCase();
    },
    
    _getDateFormat: function(key)
    {
        return Jel.Date.FORMAT[Jel.String.decamelize(key.camelize(), "_").toUpperCase()];        
    },
    
    _getCompareDisplay: function(value, format)
    {
        if (format && this._getDateFormat(format))
        {
            // format the value as if it is a date
            return this._getDateCompare(value, format);
        }
        
        if (!Jel.Validator.numericType(value))
            return '"' + value + '"';
            
        return value;
    },
    
    _getDateCompare: function(value, format)
    {
        // first, assume they have specified the time in UTC format with a "T" delimiter, but without special symbols (:, and -)
        var ret = Jel.Date.convert(value, "YmdTGis", format);
          
        if (ret)
        {
            return ret;
        }
        else
        {
            // assume they have specified UTC, Year Month Day only
            var ret = Jel.Date.convert(value, "Ymd", format);
            
            if (ret)
            {
                return ret;
            }
        }
        
        return value;
    },
    
    _parseValidationClass: function(className, validatorName)
    {
        // returns an info object for use with the validation routine
        
        var matches;
        var info = {};
        
        switch (validatorName)
        {
            case "range":
            case "range-ci":
            case "int-range":
            case "float-range":
            case "numeric-range":
            case "length-range":
            {
                matches = className.match(validatorName + "(?:\-([\-A-Za-z0-9T]*?)(::|:)([\-A-Za-z0-9T]*?))$");
                
                if (matches)
                {
                    this._setValidatorBounds(matches[1], matches[3], matches[2], info);
                }
                else
                {
                    return false;
                }
                
                break;
            }
            case "date":       
            case "date-eq":    
            case "date-gt":    
            case "date-lt":    
            case "date-le":    
            case "date-ge":    
            case "date-future":
            case "date-past": 
            case "time":       
            case "time-eq":    
            case "time-gt":    
            case "time-lt":    
            case "time-le":    
            case "time-ge":    
            case "time-later":
            case "time-earlier": 
            {
                matches = className.match(validatorName + "(?:\-([\-A-Za-z0-9T]*?))?-" + this.dateFormatPattern + "$");
                        
                if (matches)
                {
                    if (matches[2])
                    {
                        info.format = this._getDateFormat(matches[2]);
                        info.formatKey = matches[2];
                    }
                    else
                    {
                        throw "time format needs to be provided in field " + field.id;
                    
                    }
                    this._setValidatorCompare(matches[1], info);
                }
                else
                {
                    return false;
                }
                
                break;
            }
            case "date-range":
            case "time-range":
            {
                matches = className.match(validatorName + "(?:\-([\-A-Za-z0-9T]*?)(::|:)([\-A-Za-z0-9T]*?))-" + this.dateFormatPattern + "$");
                
                if (matches)
                {
                    if (matches[4])
                    {
                        info.format = this._getDateFormat(matches[4]);
                        info.formatKey = matches[4];
                    }
                    else
                        throw "time format needs to be provided in field " + field.id;

                    this._setValidatorBounds(matches[1], matches[3], matches[2], info);
                }
                else
                {
                    return false;
                }
                
                break;
            }
            default:
            {
                matches = className.match(validatorName + "(?:\-?([\-A-Za-z0-9T]*?))$");
                
                
                if (matches)
                {
                    this._setValidatorCompare(matches[1], info);
                }
                else
                {
                    return false;
                }
                
                break;
            }
        }

        return info;
    },

    _setDisabledFieldByName: function(name, disabled)
    {
        if (this.nameFields[name] && this.nameFields[name].length)
        {
            this.nameFields[name].each
            (
                function(field)
                {
                    this._setDisabledFields([field.id], disabled);
                }
                .bind(this)
            );
        }
    },

    _setDisabledFields: function(fieldIds, disabled)
    {
        $A(fieldIds).each
        (
            function(fieldId)
            {
                if ($(fieldId))
                    this.disabled[fieldId] = disabled;
                else
                    this._setDisabledFieldByName(fieldId, disabled); // try to disable by field name
            }
            .bind(this)
        );    
    },
    
    _setValidatorCompare: function(key, info)
    {
        if (key)
        {
            info.compare = this._getCompareValue(key, info.format);
            
            var fieldId = key.replace(this.options.suffixCompareField, "");
            
            if (this.fields[fieldId] && fieldId != key)
                info.compareField = fieldId;

            if (info.compareField)
                info.compare = this.fields[fieldId].value;
        }
    },
    
    _setValidatorBounds: function(lower, upper, mode, info)
    {
        if (lower && upper && mode)
        {
            info.lower = this._getCompareValue(lower, info.format);
            info.upper = this._getCompareValue(upper, info.format);
                
            var lowerFieldId = lower.replace(this.options.suffixCompareField, "");
            var upperFieldId = upper.replace(this.options.suffixCompareField, "");
                    
            if (this.fields[lowerFieldId] && lowerFieldId != lower)
                info.lowerField = lowerFieldId;
            
            if (this.fields[upperFieldId] && upperFieldId != upper)
                info.upperField = upperFieldId;

            if (info.lowerField)
                info.lower = this.fields[lowerFieldId].value;

            if (info.upperField)
                info.upper = this.fields[upperFieldId].value;

            info.inclusive = (mode == '::');
        }
    },
    
    _getCompareValue: function(value, format)
    {
        if (format)
        {
            // convert to the correct date compare
            return this._getDateCompare(value, format);
        }
        else
        {
            // check for floating point values
            var matches = value.match(/[0-9]+?p[0-9]+/);
        
            if (matches)
            {
                return value.replace("p", ".");
            }
        }
        
        return value;
    },
    
    _setupField: function(field)
    {
        field = $(field);
        
        if (!field)
            throw ("setup field: field does not exist");
            
        if (field.id)
            this.fields[field.id] = field;
            
        // associate default field name (only works when field id is in correct language, most times will probably be english)
        this.fieldLabels[field.id] = this._idToPhrase(field.id);
        
        // if the form control is a checkbox or radio button, try to find an element with id = "for-[name]" where [name] matches the name attribute of the control
        // if found, regard this as the label
        
        if (Form.Element.isInputRadio(field) || Form.Element.isInputCheckbox(field))
        {
            if (field.name)
            {
                if ($('for-' + field.name))
                {
                    this.labels[field.id] = $('for-' + field.name);
                    this.fieldLabels[field.id] = Jel.String.trim($('for-' + field.name).innerHTML);
                }
            
                // store named fields for use later
                if (this.nameFields[field.name])
                    this.nameFields[field.name].push(field);
                else
                    this.nameFields[field.name] = [field];
            }
        }
        
        if (this.options.validateInline)
        {
            Event.observe(field, 'blur', this.observers._fieldOnBlur);
        }
    },
    
    _dropField: function(field)
    {
        field = $(field);

        delete this.fields[field.id];
        delete this.fieldLabels[field.id];
        delete this.labels[field.id];
        delete this.nameFields[field.name];
        delete this.errorMessages[field.id];
    },
    
    _idToPhrase: function(id)
    {
        return Jel.String.titleCase(Jel.String.normalize(id).replace("-", " "));
    },

    _associateLabel: function(label)
    {
        var forId;
        var element;
        
        if (label.attributes['for'])
        	forId = label.attributes['for'].value;	
        else
        	forId = label.getAttribute('for');
        
        if (forId)
            element = $(forId);
        
        if (element)
        {
            if (!this.labels[forId])
                this.labels[forId] = label;
            
            if (!this.nameFields[element.name])
                this.fieldLabels[forId] = Jel.String.trim(label.innerHTML);
        }
    },
    
    _init: function()
    {
        this._valid = null;
        
        this.elements = new Jel.ElementCache
                        (
                            {
                                labels: 'form#' + this.form.id + ' label'
                            } 
                        );
        
        this.validationErrors = {};
        
        this.inlineFields = {};
        
        this.values = {};
        
        this.inlines = {};
        this.fields = {};
        this.labels = {};
        this.nameFields = {};
        this.nameErrors = {};
        
        this.disabled = {};
        
        this.fieldLabels = {};
        this.errorMessages = {};
        this.errorInlineMessages = {};
        
        // cache our fields for reference in validation methods
        
        $A(this.form.elements).each
        (
            function(field)
            {
                this._setupField(field);
            }
            .bind(this)
        );
        
        // cache our labels
        this.elements.labels.each
        (
            this._associateLabel.bind(this)
        );
        
    }
},
{
    ALERT_WRAP_LENGTH: 28
}
);