// OVERVIEW
// 
// This validation module works by utilizing custom "input validation"
// attributes added to the form elements that needed checking. So for
// example a text field that is required would use this:
//
//     <input type="text" name="Summary" value="" ap-required="1">
//
// The above says that the Summary text field is "required" (i.e. cannot
// be blank when submitted).  Note that the all of the "iv" custom
// attributes need to be in lowercase.
//
// We have three levels of checking that go on here in terms of our API:
//
// 1. Confirmation Level - validate and log error messages.
// 2. Validation Level - validate and compose a single error message.
// 3. IsA Level - validate with a simple boolean response (yay or nay).
//
// Clients of this code can rely on the funcs at any of the levels as needed.
// However, in most all cases they will just call the confirm_input() method
// which does a full confirmation and report on all form input data.
//
//
// CUSTOM ATTRIBUTES
// 
// ap-status {on|off}
//		- the default is "on".
//		- this is good as a master switch to turn validation off,
//		  effectively "commenting out" other ap attribs.
//		- this may have other modes in the future (not just a boolean).
//
// ap-desc {the description used during error reporting}
//		- if set to "%i" then only the ap-item will be used.
//
// ap-item {the itemized description used during error reporting}
//		- "itemization" means "numbering the input form elements".
//		- this is typically a "question number" from a form.
//
// ap-type {text|email|phone|money|url|int|float|word|aword|ip|age|zip|countrycode|tld}
//		- the default is "text" (good old generic text).
//		- not all types are currently implemented, just these:
//		- email, phone, int, word, aword
//
// ap-required {1|0} (required value)
//		- the most common attribute set on a text field.
//
// ap-reqother {1|0} (required other value)
//		- this is typically used when a pulldown is set to "Other".
//		- and a text field is available to describe what "Other" means.
//              - note: using ap-reqif can get the same effect as this and is
//		- more flexible in other cases of setting constraints.
//
// ap-reqif	{"name|is|val"} (required if name's value is set to "val").
// 		- "name|like|pat" (required if name's value matches "pat").
//
// ap-reqmatch {"name"} (required to match name's value)
//
// ap-pattern {generic regex matching}
//
// ap-min {minimum value for a number}
//
// ap-max {maximum value for a number}
//
// ap-minlen {minimum length (num chars) for the input}
//
// ap-maxlen {maximum length (num chars) for the input}
//
// ap-notrim {turn head/tail whitespace removal off for a textfield}
//
// ap-realemail {0|1}
//		- checks to make sure an email address is NOT anonymous.
//		- anonymous addy's are things like yahoo.com, hotmail.com, etc.
//		- the list of anon addys is okay but definitely not complete.
//
// ap-evilemail {0|1}
//		- checks to make sure an email address is NOT from a competitor.
//		- thus addresses like: documentum, stellent, vignette, etc.
//
//
// OTHER DESIRED FEATURES
//
// - need a method to turn all validation off across all elements

FormValidation = function( form )
{
    this.form         = form;			// the form we are validating
    this.elem_name    = '';				// current element name
    this.elem         = 0;				// current element object
    this.elog         = new Array();	// error log
    this.attrib_initd = 0;				// attributes initd?
};


// public methods

// handle callbacks that validate a form
//
// This function is designed to be called by a <form> tag's onSubmit handler.
// Returns: true if the form can be submitted; false otherwise.
FormValidation.prototype.validate = function()
{
    var form = this.form;

    // Initialize custom attributes for certain browsers.
    this.attrib_init();

    // Check their input and display a dialog of errors if needed.
    var valid = this.confirm_input( 'default', 'default' );

    // Clear out the log of what the user just saw for error messages.
    this.elog_clear();

    // Return the answer.
    return ( valid );
};
// handle callbacks that validate a form and then submit it
FormValidation.prototype.submit = function()
{
    var form = this.form;

    // Initialize custom attributes for certain browsers.
    this.attrib_init();

    // If we fail validation, an alert is shown and the submit is voided.
    if ( this.confirm_input( 'default', 'default' ) ) 
		form.submit();

    // Clear out the log of what the user just saw for error messages.
    this.elog_clear();
};
// cmd_reset - handle callbacks that reset the form (for symmetry mostly).
FormValidation.prototype.reset = function()
{
    this.form.reset();
};
// handle callbacks that validate a form and then submit it
//
// This is a special version of "submit" that allows the caller to pass
// a "command symbol" back to the cgi as a hidden value in the form.
// The purpose of this is to allow multi-page forms to indicate whether
// the submission is meant to go "back", or "next", or "finish", or "reset".
//
// >> cmd_elem  - the form element that receives the cmd parameter.
// >> cmd - the command to process.
FormValidation.prototype.process = function( cmd_elem, cmd )
{
    // Locate the form and assign the command.
    var form = cmd_elem.form;	// i.e. this elem's "parent"
    cmd_elem.value = cmd;		// store the cmd in the form

    // If we are resetting then it's simple.
    if ( cmd == 'reset' ) 
	{ 
		form.reset(); 
		return; 
	}

    // If we are backing up then we do not have to validate.
    if ( cmd == 'back' ) 
	{ 
		form.submit(); 
		return; 
	}

    // We now presume we are doing a 'next', or a 'finish', etc.
    // This implies we need bulk input validation.

    // Initialize custom attributes for certain browsers.
    this.attrib_init();

    // If we fail validation, an alert is shown and the submit is voided.
    if ( this.confirm_input( 'default', 'default' ) ) 
		form.submit();

    // Clear out the log of what the user just saw for error messages.
    this.elog_clear();
};
// make sure attributes are init'd if necessary
//
// Returns: flag stating if attrib_init_data() was called or not.
FormValidation.prototype.attrib_init = function()
{
    if ( this.attrib_initd )
	{
		return ( 0 );
	}    
    else if ( this.attrib_init_needed() ) 
	{
		this.attrib_init_data();
		this.attrib_initd = 1;
	
		return ( 1 );
    }
    else 
	{
		return ( 0 );
    }
};
// state whether attributes should be programatically set
FormValidation.prototype.attrib_init_needed = function()
{
    var is_msie = ( navigator.appName.indexOf( "Microsoft" ) != -1 );
    return ( !is_msie );
};
// a no-op which the caller should override
//
// This function exists because netscape navigator/communicator does not
// allow for setting custom attributes on elements directly in the html.
// In order for the attributes to show up, they must be procedurally set
// via javascript. IE can get by on just the HTML tag definitions alone.
//
// When this function is defined by the caller it should end up with code
// like this:
//
// var form = this.form;
// form.name['ap-required']  = 1;
// form.email['ap-required'] = 1;
// form.email['ap-type']     = 'email';
//
// Or like this:
//
// this.attrib_set( 'ap-required', '1',     'user_name',     'user_email' );
// this.attrib_set( 'ap-type',     'email', 'user_email',    'comp_email' );
// this.attrib_set( 'ap-type',     'int',   'num_employees', 'num_years'  );
//
// A combination of both methodologies is also possible.
//
// The caller should define one of two functions:
//
// 1. FormValidation.prototype.attrib_init_data - to override this method.
// 2. ap_attrib_init_data - to provide their own function.
FormValidation.prototype.attrib_init_data = function()
{
    var rc = 0;
    
	if ( typeof( ap_attrib_init_data ) == 'function')
		rc = ap_attrib_init_data( this );
    
    return ( rc );
};
// set an attribute value for a list of elements
//
// This goes out of it's way to be safe in case we screw up our data.
FormValidation.prototype.attrib_set = function( attrib, val /* and then a list of eids */ )
{
    var e;
    
	for ( var i = 2; i < arguments.length; ++i ) 
	{
		e = this.form[arguments[i]];
	
		if ( e )
			e[attrib] = val;
    }
};
// set a bunch of attributes on a single element
//
// Returns: the number of name / value pairs processed.
FormValidation.prototype.attrib_config = function( eid /* and then pairs of names/values */ )
{
    var e = this.form[eid];
    
	if ( !e )
		return ( 0 );
		
    var limit = arguments.length - 1;
    var count = 0;
    var name, value;
    
	for ( var i = 1; i < limit; i += 2 ) 
	{
		name  = arguments[i + 0];
		value = arguments[i + 1];
	
		if ( name ) 
			e[name] = value;
		
		++count;
    }
	
    return ( count );
};
// check all values in the form using attributes for hints
//
// There are two phases to checking a given element.
// 1. An existance check for a non-blank value if it is required.
// 2. A syntax check.
FormValidation.prototype.confirm_input = function( prefix, suffix )
{
    var e;						// form element object
    var i;						// loop index
    var type;					// the type of the value
    var val;					// generic value
    var gtable = new Object();	// group hit table
    var group;					// group element
    var is_group;				// is it a group?

    for ( i = 0; i < this.form.length; ++i ) 
	{
		e = this.form.elements[i];
		val = e[FormValidation._ap_status];
	
		if ( val == 'off' )
			continue;

		// Skip this if the element has already been checked via a group.
		if ( gtable[e.name] ) 
			continue;

		// Fold group elements to a single element.
		// This is done by monitoring which groups we've done in the gtable.
		// And we only allow the first element of a group to "speak for it".
		// Both checkboxes and radio buttons can be grouped.
		type = e.type;
	
		if ( type == 'checkbox' || type == 'radio' ) 
		{
	    	group = this.form[e.name];
	    
			if ( group.length > 1 ) 
			{
				gtable[e.name] = 1;	// this group has been seen
				is_group = true;	// we are in "group mode" now

				// Transfer the first group elem's props to the group itself.
				// xxx: it would be better to have a func to loop on props.
				// We only do the transfer if the group "seems" to have not
				// already been given "iv" attributes.  It *will* already
				// have them when running under netscape due to the way we
				// assign attributes procedurally.  This is because we assign
				// the attributes to the group itself, not the individual elems.
		
				if ( !group[FormValidation._ap_item] ) 
				{
		    		group[FormValidation._ap_required] = e[FormValidation._ap_required];
		    		group[FormValidation._ap_reqother] = e[FormValidation._ap_reqother];
		    		group[FormValidation._ap_reqif]    = e[FormValidation._ap_reqif];
		    		group[FormValidation._ap_item]     = e[FormValidation._ap_item];
		    		group[FormValidation._ap_desc]     = e[FormValidation._ap_desc];
		    		group[FormValidation._ap_type]     = e[FormValidation._ap_type];
				}

				// Switch over to using the group as the element.
				e = group;
	    	}
		}

		// Trim textfields unless the ap-notrim attribute is there.
		if ( type == 'text' ) 
		{
	    	var notrim = e[FormValidation._ap_notrim] == '1';
	    
			if ( !notrim ) 
			{
				var curr_value = e.value;
				var trim_value = this.trim( curr_value );
		
				if ( trim_value != curr_value )
		    		e.value = trim_value;
	    	}
		}

		// Confirm existance.
		if ( e[FormValidation._ap_required] == '1' ) 
		{
	    	if ( !this.confirm_required( e ) ) 
				continue;
		}
		else if ( e[FormValidation._ap_reqother] == '1' ) 
		{
	    	if ( !this.confirm_reqother( e ) ) 
				continue;
		}
		else if ( e[FormValidation._ap_reqif] ) 
		{
	    	if ( !this.confirm_reqif( e ) ) 
				continue;
		}
		else if ( e[FormValidation._ap_reqmatch] ) 
		{
	    	if ( !this.confirm_reqmatch( e ) ) 
				continue;
		}

		// Confirm size limits.
		if ( !this.confirm_length( e ) ) 
			continue;

		// Confirm syntax based on type.
		type = e[FormValidation._ap_type];
	
		if ( !type )
			type = 'text';
			
		if ( type == "email" ) 
		{
	    	if ( !this.confirm_email( e ) )
				continue;
		}
		else if ( type == 'phone' ) 
		{
	    	if ( !this.confirm_phone( e ) )
				continue;
		}
		else if ( type == 'word' ) 
		{
	    	if ( !this.confirm_word( e ) )
				continue;
		}
		else if ( type == 'aword' ) 
		{
	    	if ( !this.confirm_aword( e ) )
				continue;
		}
		else if ( type == 'int' ) 
		{
	    	if ( !this.confirm_int( e ) )
				continue;
		}

		// Confirm syntax based on generic pattern.
		var pat_str = e[FormValidation._ap_pattern];
	
		if ( pat_str ) 
		{
	    	if ( !this.confirm_pattern( e ) ) 
				continue;
		}
    }

    // If there were any errors, display them via an alert and return false.
    // XXX: it would be wise to limit the number of errors displayed.
    if ( this.elog_count() ) 
	{
	 	this.elog_alert( prefix, suffix ); 
		return false; 
	}

    // The input was good.
    return true;
};
// process a required element
FormValidation.prototype.confirm_required = function( elem )
{
    var val   = this.get_value(elem);
    var blank = val == null || this.is_blank( val );
	
    if ( blank ) 
	{
		this.elog_append( this.get_desc( elem ) + ' is blank.' );
		return false;
    }
    
	return true;
};
// process a required-other element
//
// This type of required value is a bit different.  A "required other"
// value occurs when a select menu has been set to "other", and the
// form designer would like to require that the user specify in a text
// field/area what they mean by "other".
FormValidation.prototype.confirm_reqother = function( elem )
{
    var val = this.get_value( elem );
    
	if ( val != "Other" )
		return true;

    // The select menu is "Other", so check the text field.
    // We enforce a specific naming convention - the text field must have
    // the same name as the select menu but with "_other" added to it.
    // We could extend this later such that the -ap-other resource itself
    // could name the other text field.
    var other_name = elem.name + '_other';
    var other_elem = this.form[other_name];
	
    if ( !other_elem )
		return true;
    
	val = this.get_value( other_elem );
    var blank = val == null || this.is_blank( val );
    
	if ( blank ) 
	{
		this.elog_append( this.get_desc( elem ) + ' is set to Other so please specify it further.' );
		return false;
    }
	
    return true;
};
// process a required-if element
//
// A "required if" value occurs when one form element is required based
// on another form element being "set" to some value.
//
// Syntax:  -ap-reqif => '<elem_name>|<mode>|<value>'
//
// Mode: "is"
// Example: 'user_location|is|custom'
// Means  : this element is required IF the user_location is set to 'custom'.
//
// Mode: "like"
// Example: 'comp_name|like|^IBM.*'
// Means  : this element is required IF the comp_name matches the pat IBM.*.
//
// Obviously the logic to "required if" could be infinitely complex.
// The present syntax atleast allows us to have a few modes of checking.
// It's mainly useful when a set of fields become required because a
// checkbox is set or a radio group has a certain value.
//
// In hindsight, using '|' as the separator char was not the best choice
// due to the collision with it's value in RegExp.
FormValidation.prototype.confirm_reqif = function( elem )
{
    var cond     = elem[FormValidation._ap_reqif];
    var v        = cond.split( '|' );
    var src_name = v[0];
    var mode     = v[1];
    var tmp      = v.slice(2);
    var thing    = tmp.join( '|' );

    // Lookup the source element and get it's value.
    var src_elem = this.form[src_name];
    var src_val  = this.get_value( src_elem );

    if ( mode == 'is' ) 
	{
		if ( src_val == thing )
			return ( this.confirm_required( elem ) );
    }
    else if ( mode == 'like' ) 
	{
		var pat = new RegExp( thing );
	
		if ( pat.test( src_val ) )
			return (this.confirm_required( elem ) );
    }
	
    return true;
};
// process a required-match element
//
// This is useful when a confirmation password must be entered and
// must match the originally typed password.
//
// Syntax:  -ap-reqmatch => '<elem_name>'
//
// Note: javascript will not allow looking at password vals so this is a no go.
FormValidation.prototype.confirm_reqmatch = function( elem )
{
    var src_name = elem[FormValidation._ap_reqmatch];
    var src_elem = this.form[src_name];
    var src_val  = this.get_value( src_elem );
    var our_val  = this.get_value( elem );

    if ( our_val != src_val ) 
	{
		this.elog_append( ''
	    	+ this.get_desc( elem ) + ' does not match '
	    	+ this.get_desc( src_elem )
		);
	
		return false;
    }

    return true;
};
// process an email element
FormValidation.prototype.confirm_email = function( elem )
{
    var val   = this.get_value( elem );
    var blank = val == null || this.is_blank( val );
	
    if ( !blank || ( blank && elem[FormValidation._ap_required] ) ) 
	{
		var result = this.validate_email( val );
	
		if ( result != 'valid' ) 
		{
	    	this.elog_append( this.get_desc( elem ) + ' ' + result );
	    	return false;
		}

		if ( elem[FormValidation._ap_realemail] == "1" ) 
		{
	    	result = this.validate_anon_email( val );
	    
			if ( result == 'valid' ) 
			{
				// This needs to be a real email but is in fact anonymous.
				this.elog_append( this.get_desc( elem ) + ' '
		    		+ "is restricted.\n\n"
		    		+ "The email address you have entered matches a list of\n"
		    		+ "restricted generic addresses (like yahoo.com,\n"
		    		+ "hotmail.com, etc). You should enter your employee\n"
		    		+ "email address for the company where you work.\n"
				);
		
				return false;
	    	}
		}

		if ( elem[FormValidation._ap_evilemail] == "1" ) 
		{
	    	result = this.validate_evil_email( val );
	    
			if ( result == 'valid' ) 
			{
				// An evil competitor is trying to use our products & services!
				// EEEK! Raise the draw-bridge! Circle the wagons!
				this.elog_append( this.get_desc( elem ) + ' '
		    		+ "is restricted.\n\n"
		    		+ "The email address you have entered matches a list of\n"
		    		+ "restricted addresses.\n"
				);
		
				return false;
	    	}
		}
    }
    
	return true;
};
// process a phone number element
FormValidation.prototype.confirm_phone = function( elem )
{
    var val   = this.get_value( elem );
    var blank = val == null || this.is_blank( val );
    
	if ( !blank || ( blank && elem[FormValidation._ap_required] ) ) 
	{
		var result = this.validate_phone( val );
	
		if ( result != 'valid' ) 
		{
	    	var str = this.get_desc( elem ) + ' ' + result + '.';
	    	var pat = /invalid char/;
	    
			if ( pat.test( result ) ) 
			{
				str += "\n";
				str += "    - Valid characters are:\n";
				str += "    - digits, space, dash, plus, period, and parens\n";
				str += "    - and the letter 'x' for extensions.";
	    	}
	    
			this.elog_append( str );
	    	return false;
		}
    }
	
    return true;
};
// process a word element
FormValidation.prototype.confirm_word = function( elem )
{
    var val     = this.get_value( elem );
    var blank   = val == null || this.is_blank( val );
    var missing = blank && elem[FormValidation._ap_required];
    
	if ( !blank ) 
	{
		var result = this.validate_word( val );
	
		if ( result != 'valid' ) 
		{
	    	this.elog_append( this.get_desc( elem ) + ' - ' + result + '.' );
	    	return (false);
		}
    }
    
	return true;
};
// process an alpha-word element
FormValidation.prototype.confirm_aword = function( elem )
{
    var val     = this.get_value( elem );
    var blank   = val == null || this.is_blank( val );
    var missing = blank && elem[FormValidation._ap_required];
    
	if ( !blank ) 
	{
		var result = this.validate_aword( val );
	
		if ( result != 'valid' ) 
		{
	    	this.elog_append( this.get_desc( elem ) + ' - ' + result + '.' );
	    	return false;
		}
    }
    
	return true;
};
// process an element against a generic pattern
FormValidation.prototype.confirm_pattern = function( elem )
{
    var pat_str = elem[FormValidation._ap_pattern];
    
	if ( !pat_str ) 
		return true;

    var val     = this.get_value( elem );
    var blank   = val == null || this.is_blank( val );
    var missing = blank && elem[FormValidation._ap_required];
    
	if ( !blank ) 
	{
		var result = this.validate_pattern( val, pat_str );
	
		if ( result != 'valid' ) 
		{
	    	this.elog_append( this.get_desc( elem ) + ' - ' + result + '.' );
	    	return false;
		}
    }
	
    return true;
};
// check the string length of the input
FormValidation.prototype.confirm_length = function( elem )
{
    // First make sure we have some size limits.
    var minlen   = elem[FormValidation._ap_minlen];
    var maxlen   = elem[FormValidation._ap_maxlen];
    var have_min = ( ( minlen != null ) && ( minlen > 0 ) );
    var have_max = ( ( maxlen != null ) && ( maxlen > 0 ) );
    
	if ( !have_min && !have_max )
		return true;

    // We need to provide some constraints so lookup the value of the elem.
    var val = this.get_value( elem );

    // Check the constraints.
    var blank   = val == null || this.is_blank( val );
    var missing = blank && elem[FormValidation._ap_required];
    
	if ( !blank ) 
	{
		var result = this.validate_length( val, minlen, maxlen );
	
		if ( result != 'valid' ) 
		{
	    	var name = this.get_desc( elem );
	    	this.elog_append( name + ' - ' + result );
	    
			return false;
		}
    }

    return true;
};
// process an integer element
FormValidation.prototype.confirm_int = function( elem )
{
    var val   = this.get_value( elem );
    var blank = val == null || this.is_blank( val );
    
	if ( !blank || ( blank && elem[FormValidation._ap_required] ) ) 
	{
		var result = this.validate_int( val );
		var name   = this.get_desc( elem );
		
		if ( result != 'valid' ) 
		{
	    	this.elog_append( name + ' ' + result );
	    	return false;
		}

		// Check the minimum limit for this integer.
		var min = elem[FormValidation._ap_min];
	
		if ( min != null && val < parseInt( min ) ) 
		{
	    	this.elog_append( name + ' must not be less than ' + min + '.' );
	    	return false;
		}

		// Check the maximum limit for this integer.
		var max = elem[FormValidation._ap_max];
	
		if ( max != null && val > parseInt( max ) ) 
		{
	    	this.elog_append( name + ' must not be greater than ' + max + '.' );
	    	return false;
		}
    }
	
    return true;
};
// verify that a required value is "there"
//
// Returns: a string; "valid" means a good address; otherwise it's an error msg.
FormValidation.prototype.validate_required = function( val )
{
    if ( val == null || val == '' || this.is_blank( val ) ) 
		return ( 'value is blank.' );
    
    return ( 'valid' );
};
// verify that an email address is valid
//
// Returns: a string; "valid" means a good address; otherwise it's an error msg.
FormValidation.prototype.validate_email = function( email )
{
    // Perform some simple sanity checks with clear error messages.
    if ( email == "" )
		return ( 'address is blank.' );
    else if ( email.indexOf( ' ' ) >= 0 )
		return ( 'address has a space in it.' );
    else if ( email.indexOf( "\t" ) >= 0 )
		return ( 'address has a tab in it.' );
    else if ( email.indexOf( ',' ) >= 0 )
		return ( 'address has a comma "," in it.' );
    else if ( email.indexOf( '#' ) >= 0 )
		return ( 'address has a pound "#" in it.' );
    else if ( email.indexOf('!') >= 0 )
		return ( 'address has an exclamation "!" in it.' );
    else if ( email.indexOf( '@' ) < 0 )
		return ( 'address has no "@" symbol.' );

    // Perform a more thorough pattern-match but a generic error message.
    var pat   = /^[a-z][a-z_0-9\.\-]*@[a-z_0-9\.\-]+\.[a-z]{2,4}$/i;
    var valid = pat.test( email );
    
	if ( !valid )
		return ( 'address format is bad.' );

    // Indicate a positive result.
    return ( 'valid' );
};
// verify that a phone number is valid
//
// A phone number must only consist of:
// - digits, spaces, dashes, plus-signs, dots, and parentheses.
// - the letter 'x' in case they mention an extension as in x1234.
// - and it must contain atleast 1 digit.
//
// Returns: a string; "valid" means a good address; otherwise it's an error msg.
FormValidation.prototype.validate_phone = function( phone )
{
    // Perform some simple sanity checks with clear error messages.
    if ( phone == "" )
		return ( 'is blank' );

    // Check for invalid characters.
    var pat = /[^0-9 \-+\.()xX]/; // everything-but (^) pattern
    var hit = pat.test( phone );
    
	if ( hit )
		return ( 'has invalid character(s)' );

    // Make sure there is atleast one digit.
    pat = /[0-9]/;
    var hit = pat.test( phone );
    
	if ( !hit )
		return ( 'has no digits' );

    // Indicate a positive result.
    return ( 'valid' );
};
// verify a string has basic alphanumeric chars
//
// A word is composed of just letters, numbers, and underscores.
// It may start with any of those characters.
// This is good for things like usernames which should not have punctuation
// characters in them.
//
// Returns: a string; "valid" means a good word; otherwise it's an error msg.
FormValidation.prototype.validate_word = function( word )
{
    if ( word != '' ) 
	{
		var pat = /^\w+$/;
	
		if ( !pat.test( word ) )
	    	return( 'invalid input (use only letters, digits, and underscores)' );
    }

    return ( 'valid' );
};
// verify a string has basic alphanumeric chars
//
// An 'a' word ("alpha word") is composed of letters, numbers, and underscores;
// And it must start with an alphabetic letter or an underscore.
// This is good for things like usernames which should not have punctuation
// characters in them.
//
// Returns: a string; "valid" means a good word; otherwise it's an error msg.
FormValidation.prototype.validate_aword = function( aword )
{
    if ( aword != '' ) 
	{
		var pat1 = /^[A-Za-z_]/;
	
		if ( ! pat1.test( aword ) )
	    	return( 'invalid input (must start with a letter or underscore)' );
	
		var pat2 = /^[A-Za-z_]\w*$/;
	
		if ( !pat2.test( aword ) )
	    	return( 'invalid input (use only letters, digits, and underscores)' );
    }

    return ( 'valid' );
};
// verify a string against a generic pattern
//
// Returns: a string; "valid" means good syntax; otherwise it's an error msg.
FormValidation.prototype.validate_pattern = function( value, pat_str )
{
    var pat = new RegExp( pat_str );
    
	if ( !pat.test( value ) )
		return( 'invalid input (does not have expected syntax)' );
    
    return ( 'valid' );
};
// verify that an email address is "generic"
//
// Returns: a string; "valid" means a good address; otherwise it's an error msg.
FormValidation.prototype.validate_anon_email = function( email )
{
    var host_pat   = /(\@|\.)(yahoo|excite|lycos|hotmail|aol|earthlink|msn|compuserve|juno|netzero|nightmail|rocketmail|newmail|runbox|hushmail|fastmail|rediffmail|mailcity|bremail|icqmail)\./i;
    var host_match = host_pat.test( email );

    if ( !host_match ) 
	{
		host_pat   = /(\@|\.)(mail)\.com$/i;
		host_match = host_pat.test( email );
    }

    if ( !host_match ) 
	{
		host_pat   = /(\@|\.)(netscape|speakeasy)\.net$/i;
		host_match = host_pat.test( email );
    }

    var dom_pat   = /\.(name|info)$/i;
    var dom_match = dom_pat.test( email );

    if ( host_match || dom_match )
		return ( 'valid' );
    
	return ( 'non-anonymous email address' );
};
// verify that an email address is from a competitor
//
// Returns: a string; "valid" means a good address; otherwise it's an error msg.
FormValidation.prototype.validate_evil_email = function( email )
{
    var evil_list = new Array(
		'agari', 
		'allaire', 
		'appliedsemantics', 
		'aptrix', 
		'atomz',
		'autonomy', 
		'boxcarsoftware', 
		'broadvision', 
		'centerrun',
		'clearforest', 
		'day', 
		'divine', 
		'documentum', 
		'ebt',
		'egrail', 
		'em3', 
		'eprise', 
		'fatwire', 
		'filenet',
		'filnet', 
		'gauss', 
		'gofilnet', 
		'hylandsoftware', 
		'imanage',
		'inso', 
		'interleaf', 
		'intranetsolutions', 
		'inxight', 
		'kintana',
		'macromedia', 
		'marimba', 
		'mediasurface', 
		'merant', 
		'microsoft',
		'mks', 
		'mohomine', 
		'ncompass', 
		'netobjects', 
		'nstein',
		'octave', 
		'openmarket', 
		'opentext', 
		'percussion', 
		'presence',
		'purpleyogi', 
		'quiver', 
		'rational', 
		'readware', 
		'reddot',
		'semio', 
		'starbase', 
		'stellent', 
		'stratify', 
		'towersoftware',
		'verity', 
		'viant', 
		'vignette', 
		'worldweb'
    );

    var evil_pat_parts = evil_list.join( '|' );
    var evil_pat_str   = '(\\@|\\.)(' + evil_pat_parts + ')\\.';
    var host_pat       = new RegExp( evil_pat_str, 'i' );
    var dom_pat        = /\.(name|info)$/i;

    if ( host_pat.test( email ) )
		return ('valid');
		
    return ( 'friendly email address' );
};
// verify that a string is not too big or too small
//
// Returns: a string; "valid" means good; otherwise it's an error message.
FormValidation.prototype.validate_length = function( str, min, max )
{
    var len = str.length;
    
	if ( min > 0 && len < min )
		return ( 'input is too short (atleast ' + min + ' chars needed)' );
    else if ( max > 0 && len > max )
		return ( 'input is too long (' + max + ' chars max)' );
    else
		return ( 'valid' );
};
// verify that a value is a float
//
// Returns: a string; "valid" means a good value; otherwise it's an error msg.
//
// Note that using parseFloat() is another option, and check for a good
// return value using isNaN() (see page 264 of rhino book).
FormValidation.prototype.validate_float = function( val )
{
    // Perform some simple sanity checks with clear error messages.
    if ( val == "" ) 
		return ( 'value is blank - expecting a number.' );
    else if ( val.indexOf( ' ' ) >= 0 ) 
		return ( 'value has a space in it - expecting only digits.' );
    else if ( val.indexOf( ',' ) >= 0 ) 
		return ( 'value has a comma "," in it - expecting only digits.' );

    // Perform a more thorough pattern-match but a generic error message.
    if ( !this.is_float( val ) )
		return ( 'value is not a number.' );

    // Indicate a positive result.
    return ( 'valid' );
};
// verify that a value is an integer
//
// Returns: a string; "valid" means a good value; otherwise it's an error msg.
//
// Note that using parseInt() is another option, and check for a good
// return value using isNaN() (see page 264 of rhino book).
FormValidation.prototype.validate_int = function( val )
{
    // Perform some simple sanity checks with clear error messages.
    if ( val == "" ) 
	{
		return ( 'value is blank - expecting a number.' );
    }
    else if ( val.indexOf( ' ' ) >= 0 ) 
	{
		return ( 'value has a space in it - expecting only digits.' );
    }
    else if ( val.indexOf( "." ) >= 0 ) 
	{
		return ( 'value has a period in it - should be an integer number.' );
    }
    else if ( val.indexOf( ',' ) >= 0 ) 
	{
		return ( 'value has a comma "," in it - expecting only digits.' );
    }

    // Perform a more thorough pattern-match but a generic error message.
    if ( !this.is_int( val ) )
		return ( 'value is not a number.' );

    // Indicate a positive result.
    return ( 'valid' );
};
// make sure a string is non-blank and contains no comma
//
// This makes sure that a string is okay to be used in a
// comma-separated-value record.
//
// Returns: a string "valid" means good; otherwise it's an error msg.
FormValidation.prototype.validate_csv = function( str )
{
    if ( this.is_blank( str ) ) 
		return ( 'value is blank.' );
    else if ( str.indexOf( ',' ) >= 0 ) 
		return ( 'value has a comma in it (not allowed).' );
    else 
		return ( 'valid' );
};
// test to see if a string is a valid floating point number
//
// Returns: true if it is a valid float; false otherwise.
//
// XXX: it turns out that parseFloat() is kind of a sucky mechanism to
// validate syntax.  For example, the following is valid using parseFloat():
// "1250.95 samolians"  - i.e. it groks first number fine, and just tosses
// the rest.  However, the CGI receiver on the server side then has to
// grok it's ass off too, which voids the benefit of client-side checking.
FormValidation.prototype.is_float = function( str )
{
    var v = parseFloat( str );
    
	if ( isNaN( v ) )
		return false;
		
    // xxx: <insert other syntax checks here> (when have time of course)
    return (true);
};
// test to see if a string contains all digits
//
// Returns: true if it is a valid integer; false otherwise.
FormValidation.prototype.is_int = function( str )
{
    var pat = /^\-?\d+$/;
    return ( pat.test( str ) );
};
// test to see if a string contains all white space
//
// Returns: true if it is blank; false otherwise.
FormValidation.prototype.is_blank = function( str )
{
    var pat = /^\s*$/;
    return ( pat.test( str ) );
};
// query the size of the error log
FormValidation.prototype.elog_count = function()
{
    return ( this.elog.length );
};
// remove all messages from the error log
FormValidation.prototype.elog_clear = function()
{
    this.elog = new Array();
};
// add an error to the beginning of the error log
FormValidation.prototype.elog_prepend = function( msg )
{
    this.elog.unshift( msg );
};
// add an error to the end of the error log
FormValidation.prototype.elog_append = function(msg)
{
    this.elog[this.elog.length] = msg;
};
// display the error log in an alert dialog
FormValidation.prototype.elog_alert = function(prefix, suffix)
{
    alert(this.elog_compose(prefix, suffix));
};
// formulate the error log into a big string
FormValidation.prototype.elog_compose = function( prefix, suffix )
{
    if ( prefix == 'default' ) 
	{
		prefix = (''
	    	+ "--------------------------------------------------------------------------\n"
	    	+ "Bitte korrigieren Sie die folgenden Felder in Ihrer Eingabe:\n"
	    	+ "--------------------------------------------------------------------------\n\n"
		);
    }

    if ( suffix == 'default' )
		suffix = '';

    return ( prefix + this.elog.join( "\n" ) + suffix );
};
// find out what's in an object
FormValidation.prototype.dump_props = function( obj )
{
	var prop;
    var win = window.open();
    var doc = win.document;

    doc.open( 'text/plain' );
    
	for ( prop in obj )
		doc.write( prop + ' => ' + obj[prop] + "\n" );
		
    doc.close();
};
// query the value of an interface element
//
// This is a bit odd because we are presuming we can express the return
// value of any UI element as a flattened string. It's not the worst
// assumption but it is an assumption.
FormValidation.prototype.get_value = function( elem )
{
    var type = elem.type;

    if ( type == 'text' ) 
	{
		return ( elem.value );
    }
    else if ( type == 'textarea' ) 
	{
		return (elem.value);
    }
    else if ( type == 'hidden' ) 
	{
		return (elem.value);
    }
    else if ( type == 'select-one' ) 
	{
		var index = elem.selectedIndex;	// -1 if nothing selected
	
		if ( index >= 0 )  
			return ( elem.options[index].value ); 
		else 
			return ( '' ); 
    }
    else if ( type == 'select-multiple' ) 
	{
		return ( this.poll_option_menu( elem, '|' ) );
    }
    else if ( elem.length > 1 ) 
	{
		return ( this.poll_btn_group( elem, '|' ) );
    }
    else if ( type == 'checkbox' ) 
	{
		return ( elem.value );
    }
    else 
	{
		return ( '' );
    }
};
// figure out the description of an element for reporting purposes
//
// We allow the form designer to provide a custom string for error reporting
// because in some cases the actual element name may be cryptic.  The
// attribute to use is "ap-desc".  If this attribute is not set then we use
// the element's given name.
//
// If the item description exists, it will prefix the standard description
// enclosed in parentheses.
//
// Like: (Item 35) User Email   is blank.
//       ^^^^^^^^^^^^^^^^^^^^   ^^^^^^^^^
//       overall description    error message
FormValidation.prototype.get_desc = function( elem )
{
    var desc = elem[FormValidation._ap_desc];
    var item = elem[FormValidation._ap_item];
    var istr = '(Item ' + item + ')';

    // A description of %i implies just use the item description.
    if ( desc == '%i' )
		return ( istr );

    if ( desc == null || desc == '' ) 
	{
		desc = elem.name;
	
		if ( desc == null || desc == '' )
			desc = 'Unknown Field';
    }

    // Add in the item description if it's there (for reference).
    if ( item && item != 'undefined' )
		desc = istr + ' ' + desc;

    return ( desc );
};
// query the value of a set of buttons
//
// This should work for both radio and checkbox button groups.
FormValidation.prototype.poll_btn_group = function( elem, sep_char )
{
	var i;
    var size   = elem.length;
    var values = Array();

    for ( i = 0; i < size; ++i ) 
	{
		if ( elem[i].checked )
			values[values.length] = elem[i].value;
    }

    var value = values.join( sep_char );
    return ( value );
};
// query the value of a set of radio buttons
FormValidation.prototype.poll_radio_group = function( rg )
{
    var size = rg.length;
    
	for ( var i = 0; i < size; ++i ) 
	{ 
		if ( rg[i].checked )
			return ( rg[i].value ); 
	}
	
    return ( '' );
};
// query the value of an option menu
//
// This is necessary mainly for multi-pick select lists.
FormValidation.prototype.poll_option_menu = function( menu, sep_char )
{
	var i;
    var options     = menu.options;
    var num_options = options.length;
    var values      = Array();

    for ( i = 0; i < num_options; ++i ) 
	{
		if ( options[i].selected )
			values[values.length] = options[i].value;
    }
	
    var value = values.join( sep_char );
    return ( value );
};
// trim  - chop leading and trailing whitespace from a string
// ltrim - chop leading whitespace from a string ("left trim")
// rtrim - chop trailing whitespace from a string ("right trim")
FormValidation.prototype.trim = function( str )
{
    str = str.replace( /^\s*/, '' );
    str = str.replace( /\s*$/, '' );

    return ( str );
};
FormValidation.prototype.ltrim = function( str )
{
    return ( str.replace( /^\s*/, '' ) )
};
FormValidation.prototype.rtrim = function( str )
{
    return (str.replace( /\s*$/, '' ) );
};


// static properties

FormValidation._ap_status	   = 'ap-status';
FormValidation._ap_desc	   = 'ap-desc';
FormValidation._ap_item	   = 'ap-item';
FormValidation._ap_type	   = 'ap-type';
FormValidation._ap_required  = 'ap-required';
FormValidation._ap_reqother  = 'ap-reqother';
FormValidation._ap_reqif	   = 'ap-reqif';
FormValidation._ap_reqmatch  = 'ap-reqmatch';
FormValidation._ap_min       = 'ap-min';
FormValidation._ap_max       = 'ap-max';
FormValidation._ap_minlen	   = 'ap-minlen';
FormValidation._ap_maxlen	   = 'ap-maxlen';
FormValidation._ap_notrim	   = 'ap-notrim';
FormValidation._ap_pattern   = 'ap-pattern';
FormValidation._ap_realemail = 'ap-realemail';
FormValidation._ap_evilemail = 'ap-evilemail';
