function JSTable(oTable) {
	this.element = oTable;
	this.tHead = oTable.tHead;
	this.tBody = oTable.tBodies[0];
	this.document = oTable.ownerDocument || oTable.document;
	this.sortColumn = null;
	this.descending = null;
	this.expandedLabels = null;
	this.detailArray = null;
	this.objectToRowMapper = null;
	this.isSortable = false;
	this.isExpanding = false;
	this.columnOffset = 0;
	
	var oThis = this;
	this._headerOnclick = function (e) {
		oThis.headerOnclick(e);
	};
	this._toggleRow = function(e){
		oThis.toggleRow(e);
	};
	// only IE needs this, for dealing with memory leak
	var win = this.document.defaultView || this.document.parentWindow;
	this._onunload = function () {
		oThis.destroy();
	};
	if (win && typeof win.attachEvent != "undefined") {
		win.attachEvent("onunload", this._onunload);
	}
	if (this.tHead.rows.length > 1){ //hack to deal with rowspan in first header row
		this.columnOffset = 1;
	}
}

JSTable.prototype.setSortTypes = function(oSortTypes,enableSorting){
	this.isSortable = enableSorting;
	this.initHeader(oSortTypes || []);
};

JSTable.prototype.setExpanding = function(labels,detailArray,objectToRowMapper){
	this.expandedLabels = labels;
	this.detailArray = detailArray;
	this.objectToRowMapper = objectToRowMapper;
	this.onbeforesort = this.prepareForSort;
	this.onsort = this.afterSort;
	this.isExpanding = true;
	this.addExpanderEventHandlers(this.tBody);
};

JSTable.gecko = navigator.product == "Gecko";
JSTable.msie = /msie/i.test(navigator.userAgent);
// Mozilla is faster when doing the DOM manipulations on
// an orphaned element. MSIE is not.
JSTable.removeBeforeSort = JSTable.gecko;

JSTable.prototype.onsort = function () {};

// binds sort type to the header cells so that reordering columns does
// not break the sort types
JSTable.prototype.initHeader = function (oSortTypes) {
	var c;
	//if(this.columnOffset == 1){
		c = this.tHead.rows[0].cells[0];
		if(oSortTypes[0] != null){
			c._sortType = oSortTypes[0];
		}
		if(this.isSortable){
			if (typeof c.addEventListener != "undefined"){
				c.addEventListener("click", this._headerOnclick, false);
			} else if (typeof c.attachEvent != "undefined")	{
				c.attachEvent("onclick", this._headerOnclick);
			}
		}
	//}
	var cells = this.tHead.rows[this.tHead.rows.length-1].cells;
	var l = cells.length;
	for (var i = 1; i < l + this.columnOffset; i++) {
		c = cells[i - this.columnOffset];
		if (oSortTypes[i] != null) {
			c._sortType = oSortTypes[i];
		}
		if(this.isSortable){
			if (typeof c.addEventListener != "undefined"){
				c.addEventListener("click", this._headerOnclick, false);
			} else if (typeof c.attachEvent != "undefined")	{
				c.attachEvent("onclick", this._headerOnclick);
			}
		}
	}
};

// remove arrows and events
JSTable.prototype.uninitHeader = function () {
	var c;
	try{
		c = this.tHead.rows[0].cells[0];
		if (typeof c.removeEventListener != "undefined"){
			c.removeEventListener("click", this._headerOnclick, false);
		} else if (typeof c.detachEvent != "undefined"){
			c.detachEvent("onclick", this._headerOnclick);
		}
		var cells = this.tHead.rows[this.tHead.rows.length-1].cells;
		var l = cells.length;
		for (var i = 0; i < l; i++) {
			c = cells[i];
			if (typeof c.removeEventListener != "undefined")
				c.removeEventListener("click", this._headerOnclick, false);
			else if (typeof c.detachEvent != "undefined")
				c.detachEvent("onclick", this._headerOnclick);
		}
	}
	catch(e){}
};

JSTable.prototype.updateHeaderArrows = function () {
	var cells = this.tHead.rows[this.tHead.rows.length-1].cells;
	var l = cells.length;
	if(this.columnOffset == 1){
		if(this.sortColumn == 0){
			this.tHead.rows[0].cells[0].className = "sort_column_" + (this.descending ? "descending" : "ascending");
			for(var i = 0; i < l; i++){
				cells[i].className = "";
			}
		} else {
			this.tHead.rows[0].cells[0].className = "";
			for(var i = 0; i < l; i++){
				if(i==(this.sortColumn - this.columnOffset)){
					cells[i].className = "sort_column_" + (this.descending ? "descending" : "ascending");
				} else {
					cells[i].className = "";
				}
			}
		}
	} else {
		for (var i = 0; i < l; i++) {
			if (i == this.sortColumn){
				cells[i].className = "sort_column_" + (this.descending ? "descending" : "ascending");
			} else {
				cells[i].className = "";		
			}	
		}
	}
};

JSTable.prototype.headerOnclick = function (e) {
	// find TD element
	var el = e.target || e.srcElement;
	while (el.tagName != "TD"){
		el = el.parentNode;
	}
	if(this.columnOffset == 0){
		this.sort(el.cellIndex);	
	} else {
		var row = el.parentNode;
		if(row.rowIndex == 0){
			this.sort(el.cellIndex);
		} else {
			this.sort(el.cellIndex + this.columnOffset);
		}
	}
};

JSTable.prototype.getSortType = function (nColumn) {
	var cell;
	if(this.columnOffset == 0 || nColumn == 0){
		cell = this.tHead.rows[0].cells[nColumn];
	} else {
		cell = this.tHead.rows[this.tHead.rows.length-1].cells[nColumn-this.columnOffset];
	}
	var val = cell._sortType;
	if (val != "")
		return val;
	return "String";
};

// only nColumn is required
// if bDescending is left out the old value is taken into account
// if sSortType is left out the sort type is found from the sortTypes array

JSTable.prototype.sort = function (nColumn, bDescending, sSortType) {
	if (sSortType == null)
		sSortType = this.getSortType(nColumn);
	// exit if None	
	if (sSortType == "None")
		return;
	
	if (bDescending == null) {
		if (this.sortColumn != nColumn){
			if(sSortType == "String"){
				this.descending = false;
			} else {
				this.descending = true;
			}
		} else {
			this.descending = !this.descending;
		}
	} else {
		this.descending = bDescending;
	}
	
	this.sortColumn = nColumn;
	this.updateHeaderArrows();
	
	if (typeof this.onbeforesort == "function")
		this.onbeforesort();
	
	var f = this.getSortFunction(sSortType, nColumn);
	var a = this.getCache(sSortType, nColumn);
	var tBody = this.tBody;
	
	a.sort(f);
	
	if (this.descending)
		a.reverse();
	
	if (JSTable.removeBeforeSort) {
		// remove from doc
		var nextSibling = tBody.nextSibling;
		var p = tBody.parentNode;
		p.removeChild(tBody);
	}
	
	// insert in the new order
	var l = a.length;
	for (var i = 0; i < l; i++)
		tBody.appendChild(a[i].element);
	
	if (JSTable.removeBeforeSort) {	
		// insert into doc
		p.insertBefore(tBody, nextSibling);
	}
	
	this.destroyCache(a);
	if (typeof this.onsort == "function")
		this.onsort();
};

JSTable.prototype.asyncSort = function (nColumn, bDescending, sSortType) {
	var oThis = this;
	this._asyncsort = function () {
		oThis.sort(nColumn, bDescending, sSortType);
	};
	window.setTimeout(this._asyncsort, 1);	
};

JSTable.prototype.getCache = function (sType, nColumn) {
	var rows = this.tBody.rows;
	var l = rows.length;
	var a = new Array(l);
	var r;
	for (var i = 0; i < l; i++) {
		r = rows[i];
		a[i] = {
			value:		this.getRowValue(r, sType, nColumn),
			element:	r
		};
	};
	return a;
};

JSTable.prototype.destroyCache = function (oArray) {
	var l = oArray.length;
	for (var i = 0; i < l; i++) {
		oArray[i].value = null;
		oArray[i].element = null;
		oArray[i] = null;
	}
}

JSTable.prototype.getRowValue = function (oRow, sType, nColumn) {
	var s;
	var c = oRow.cells[nColumn];
	if (typeof c.innerText != "undefined")
		s = c.innerText;
	else
		s = this.getInnerText(c);
	return this.getValueFromString(s, sType);
};

JSTable.prototype.getInnerText = function (oNode) {
	var s = "";	
	var cs = oNode.childNodes;
	var l = cs.length;
	for (var i = 0; i < l; i++) {
		switch (cs[i].nodeType) {
			case 1: //ELEMENT_NODE
				s += this.getInnerText(cs[i]);
				break;
			case 3:	//TEXT_NODE
				s += cs[i].nodeValue;
				break;
		}
	}
	return s;
}

JSTable.prototype.getValueFromString = function (sText, sType) {
	switch (sType) {
		case "Number":
			if(sText=="*"){
				return -10000000;
			} else {
				var re = /%/g;
				return Number(sText.replace(re,''));
			}
		case "CaseInsensitiveString":
			return sText.toUpperCase();
		case "Date":
			var parts = sText.split("-");
			var d = new Date(0);
			d.setFullYear(parts[0]);
			d.setDate(parts[2]);
			d.setMonth(parts[1] - 1);			
			return d.valueOf();	
		case "TrailingNumber":
			var parts = sText.split(" ");
			return Number(parts[parts.length - 1]);
		case "EducationLevel":
			switch(sText){
				case "High School":
					return 1;
				case "B.A.":
					return 2;
				case "B.A. + 30 hours":
					return 3;
				case "M.A.":
					return 4;
				case "M.A. + 30 hours":
					return 5;
				case "Ph.D":
					return 6;
			}
		case "Money":
			var re = /,/g;
			return Number(sText.slice(1).replace(re,''));
	}
	if(sText == "Average"){
		return "ZZZZZZZ";
	} else {
		return sText;
	}
};

JSTable.prototype.getSortFunction = function (sType, nColumn) {
	return function compare(n1, n2) {
		if (n1.value < n2.value)
			return -1;
		if (n2.value < n1.value)
			return 1;
		return 0;
	};
};

JSTable.prototype.createDetailTable = function(rowArray){
	var newTable = document.createElement("TABLE");
	newTable.className = "detail";
	var tableHead = document.createElement("THEAD");
	var currentRow = document.createElement("TR");
	for(var i = 0; i < this.labels.length; i++){
		this.addCellToRow(currentRow,this.labels[i]);
	}
	tableHead.appendChild(currentRow);
	newTable.appendChild(tableHead);
	var tableBody = document.createElement("TBODY");
	for(var i = 0;i<rowArray.length;i++){
		var currentItem = rowArray[i];
		var currentRow = document.createElement("TR");
		if(this.objectToRowMapper){
			currentItem = this.objectToRowMapper(currentItem);
		}
		for(var j=0; j < currentItem.length; j++){
			this.addCellToRow(currentRow,currentItem[j]);
		}
		tableBody.appendChild(currentRow);
	}
	newTable.appendChild(tableBody);
	return newTable; 
};

JSTable.prototype.addCellToRow = function(row,cellText){
	currentCell = document.createElement("TD");
	currentText = document.createTextNode(cellText);
	currentCell.appendChild(currentText);
	row.appendChild(currentCell);
};

JSTable.prototype.expandRow = function(row){
	row.firstChild.className = "showCollapser";
	var itemArray = this.detailArray[row.id];
	var detailTable = this.createDetailTable(itemArray);
	var newRow = document.createElement("TR");
	var newCell = document.createElement("TD");
	newCell.setAttribute("colSpan",row.cells.length);
	newCell.className = "expanded";
	newCell.appendChild(detailTable);
	newRow.appendChild(newCell);
	this.tBody.insertBefore(newRow,row.nextSibling);
};

JSTable.prototype.collapseRow = function(row){
	row.firstChild.className = "showExpander";
	this.tBody.removeChild(row.nextSibling);
};

JSTable.prototype.toggleRow = function(e){
	var row = dom.getEventTrigger(e);
	while(row.tagName != "TR"){
		row = row.parentNode;
	}
	if(row.getAttribute("TOGGLE_STATE") == "expanded"){
		row.setAttribute("TOGGLE_STATE","collapsed");
		this.collapseRow(row);
	} else {
		row.setAttribute("TOGGLE_STATE","expanded");
		this.expandRow(row);
	}
};

JSTable.prototype.collapseAllRows = function(retainExpandedState){
	var rows = this.tBody.childNodes;
	for(var i=0;i<rows.length;i++){
		var row = rows[i];
		if(row.nodeType == 1){
			if(row.getAttribute("TOGGLE_STATE") == "expanded"){
				this.collapseRow(row);
				if(!retainExpandedState){
					row.setAttribute("TOGGLE_STATE","collapsed");
				}
			}
		}
	}
};

JSTable.prototype.restoreExpandedRows = function(){
	var rows = this.tBody.childNodes;
	for(var i=0;i<rows.length;i++){
		var row = rows[i];
		if(row.nodeType == 1){
			if(row.getAttribute("TOGGLE_STATE") == "expanded"){
				this.expandRow(row);
			}
		}
	}
};

JSTable.prototype.expandAllRows = function(){
	var rows = this.tBody.childNodes;
	for(var i=0;i<rows.length;i++){
		var row = rows[i];
		if(row.id.substr(0,1) == 'n'){
			if(row.nodeType == 1){
				if(row.getAttribute("TOGGLE_STATE") != "expanded"){
					row.setAttribute("TOGGLE_STATE","expanded");
					this.expandRow(row);
				}
			}
		}
	}
};

JSTable.prototype.prepareForSort = function(){
	this.JSTable.collapseAllRows(true);	//'this' refers to the SortableTable object
};

JSTable.prototype.afterSort = function(){
	this.JSTable.restoreExpandedRows(); //'this' refers to the SortableTable object
};
JSTable.prototype.addExpanderEventHandlers = function(tbody){
	var rows = tbody.rows;
	var len = rows.length;
	for(var i = 0; i < len; i++){
		dom.addEvent(rows[i],"click",this._toggleRow,false);
	}
};

JSTable.prototype.insertAverageRow = function(rowName,className){
	var aryPercents = new Array();
	var rows = this.tBody.rows;
	var numRows = rows.length;
	var numValidRows = numRows;
	var numCells = rows[0].cells.length;
	var arySum = new Array(); //for holding running sum
	var aryCount = new Array(); //for holding running count of valid cell values
	for(var i=1; i<numCells;i++){	//initialize a 1-based array with zeros for each column, and an array to hold false values for percents
		arySum[i] = Number(0);
		aryCount[i] = Number(0);
		aryPercents[i] = false;
	}
	var cellValue;
	var re = /(,|%)/g;	//filter out commas and percents when calculating averages
	for(var i=0; i<numRows; i++){	//for each row in the table body
		var cells = rows[i].cells;	//grab the array of cells for this row
		for(j=1; j<numCells; j++){	//for each cell, leaving out the first
			//add the value of this cell to the sum for this column
			cellValue = cells[j].firstChild.nodeValue;
			if(cellValue.indexOf("%") > -1){
				aryPercents[j] = true;
			}
			cellValue = Number(cellValue.replace(re,''));
			//if this cell value is not a number, don't add it to the running sum,
			//and don't increment the counter for this column
			if(isNaN(cellValue)){
				continue;
			} else {
				arySum[j] += cellValue;
				aryCount[j]++;
			}
		}
	}
	//create the average row
	var newRow = document.createElement("TR");
	dom.addCellToRow(newRow,rowName);
	var cellText;
	for(var i=1; i<numCells; i++){
		if(aryCount[i] > 0){	
			cellText = insertComma((Math.round(10 * arySum[i] / aryCount[i]))/ 10);
		} else { //avoid dividing by 0, if no valid rows
			cellText = "*";
		}
		if(aryPercents[i]){
			cellText += "%";
		}
		dom.addCellToRow(newRow,cellText);
	}
	this.tBody.appendChild(newRow);
};

JSTable.prototype.showMaxMin = function(){
	var cells = this.tHead.rows[this.tHead.rows.length-1].cells;	//get header rows
	var l = cells.length;
	var cell,f,a,m,min,max;
	for(var i=1; i<l; i++){	//for each cell in the header
		cell = cells[i];
		a=this.getCache(cell._sortType,i);
		f=this.getSortFunction(cell._sortType,i);
		a.sort(f)
		//set min levels
		m = a.length;
		min = a[0].value;
		max = a[m-1].value;
		for(var j=0;j<m;j++){
			if(a[j].value == min){
				a[j].element.cells[i].className = "minvalue";
			} else {
				break;
			}
		}
		for(var j=m-1;j > -1;j--){
			if(a[j].value == max){
				a[j].element.cells[i].className = "maxvalue";
			}
		}
		this.destroyCache(a);
	}
};
	
	
JSTable.prototype.destroy = function () {
	this.uninitHeader();
	var win = this.document.parentWindow;
	if (win && typeof win.detachEvent != "undefined") {	// only IE needs this
		win.detachEvent("onunload", this._onunload);
	}	
	this._onunload = null;
	this.element = null;
	this.tHead = null;
	this.tBody = null;
	this.document = null;
	this._headerOnclick = null;
	this.sortTypes = null;
	this._asyncsort = null;
	this.onsort = null;
	this.expandedLabels = null;
	this.detailArray = null;
	this.objectToRowMapper = null;
	this.isSortable = null;
	this.isExpanding = null;
};
