/**
 * Defines the standard set of JavaScript functions that our pages need to work properly.
 * This includes functions for generating popup windows, validating form fields, determining
 * the effective length of textareas, and showing/hiding the menu sidebar.
 * 
 * @package misc
 * 
 * @copyright Copyright 2004 Oregon State University
 * Licensed under the Open Software License version 2.1 (see ../LICENSE)
 * @author Sean Callan-Hinsvark
 */
//////////////////////////////////////////////////////////////////////


/**
 * Create a new popup window.  We return false because this will be used as the onclick
 * event of links and we don't want the link's href attribute to take effect.
 * 
 * @param string url - The URL of the help page to display.
 */
function w_popup(url)
{
	window.open(url, '_blank', 'menubar=yes,toolbar=yes,location=no,directories=no,status=no,scrollbars=yes,resizable=yes,width=600,height=450');
	return false;
}


/**
 * Rounds a number to the nearest integer.  If the number is exactly halfway between two
 * integers then it is rounded to the even integer.
 * 
 * This function is used to ensure that the rounding done in JavaScript is consistent with
 * the rounding done in other parts of the system.
 * 
 * @param number number - The number to be rounded.
 * @return integer
 */
function w_round(number)
{
	// Initially round toward positive infinity.
	var round_pos = Math.floor(number + 0.5);
	
	// If the number was exactly halfway between two integers and we rounded to an odd
	// number then we should have rounded the other way (to an even number).
	if (Math.abs(number - round_pos) == 0.5 && (round_pos % 2) != 0)
		return round_pos - 1;
	else
		return round_pos;
}


/**
 * Validates a form field, returning whether the field was successfully validated.
 * 
 * The field is validated differently depending on its type.  Select lists and
 * checkbox/radio inputs ignore the data type and only check whether an option
 * has been selected if the field is required.  Any other fields are assumed to
 * be text/password inputs or textareas and are validated according to both the
 * required flag and the data type.
 * 
 * For date validation the accepted year range is 1970-2037 because PHP does not
 * support dates outside of this range.
 * 
 * @param object field_obj - The form field.
 * @param boolean required_bool - Whether the form field requires user input.
 * @param string data_type_str - Optional data type the input should be validated as (should be 'int', 'float', 'date', 'email', or '').
 * @return boolean
 */
function w_validate_form_field(field_obj, required_bool, data_type_str)
{
	// Select lists are special cases.  We only care whether they're required or not.
	// If the selected value is the empty string or zero then we consider it "unselected".
	if (field_obj.type == 'select-one' || field_obj.type == 'select-multiple')
	{
		if (!required_bool)
			return true;
		
		var i = field_obj.selectedIndex;
		if (i == -1 || field_obj.options[i].value == '' || field_obj.options[i].value == 0)
			return false;
		else
			return true;
	}
	
	// Checkbox/radio inputs are special cases.  We only care whether they're required or not.
	// First we handle single checkbox/radio inputs.
	if (field_obj.type == 'checkbox' || field_obj.type == 'radio')
	{
		if (!required_bool)
			return true;
		
		return field_obj.checked;
	}
	// Next we handle groups of checkbox/radio inputs, which are accessed like arrays.
	if (field_obj[0] && (field_obj[0].type == 'checkbox' || field_obj[0].type == 'radio'))
	{
		if (!required_bool)
			return true;
		
		for (var i = 0; i < field_obj.length; i++)
		{
			if (field_obj[i].checked)
				return true;
		}
		return false;
	}
	
	// All other fields should be inputs of type 'text', 'password', or 'textarea'...
	var value_str = field_obj.value;
	
	// No input is fine if it isn't required and bad if it is required.
	if (w_empty_field(field_obj))
		return !required_bool;
	
	// The type will be 'int', 'float', 'date', 'email', 'password', or '' (always lowercase).
	switch (data_type_str)
	{
		case 'int':
			return (/^\s*[0-9]+\s*$/).test(value_str);
		
		case 'float':
			return (/^\s*[0-9]+(\.[0-9]+)?\s*$/).test(value_str);
		
		case 'date':
			if (!(/^\s*[0-9]{1,2}\/[0-9]{1,2}\/[0-9]{4}\s*$/).test(value_str))
				return false;
			
			var date_part = value_str.split('/');
			if (date_part.length != 3)
				return false;
			
			var month = parseInt(date_part[0], 10);
			if (isNaN(month) || month < 1 || month > 12)
				return false;
			
			var day = parseInt(date_part[1], 10);
			if (isNaN(day) || day < 1 || day > 31)
				return false;
			
			var year = parseInt(date_part[2], 10);
			if (isNaN(year) || year < 1970 || year > 2037)
				return false;
			
			return true;
		
		case 'email':
//			return (/^\s*\w+([-\.]\w+)*\@(\w+\.)+\w+\s*$/).test(value_str);
			return (/^\s*([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9_\-])+\.)+([a-zA-Z0-9]{2,6})+\s*$/).test(value_str);
		
		// Passwords must be at least six characters long and contain at lease one
		// character that isn't a letter.
		case 'password':
			return (/^\s*\S{6,}\s*$/).test(value_str) && (/[^ a-zA-Z]/).test(value_str);
		
		default:
			return true;
	}
}


/**
 * Returns whether a form field has an empty value or not.  A field that only contains
 * whitespace is also considered empty.
 * 
 * @param object field_obj - The form field.
 * @return boolean
 */
function w_empty_field(field_obj)
{
	var chr = '';
	for (var i = 0; i < field_obj.value.length; i++)
	{
		chr = field_obj.value.charAt(i);
		if (chr != ' ' && chr != '\n' && chr != '\r' && chr != '\t')
			return false;
	}
	return true;
}


/**
 * Calculates a textarea's length.  This is necessary because some browsers/OSs only count
 * end-of-lines as a single character, but when we go to store the textarea's contents
 * in the database the end-of-lines count as two characters (CR+LF).  So to avoid errors
 * from trying to insert something bigger than the database field we need to make sure the
 * data is small enough before they submit the form.
 * 
 * The database stores end-of-lines as CR+LF (two characters).  In Gecko/Mozilla-based
 * browsers and probably all non-Windows browsers the JavaScript textarea.value.length
 * property will only count end-of-lines as single characters.  On Mac or Unix/Linux
 * browsers this is because the OS defines end-of-lines as just CR or LF, respectively.
 * Gecko/Mozilla-based browsers seem to always use LF for end-of-lines, no matter what the OS.
 * 
 * When we need to limit textarea input we need to know the length of the data as it will
 * actually be stored in the database, so if the JavaScript textarea.value.length only
 * counts end-of-lines as single characters then we need to count them again and add that
 * to the original length.  To do this we count the number of CRs and the number of LFs.
 * If they're the same then textarea.value.length will already count end-of-lines as two
 * characters.  If they're not the same then we add both numbers to textarea.value.length,
 * because one of them is the number of end-of-lines and one of them is zero.  This is true
 * because we either have CR+LF pairs, just CRs, or just LFs.
 * 
 * @param object textarea - The textarea form field.
 * @return integer
 */
function w_get_textarea_len(textarea)
{
	var textarea_len = textarea.value.length;
	var CR = -1, LF = -1;
	
	var last_index = -1;
	do {
		last_index = textarea.value.indexOf( '\r', last_index + 1 );
		CR++;
	} while ( last_index != -1 );
	
	last_index = -1;
	do {
		last_index = textarea.value.indexOf( '\n', last_index + 1 );
		LF++;
	} while ( last_index != -1 );
	
	if ( CR != LF )
		textarea_len += (CR + LF);
	
	return textarea_len;
}


/* Functions to manage hiding/showing the menu sidebar. */
function w_menu_hide_enable()
{
	if (!document.getElementById)
	{
		alert('Your browser does not support this feature.');
		return;
	}
	
	document.getElementById('menu-hide-link').style.display = 'none';
	document.getElementById('menu-show-link').style.display = 'inline';
	w_menu_hide();
}
function w_menu_hide_disable()
{
	document.getElementById('menu-hide-link').style.display = 'inline';
	document.getElementById('menu-show-link').style.display = 'none';
	document.getElementById('content-cell').onmouseover = w_do_nothing;
	document.getElementById('menu-block').style.top = '0';
}
function w_menu_hide()
{
	document.getElementById('content-cell').onmouseover = w_do_nothing;
	document.getElementById('menu-block').style.display = 'none';
	document.getElementById('menu-cell').onmouseover = w_menu_show;
}
function w_menu_show()
{
	document.getElementById('menu-cell').onmouseover = w_do_nothing;
	document.getElementById('menu-block').style.display = 'block';
	document.getElementById('content-cell').onmouseover = w_menu_hide;
	
	// Adjust the relative position of the menu so it is shown no matter how far down
	// the page the user has scrolled.
	var pos;
	if (window.pageYOffset != undefined)
	{
		pos = window.pageYOffset;
	}
	else if (document.documentElement && document.documentElement.scrollTop != undefined)
	{
		pos = document.documentElement.scrollTop;
	}
	else if (document.body && document.body.scrollTop != undefined)
	{
		pos = document.body.scrollTop;
	}
	else
	{
		pos = 0;
	}
	pos = pos - 47;  // The menu usually starts 47 pixels from the top of the page.
	if (pos < 0)
		pos = 0;
	document.getElementById('menu-block').style.top = pos + 'px';
}
function w_do_nothing() {}
