/**
 * Written by Neil Crosby. 
 * http://www.workingwith.me.uk/articles/scripting/standardista_table_sorting
 *
 * This module is based on Stuart Langridge's "sorttable" code.  Specifically, 
 * the determineSortFunction, sortCaseInsensitive, sortDate, sortNumeric, and
 * sortCurrency functions are heavily based on his code.  This module would not
 * have been possible without Stuart's earlier outstanding work.
 *
 * Use this wherever you want, but please keep this comment at the top of this file.
 *
 * Copyright (c) 2006 Neil Crosby
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy 
 * of this software and associated documentation files (the "Software"), to deal 
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
 * copies of the Software, and to permit persons to whom the Software is 
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in 
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 
 * SOFTWARE.
 **/
 /**
  * Modified by Nurgees Banu Sulthan, Broad Institute, MIT
  * 1. The code has been modified to support sort by multiple key coulmns
  * 2. The sort order is configurable
  *
  * How to configure the sort order?
  * The sort order has to be mentioned as a class defined in the coulmn header 
  * section eg <th class='sortorder_0123'>Date</th>
  * The class 'sortorder_0123' illustrates that sorting has to be done on first, 
  * second, third and fourth coulmn, '0' being the first coulmn.    
  *
  * How it works?
  * When the heading is clicked, the sort routine is identified dynamically 
  * based on the data pattern stored in the column. There are various sort routines like
  * 1. sortDate
  * 2. sortCaseInsensitive
  * 3. SortCurrency
  * 4. sortNumeric
  * 5. sortIP
  * and then the sortSequence function is invoked from inside the sort routine. It
  * gets the sort pattern from the class defined in the coulmn header section
  * eg <th class='sortorder_0123'>Date</th>, gets the substring '0123', extracts 
  * sorting index one by one starting from the second index as the sort based on first 
  * index is already done dynamically identifies what kind of sort need to be performed
  * on the sort index column based on the data pattern and triggers the appropriate sort 
  * functions
  *
  * If a table is marked with class 'autostripe', then rows will automatically have odd and
  * even classes added to them.
 **/
var standardistaTableSorting = {

	that: false,
	isOdd: false,
    // Identifies the heading Clicked 
	isEntrySortDate: false,
    isEntrySortCaseInsensitive: false,
	isEntrySortCurrency: false,
	isEntrySortNumeric: false,
	isEntrySortIP: false,
	sortCaseInsensitiveCount : 0,
	sortDateCount : 0,
	sortCurrencyCount : 0,
	sortNumericCount : 0,
	sortIPCount : 0,
	sequenceIterator : 0,

    // Represents the sort order
	sortPattern : 0,
	// To retain the sort column index
	actualSortColumnIndex : -1,
	sortColumnIndex : -1,
	lastAssignedId : 0,
	newRows: -1,
	lastSortedTable: -1,
	tableId: 0,

	/**
	 * Initialises the Standardista Table Sorting module
	 **/
	init : function() {
		// first, check whether this web browser is capable of running this script
		if (!document.getElementsByTagName) {
			return;
		}
		
		this.that = this;
		
		this.run();
		
	},
	
	/**
	 * Runs over each table in the document, making it sortable if it has a class
	 * assigned named "sortable" and an id assigned.
	 **/
	run : function() {
		var tables = document.getElementsByTagName("table");
		
		for (var i=0; i < tables.length; i++) {
			var thisTable = tables[i];
			
			if (css.elementHasClass(thisTable, 'sortable')) {
				this.makeSortable(thisTable);
			}
		}
	},
	
    tableToSort : function() {
		var tables = document.getElementsByTagName("table");
		
		for (var i=0; i < tables.length; i++) {
			var thisTable = tables[i];
			if(thisTable.id == tableId){			
				if (css.elementHasClass(thisTable, 'sortable')) {
					return thisTable;
				}
			}
		}
		return 0;
	},
	/**
	 * Makes the given table sortable.
	 **/
	makeSortable : function(table) {
	
		// first, check if the table has an id.  if it doesn't, give it one
		if (!table.id) {
			table.id = 'sortableTable'+this.lastAssignedId++;
		}
		tableId = table.id;		
		// if this table does not have a thead, we don't want to know about it
		if (!table.tHead || !table.tHead.rows || 0 == table.tHead.rows.length) {
			return;
		}
		
		// we'll assume that the last row of headings in the thead is the row that 
		// wants to become clickable
		var row = table.tHead.rows[table.tHead.rows.length - 1];
		for (var i=0; i < row.cells.length; i++) {
		
			// create a link with an onClick event which will 
			// control the sorting of the table
			var linkEl = createElement('a');
			linkEl.href = '#';
			linkEl.onclick = this.headingClicked;
			linkEl.setAttribute('columnId', i);
			//sort pattern for the column
			linkEl.setAttribute('sortPattern', row.cells[i].className);
			linkEl.title = 'Click to sort';
			
			// move the current contents of the cell that we're 
			// hyperlinking into the hyperlink
			var innerEls = row.cells[i].childNodes;
			for (var j = 0; j < innerEls.length; j++) {
				linkEl.appendChild(innerEls[j]);
			}
			
			// and finally add the new link back into the cell
			row.cells[i].appendChild(linkEl);

			var spanEl = createElement('span');
			spanEl.className = 'tableSortArrow';
			spanEl.appendChild(document.createTextNode('\u00A0\u00A0'));
			row.cells[i].appendChild(spanEl);

		}
	
		if (css.elementHasClass(table, 'autostripe')) {
			this.isOdd = false;
			var rows = table.tBodies[0].rows;
		
			// We appendChild rows that already exist to the tbody, so it moves them rather than creating new ones
			for (var i=0;i<rows.length;i++) { 
				this.doStripe(rows[i]);
			}
		}
	},
	
	headingClicked: function(e) {
		var that = standardistaTableSorting.that;
		
		// linkEl is the hyperlink that was clicked on which caused
		// this method to be called
		var linkEl = getEventTarget(e);
		
		// directly outside it is a td, tr, thead and table
		var td     = linkEl.parentNode;
		var tr     = td.parentNode;
		var thead  = tr.parentNode;
		var table  = thead.parentNode;
		
		// if the table we're looking at doesn't have any rows
		// (or only has one) then there's no point trying to sort it
		if (!table.tBodies || table.tBodies[0].rows.length <= 1) {
			return false;
		}

		// the column we want is indicated by td.cellIndex
		var column = linkEl.getAttribute('columnId') || td.cellIndex;
	    sortPattern = linkEl.getAttribute('sortPattern');

		var index = sortPattern.indexOf('_');
		sequenceIterator = index+1;
		//var column = td.cellIndex;
		
		// find out what the current sort order of this column is
		var arrows = css.getElementsByClass(td, 'tableSortArrow', 'span');
		var previousSortOrder = '';
		if (arrows.length > 0) {
			previousSortOrder = arrows[0].getAttribute('sortOrder');
		}
		
		// work out how we want to sort this column using the data in the first cell
		// but just getting the first cell is no good if it contains no data
		// so if the first cell just contains white space then we need to track
		// down until we find a cell which does contain some actual data
		var itm = ''
		var rowNum = 0;
		while ('' == itm && rowNum < table.tBodies[0].rows.length) {
			itm = that.getInnerText(table.tBodies[0].rows[rowNum].cells[column]);
			rowNum++;
		}
		var sortfn = that.determineSortFunction(itm);

		// To get the sort function name based on data pattern 
		// to manually invoke it rather than event driven
		var sortfnName = that.determineSortFunctionName(itm);
		
		// Identifies which is the entry sort routine based on heading clicked
		// setting all default values to entry sort routine identifier

		that.resetData();
		
		//Sets the entry sort routine identifier to true
		switch(sortfnName)
		{
			case 'sortDate':
			  isEntrySortDate = true;
	 		  sortDateCount++;
			  break;    
			case 'sortCaseInsensitive':
			  isEntrySortCaseInsensitive = true;
			  sortCaseInsensitiveCount++;
			  break;    
			case 'sortCurrency':
			  isEntrySortCurrency = true;	
	 		  sortCurrencyCount++;
			  break;    
			case 'sortNumeric':
			  isEntrySortNumeric = true;	
			  sortNumericCount++;
			  break;    
			case 'sortIP':
			  isEntrySortIP = true;	
			  sortIPCount++;
			  break;    
		}	

		//ends here
		// if the last column that was sorted was this one, then all we need to 
		// do is reverse the sorting on this column
		if (table.id == that.lastSortedTable && column == that.sortColumnIndex) {
			newRows = that.newRows;
			newRows.reverse();
		// otherwise, we have to do the full sort
		} else {
			that.sortColumnIndex = column;
			that.actualSortColumnIndex = column;
			var newRows = new Array();

			for (var j = 0; j < table.tBodies[0].rows.length; j++) { 
				newRows[j] = table.tBodies[0].rows[j]; 
			}
			newRows.sort(sortfn);
		}
		that.moveRows(table, newRows);
		that.newRows = newRows;
		that.lastSortedTable = table.id;
		
		// now, give the user some feedback about which way the column is sorted
		
		// first, get rid of any arrows in any heading cells
		var arrows = css.getElementsByClass(tr, 'tableSortArrow', 'span');
		for (var j = 0; j < arrows.length; j++) {
			var arrowParent = arrows[j].parentNode;
			arrowParent.removeChild(arrows[j]);

			if (arrowParent != td) {
				spanEl = createElement('span');
				spanEl.className = 'tableSortArrow';
				spanEl.appendChild(document.createTextNode('\u00A0\u00A0'));
				arrowParent.appendChild(spanEl);
			}
		}
		
		// now, add back in some feedback 
		var spanEl = createElement('span');
		spanEl.className = 'tableSortArrow';
		if (null == previousSortOrder || '' == previousSortOrder || 'DESC' == previousSortOrder) {
			spanEl.appendChild(document.createTextNode(' \u2191'));
			spanEl.setAttribute('sortOrder', 'ASC');
		} else {
			spanEl.appendChild(document.createTextNode(' \u2193'));
			spanEl.setAttribute('sortOrder', 'DESC');
		}
		
		td.appendChild(spanEl);
		// resets the entry sort routine identifier to false
		that.resetData();
		// resets sort column index to the original value for reverse process
		that.sortColumnIndex = column;
		return false;
	},

	resetData : function(){
		isEntrySortDate = false;
		isEntrySortCaseInsensitive = false;
        isEntrySortCurrency = false;	
	    isEntrySortNumeric = false;	
        isEntrySortIP = false;
		sortCaseInsensitiveCount = 0;
		sortDateCount = 0;
		sortCurrencyCount = 0;
		sortNumericCount = 0;
		sortIPCount = 0;
	}, 

	getInnerText : function(el) {
		
		if ('string' == typeof el || 'undefined' == typeof el) {
			return el;
		}

		if (el.innerText) {
			return el.innerText;  // Not needed but it is faster
		}

		var str = el.getAttribute('standardistaTableSortingInnerText');
		if (null != str && '' != str) {
			return str;
		}
		str = '';

		var cs = el.childNodes;
		var l = cs.length;
		for (var i = 0; i < l; i++) {
			// 'if' is considerably quicker than a 'switch' statement, 
			// in Internet Explorer which translates up to a good time 
			// reduction since this is a very often called recursive function
			if (1 == cs[i].nodeType) { // ELEMENT NODE
				str += this.getInnerText(cs[i]);
				//break;
			} else if (3 == cs[i].nodeType) { //TEXT_NODE
				str += cs[i].nodeValue;
				//break;
			}
		}

		str = str.replace(/^\s+/g, '').replace(/\s+$/g, '');
		
		// set the innertext for this element directly on the element
		// so that it can be retrieved early next time the innertext
		// is requested
		el.setAttribute('standardistaTableSortingInnerText', str);
		
		return str;
	},

	determineSortFunction : function(itm) {
		
		var sortfn = this.sortCaseInsensitive;
		
		if (itm.match(/^\d\d[\/-]\d\d[\/-]\d\d\d\d$/)) {
			sortfn = this.sortDate;
		}
		if (itm.match(/^\d\d[\/-]\d\d[\/-]\d\d$/)) {
			sortfn = this.sortDate;
		}
		if (itm.match(/^[£$]/)) {
			sortfn = this.sortCurrency;
		}
		if (itm.match(/^\d?\.?\d+$/)) {
			sortfn = this.sortNumeric;
		}
		if (itm.match(/^[+-]?\d*\.?\d+([eE]-?\d+)?$/)) {
			sortfn = this.sortNumeric;
		}
    		if (itm.match(/^([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])$/)) {
        		sortfn = this.sortIP;
   		}

		return sortfn;
	},

	determineSortFunctionName : function(itm) {
		
		var sortfn = 'sortCaseInsensitive';
		
		if (itm.match(/^\d\d[\/-]\d\d[\/-]\d\d\d\d$/)) {
			sortfn = 'sortDate';
		}
		if (itm.match(/^\d\d[\/-]\d\d[\/-]\d\d$/)) {
			sortfn = 'sortDate';
		}
		if (itm.match(/^[£$]/)) {
			sortfn = 'sortCurrency';
		}
		if (itm.match(/^\d?\.?\d+$/)) {
			sortfn = 'sortNumeric';
		}
		if (itm.match(/^[+-]?\d*\.?\d+([eE]-?\d+)?$/)) {
			sortfn = 'sortNumeric';
		}
    		if (itm.match(/^([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])$/)) {
        		sortfn = 'sortIP';
   		}

		return sortfn;
	},
	
	sortCaseInsensitive : function(a, b) {
		var that = standardistaTableSorting.that;
		if(sortCaseInsensitiveCount == 1){
			sortCaseInsensitiveCount++;
			that.sortColumnIndex = that.actualSortColumnIndex;
		}
		var aa = that.getInnerText(a.cells[that.sortColumnIndex]).toLowerCase();
		var bb = that.getInnerText(b.cells[that.sortColumnIndex]).toLowerCase();
		if (aa==bb) {
			if(isEntrySortCaseInsensitive){
				sequenceIterator++;
				return that.sortSequence(a,b);
			}else{
				return 0;
			}
		} else if (aa<bb) {
			return -1;
		} else {
			return 1;
		}
	},
	
	getItem : function(column){
		var that = standardistaTableSorting.that;
		var table = that.tableToSort();
		var itm = ''
		var rowNum = 0;

		while ('' == itm && rowNum < table.tBodies[0].rows.length) {
				itm = that.getInnerText(table.tBodies[0].rows[rowNum].cells[column]);
				rowNum++;
		}
		return itm;
	},

	//Have to develop a sequence engine
	sortSequence : function(a,b){
		var that = standardistaTableSorting.that;
		var rval = -1;
		for(var i = sequenceIterator; i <= (sortPattern.length-1); i++){
				coulmn = sortPattern.charAt(i);
	 			var itm = that.getItem(coulmn);
				var sortfn = that.determineSortFunctionName(itm);
				that.sortColumnIndex = coulmn;
				rval = eval('that.'+sortfn+'(a,b)');
				if(rval == 0){
					continue;
				}
				break;
		}
		that.setData();
		return rval;	
	},

    setData : function(){
		if (isEntrySortCaseInsensitive){
			sortCaseInsensitiveCount = (isEntrySortCaseInsensitive ? 1 : 0);
		}else if (isEntrySortDate){
			sortDateCount = (isEntrySortDate ? 1 : 0);
		}else if (isEntrySortCurrency){
			sortCurrencyCount = (isEntrySortCurrency ? 1 : 0);
		}else if (isEntrySortNumeric){
			sortNumericCount = (isEntrySortNumeric ? 1 : 0);
		}else if (isEntrySortIP){
			sortIPCount = (isEntrySortIP ? 1 : 0);
		}
		sequenceIterator = sortPattern.indexOf('_') + 1;
	},

	sortDate : function(a,b) {
		var that = standardistaTableSorting.that;
		if(sortDateCount == 1){
			sortDateCount++;
			that.sortColumnIndex = that.actualSortColumnIndex;
		}
		// y2k notes: two digit years less than 50 are treated as 20XX, greater than 50 are treated as 19XX
		var aa = that.getInnerText(a.cells[that.sortColumnIndex]);
		var bb = that.getInnerText(b.cells[that.sortColumnIndex]);
		var dt1, dt2, yr = -1;
		
		if (aa.length == 10) {
			dt1 = aa.substr(6,4)+aa.substr(3,2)+aa.substr(0,2);
		} else {
			yr = aa.substr(6,2);
			if (parseInt(yr) < 50) { 
				yr = '20'+yr; 
			} else { 
				yr = '19'+yr; 
			}
			dt1 = yr+aa.substr(3,2)+aa.substr(0,2);
		}
		
		if (bb.length == 10) {
			dt2 = bb.substr(6,4)+bb.substr(3,2)+bb.substr(0,2);
		} else {
			yr = bb.substr(6,2);
			if (parseInt(yr) < 50) { 
				yr = '20'+yr; 
			} else { 
				yr = '19'+yr; 
			}
			dt2 = yr+bb.substr(3,2)+bb.substr(0,2);
		}

		if (dt1==dt2) {
			if(isEntrySortDate){
				sequenceIterator++;
				return that.sortSequence(a,b);
			}else{
				return 0;
			}
		} else if (dt1<dt2) {
			return -1;
		}else{
			return 1;
		}
	},

	sortCurrency : function(a,b) {
		var that = standardistaTableSorting.that;
		if(sortCurrencyCount == 1){
			sortCurrencyCount++;
			that.sortColumnIndex = that.actualSortColumnIndex;
		}
		var aa = that.getInnerText(a.cells[that.sortColumnIndex]).replace(/[^0-9.]/g,'');
		var bb = that.getInnerText(b.cells[that.sortColumnIndex]).replace(/[^0-9.]/g,'');
		if(parseFloat(aa)==parseFloat(bb)){
			if(isEntrySortCurrency){
				sequenceIterator++;
				return that.sortSequence(a,b);
			}else{
				return 0;
			}
		}else if(parseFloat(aa)<parseFloat(bb)){
			return -1;
		}else{
			return 1;
		}
		//return parseFloat(aa) - parseFloat(bb);
	},

	sortNumeric : function(a,b) { 
		var that = standardistaTableSorting.that;
		if(sortNumericCount == 1){
			sortNumericCount++;
			that.sortColumnIndex = that.actualSortColumnIndex;
		}
		var aa = parseFloat(that.getInnerText(a.cells[that.sortColumnIndex]));
		var aaIsNaN = isNaN(aa);
		var bb = parseFloat(that.getInnerText(b.cells[that.sortColumnIndex])); 
		var bbIsNaN = isNaN(bb);
		if (aaIsNaN && bbIsNaN)
			return 0;
		else if (aaIsNaN)
			return -1;
		else if (bbIsNaN)
			return 1;
		if(aa==bb){
			if(isEntrySortNumeric){
				sequenceIterator++;
				return that.sortSequence(a,b);
			}else{
				return 0;
			}
		}else if (aa<bb){
			return -1;
		}else{
			return 1;
		}
	},

	makeStandardIPAddress : function(val) {
		var vals = val.split('.');

		for (x in vals) {
			val = vals[x];

			while (3 > val.length) {
				val = '0'+val;
			}
			vals[x] = val;
		}

		val = vals.join('.');

		return val;
	},

	sortIP : function(a,b) { 
		var that = standardistaTableSorting.that;
		if(sortIPCount == 1){
			sortIPCount++;
			that.sortColumnIndex = that.actualSortColumnIndex;
		}
		var aa = that.makeStandardIPAddress(that.getInnerText(a.cells[that.sortColumnIndex]).toLowerCase());
		var bb = that.makeStandardIPAddress(that.getInnerText(b.cells[that.sortColumnIndex]).toLowerCase());
		if (aa==bb) {
			//return 0;
			if(isEntrySortIP){
				sequenceIterator++;
				return that.sortSequence(a,b);
			}else{
				return 0;
			}
		} else if (aa<bb) {
			return -1;
		} else {
			return 1;
		}
	},

	moveRows : function(table, newRows) {
		this.isOdd = false;

		// We appendChild rows that already exist to the tbody, so it moves them rather than creating new ones
		for (var i=0;i<newRows.length;i++) { 
			var rowItem = newRows[i];

			this.doStripe(rowItem);

			table.tBodies[0].appendChild(rowItem); 
		}
	},
	
	doStripe : function(rowItem) {
		if (this.isOdd) {
			css.addClassToElement(rowItem, 'odd');
			css.removeClassFromElement(rowItem, 'even');
		} else {
			css.addClassToElement(rowItem, 'even');
			css.removeClassFromElement(rowItem, 'odd');
		}
		
		this.isOdd = !this.isOdd;
	}

}

function standardistaTableSortingInit() {
	standardistaTableSorting.init();
}

addEvent(window, 'load', standardistaTableSortingInit)
