/** 
	Sort_dataTable.js
	
	Adds custom behaviors to tables of class "DataTable" on the HTML page. Works in DOM browsers
	
	© 2004 School Success, a division of Pearson Education, Inc (www.schoolsuccess.net)
	
	This script cannot be used without the explicit consent of the copyright holder.
*/

// Static initialization scripts
{
	// Keep track of border size as browsers tend to differ on cell widths and heights if borders are present
	var st_ghostBorderWidth = 2;

	//***************************
	// Sorting Variables
	var st_SORT_DESC = -1; // -ve Orders are descending, +ve are ascending;
	var st_SORT_ASC = 1; // -ve Orders are descending, +ve are ascending;
	var st_SORT_DEFAULT = st_SORT_ASC;
	
	var st_curSortCol = null;
	
	//***************************
	// Dragging Variables
	var st_dragCol; //Column being dragged (pointer)
	var st_dragGhost; //DragGhost to visualize drag (red div)
	var st_dragBar = null; //DragBar to visualize drop
	var st_dropIndex = 0; //Keep track of the column index we are dropping onto. (Done globally as it is set during the move but used in the EndDrag method


	// Array for Images we will be using
	var st_img = new Array(0);

	// Spacer image for beautification			
	st_img["spacer"] = document.createElement("img");
	st_img["spacer"].src = "/assets/images/spacer.gif";
	st_img["spacer"].width = 5;

	
	// Sort Ascending Image
	st_img[st_SORT_DESC] = document.createElement("img");
	st_img[st_SORT_DESC].src = "/assets/images/common/sort_asc.gif";
	st_img[st_SORT_DESC].srcHighlight = "/assets/images/common/sort_asc_blue.gif";
	st_img[st_SORT_DESC].style.cursor = "pointer";
	st_img[st_SORT_DESC].title = "Sort this column in Ascending Order";
	st_img[st_SORT_DESC].align = "middle";
				
	// Sort Descendimg Image
	st_img[st_SORT_ASC] = document.createElement("img");
	st_img[st_SORT_ASC].src = "/assets/images/common/sort_desc.gif";
	st_img[st_SORT_ASC].srcHighlight = "/assets/images/common/sort_desc_blue.gif";
	st_img[st_SORT_ASC].style.cursor = "pointer";
	st_img[st_SORT_ASC].title = "Sort this column in Descending Order";
	st_img[st_SORT_ASC].align = "middle";
	
	// Drag Handle Image
	st_img["handle"] = document.createElement("img");
	st_img["handle"].style.cursor = "move";
	st_img["handle"].src = "/assets/images/common/drag_handle.gif";
	st_img["handle"].height = 20;
	st_img["handle"].width = 6;
	st_img["handle"].title = "Drag to reposition this column";
	st_img["handle"].align = "absMiddle";
			

	registerEventListener(new eventListener(window,EVENT_TYPE_ONLOAD,st_init));
	registerEventListener(new eventListener(document,EVENT_TYPE_ONMOUSEMOVE,st_dragOn));
	registerEventListener(new eventListener(document,EVENT_TYPE_ONMOUSEUP,st_dragEnd));
}

/*
	The Magic function. st_init (registered as a window.onload listener) will grab
	all the <table class="DataTable"> elements on the current page. For those that specify
	the dynamic attribute, it will install event handlers to allow reordering and sorting
	of the columns in the table.

*/
function st_init() {
	var i,j,k;

	// ******************************
	// install event handlers for 
	// column header clicks and drags
	
	// first grab all tables in this document.
	var allTables = document.getElementsByTagName("TABLE");
	
	// too many tables makes this too hard to do
	if (allTables.length > 50) return;
	// vars for the loops below
	var thisTable,thisTableHead,thisTableCell,thisImage,thisSpacer,thisCellSort,rowHeight;
     
	// loop over the tables 
	for (i=0;i<allTables.length;i++) {
		thisTable = allTables[i];
		//find the ones that are "datatable"s
		if (thisTable.className 
			&& thisTable.className.toLowerCase().indexOf("datatable") != -1
			&& eval(thisTable.getAttribute("dynamic"))) {
			// Now grab all the THEAD in this table
			// (though we only expect one and will only deal with the first)
			thisTableHead = thisTable.getElementsByTagName("THEAD");
			
			// Do a preliminary loop over all the cells in the first thead,
			// and make sure that all the th's have noWrap set to true. This
			// ensure that the height of this row is uniform before we insert
			// the images.
			for (j=0;j<thisTableHead[0].rows.length;j++)
				for (k=0;k<thisTableHead[0].rows[j].cells.length;k++)
					thisTableHead[0].rows[j].cells[k].noWrap = true;
				
			rowHeight = thisTableHead[0].rows[0].cells[0].clientHeight - (isIE?0:st_ghostBorderWidth);

			// Loop over the first row of cells in the first tHead
			for (j=0;j<thisTableHead[0].rows[0].cells.length;j++) {
				thisTableCell = thisTableHead[0].rows[0].cells[j];
				thisCellSort = thisTableCell.getAttribute("sort");

				// if the cell has the sort parameter, add the sort 
				// toggle image and the behavior handler	
				if (thisCellSort) {
					thisSpacer = st_img["spacer"].cloneNode(false);
					thisImage = st_img[st_SORT_DEFAULT].cloneNode(false);
					thisImage.onclick = st_sortColumn;
					
					thisTableCell.insertBefore(thisSpacer,thisTableCell.childNodes[0]);
					thisTableCell.insertBefore(thisImage,thisSpacer);
					thisTableCell.theImage = thisImage;
					//keep track of the last sort direction on each column.
					thisTableCell.sortDirection = st_SORT_DEFAULT;
					
					if (thisCellSort.toLowerCase() == "date") {
						thisTableCell.comparator = new DateComparator();
					} else if (thisCellSort.toLowerCase() == "string") {
						thisTableCell.comparator = new StringComparator();
					} else if (thisCellSort.toLowerCase() == "number") {
						thisTableCell.comparator = new NumberComparator();
					} else {
						thisTableCell.comparator = new Comparator();
					}
				}
				
				
				
				// add the cell dragging behavior and handle image
				//(but only if this cell is not a colspan
				if (thisTableCell.colSpan == 1) {
					thisTableCell.style.padding = "3px";
					thisSpacer = st_img["spacer"].cloneNode(false);
					thisImage = st_img["handle"].cloneNode(false);
					thisImage.height = rowHeight;
				
					thisTableCell.insertBefore(thisSpacer,thisTableCell.childNodes[0]);
					thisTableCell.insertBefore(thisImage,thisSpacer);
					// install the drag behavior
					thisImage.onmousedown = st_dragStart;
				}				
				
			}
		}
	}
}



// Start a drag
function st_dragStart(event) {
	// what column are we dragging?
	st_dragCol = getEventSource(event).parentNode;	
	
	// normalize the event
	event = normalizeEvent(event);
	
	st_dragCol.style.color = "red";
	
	// Grab the page coordinates of the column header we are dragging
	var curCoordinates = getNodePosition(st_dragCol);
	
	// Create the dragGhost element		
	st_dragGhost = document.createElement("DIV");
	st_dragGhost.style.position = "absolute";
	st_dragGhost.style.border = st_ghostBorderWidth + "px solid red";
	
	// Create the dropBar element
	st_dragBar = st_dragGhost.cloneNode(false);
	
	// Append them to the document
	document.body.appendChild(st_dragGhost);
	document.body.appendChild(st_dragBar);
	
	// Now size them to the right proportions
	st_dragGhost.style.width = st_dragCol.clientWidth - (isIE?0:st_ghostBorderWidth);
	st_dragGhost.style.height = st_dragCol.clientHeight - (isIE?0:st_ghostBorderWidth); 
	st_dragGhost.style.top = curCoordinates["top"];
	st_dragGhost.style.left = curCoordinates["left"];

	st_dragBar.style.width = 0;
	st_dragBar.style.height = st_dragCol.clientHeight - (isIE?0:st_ghostBorderWidth);
	st_dragBar.style.top = curCoordinates["top"];
	st_dragBar.style.left = curCoordinates["left"] - 3;
	
	return false;
}


// Move the damn thing.
function st_dragOn (event) {
	
	// if we have a dragCol then we are dragging. Otherwise the event is ignored.
	if (st_dragCol) {
		// normalize the event.
		event = normalizeEvent(event);
				
		// **************************
		// reposition the drag ghost
		if (event.clientX < 15) {
			//we are on the left edge of the window. Force scrolling if necessary
			if(document.body.scrollLeft > 0) window.scrollBy(-10,0);
			// force the Ghost to the left edge of the window
			st_dragGhost.style.left = document.body.scrollLeft;
		} else if (event.clientX > document.body.clientWidth - 15) {
			// force the Ghost to the right edge of the window - but leave it a bit visible
			st_dragGhost.style.left = document.body.clientWidth + document.body.scrollLeft - 10;
			//we are on the right edge of the window. Force scrolling if necessary
			window.scrollBy(10,0);
		} else st_dragGhost.style.left = event.clientX  + document.body.scrollLeft - 20;
		

		
		// **************************
		// reposition the drop target
		
		var thisRow = st_dragCol.parentNode;
		var rowLeftEdge = getNodePosition(thisRow.cells[0])["left"]; //left most extent of the Row
		var rowRightEdge = rowLeftEdge +  thisRow.offsetWidth; //right most extent of the Row
		var ghostLeftEdge = parseInt(st_dragGhost.style.left); //left extent of the dragGhost
		var myCurX; 
		
		
		if (ghostLeftEdge < rowLeftEdge) {
			// We are to the left of the whole row. Insertion position is before the first column.
			st_dragBar.style.left = rowLeftEdge;
			st_dropIndex = 0;
		} else if (ghostLeftEdge > rowRightEdge) {
			// We are to the right of the whole row. Insertion position is after the last column.
			st_dragBar.style.left = rowRightEdge;
			st_dropIndex = thisRow.cells.length;
		} else {
			// We are somewhere in the middle. Insertion position needs to be calculated. 
			for(var i=1;i<thisRow.cells.length;i++) {
				myCurX = getNodePosition(thisRow.cells[i])["left"];

				if (ghostLeftEdge < myCurX) {
					st_dragBar.style.left = myCurX;
					st_dropIndex = i;
					break;
				}
			}
		}
		
		// debug
		//st_dragGhost.innerHTML = "<b>" + st_dragCol.cellIndex + ", " + st_dropIndex + "</b>";
		return false;
		
	}
	
}

function st_dragEnd() {
	if(st_dragCol) {
		//now move the freaking column.
		var thisTable = st_dragCol.parentNode.parentNode.parentNode;
		var i,j, x,z;
		var thisRow,spannedRow,thisCell;
		var targetCellArray;
		var nRowSpanTest,ignoreSpanRows;
	
		var adjustedDropIndex=0, adjustedDragIndex=0;
		var realDropIndex=0, realDragIndex=0;
		var dropCell, dragCell;
		

		//Before we start we need to adjust the drop pos and the drag pos
		//This gives us the positions, in terms of the number of columns on
		//the table. (ie: taking into account any colspans)
		for (x=0;x<st_dropIndex;x++) 
			realDropIndex += st_dragCol.parentNode.cells[x].colSpan;

		for (x=0;x<st_dragCol.cellIndex;x++) 
			realDragIndex += st_dragCol.parentNode.cells[x].colSpan;

		
		// We do this only if the position of the drop is not the position of 
		// the drag, or immediately adjacent to the drag on the right side (or target + colSpan)
		if (st_dropIndex != st_dragCol.cellIndex && st_dropIndex != st_dragCol.cellIndex + st_dragCol.colSpan) {
		
			//Loop over all the rows in the table so that we can move all the cells
			for (i=0;i<thisTable.rows.length;i++) {
				thisRow = thisTable.rows[i];

				//figure out if we are dragging a rowspan cell,  in which case we ignore the rows in the span.
				adjustedDragIndex = st_getAdjustedCellIndex(realDragIndex,thisRow,null);
				
				// IF we are out of bounds we needn't even bother
				if (adjustedDragIndex < 0 || adjustedDragIndex > thisRow.cells.length-1) continue;
				
				//If the drag cell itself is a rowspan, we can ignore the drag in the rows it spans (DUH)
				ignoreSpanRows = thisRow.cells[adjustedDragIndex].rowSpan > 1;
				


				// Now, does this row have any rowSpans. If it does we will need to treat 
				// the rowspans within this iteration of the loop.
				nRowSpanTest=1;
				
				// how many rows are we spanning (find out the largest rowspan in the Row)
				for (j=0;j<thisRow.cells.length;j++)
					if (thisRow.cells[j].rowSpan > nRowSpanTest)
						nRowSpanTest = thisRow.cells[j].rowSpan;
				
				//Decrement one for easier math
				nRowSpanTest--;


				
				
				// if we find a rowSpan, then we need to figure out for each of
				// the spanned rows, what the drop and target positions are. Do
				// this before we move anything in the curRow or we will get thrown
				// off (notice the reverse loop)
				for (j=nRowSpanTest+i;j>=i;j--) {
					spannedRow = thisTable.rows[j];

					if (j==i || !ignoreSpanRows) {

						//Adjust the drag position. We start off at the realDropIndex, 
						//and subtract from there
						adjustedDragIndex = st_getAdjustedCellIndex(realDragIndex,thisRow,spannedRow);
						adjustedDropIndex = st_getAdjustedCellIndex(realDropIndex,thisRow,spannedRow);
					

					
						if (adjustedDragIndex >= 0 && 
							adjustedDropIndex >= 0 && 
							adjustedDropIndex != adjustedDragIndex &&
							adjustedDropIndex != adjustedDragIndex + st_dragCol.colSpan) {
						
							dropCell = spannedRow.cells[adjustedDropIndex];
							dragCell = spannedRow.cells[adjustedDragIndex];
							
							if (dragCell) {
								targetCellArray = new Array(dragCell);
								spannedRow.removeChild(dragCell);
								
								//If our dragged column is a span, then we need to move more than
								//one column.
								if (st_dragCol.colSpan > 1) 
									for (x=1;x<st_dragCol.colSpan;x++) 
										targetCellArray[targetCellArray.length] = spannedRow.removeChild(spannedRow.cells[adjustedDragIndex+x]);
									
								
								if(dropCell) 
									for (x=targetCellArray.length-1;x>=0;x--)
										spannedRow.insertBefore(targetCellArray[x],dropCell);
								else 
									for (x=0;x<targetCellArray.length;x++)
										spannedRow.appendChild(targetCellArray[x]);
							}					
						}
					}
				}

				i += nRowSpanTest;
				
			}
			
		}
		// Prepare for another drag, by setting everything back to null.
		document.body.removeChild(st_dragGhost);
		document.body.removeChild(st_dragBar);
		st_dragCol.style.color = "";
		
		st_dragBar = null;
		st_dragGhost = null;
		st_dragCol = null;
		
		return false;
	}
}

/*
	Helper function to help us get the index of cells being moved, based on the
	real index (as far as columns go);
*/
function st_getAdjustedCellIndex(realIndex,HTMLleadRow,HTMLspannedRow) {
	var adjustedIndex = realIndex;
	var thisCell;
	var isSpannedRow = HTMLspannedRow && HTMLspannedRow!==HTMLleadRow;
	
	if (isSpannedRow)
		for (var x=0;x<realIndex;x++) {
			thisCell = HTMLleadRow.cells[x];
			if (!thisCell) break;
			if (thisCell.rowSpan > 1) adjustedIndex -= thisCell.colSpan;
			else adjustedIndex += 1 - thisCell.colSpan;
		}
	else 
		for (var x=0;x<adjustedIndex;x++) {
			thisCell = HTMLleadRow.cells[x];
			if (!thisCell) break;
			adjustedIndex += 1 - thisCell.colSpan;
		}
	return adjustedIndex;
}

/*
	Sort the column (identified by the th in which the image that is the event src is in)
*/
function st_sortColumn(event) {
	
	var srcColumn = getEventSource(event).parentNode;
	var pos = srcColumn.cellIndex;
	
	var thisTable = srcColumn.parentNode.parentNode.parentNode;
	var allBodies = thisTable.tBodies;
	var thisRow,previousRow;
	var keyCell;
	var thisEntry;
	var theRowsSort;			
	var m,n,r;
	var bTest;
	
	srcColumn.theImage.style.cursor = "pointer";
	
	// Loop over the table bodies. We do this as each body may be a subset that needs 
	// to be grouped the way it is.
	for (m=0;m<allBodies.length;m++) {
		
		// Exclude tBodies with classes from being reordered
		if (!allBodies[m].className) {
			/*
				Now loop over the rows in the current TBODY (forward loop) and create an 
				st_sortEntry (helper object) which is stored in a temporary array that
				is then fed to the st_quickSort function. 
				
			*/
			theRowsSort = new Array();
	
			for (n=0;n<allBodies[m].rows.length;n++) {
				thisRow = allBodies[m].rows[n];
				
				// If we had a row before this one, check that none of its cells are rowspanned. 
				// though we only do that check for real if the number of cells in thisRow
				// is less than the number of cells in the previousRow
				if (previousRow && previousRow.cells.length > thisRow.cells.length) {
					bTest=false;
					//any one cell being rowspanned means we can break out of the for.
					for (r=0;r<previousRow.cells.length;r++) {
						if (previousRow.cells[r].rowSpan > 1) {
							bTest = true;
							break;
						}
					}
					// just store the Row in the positional array in the "previousEntry" (still
					// thisEntry from the previous round at this juncture).
					if (bTest) {
						thisEntry.value[thisEntry.value.length] = thisRow;
					}
				} else {
					// Good, a new "real" row
					
					// Grab the cell we are sorting by.
					keyCell = thisRow.cells[pos];
		
					//IE AND Mozilla cannot agree about textContent
					if (keyCell.textContent) { 
						//Create the EntryHelper object, passing the sorting cell text as the key, and the row as the value
						thisEntry = new st_sortEntry(keyCell.textContent,new Array(thisRow));
					} else {
						thisEntry = new st_sortEntry(keyCell.innerText,new Array(thisRow));
					}
					theRowsSort[theRowsSort.length] = thisEntry;
					
					//Change the previousRow pointer
					previousRow = thisRow;				
				}	
				//Remove the row from the table
				//allBodies[m].removeChild(thisRow);
			}
			// Feed the temp array to quickSort. Use the comparator for the column
			st_quickSort(theRowsSort,0,theRowsSort.length-1,srcColumn.comparator);
			
			// Now remove the rows in reverse order
			for (n=allBodies[m].rows.length - 1;n>=0;n--) 
				allBodies[m].removeChild(allBodies[m].rows[n]);
			
			// Now reinsert the rows in order 
			if (srcColumn.sortDirection == st_SORT_DESC)
				for (n=0;n<theRowsSort.length;n++) 
					for (r=0;r<theRowsSort[n].value.length;r++)
						allBodies[m].appendChild(theRowsSort[n].value[r]);
			else if (srcColumn.sortDirection == st_SORT_ASC) 
				for (n=theRowsSort.length - 1; n>=0;n--) 
					for (r=0;r<theRowsSort[n].value.length;r++)
						allBodies[m].appendChild(theRowsSort[n].value[r]);
		}
	}
	
	if(st_curSortCol && st_curSortCol !== srcColumn) {
		st_curSortCol.sortDirection = st_SORT_DEFAULT;
		st_curSortCol.theImage.src = st_img[st_curSortCol.sortDirection].src;
		st_curSortCol.theImage.title = st_img[st_curSortCol.sortDirection].title;
	}
			
	st_curSortCol = srcColumn;
	
	srcColumn.sortDirection = srcColumn.sortDirection * -1;
	srcColumn.theImage.src = st_img[srcColumn.sortDirection].srcHighlight;
	srcColumn.theImage.title = st_img[srcColumn.sortDirection].title;
	
	srcColumn.theImage.style.cursor = "pointer";
		
	return false;
	
}


function st_quickSort (entry_array,low,high,comparator) {
	//  lo is the lower index, hi is the upper index
	//  of the region of array entry_array that is to be sorted
    var i=low, j=high, h;
    var x=entry_array[parseInt((low+high)/2)];

    //  partition
    do {    
        while (comparator.compare(entry_array[i].key,x.key)<0 || 
			   (comparator.compare(entry_array[i].key,x.key) == 0 
			    && entry_array[i].secondaryKey < x.secondaryKey)) i++; 
        while (comparator.compare(entry_array[j].key,x.key)>0 || 
			   (comparator.compare(entry_array[j].key,x.key) == 0 
			    && entry_array[j].secondaryKey > x.secondaryKey)) j--;
        if (i<=j)
        {
            h=entry_array[i]; entry_array[i]=entry_array[j]; entry_array[j]=h;
            i++; j--;
        }
    } while (i<=j);

    //  recursion
    if (low<j) st_quickSort(entry_array, low, j, comparator);
    if (i<high) st_quickSort(entry_array, i, high, comparator);
}

/*
	Helper class for the quick sort function. By using these specialized entries, 
	we can make quicksort a lot more efficient as we do not need to keep other collections
	to later retrieve the rows after the sort.
*/
function st_sortEntry (key,value) {
	this.key = key;
	this.value = value;
	this.secondaryKey = this.value[0].textContent?this.value[0].textContent:this.value[0].innerText;
}


