/*********************************************************************************************
COPYRIGHT 2008 Joel Coehoorn 
This code is provided without warranty.  Use at your own risk. 
You are permitted to use this code however you want, including restribute and/or modify it,
as long as you either:
1) You retain this copyright notice.
or
2) You notify the author first.  Only notification is required, not permission.
*********************************************************************************************/
// tblID is the only required parameter.  It is the unique ID of the table to sort
// maxRows and maxRowsExceededCallback are optional.  If the table has more rows than maxRows,  maxRowsExceededCallback() is called
// default maxRows value is 5000, pass a negative value to set for no maximum
function TableSorter(tblID, maxRows, maxRowsExceededCallback)
{  
	// *******PRIVATE MEMBERS / CONSTRUCTOR**************
	var table = document.getElementById(tblID);
	var defaultMax = 5000;
	var maxrows = maxRows || defaultMax;
	var maxRowsExceededFn = maxRowsExceededCallback;
		
	// *********PUBLIC METHODS************
	
	// sets up default sort method for the whole table (will replace any custom sortTypes set on the table), can be chained
	//(maxRows /*[optional, default 5000] max number of rows to sort- if more than this sorting will be disabled */)  
	// This work is normally done by the constructor, so there's no need to call this unless styles were also removed at some point
	this.setDefaultSortStyles = setDefaultSortStyles;  
	
	// sets a custom sort style for one column, can be chained
	// (colIdx, sortType, dir)
	// colIdx (required) is the index for the column to setup.  Columns are 0-indexed
	// dir (optional) is the direction the column is sorted when not already sorting on this column.  Can be either 1 (ascending) or -1 (descending).  Default is ascending.
	// sortType (optional) is the type of sort used.  Valid sort types: "date", "numeric", "text", "case-insenstive"
	/* sortType can also be a function that accepts one argument.  The function should return the "normalized" equivalent of the argument.
		For example, a case-insensitive string function would look like this:  fn(a) { return a.toUpperCase();} */
	this.setColumnSortStyle = setColumnSortStyle;
	
	// removes all sorting from the table, can be chained
	this.removeSorting = removeSorting; 
	
	// removes sorting from a single column (0-indexed), can be chained
	// ( colIdx /* the index of the column to remove sorting */)
	this.removeSortingForColumn = removeSortingForColumn;
	
	// (useCallback /* [optional, default false] bool, determines whether to call maxRowsExceededFn if there are too many rows*/)  
	//returns true if the table has fewer rows than the max, otherwise false. 
	this.checkMaxRows = checkMaxRows; 
	
	// sets a new callBack function to use when too many rows are detected: can be chained
	// (callBack) -- the function to call, or null.  If it's not null or a function, nothing happens.
	this.setMaxRowsExceededCallback = setMaxRowsExceededCallback;
	
	// sets a new maxrows value and checks to make sure the table is within the new constraint, uses the callback if the table fails, can be chained
	// (maxRows) -- new value.  0/null is ignored(existing value is used- check still happens), negative value means no maximum
	this.setMaxRows = setMaxRows;		
	
/* *************************************************************************************************************** */
/*     PUBLIC API IS ABOVE THIS POINT.  EVERYTHING BELOW IS AN IMPLEMENTATION DETAIL                                                 */
/******************************************************************************************************************/
	
	// ***** PUBLIC METHOD IMPLEMENTATIONS*********
	function setColumnSortStyle(colIdx, sortType, dir)
	{
		if (!table) return this;
		if (!checkMaxRows(false)) return this;
		
		var head = table.rows[0];
		var cell = head.cells[colIdx];
		cell.onclick = function () {OnSortClick(cell, sortType, dir);};
		cell.onmouseover=function() {cell.style.cursor = "pointer";};
		cell.onmouseout=function() {cell.style.cursor = "auto";};
		return this; // for chaining
	}
	
	// maxRows (integer, default 5000)  
	// Use a negative number to indicate no maximum.
	// Optional: function called when object is constructed
	function setDefaultSortStyles(maxRows)
	{
		maxRows = maxRows || defaultMax;
		removeSorting();
		maxrows = maxRows;
		
		if (!table || table.rows.length < 3) return this;
		if (!checkMaxRows(true)) return this;
  
		var head = table.rows[0];
		for (var c = 0,cl=head.cells.length;c<cl;c+=1)
		{
			setColumnSortStyle(c);
		}
		return this; // for chaining
	}
	
	function checkMaxRows(useCallback)
	{
		if (maxrows > 0 && table.rows.length > maxrows)
		{
			if( maxRowsExceededFn && useCallback ) maxRowsExceededFn();	
			return false; 
		}
		return true;
	}
	
	function removeSorting()
	{
		var head = table.rows[0];
		for (var c = 0,cl=head.cells.length;c<cl;c+=1)
		{
			removeSortingForColumn(c);
		}
		return this; // for chaining
	}
	
	function removeSortingForColumn(colIdx)
	{
		var cell = table.rows[0].cells[colIdx];
		if (cell)
		{
			cell.onclick = null;
			cell.onmouseover= null;
			cell.onmouseout=null;
		}
		return this; // for chaining
	}
	
	function setMaxRowsExceededCallback(callBack)
	{
		if (!callBack || typeof(callBack) === "function")
			maxRowsExceededFn = callBack;
		return this; // for chaining
	}
	
	function setMaxRows(maxRows)
	{
		maxrows = maxRows || maxrows;
		checkMaxRows(true);
		return this;
	}

	// ********* PRIVATE METHOD IMPLEMENTATIONS ****************
		
	function sortTable(colIdx, dir, sortType)
	{
		// Get and validate table to sort
		if (!table || table.rows.length <= 3) return false; 
		if (!checkMaxRows(true)) return false;
			 		 
		// default normalization function (text- case insensitve) (may be overridden depending on sort data type)
		var fnNorm = function(val) { return val.toUpperCase();}; 
	   
		// Check sort type and set Normalization function
		sortType = sortType || "auto"; //default sortType if none specified
		if (typeof(sortType) === "function")
			fnNorm = sortType;
		else if (sortType === "auto")
		{
			/* Find first row with a value for this column and use the value
			   to guess at the type of sort (date, numeric, text) and set the
			   normalization function */
			var itm = ""; 
			for (var i=1,il=table.rows.length;itm === "" && i < il;i+=1)
				itm = innerText(table.rows[i].cells[colIdx]).replace(/^\s+|\s+$/g,"");
				
			if (!isNaN(Date.parse(itm)))  // first item is a date
				sortType = "date";
			else if (!isNaN(itm)) // first item is a number
				sortType = "numeric";	
			else sortType = "text";	        
		}
		
		if (sortType === "date")
			fnNorm = function(val) {var d = Date.parse(val); if (isNaN(d)) return val.toUpperCase(); else return d;};
		else if (sortType === "numeric")
			fnNorm = function(val) {var d = +val; if (isNaN(d)) return val.toUpperCase(); else return d;};
		else if (sortType === "case-sensitive")
			fnNorm = function(val) {return val;};
		
		// now we can build the sort function   
		var fnSort = function(a,b)
			{   // a and b are table rows to compare- need to extract correct values from those cells (colIdx will be in scope with correct value at time this is called)
				var c = fnNorm(innerText(a.cells[colIdx]));
				var d = fnNorm(innerText(b.cells[colIdx]));
								
				// can't just use "return (c-d)*dir;"
				// it may not have a good conversion to a numeric value for use with the - operator
				if (c==d) return 0; // use fuzzy compare rather than strong===
				if (c<d) return -1*dir;
				return dir; 
			}
	
		// build temporary array of rows to sort        
		var newRows = new Array();
		for (var j=1,jl=table.rows.length;j<jl;j+=1) // start at 1, row 0 is header
			newRows[j-1]=table.rows[j];
			
		// SORT!!
		newRows.sort(fnSort);
		  
		// update table with sorted rows
		var tablebody = table.getElementsByTagName("tbody")[0] || table.rows[0].parentNode;
		for (var k=0,kl=newRows.length;k<kl;k+=1)
			tablebody.appendChild(newRows[k]);  
			//something in the DOM knows you appended a row that already exists, and will just move it
		
		// Apply alternating row styles
		if (RowCss === AltCss && RowClass === AltClass) return true; 
		for (var l=1,ll=table.rows.length;l<ll;l+=1)
		{
			if (l%2>0)
			{
				table.rows[l].style.cssText = RowCss;
				table.rows[l].className = RowClass;
			}
			else
			{
				table.rows[l].style.cssText=AltCss;                 
				table.rows[l].className = AltClass;
			}
		}  
		return true;
	}
		
	// used for preserving alternative row styles after sorts
	var RowCss, AltCss, RowClass, AltClass;
	
	function OnSortClick(cell, sortType, dir)
	{	
		// get sort direction, arrow
		dir = dir || 1; // default is ascending
		if (cell.innerHTML.indexOf(" \u25B2") > -1)           
			dir = -1;
		else if (cell.innerHTML.indexOf(" \u25BC") > -1)
			dir = 1;
		var arrow = (dir > 0)?" \u25B2":" \u25BC"; 
		
		// check for alternating row styles before calling setTimeout, to prevent accidentally grabbing
		// the wrong style if there is javascript to track the current rows on mouse hover
		// I thought about moving this to the constructor, but I want it to happen on every sort in case the styles can change now and then
		RowCss = table.rows[1].style.cssText;
		AltCss = table.rows[2].style.cssText;
		RowClass = table.rows[1].className;
		AltClass = table.rows[2].className;
			 
		// set progress cursor: for large tables this will eat up significant cpu and lock up browser
		document.body.style.cursor = "progress";	
		setTimeout(function() // allow this method to return so browser can update cursor
		{
			// SORT   
			var success = sortTable(cell.cellIndex, dir, sortType);
			document.body.style.cursor = "auto";	
			if (!success) return;
			
			//remove all arrows
			for (var c = 0,cl=table.rows[0].cells.length;c<cl;c+=1)
			{
				var curCell = table.rows[0].cells[c];
				curCell.innerHTML = curCell.innerHTML.replace(" \u25B2", "").replace(" \u25BC", "");
			}
		  
			// set new arrow
			cell.innerHTML += arrow;
		}, 40); 
		// you can adjust the timeout to make immediate progress cursor appearance more (longer) or less(shorter) reliable, 
		// Too long and it delays the sort too much, which isn't needed or desirable for small (normal tables)
		// Too short and often the cursor won't appear.
	}
	
	// innerText - get text value from an element
	//  this is a little simplistic: for example, it may not work well if there are tags like <span> inside the cell
	function innerText(element)
	{
		if (typeof element === "string" || typeof element === "undefined") return element;
		if (element.innerText) return element.innerText;
		if (element.innerHTML) return element.innerHTML;
		return "";
	}
	
	setDefaultSortStyles(maxRows);
}