function buildTableSorter(pcID, paColumns, pnInitialColumn, poOnSortCompleteHandler)
{
	var loSorter = new TableSorter2(pcID);
	for(var i = 0; i < paColumns.length; i++)
	{
		loSorter.addColumn(paColumns[i], i);
	}
	loSorter.initialize(pnInitialColumn);
	
	if(poOnSortCompleteHandler)
		loSorter.onSortComplete = poOnSortCompleteHandler;

	// save a reference to the sorter on the table so we can get to it again if we need to
	loSorter.oTable.oSorter = loSorter;
}

function TableSorter2(pcID)
{
	this.oTable = document.getElementById(pcID);
	this.oTBody = this.oTable.tBodies[0];
	this.aColumns = [];
	this.lGrouped = false;
	this.nCurrentCol = -1;
	this.lSubtotals	= false;
	this.onSortComplete = null;
	
	return this;
}

// detect browser here if necessary and disable tablesorter
if(false)
{
	// table sorter not supported
	TableSorter2.prototype.addColumn = function(poColumn) {};
	TableSorter2.prototype.initialize = function(paColumns, pnInitial) {};
}
else
{
	// table sorter supported
	TableSorter2.prototype.addColumn = function(poColumn, pnCol)
	{
		if(this.oTBody.rows.length > 0)
		{
			this.aColumns[this.aColumns.length] = poColumn;

			if(typeof(poColumn.cType)=="string")
			{
				this.lSubtotals = this.lSubtotals || poColumn.lSubtotal;
			
				var loTH = this.oTable.tHead.rows[0].cells[pnCol];
				loTH.className = poColumn.lDescending ? "sortable desc" : "sortable";

				var loHandler = function(e,n){ return function() { e.sort(n); } }(this,pnCol);
				if(loTH.addEventListener) // DOM syntax
				{
					loTH.addEventListener("click", loHandler, false);
				}
				else
				{
					if(loTH.attachEvent) // IE syntax
					{
						loTH.attachEvent("onclick", loHandler);
					}
					else // old style
					{
						loTH.onclick = loHandler;
					}
				}
			}
		}
	}

	TableSorter2.prototype.initialize = function(pnInitial)
	{
		if(this.oTBody.rows.length > 0)
		{
			if(!this.hasGroups())
			{
				this.removeGroups = function() {};
				this.addGroups = function() {};
				this.collapseGroups = function() {};
			}
	
			if(typeof(pnInitial)=="number" && pnInitial >= 0)
			{
				this.sort(pnInitial);
			}
		}
	}

	TableSorter2.prototype.hasGroups = function() 
	{
		for(var i = 0; i < this.aColumns.length; i++)
			if(this.aColumns[i].lGroup)
				return true;
		return false; 
	};

	TableSorter2.prototype.sort = function(pnCol) 
	{ 
		var ld1 = new Date();

		this.hide();
		this.deselectCol();
		
		if(this.lGrouped)
			this.removeGroups(new RegExp("\\bsortGroup\\b"));

		if(this.nCurrentCol == pnCol)
			this.aColumns[pnCol].lDescending = !this.aColumns[pnCol].lDescending;
		this.nCurrentCol = pnCol;

		this.sortRows(pnCol);

		if(this.aColumns[pnCol].lGroup)
		{
			this.addGroups("sortGroup", pnCol, "subtotal");
		
			if(this.aColumns[pnCol].lCollapse)
				this.collapseGroups(new RegExp("\\bsortGroup\\b"), this.aColumns[pnCol].cInactive);
		}
		
		this.selectCol(pnCol);
		this.show();
		
		if(this.onSortComplete) this.onSortComplete(this.oTable);
		
		var ld2 = new Date();

		this.addDebugInfo((ld2-ld1)/1000);
	};

	TableSorter2.prototype.hide = function() 
	{ 
		this.oTBody.style.display = "none";
	};
	TableSorter2.prototype.show = function()
	{ 
		this.oTBody.style.display = "";
	};
	TableSorter2.prototype.selectCol = function(pnCol) 
	{ 
		var loRows = this.oTable.tHead.rows;

		for(var i = 0; i < loRows.length; i++)
		{
			var loTD = this.oTable.tHead.rows[i].cells[pnCol];
			if(loTD)
			{
				loTD.className += (this.aColumns[pnCol].lDescending ? " rcurrent" : " current");
			}
		}
	};
	TableSorter2.prototype.deselectCol = function() 
	{ 
		var loRows = this.oTable.tHead.rows;
		for(var i = 0; i < loRows.length; i++)
		{
			var r = loRows[i];

			for(var j = 0; j < r.cells.length; j++)
			{
				var c = r.cells[j];

				if(c.className.match("sortable"))
				{
					c.className = c.className.replace("current","");
					c.className = c.className.replace("rcurrent","");
				}
			}
		}
	};
	// put this function into a global variable first, rather than straight onto the
	// prototype so that we can restore it if we need to 
	// (i.e. after if it has been remvoed by the initialize() method)
	var TableSorter2_removeGroups = function(pcTRClass) 
	{ 
		this.lGrouped = false;
		for(var i=this.oTBody.rows.length-1; i>=0; i--)
		{
			this.oTBody.rows[i].style.display = "";
			if(this.oTBody.rows[i].className.match(pcTRClass))
			{
				this.oTBody.removeChild(this.oTBody.rows[i]);
			}
		}
	};
	TableSorter2.prototype.removeGroups = TableSorter2_removeGroups;

	TableSorter2.prototype.addGroups = function(pcTRClass, pnCol, pcSubtotalClass) 
	{ 
		this.lGrouped = true;
		var lnCols = this.oTBody.rows[0].cells.length;
		var lcGroupValue = this.oTBody.rows[this.oTBody.rows.length-1].cells[pnCol].getAttribute("rme-sortGroup");
		var lnRows = 0;

		var lnPrevRow = this.oTBody.rows.length;
		var laTotal = new Array(lnCols);
		this.resetSubtotal(laTotal);	

		for(var i=this.oTBody.rows.length-1; i>0; i--)
		{
			this.computeSubtotal(laTotal, this.oTBody.rows[i].cells);	
			lnRows ++;
			
			if(lcGroupValue != this.oTBody.rows[i-1].cells[pnCol].getAttribute("rme-sortGroup"))
			{
				if(lcGroupValue != null && lcGroupValue.length > 0)
				{
					if(lnRows>1)
						this.addGroupSubtotalRow(laTotal, lnPrevRow, pcTRClass+" "+pcSubtotalClass);
					this.addGroupRow(lcGroupValue, i, lnCols, lnRows, pcTRClass, pcSubtotalClass);
					lnPrevRow = i;
				}
				lcGroupValue = this.oTBody.rows[i-1].cells[pnCol].getAttribute("rme-sortGroup");
				lnRows = 0;
				this.resetSubtotal(laTotal);
			}
		}

		this.computeSubtotal(laTotal, this.oTBody.rows[0].cells);	
		if(lnRows>0)
			this.addGroupSubtotalRow(laTotal, lnPrevRow, pcTRClass+" "+pcSubtotalClass);
		this.addGroupRow(lcGroupValue, 0, lnCols, lnRows+1, pcTRClass, pcSubtotalClass);
	};
	TableSorter2.prototype.addGroupRow = function(pcValue, pnRow, pnCols, pnRows, pcTRClass, pcSubtotalClass, plNoCollapse) 
	{
		var loNewRow = document.createElement("TR");
		loNewRow.className = pcTRClass
		var loNewTD = loNewRow.appendChild(document.createElement("TD"));
		loNewTD.setAttribute("colSpan",pnCols.toString());

// 2004-01-19 tom - use DOM syntax
		loNewTD.appendChild(document.createTextNode(pcValue + " "));

		if(pnRows >= 5)
		{
			// 2005-07-11 tom - add row count to group header
			var loCount = document.createElement("SPAN");
			loCount.className = "count";
			loCount.appendChild(document.createTextNode("("+pnRows.toString()+")"));
			loNewTD.appendChild(loCount);
		}
		
		if(plNoCollapse)
		{
			loNewTD.collapse = function() {return false;};
		}
		else
		{
			loNewTD.collapse = function(poTR, poTBody, pcStop){return function()
			{
				var lcDisplay = "none";
				if(poTR.className.match("collapsed"))
				{
					lcDisplay = "";
					poTR.className = poTR.className.replace("collapsed","");
				}
				else
				{
					poTR.className += " collapsed";
				}
			
				for(var i=poTR.rowIndex; i<poTBody.rows.length; i++)
				{
					if(poTBody.rows[i].className.match(pcTRClass) && !poTBody.rows[i].className.match(pcStop))
					{
						break;
					}
					poTBody.rows[i].style.display = lcDisplay;
				}
	
				return false;
			};}(loNewRow, this.oTBody, pcSubtotalClass);
		}
		
		this.addClickEvent(loNewTD, loNewTD.collapse);
			
		this.oTBody.insertBefore(loNewRow, this.oTBody.rows[pnRow]);
	}
	TableSorter2.prototype.addClickEvent = function(poTD, poHandler)		
	{
// 2004-01-19 tom - try and get this working in as many browsers as possible...
		if(poTD.addEventListener) // DOM syntax
		{
			poTD.addEventListener("click", poHandler, false);
		}
		else
		{
			if(poTD.attachEvent) // IE syntax
			{
				poTD.attachEvent("onclick", poHandler);
			}
			else // old style
			{
				poTD.onclick = poHandler;
			}
		}
	}
	
	TableSorter2.prototype.resetSubtotal = function(paSubtotal)
	{
		if(this.lSubtotals)
		{
 			for(var i = 0; i < paSubtotal.length; i++)
	 		{
 				if(this.aColumns[i].lSubtotal)
	 			{
 					paSubtotal[i] = 0;
 				}
 				else
	 			{
 					paSubtotal[i] = "";
 				}
	 		}
		}
	}
	TableSorter2.prototype.computeSubtotal = function(paSubtotal, paCells)
	{
		if(this.lSubtotals)
		{
 			for(var i = 0; i < paSubtotal.length; i++)
	 		{
 				if(this.aColumns[i].lSubtotal)
	 			{
 					paSubtotal[i] += this.aColumns[i].getVal(paCells[i]);
 				}
	 		}
		}
	}
	TableSorter2.prototype.addGroupSubtotalRow = function(paTotal, pnRow, pcTRClass) 
	{
		if(this.lSubtotals)
		{
			var loNewRow = document.createElement("TR");
			loNewRow.className = pcTRClass
	
			for(var i = 0; i < paTotal.length; i++)
			{
				loNewRow.appendChild(this.aColumns[i].getTD(paTotal[i]));
			}
			if(pnRow < this.oTBody.rows.length)
			{
				this.oTBody.insertBefore(loNewRow, this.oTBody.rows[pnRow]);
			}
			else
			{
				this.oTBody.insertBefore(loNewRow, null);
			}
		}
	}
	TableSorter2.prototype.collapseGroups = function(pcGroupTR, pcInactiveTR) 
	{ 
		var loRows = this.oTBody.rows;
		if(loRows && loRows.length > 0)
		{
			var llInactive = true;
			for(var i = loRows.length-1; i >= 0; i--)
			{
				if(loRows[i].className.match(pcGroupTR))
				{
					if(llInactive)
					{
						loRows[i].cells[0].collapse();
					}
					llInactive = true;
					continue;
				}
				if(!loRows[i].className.match(pcInactiveTR))
				{
					llInactive = false;
				}
			}
		}				
	};

	TableSorter2.prototype.sortRows = function(pnCol)
	{
		var loRows = this.oTBody.rows, loCol = this.aColumns[pnCol], llReverse = loCol.lDescending;
		
		// this has been optimised a bit so isn't as clear as it was...
		var n = loRows.length;

		// this array stores the results of the getVal() function so we don't do too much DOM traversing
		var vs = new Array(n);

		// since we're going to need all of these, might as well get them first, shouldn't achieve much to get them on demand
		var i,j;
		for(i=0; i < n; i++)
		{
			vs[i] = loCol.getVal(loRows[i].cells[pnCol]);
		}

		// now the main sort:
		for(i=0; i < n-1; i++)
		{
			var lvMinVal = vs[i];
			var lnMinRow = i;

			if(llReverse)
			{
				for(j=i+1; j < n; j++)
				{
					if(vs[j] > lvMinVal)
					{
						lnMinRow = j;
						lvMinVal = vs[j];
					}
				}
			}
			else
			{
				for(j=i+1; j < n; j++)
				{
					if(vs[j] < lvMinVal)
					{
						lnMinRow = j;
						lvMinVal = vs[j];
					}
				}
			}

			// need tbody here to call InsertBefore() etc..
			this.swapRows(i, lnMinRow);

			// swap saved values too (not a straight swap as we removed & inserted before)
			// so, grab last value (remove), then shuffle all intervening rows up one, and insert...
			var tv = vs[lnMinRow];
			for(var ti = lnMinRow - 1; ti >= i; ti--)
			{
				vs[ti+1] = vs[ti]; 
			}
			vs[i] = tv;
		}
	}

	TableSorter2.prototype.swapRows = function(pnRow1, pnRow2)
	{
		if(pnRow1 != pnRow2)
		{
			// we don't really need to swap the two rows, just put the second before the first...
			var temp = this.oTBody.removeChild(this.oTBody.rows[pnRow2]);
			this.oTBody.insertBefore(temp,this.oTBody.rows[pnRow1]);
		}
	}

	TableSorter2.prototype.addDebugInfo = function(pnTime)
	{
		var loDiv = this.oTable.previousSibling;
		while(loDiv && (loDiv.nodeType != 1 || loDiv.className != "debugInfo"))
		{
			loDiv = loDiv.previousSibling;
		}
		if(loDiv)
		{
			var loSpan = loDiv.lastChild;
			while(loSpan && (loSpan.nodeType != 1 || loSpan.tagName != "SPAN"))
			{
				loSpan = loSpan.previousSibling;
			}
			if(loSpan)
			{
				loSpan.innerHTML = "Sort: "+pnTime+"s";
			}
		}
	}

}

function TableSorter2Column(pcType, plDescending, plGroup, pcInactive, plSubtotal)
{
	this.cType = pcType;
	this.lDescending = plDescending;
	this.lGroup = plGroup;
	if(typeof(pcInactive)=="string")
	{
		this.lCollapse = true;
		this.cInactive = pcInactive;	
	}
	else
	{
		this.lCollapse = false;
	}
	this.lSubtotal = plSubtotal
	
	pcType = typeof(pcType)=="string" ? pcType : "";

	switch(pcType)
	{
		case 'N':
		case 'n':
			this.getValEx = function(v) { var n = parseFloat(v); if(!isNaN(n)) return n; return v; };
			break;

		case 'D':
		case 'd':
			this.getValEx = function(v) { var d = this.parseDate(v); if(d) return d; return v; };
			break;

		case 'C':
		default:
			this.getValEx = function(v) { return v.toUpperCase(); };
			break;
	}
		
	return this;
}
TableSorter2Column.prototype.getVal = function(poTD)
{
	if(poTD.xRMESortKey == null)
	{
 		poTD.xRMESortKey = poTD.getAttribute("rme-sortKey");
	 	if(poTD.xRMESortKey == null)
 		{
 			poTD.xRMESortKey = this.normalizeString(this.getTextValue(poTD));
	 	}
		poTD.xRMESortKey = this.getValEx(poTD.xRMESortKey);
	}
	return poTD.xRMESortKey;
}

if (document.ELEMENT_NODE == null) 
{
  document.ELEMENT_NODE = 1;
  document.TEXT_NODE = 3;
}

TableSorter2Column.prototype.getTextValue = function(el)
{
	var i;
	var s = "";

	// Find and concatenate the values of all text nodes contained within the element.
	for (i = 0; i < el.childNodes.length; i++)
	{
		if(el.childNodes[i].nodeType == document.TEXT_NODE)
		{
			s += el.childNodes[i].nodeValue;
		}
		else if(el.childNodes[i].nodeType == document.ELEMENT_NODE 
				&& el.childNodes[i].tagName == "BR")
		{
			s += " ";
		}
		// 1/10/2002 tom - extract the alt text of images, so we can sort by them too
	    else if(el.childNodes[i].nodeType == document.ELEMENT_NODE
				&& el.childNodes[i].tagName == "IMG")
		{
			s += el.childNodes[i].alt;
		}
	    else
		{
			// Use recursion to get text within sub-elements.
			s += this.getTextValue(el.childNodes[i]);
		}
	}
	return s;
}
// Convert a date string in the form dd/mm/yyyy to yyyymmdd
TableSorter2Column.prototype.parseDate = function(pcDate) 
{
	var re = this.re_isdate.exec(pcDate);
	var ret;

	if(re)
	{
		var lcYear = re[3];
		if(lcYear.length == 2)
		{
			if(parseInt(lcYear) < 50)
			{
				lcYear = "20"+lcYear;
			}
			else
			{
				lcYear = "19"+lcYear;
			}
		}
		ret = lcYear + ( (parseInt(re[2]) > 9) ? "" : "0" ) + re[2]
					+ ( (parseInt(re[1]) > 9) ? "" : "0" ) + re[1];

		if(re.length > 4)
		{
			if(re[9])
			{
				if(re[9].toLowerCase() == "pm" && parseInt(re[5]) < 12)
					ret += (parseInt(re[5])+12).toString();
				else if(parseInt(re[5]) > 9)
					ret += re[5];
				else
					ret += "0"+re[5];
			}
			else
			{
				if(parseInt(re[5]) > 9)
					ret += re[5];
				else
					ret += "0"+re[5];
			}

			ret += re[6] + ( re[7] ? re[7].substr(1,2) : "" );
		}
	}

	return ret;
}
// Regular Expression to match a date:  dd/mm/yyyy or dd-mm-yyyy, dd && mm can be one/two digit
TableSorter2Column.prototype.re_isdate = new RegExp("(\\d{1,2})[/-](\\d{1,2})[/-](\\d{2,4})" 
									+ "(\\s+(\\d{1,2}):(\\d{2})(:(\\d{2}))?\\s*([aApP][mM])?)?", "");
TableSorter2Column.prototype.normalizeString = function(s) 
{
	s = s.replace(this.re_nbsp," ");		// remove &nbsp; if any

	s = s.replace(this.re_whtSpMult, " ");  // Collapse any multiple whites space.
	s = s.replace(this.re_whtSpEnds, "");   // Remove leading or trailing white space.

	return s;
}
// Regular expressions for normalizing white space. (BRAINJAR code)
TableSorter2Column.prototype.re_whtSpEnds = new RegExp("^\\s*|\\s*$", "g");
TableSorter2Column.prototype.re_whtSpMult = new RegExp("\\s\\s+", "g");
TableSorter2Column.prototype.re_nbsp = new RegExp(String.fromCharCode(160),"gi");	// &nbsp; == unicode 160

TableSorter2Column.prototype.getTD = function(pnTotal)
{
	var loTD = document.createElement("TD");
	loTD.appendChild(document.createTextNode(this.lSubtotal ? pnTotal.toString() : ""));
	return loTD;
}

function TableSorter2NumberColumn(plDescending,plSubtotal,pnDecimals,pcClass)
{
	var obj = new TableSorter2Column("N",plDescending,false,false,plSubtotal);
	if(plSubtotal)
	{
		obj.getTD = function(cls){return function(pnTotal)
		{
			var loTD = document.createElement("TD");
			loTD.className = cls;
			loTD.appendChild(document.createTextNode(this.formatVal(pnTotal)));
			return loTD;
		};}(pcClass);

		obj.formatVal = function(precision){return function(pnValue)
		{
			return pnValue.toFixed(precision).toString();
		};}(pnDecimals);
	}
	return obj;
}

function TableSorter2CurrencyColumn(plDescending,plSubtotal)
{
	var obj = new TableSorter2Column("N",plDescending,false,false,plSubtotal);
	if(plSubtotal)
	{
		obj.getTD = function(pnTotal)
		{
			var loTD = document.createElement("TD");
			loTD.className = (pnTotal > 0) ? (pnTotal < 0) ? "currency DR" : "currency CR" : "currency";
			loTD.appendChild(document.createTextNode(this.formatVal(pnTotal)));
			return loTD;
		};

		obj.formatVal = function(pnValue)
		{
			var lcVal = pnValue.toFixed(2).toString(); 
			for(var i = lcVal.length - 6; i > 0; i-=3)
			{
				lcVal = lcVal.substr(0,i) + "," + lcVal.substr(i);
			}
			return "$"+lcVal;
		};
	}
	return obj;
}

