// This code originally comes from:
// http://www.kryogenix.org/code/browser/sorttable/

// 2004-12-03: Modifications by mcramer@pbs.org: http://blog.webkist.com/archives/000043.html
// 2005-01-07: Modifications by Anthony.Garrett@aods.org (tested IE 6.0.28, Opera 7.54, Firefox 1.0)
//             [Problem: Firefox 1.0 won't display the span style for the up and down arrows.]
// 2005-01-11: Anthony.Garrett@aods.org Fixed small bug:
//             Error occurred when clicking on column header link just to the right of the text.
// 2005-01-11: mcramer@pbs.org integrated AG's fixes and added support for <select> sorting.
// 2005-01-13: mcramer@pbs.org: Caching optimizations. Should be faster on big tables.
// 2005-03-14: Modified by Demi Benson - work arounds for issues with Safari and Mozilla
//             also added try-catch blocks for suspect sections
// 2005-04-29: Changes by Demi Benson - added handling for the latest Safari w.r.t. ".cells"
// 2006-06-26: added 'unsort' to allow individual columns to be non-sortable

// How to Use Sortable Tables
//
// For sortable tables, the table must have an ID attribute, although this may be anything.
// The table must also have in its class the string "sortable", either on its own or bounded by spaces.
// 
// It is recommended that the structure of the table follow this hierarchy:
// TABLE 
//   THEAD
//     TR
//       TH
//       TH...
//   TBODY
//     TR...
// 
// To make a specific column non-sorting, for instance if it contains checkboxes, 
// add the class "unsort" to the TH tag for that column.  Doing so does not add the 
// javascript link to any text, nor does it add the up/down arrow character.

// How to Use Zebra Stripes
// 
// A number of class attributes should be used for striped tables:
// Any tables must have the class "zebra", either on its own or bounded by spaces.
// This can be used in conjunction with "sortable" from above.
// The CSS classes "odd" and "even" should be defined with the appropriate color schemes.
// The CSS class "ruled" should be defined if the mouse-over highlighting will be used.


addEvent(window, 'load', sortables_init);

var SORT_COLUMN_INDEX;
var show_debugging_alerts = false;

// Set this array up for any language you choose.  
// You can use any strings you like for the months as long as the array matches the data.
var monthName = new Array('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
                          'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec');

function sortables_init() {
	// Find all tables with class 'sortable' and make them sortable
	if (!document.getElementsByTagName)
		return;
	tbls = document.getElementsByTagName('table');
	for (ti = 0; ti < tbls.length; ti++) {
		thisTbl = tbls[ti];
		if (((' ' + thisTbl.className + ' ').indexOf(' sortable ') != -1) && (thisTbl.id)) {
			ts_makeSortable(thisTbl);
		}
	}
	stripe();
}

function ts_makeSortable(table) {
	var firstRow;
	var FRCells;
	for (var i = 0; i < table.rows.length; i++) {
		firstRow = table.rows[i];
		if (!firstRow) {
			if(show_debugging_alerts) { alert('In function ts_makeSortable() tried to access a non-existant row'); }
		}
		FRCells = TDTH_cells(firstRow);
		if (FRCells)		// DB - this row has either TH or TD cells
			break;
		if (i == 0) {		// DB - first row
			var headcells = firstRow.getElementsByTagName('TH');
			if (!headcells) {
				if(show_debugging_alerts) { alert('In function ts_makeSortable, no headcells found'); }
			}
			else {			// DB - this row has TH tags instead of TDs, use this for our FRCells
				FRCells = headcells;
				break;
			}
		}
		if ((i != 0) && (firstRow.cells && firstRow.cells.lemgth && firstRow.cells[0].nodeName == 'TD')) {
			firstRow = table.rows[i - 1];
			// DB - blindly assumes that without a TH, the first TR we find is good
			// DB - since i put in TDTH_cells, this shouldn't be a problem
			FRCells = firstRow.cells;
			break;
		}
	}


	if (!FRCells) {		// should always have some array of first-row cells
		return;
	}

	// IE 6.0.28  Resets checkbox values to their initial state on sort.
	// BUG (see http://www.quirksmode.org/bugreports/archives/2004/10/moving_checkbox.html)
	//	 solution 1: Don't use a crappy browser like IE.
	//	 solution 2: Store the checkbox state and restore after move.
	// --> solution 3: Set defaultChecked to the value of checked each time the checkbox is changed.

	// AG Avoid IE bug that uses "defaultChecked" when it should use "checked" on move of element
	var inputs = document.getElementsByTagName('input');
	for (var i = 0; i < inputs.length; i++) {
		if (inputs[i].type.toLowerCase() == 'checkbox')
			addEvent(inputs[i], 'change', ts_persistCheckbox);
	}

	// We have a first row: assume it's the header, and make its contents clickable links
	for (var i = 0; i < FRCells.length; i++) {
		var cell = FRCells[i];
		if ((' ' + cell.className + ' ').indexOf(' unsort ') != -1) {
			// don't let this header be sortable
			continue
		}
		var link = document.createElement('a');
		link.href = '#';
		link.style.textDecoration = 'none';
		// link.className = "sortheader";	// AG Added (makes the styling work)
		addEvent(link, "click", ts_resortTable);
		var l = cell.childNodes.length;
		while (cell.childNodes.length > 0) {
			link.appendChild(cell.childNodes[0]);
		}
		var span = document.createElement('span');
		span.className = 'sortarrow';
		span.innerHTML = '&nbsp;&nbsp;';
		link.appendChild(span);
		cell.appendChild(link);
	}
}

function ts_getInnerText(el) {
	if (typeof el == 'string' || typeof el == 'undefined')
		return el;

	if(el.ts_allText)
	  return el.ts_allText;

	var str = new Array();
	var cs = el.childNodes;

	for (var i = 0; i < cs.length; i++) {
		switch (cs[i].nodeType) {
		case 1: // ELEMENT_NODE
			if(cs[i].tagName.toLowerCase() == 'input') {
				if(cs[i].type.toLowerCase() == 'text')
					str.push(cs[i].value)
				else if(cs[i].type.toLowerCase() == 'checkbox')
					str.push(cs[i].checked)
				else
					str.push(ts_getInnerText(cs[i]));
			}
			else if(cs[i].tagName.toLowerCase() == 'select') {
				str.push(cs[i].options[cs[i].selectedIndex].value);
			} 
			else {
				str.push(ts_getInnerText(cs[i]));
			}
			break;
		case 3: // TEXT_NODE
			str.push(cs[i].nodeValue);
			break;
		}
	}
	// Save the extracted text for later. This costs the client RAM,
	// but saves major CPU when the cell contents are particularly
	// complex.
	return el.ts_allText = str.join(' ');
}

function ts_expireCache(el) {
	var cs = el.childNodes;
	if(typeof el.ts_allText == 'undefined')
		return false

	for (var i = 0; i < cs.length; i++) {
		if(cs[i].nodeType == 1) { // ELEMENT_NODE
			// We need to expire the cache if the element
			// contains any form-type nodes, just in case
			// the user changes their value.
			if(cs[i].tagName.toLowerCase() == 'input')
				return delete el.ts_allText
			else if(cs[i].tagName.toLowerCase() == 'select')
				return delete el.ts_allText
			else if(ts_expireCache(cs[i])) 
				return delete el.ts_allText
		}
	}
	return false
}

function ts_persistCheckbox(event) {
	// AG - Avoids IE bug
	var chkbox = event.currentTarget ? event.currentTarget : event.srcElement;
	chkbox.defaultChecked = chkbox.checked;
	return true;
}

function ts_resortTable(event) {
	var lnk = event.currentTarget ? event.currentTarget : event.srcElement;
	// AG - IE doesn't support "currentTarget", must use "srcElement" instead.
	// get the span
	var span;
	if(lnk.tagName && lnk.tagName.toLowerCase() == 'span')
		span = lnk;
	else {
		for (var ci=0; ci<lnk.childNodes.length; ci++) {
			if(lnk.childNodes[ci].tagName && lnk.childNodes[ci].tagName.toLowerCase() == 'span')
				span = lnk.childNodes[ci];
		}
	}
	var td = lnk.parentNode;
	while((td.tagName != 'TD') && (td.tagName != 'TH')) {
		td = td.parentNode;
		if(show_debugging_alerts) { alert('popping up a level to ' + td.tagName); }
	}
		
	var column = -1;
	if (td.parentNode.children == undefined) {
		// DB - testing shows that Mozilla/IE/FF can't figure out .children
		// but can understand .cellIndex
		// Safari has the opposite problem.
		column = td.cellIndex;
		if(show_debugging_alerts) { alert('column number from td.cellIndex = ' + column); }
	}
	else {
		var found = 0;
		var tdindex = -1;
		try {
			var tr = td.parentNode;
			if(show_debugging_alerts) { alert('looking for matching child in ' + tr.tagName); }
			for(var k=0; k < tr.children.length ; k++) {
				var tdname = tr.children[k].tagName.toLowerCase() ;
				if( tdname == 'td' || tdname == 'th') {
					tdindex++;
				}
				if (tr.children[k] == td) {
					column = tdindex;
					found = 1;
					break;
				}
			}
		}
		catch(e) { alert('In function ts_resortTable(A): ' + e.message); }
		if( !found) {
			alert('column not found!');	
		}
		if(show_debugging_alerts) { alert('column number from tr.children = ' + column); }
	}
	var table = getParent(td, 'TABLE');

	var link_row = td.parentNode.rowIndex;
	var nonHeaderRowIndex = 0;
	if (link_row == 0) {			// DB - the first row is the header, so don't try to sort that
		nonHeaderRowIndex++;
	}
	try {
		for ( ; nonHeaderRowIndex < table.rows.length; nonHeaderRowIndex++) {
			if(table.rows[nonHeaderRowIndex].cells && table.rows[nonHeaderRowIndex].cells.length && 
				 table.rows[nonHeaderRowIndex].cells[0].nodeName == 'TD') {
				break;
			}
		}
	}
	catch(e) { alert('In function ts_resortTable(B): ' + e.message); }

	// If 0, the table has no rows. If >= table.rows.length, it has no data.
	if(nonHeaderRowIndex == 0 || nonHeaderRowIndex >= table.rows.length) {
		return;
	}

	// Work out a type for the column
	if(show_debugging_alerts) { alert('before calling TDTH_cells( table.rows[ ' + nonHeaderRowIndex + ' ]'); }
	var FirstRowCells = TDTH_cells(table.rows[nonHeaderRowIndex]);
	if(show_debugging_alerts) { alert('after calling TDTH_cells'); }
	var theCell = FirstRowCells[column];
	if(show_debugging_alerts) { alert('after getting theCell for column ' + column); }
	var itm = ts_getInnerText(theCell);
	if(show_debugging_alerts) { alert('sample data is ' + itm); }

	var dateregex = new RegExp("^\\d\\d[\\/-](\\d\\d|" +
								monthName.join("|") +
					 			')[\\/-]\\d\\d(\\d\\d)?$', "i");
	var timeregex1 = new RegExp("^((\\d+[hHmM])?\\d\\d[mMsS])?\\d\\d[sS]?$");
	var timeregex2 = new RegExp("^((\\d+:)?\\d\\d:)?\\d\\d$");
	var IPregex    = new RegExp("^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$");

	// A date in dd/mm/yy format.
	if (itm.match(dateregex)) {
		sortfn = ts_sort_date;
		if(show_debugging_alerts) { alert('matches date'); }
	}
		
	// Currency, like $10.34
	else if (itm.match(/^[£$€]/)) {
		sortfn = ts_sort_currency; // AG Added Euro
		if(show_debugging_alerts) { alert('matches currency'); }
	}

	// 123.123.123.123
	else if (itm.match(IPregex)) {
		sortfn = ts_sort_ip;		// DB
		if(show_debugging_alerts) { alert('matches IP'); }
	}

	// 234.123
	else if (itm.match(/^[0-9]+(,[0-9][0-9][0-9])*(\.([0-9])+)*$/g)) {
		sortfn = ts_sort_numeric;
		if(show_debugging_alerts) { alert('matches numeric'); }
	}
	
	// A time value in XXhYYmZZs or some combination of those parts
	else if (itm.match(timeregex1)) {
		sortfn = ts_sort_time1;		// DB 
		if(show_debugging_alerts) { alert('matches time1'); }
	}

	// A time value in hh:mm:ss
	else if (itm.match(timeregex2)) {
		sortfn = ts_sort_time2;		// DB 
		if(show_debugging_alerts) { alert('matches time2'); }
	}

	// Everything else.
	else {
		sortfn = ts_sort_caseinsensitive;
		if(show_debugging_alerts) { alert('no matches, column=' + column); }
	}

	SORT_COLUMN_INDEX = column;

	var newRows = new Array();
	for (var j=nonHeaderRowIndex; j<table.rows.length; j++) {
		newRows[j - nonHeaderRowIndex] = table.rows[j];
	}
	
	newRows.sort(sortfn);

	if (span.getAttribute('sortdir') == 'down') {
		ARROW = '&nbsp;&uarr;';
		newRows.reverse();
		span.setAttribute('sortdir', 'up');
	} 
	else {
		ARROW = '&nbsp;&darr;';
		span.setAttribute('sortdir', 'down');
	}
	
	// We appendChild rows that already exist to the tbody, so it moves them rather 
	// than creating new ones 
	// don't do sortbottom rows
	for (var i=0; i < newRows.length; i++) {
		if (!newRows[i].className ||
			(newRows[i].className &&
			 (newRows[i].className.indexOf('sortbottom') == -1))) {
			table.tBodies[0].appendChild(newRows[i]);
		}
		// We've sorted the list already, so we need to make
		// sure any cells with changable values are expired.
		ts_expireCache(newRows[i].cells[SORT_COLUMN_INDEX]);
	}
	// DB - i don't know what 'sortbottom' rows are for, but on my tables they've never been used
	// do sortbottom rows only
	for (i = 0; i < newRows.length; i++) {
		if (newRows[i].className &&
			(newRows[i].className.indexOf('sortbottom') != -1)) {
			table.tBodies[0].appendChild(newRows[i]);
		}
	}

	// Delete any other arrows that may be showing
	var allspans = document.getElementsByTagName('span');
	for (var ci = 0; ci < allspans.length; ci++) {
		if (allspans[ci].className == 'sortarrow') {
			if (getParent(allspans[ci], 'table') == getParent(lnk, 'table')) {	// in the same table as us?
				allspans[ci].innerHTML = '&nbsp;&nbsp;';
			}
		}
	}
	span.innerHTML = ARROW;

	stripe();
}

function getParent(el, pTagName) {
	if (el == null)
		return null;
	else if (el.nodeType == 1 && el.tagName.toLowerCase() == pTagName.toLowerCase())	// Gecko bug, supposed to be uppercase
		return el;
	else
		return getParent(el.parentNode, pTagName);
}

function ts_sort_date(a, b) {
	// y2k notes: Two digit years less than 50 are treated as 20XX, 
	// greater than 50 are treated as 19XX.

	aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]);
	bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]);

	aaBits = aa.split(/\/|-/);
	for (i = 0; i < monthName.length; i++) { // AG Convert Alpha month to two digit month
		if (monthName[i].toLowerCase() == aaBits[1].toLowerCase()) {
			aa = aaBits[0] + '/' + (i < 9 ? '0' : '') + (i + 1) + '/' + aaBits[2];
			break;
		}
	}

	if (aa.length == 10) {
		dt1 = aa.substr(6, 4) + aa.substr(3, 2) + aa.substr(0, 2);
	} 
	else {
		yr = aa.substr(6, 2);
		dt1 = (parseInt(yr, 10) < 50 ? '20' : '19') + yr + aa.substr(3, 2) + aa.substr(0, 2);
	}

	bbBits = bb.split(/\/|-/);
	for (i = 0; i < monthName.length; i++) { // AG: Convert Alpha month to two digit month
		if (monthName[i].toLowerCase() == bbBits[1].toLowerCase()) {
			bb = bbBits[0] + '/' + (i < 9 ? '0' : '') + (i + 1) + '/' + bbBits[2];
			break;
		}
	}

	if (bb.length == 10) {
		dt2 = bb.substr(6, 4) + bb.substr(3, 2) + bb.substr(0, 2);
	} 
	else {
		yr = bb.substr(6, 2);
		dt2 = (parseInt(yr, 10) < 50 ? '20' : '19') + yr + bb.substr(3, 2) + bb.substr(0, 2);
	}

	if (dt1 == dt2)
		return 0;
	if (dt1 < dt2)
		return -1;
	return 1;
}

function ts_sort_time1(a, b) {
	aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).split(/[^0-9]/g);
	bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).split(/[^0-9]/g);

	if (aa.length != bb.length) {
		return 0;
	}
	if ((aa.length > 0) && (parseInt(aa[0], 10) != parseInt(bb[0], 10))) {
		return parseInt(aa[0], 10) - parseInt(bb[0], 10);
	}
	if ((aa.length > 1) && (parseInt(aa[1], 10) != parseInt(bb[1], 10))) {
		return parseInt(aa[1], 10) - parseInt(bb[1], 10);
	}
	if ((aa.length > 2) && (parseInt(aa[2], 10) != parseInt(bb[2], 10))) {
		return parseInt(aa[2], 10) - parseInt(bb[2], 10);
	}
	return 0;
}	
function ts_sort_time2(a, b) {
	aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).split(/:/g);
	bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).split(/:/g);

	if (aa.length != bb.length) {
		return 0;
	}
	if ((aa.length > 0) && (parseInt(aa[0], 10) != parseInt(bb[0], 10))) {
		return parseInt(aa[0], 10) - parseInt(bb[0], 10);
	}
	if ((aa.length > 1) && (parseInt(aa[1], 10) != parseInt(bb[1], 10))) {
		return parseInt(aa[1], 10) - parseInt(bb[1], 10);
	}
	if ((aa.length > 2) && (parseInt(aa[2], 10) != parseInt(bb[2], 10))) {
		return parseInt(aa[2], 10) - parseInt(bb[2], 10);
	}
	return 0;
	
}

function ts_sort_currency(a, b) {
	aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).replace(/[^0-9.]/g, '');
	bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).replace(/[^0-9.]/g, '');
	return parseFloat(aa) - parseFloat(bb);
}

function ts_sort_ip(a, b) {
	aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).split(/\./g);
	bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).split(/\./g);
	
	if (parseInt(aa[0], 10) != parseInt(bb[0], 10)) {
		return parseInt(aa[0], 10) - parseInt(bb[0], 10);
	}
	if (parseInt(aa[1], 10) != parseInt(bb[1], 10)) {
		return parseInt(aa[1], 10) - parseInt(bb[1], 10);
	}
	if (parseInt(aa[2], 10) != parseInt(bb[2], 10)) {
		return parseInt(aa[2], 10) - parseInt(bb[2], 10);
	}
	return parseInt(aa[3], 10) - parseInt(bb[3], 10);
}

function ts_sort_numeric(a, b) {
	aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).replace(/,/g, '');
	aaParse = parseFloat(aa);
	if (isNaN(aaParse))
		aaParse = 0;
		
	bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).replace(/,/g, '');
	bbParse = parseFloat(bb);
	if (isNaN(bbParse))
		bbParse = 0;
	return aaParse - bbParse;
}

function ts_sort_caseinsensitive(a, b) {
	aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).toLowerCase();
	bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).toLowerCase();
	if (aa == bb)
		return 0;
	if (aa < bb)
		return -1;
	return 1;
}

function ts_sort_default(a, b) {
	aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]);
	bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]);
	if (aa == bb)
		return 0;
	if (aa < bb)
		return -1;
	return 1;
}

function addEvent(elm, evType, fn, useCapture) {
// addEvent cross-browser event handling for IE5+,  NS6 and Mozilla
// By Scott Andrew
	if (elm.addEventListener) {
		elm.addEventListener(evType, fn, useCapture);
		return true;
	} 
	else if (elm.attachEvent) {
		var r = elm.attachEvent('on' + evType, fn);
		return r;
	} 
	else {
		alert('Handler could not be added');
	}
}

/*
	Function to get around the ".cells" construct, which does not work in Safari
	Added by Demi Benson, March 2005
*/
function TDTH_cells(in_obj) {
	if (in_obj == null) {			// must be a valid object
		if(show_debugging_alerts) { alert('TDTH_cells was passed NULL object!'); }
		return null;
	}
	if (in_obj.tagName != 'TR') {	// if not a table row, what are we doing here?
		if(show_debugging_alerts) { alert('TDTH_cells was passed a non-TR object!'); }
		return null;
	}
	// at this point the in_obj must be a TR
	var cells;
	cells = in_obj.getElementsByTagName('th');
	if (cells != null && cells.length > 0) {			// this row has TH elements
		if(show_debugging_alerts) { alert('TDTH_cells found ' + cells.length + ' TH cells'); }
		return cells;
	}
	cells = in_obj.getElementsByTagName('td');
	if (cells != null && cells.length > 0) {			// this row has TD elements
		if(show_debugging_alerts) { alert('TDTH_cells found ' + cells.length + ' TD cells'); }
		return cells;
	}
	if(show_debugging_alerts) { alert('TDTH_cells found no TD or TH cells!'); }
	return null;
	// returns the array of child TD elements or TH elements located in this TR
} // function TDTH_cells


/* 
	Javascript to style odd/even table rows
	Derived from 'Zebra Tables' by David F. Miller (http://www.alistapart.com/articles/zebratables/)
	
	Modified by Jop de Klein, february 2005
	jop at validweb.nl
	http://validweb.nl/artikelen/javascript/better-zebra-tables/
	
	Modified by Demi Benson, feb 2005 - explicitly adds and subtracts even/odd for use in sortable tables
*/

function stripe() {
	var tables = document.getElementsByTagName('table');	

	for(var x=0; x != tables.length; x++){
		var table = tables[x];
		if ((' ' + table.className + ' ').indexOf(' zebra ') == -1) {
			// not class 'zebra'? then don't stripe
			continue;
		}
		
		var tbodies = table.getElementsByTagName('tbody');
		
		for (var h = 0; h < tbodies.length; h++) {
			var even = false;
			var trs = tbodies[h].getElementsByTagName('tr');
			
			for (var i = 0; i < trs.length; i++) {
				if (trs[i].className.indexOf('hide') != -1) {
					// has class 'hide', so not visible, so don't stripe this row
					continue;
				}
				trs[i].className = trs[i].className.replace('odd', ' ');
				trs[i].className = trs[i].className.replace('even', ' ');
				trs[i].onmouseover = function(){
					this.className += ' ruled'; return false
				}
				trs[i].onmouseout = function(){
					this.className = this.className.replace(' ruled', ''); return false
				}
				
				if(even) {
					trs[i].className += ' even ';
				}
				else {
					trs[i].className += ' odd ';
				}
				
				even = !even;
			}
		}
	}
}
