/* sortTable data type constants */
var SORTTABLE_TYPE_DEFAULT = 0;
var SORTTABLE_TYPE_TEXT = 1;
var SORTTABLE_TYPE_NUMBER = 2;
var SORTTABLE_TYPE_DATE_DDMMYY = 3;
var SORTTABLE_TYPE_DATE_MMDDYY = 4;
var SORTTABLE_TYPE_SHORTCURRENCY_PREFIX = 5;
var SORTTABLE_TYPE_SHORTCURRENCY_POSTFIX = 6;
var SORTTABLE_TYPE_LONGCURRENCY_PREFIX = 7;
var SORTTABLE_TYPE_LONGCURRENCY_POSTFIX = 8;
var SORTTABLE_TYPE_MAX = 8;	/* just for bounds checking */


/**
 * \brief Sort the data in a table.
 *
 * \param idOrObject is the id or table element to sort.
 * \param sortOn is the index of the table column on which to sort.
 *
 * The table object to sort must have a &lt;tbody&gt; and &lt;thead&gt;. The
 * head is used to determine the set of rows that should not be included in
 * the sort because they are column headings, and will have a custom attribute
 * added indicating the data type for the column. The body is used to determine
 * exactly which rows to include in the sort. This ensures that rows in a
 * &lt;tfoot&gt; section are not included in the sort so that tables with
 * summary data can keep the summary data at the bottom regardless of sort.
 *
 * TODO there are some issues, I think, with sorting numeric columns with empty
 * cells. The issue is probably in the comparison method and is likely being
 * caused by NaN values. Make NaN values get sorted high.
 */
function sortTable( idOrObject, sortOn, comparator ) {
	var table = idOrObjectToObject(idOrObject);

	if(!table || "TABLE" != table.nodeName) return;

	/* get an array of <th> elements in the last row of <thead> */
	/* get all <thead> children of <table> */
	var headings = table.getElementsByTagName("thead");
	if(!headings || 0 == headings.length) return;

	/* get all <tr> elements in first <thead> (should only be one anyway) */
	headings = headings[0].getElementsByTagName("tr");
	if(!headings || 0 == headings.length) return;

	/* get all <th> elements in last <tr> in <thead> */
	headings = headings[headings.length - 1].getElementsByTagName("th");
	if(!headings || 0 == headings.length) return;

	/* get the <tbody> - again, should be only one */
	var tbody = table.getElementsByTagName("tbody");
	if(!tbody || 0 == tbody.length) return;
	tbody = tbody[0];

	var rows = tbody.getElementsByTagName("tr");
	if(!rows || rows.length < 2) return;

	if("undefined" == typeof(table.xSortedOn)) table.xSortedOn = -1;

	/* ensure that table has enough columns to sort by sortOn */
	if(headings.length <= sortOn) return;

	/* extract the data to sort */
	var rowArray = new Array();
	for(var i = 0, l = rows.length; i < l; i++) {
		rowArray[i] = new Object;
		rowArray[i].xOldSortIndex = i;
		rowArray[i].xSortValue = cellText(rows[i].getElementsByTagName("td")[sortOn]);
	}
	
	if(sortOn == table.xSortedOn) {
		rowArray.reverse();
	}
	else {
		/* guess and cache the data type */
		if("function" != typeof(comparator)) {
			if("undefined" == typeof(headings[sortOn].xDataType)) {
				/* attempt to guess data type */
				headings[sortOn].xDataType = SORTTABLE_TYPE_DEFAULT;
				var guess = -1;

				/* guess the data type. if the first two successful guesses agree, the
				* guessed data type is used; otherwise, the default data type is used*/
				for(var i = 0, l = rows.length; i < l; i++) {
					var myGuess = guessDataType(rowArray[i].xSortValue);
					if(-1 == myGuess) continue;
					if(-1 == guess) guess = myGuess;
					else if(guess == myGuess) break;
					else {
						/* two guesses, different types: treat as default */
						guess = SORTTABLE_TYPE_DEFAULT;
						break;
					}
				}

				if(-1 < guess) headings[sortOn].xDataType = guess;
			}

			/* decide which comparison function to use based on column data type */
			switch(headings[sortOn].xDataType) {
				default:
				case SORTTABLE_TYPE_DEFAULT:
				case SORTTABLE_TYPE_TEXT:
					comparator = compareText;
					break;

				case SORTTABLE_TYPE_NUMBER:
					comparator = compareNumeric;
					break;

				case SORTTABLE_TYPE_DATE_DDMMYY:
					comparator = compareDateEU;
					break;

				case SORTTABLE_TYPE_DATE_MMDDYY:
					comparator = compareDateUS;
					break;

				case SORTTABLE_TYPE_SHORTCURRENCY_PREFIX:
					comparator = compareShortCurrencyPrefix;
					break;

				case SORTTABLE_TYPE_SHORTCURRENCY_POSTFIX:
					comparator = compareShortCurrencyPostfix;
					break;

				case SORTTABLE_TYPE_LONGCURRENCY_PREFIX:
					comparator = compareLongCurrencyPrefix;
					break;

				case SORTTABLE_TYPE_LONGCURRENCY_POSTFIX:
					comparator = compareLongCurrencyPostfix;
					break;

				case SORTTABLE_TYPE_CURRENCY_POSTFIX:
					comparator = compareCurrencyPostfix;
					break;
			}
		}

		/* sort the data */
		rowArray.sort(comparator);
		table.xSortedOn = sortOn;
	}
	
	/* re-populate table */
	var newTbody = document.createElement("tbody");
	for(var i = 0, l = rowArray.length ; i < l; i++) {
		newTbody.appendChild(rows[rowArray[i].xOldSortIndex].cloneNode(true));
	}

	/* thead can become misaligned in FF if we use using replaceChild() */
	table.insertBefore(newTbody, tbody);
	table.removeChild(tbody);
}


/**
 * \brief Guesses the data type of a string value.
 *
 * \param v is the value whose type needs to be guessed.
 *
 * The following types are supported:
 * - short dates in EU (DD/MM/YY) or US (MM/DD/YY) formats.
 * - long dates in EU (DD/MM/YYYY) or US (MM/DD/YYYY) formats.
 * - numbers (integer or real) in decimal notation, including an optional
 *   preceding sign
 * - currencies with a currency symbol either prefix or postfix
 * - currencies with a 3-letter currency identifier either prefix or postfix
 *
 * Leading and trailing whitespace is not strictly supported. Whitespace
 * between the optional numeric sign and the number itself and between the
 * currency symbol or identifier and the quantity is.
 *
 * Dates in SQL format (YYYY-MM-DD) are not specifically identified as they
 * will sort properly using the text comparison function.
 *
 * If the data type cannot be identified, the data is assumed to be text.
 *
 * \return A data type constant, or -1 on error or if the value is empty.
 */
function guessDataType( v ) {
	if("undefined" == typeof(guessDataType.shortDateRegex)) {
		guessDataType.shortDateRegex = /^([0-9]{2})[\.\/]([0-9]{2})[\.\/]([0-9]{2})$/;
		guessDataType.longDateRegex = /^([0-9]{2})[\.\/]([0-9]{2})[\.\/]([0-9]{4})$/;
		guessDataType.integerRegex = /^[+-]{0,1} *[0-9]+$/;
		guessDataType.realRegex = /^[\+-]{0,1} *[0-9]*[\.\,]{0,1}[0-9]+$/;
		guessDataType.shortCurrencyPrefixRegex = /^(\£|\$|\€) *[0-9]+([\.\,]{0,1}[0-9]{2}){0,1}$/;
		guessDataType.shortCurrencyPostfixRegex = /^[0-9]+([\.\,]{0,1}[0-9]{2}){0,1} *(\£|\$|\€)$/;
		guessDataType.longCurrencyPrefixRegex = /^[A-Z]{3} *[0-9]+([\.\,]{0,1}[0-9]{2}){0,1}$/;
		guessDataType.longCurrencyPostfixRegex = /^[0-9]+([\.\,]{0,1}[0-9]{2}){0,1} *[A-Z]{3}$/;
	}

	if(typeof(v) != "string" || "" == v) return -1;

	var result = guessDataType.shortDateRegex.exec(v);
	if(result && 4 == result.length) {
		var today = new Date();
		var year = "" + (result[3] > 50 ? today.getFullYear() - 1 : today.getFullYear());

		/* force the base as leading "0"s cause parseInt to assume octal */
		year = parseInt(year.substr(0,2) + result[3], 10);
		var month = parseInt(result[2], 10);
		var day = parseInt(result[1], 10);

		if(isValidDate(day, month, year)) return SORTTABLE_TYPE_DATE_DDMMYY;
		if(isValidDate(month, day, year)) return SORTTABLE_TYPE_DATE_MMDDYY;
	}

	result = guessDataType.longDateRegex.exec(v);
	if(result && 4 == result.length) {
		/* force the base as leading "0"s cause parseInt to assume octal */
		var year = parseInt(result[3], 10);
		var month = parseInt(result[2], 10);
		var day = parseInt(result[1], 10);

		if(isValidDate(day, month, year)) return SORTTABLE_TYPE_DATE_DDMMYY;
		if(isValidDate(month, day, year)) return SORTTABLE_TYPE_DATE_MMDDYY;
	}

	if(v.match(guessDataType.integerRegex) || v.match(guessDataType.realRegex)) return SORTTABLE_TYPE_NUMBER;
	if(v.match(guessDataType.shortCurrencyPrefixRegex)) return SORTTABLE_TYPE_SHORTCURRENCY_PREFIX;
	if(v.match(guessDataType.shortCurrencyPostfixRegex)) return SORTTABLE_TYPE_SHORTCURRENCY_POSTFIX;
	if(v.match(guessDataType.longCurrencyPrefixRegex)) return SORTTABLE_TYPE_LONGCURRENCY_PREFIX;
	if(v.match(guessDataType.longCurrencyPostfixRegex)) return SORTTABLE_TYPE_LONGCURRENCY_POSTFIX;
	return SORTTABLE_TYPE_TEXT;
}


/**
 * \brief Extracts the displayed text in a table cell.
 *
 * \param cell is the cell whose core text is sought.
 *
 * Because table cell text can be marked up in various nested ways, table
 * sorting can't simply use the &lt;td&gt; node's value as the basis for the
 * sort. The actual text may be embedded in &lt;a&gt; or &lt;strong&gt; or
 * &lt;span&gt; or various other tags, and to use the tags as part of the sort
 * basis would pollute the sort because the attributes of the tags would
 * influence it. This function, given a table cell, will attempt to gather the
 * displayed text by traversing the cell's child nodes until it finds a simple
 * text node. Only the first child of each embedded DOM element is considered
 * (so embedded tables or lists may not sort properly), and only text nodes and
 * form elements will result in some text being returned.
 *
 * A future extension may support extraction of the \c alt attribute from 
 * &lt;img&gt; tags being extracted.
 *
 * \return The displayed text, or an empty string if no text could be found.
 */
function cellText( cell ) {
	if(!cell || "object" != typeof(cell) || !cell.nodeName || "TD" != cell.nodeName) return "";

	while(cell) {
		if("#text" == cell.nodeName)
			return cell.nodeValue;
		
		/* for form elements, return the current value */
		if("SELECT" == cell.nodeName) {
			/* for multi-select lists, comma-separate selected values */
			if(true == cell.multiple) {
				var first = true;
				var ret = "";

				for(var i = 0; i < cell.options.length; i++) {
					if(true == cell.options[i].selected) {
						if(first) first = false;
						else ret += ",";
						
						ret += cell.options[i].text;
					}
				}

				return ret;
			}
			else
				return cell.options[cell.selectedIndex].text;
		}

		if("TEXTAREA" == cell.nodeName || "INPUT" == cell.nodeName)
			return cell.value;
		
		cell = cell.firstChild;
	}

	return "";
}


/* these are "private" and not suitable as comparators for sortTable() */
function numericComparator(a, b) {
	if(isNaN(a)) {
		if(isNaN(b)) return 0;
		return 1;
	}

	if(isNaN(b)) return -1;
	return (a - b);
}

/* these are the core comparators suitable for sortTable() */
function compareText(a, b) {
	var aVal = a.xSortValue;
	var bVal = b.xSortValue;
	return (aVal == bVal ? 0 : (aVal > bVal ? 1 : -1));
}

function compareNumeric(a, b) {
	return numericComparator(parseFloat(a.xSortValue), parseFloat(b.xSortValue));
}

function compareShortCurrencyPrefix(a, b) {
	return numericComparator(parseFloat(a.xSortValue.substr(1)), parseFloat(b.xSortValue.substr(1)));
}

function compareShortCurrencyPostfix(a, b) {
	return numericComparator(parseFloat(a.xSortValue.substring(0, a.xSortValue.length - 1)), parseFloat(b.xSortValue.substr(0, a.xSortValue.length - 1)));
}

function compareLongCurrencyPrefix(a, b) {
	return numericComparator(parseFloat(a.xSortValue.substr(3)), parseFloat(b.xSortValue.substr(3)));
}

function compareLongCurrencyPostfix(a, b) {
	return numericComparator(parseFloat(a.xSortValue.substring(0, a.xSortValue.length - 3)), parseFloat(b.xSortValue.substr(0, a.xSortValue.length - 3)));
}

function compareDateEU(a, b) {
	return numericComparator(parseInt(a.xSortValue.substr(6) + a.xSortValue.substr(3, 2) + a.xSortValue.substr(0, 2)), parseInt(b.xSortValue.substr(6) + b.xSortValue.substr(3, 2) + b.xSortValue.substr(0, 2)));
}

function compareDateUS(a, b) {
	return numericComparator(parseInt(a.xSortValue.substr(6) + a.xSortValue.substr(0, 2) + a.xSortValue.substr(3, 2)), parseInt(b.xSortValue.substr(6) + b.xSortValue.substr(0, 2) + b.xSortValue.substr(3, 2)));
}

