/**
 * Sorter
 *
 * A class for sorting HTML document elements, typically table cells.
 *
 * Dependencies:
 *  Cookies.js
 *  Properties.js
 *  FunctionList.js

 when the query string is used it should completely overwrite any saved state, not overlay it;
 the same goes for the comparator, unless an additional parameter is present that indicates it should be additive

 another: state management should be externalized somehow instead of inlined here; it seems to be an external
 concern.  this would be parallel with the filtering code.
 */

function Sorter()
{
	var self = this;
	var itsRowGroups = new Array();
	var itsSortSpecifications = new Properties();
	var itsCurrentSortName;
	var itsCurrentIsDescending = false;
	var itsListeners = new FunctionList();
	var itsStateCookie = null;
	var kSortNamePropertyName = "sortName";
	var kIsDescendingPropertyName = "isDescending";

	/* set inStart = "" to just grab the whole row and not individual cells */
	this.buildRow = function(inPrefix,inStart)
	{
		var theRow;
		var theElement;

		theRow = new Array();
		if (inStart === undefined)
			inStart = 1;
		for (; ;)
		{
			theElement = document.getElementById(inPrefix + inStart);
			if (theElement == null)
				break;
			theRow[theRow.length] = theElement;
			if (inStart == "")
				break;
			inStart++;
		}

		return (theRow.length > 0 ? theRow : null);
	};

	/* 	if inColumnStart is undefined, only the table row elements are loaded,
		otherwise all table cells are */
	this.buildRowGroup = function(inPrefix,inSeparator,inRowStart,inColumnStart)
	{
		var theRows;
		var theRow;

		theRows = new Array();
		if (inRowStart === undefined)
			inRowStart = 1;
		if (inSeparator === undefined)
			inSeparator = ".";
		for (; ;)
		{
			if (inColumnStart === undefined)
			{
				theRow = self.buildRow(inPrefix + inRowStart,"");
			}
			else
			{
				theRow = self.buildRow(inPrefix + inRowStart + inSeparator,inColumnStart);
			}
			if (theRow == null)
				break;
			theRows[theRows.length] = theRow;
			inRowStart++;
		}

		return (theRows.length > 0 ? theRows : null);
	};


	this.addRowGroup = function(inRowGroup)
	{
		if (inRowGroup == null)
		{
			throw {
				name: "SorterError",
				message: "Cannot add null row group"
			};
		}

		itsRowGroups[itsRowGroups.length] = inRowGroup;

		return (self);
	};


	this.buildAndAddRowGroup = function(inPrefix,inSeparator,inRowStart,inColumnStart)
	{
		var theGroup;

		theGroup = self.buildRowGroup(inPrefix,inSeparator,inRowStart,inColumnStart);
		if (theGroup !== null)
			self.addRowGroup(theGroup);
		return (theGroup);
	};


	this.buildAndAddRowGroups = function(inPrefix,inSeparator,inGroupStart,inRowStart,inColumnStart)
	{
		var theGroup;

		if (inGroupStart === undefined)
			inGroupStart = 1;
		if (inSeparator === undefined)
			inSeparator = ".";
		for (; ;)
		{
			theGroup = self.buildAndAddRowGroup(inPrefix + inGroupStart + inSeparator,inRowStart,inColumnStart);
			if (theGroup == null)
				break;
			inGroupStart++;
		}

		return (self);
	};


	this.addSortSpecification = function(inName,inSortSpecification)
	{
		itsSortSpecifications.setProperty(inName,inSortSpecification);
		return (self);
	};


	this.addListener = function(inFunction)
	{
		itsListeners.add(inFunction);
		return (self);
	};


	this.removeListener = function(inFunction)
	{
		itsListeners.remove(inFunction);
	};

	this.sort = function(inSortName,inIsDescending)
	{
		var theRows;
		var theSortedRows;
		var theOldRow;
		var theSortedRow;
		var theNewRow;
		var i;
		var j;
		var theRowGroupIndex;
		var thePlaceholder;

		if (inIsDescending === undefined)
			inIsDescending = false;
		else if (inIsDescending == "!")
			inIsDescending = inSortName == itsCurrentSortName ? !itsCurrentIsDescending : false;
		if (inSortName == itsCurrentSortName && inIsDescending == itsCurrentIsDescending)
			return;

		for (theRowGroupIndex = 0; theRowGroupIndex < itsRowGroups.length; theRowGroupIndex++)
		{
			theRows = itsRowGroups[theRowGroupIndex];
			theSortedRows = theRows.slice(0);
			for (i = 0; i < theSortedRows.length; i++)
				theSortedRows[i].oldIndex = i;

			theSortedRows.sort(itsSortSpecifications.getProperty(inSortName));

			if (inIsDescending)
				theSortedRows.reverse();

			for (i = 0; i < theRows.length; i++)
			{
				theOldRow = theRows[i];
				theSortedRow = theSortedRows[i];
				if (theOldRow === theSortedRow)
					continue;

				theNewRow = theSortedRow.slice(0);
				for (j = 0; j < theOldRow.length; j++)
				{
					if (theOldRow[j] !== theNewRow[j])
					{
						if (theNewRow[j].parentNode != null)
						{
							thePlaceholder = theNewRow[j].ownerDocument.createComment("");
							theNewRow[j].parentNode.replaceChild(thePlaceholder,theNewRow[j]);
							theRows[theSortedRow.oldIndex][j] = thePlaceholder;
						}
						theOldRow[j].parentNode.replaceChild(theNewRow[j],theOldRow[j])
					}
				}
				theSortedRows[i] = theNewRow;
			}
			itsRowGroups[theRowGroupIndex] = theSortedRows;
		}

		self.setCurrentSortName(inSortName);
		self.setCurrentIsDescending(inIsDescending);
		self.saveState();

		itsListeners.call(self,inSortName,inIsDescending);

		return (self);
	};

	this.saveState = function(inCookie)
	{
		var theState;

		if (itsStateCookie != null)
		{
			theState = new Properties();
			theState.setProperty(kSortNamePropertyName,itsCurrentSortName);
			theState.setProperty(kIsDescendingPropertyName,itsCurrentIsDescending ? "true" : "");
			itsStateCookie.setValue(theState.asQueryString());
		}

		return (self);
	};


	this.restoreState = function()
	{
		var theState;
		var theSortName;
		var isDescending;

		if (itsStateCookie != null)
		{
			theState = new Properties();
			theState.parseQueryString(itsStateCookie.getValue());
			theSortName = theState.getProperty(kSortNamePropertyName);
			isDescending = theState.getProperty(kIsDescendingPropertyName) == "true" ? true : false;
			if (theSortName != null)
				self.sort(theSortName,isDescending);
		}

		return (self);
	};


	this.setStateCookie = function(inCookie)
	{
		itsStateCookie = inCookie;
	};


	this.getCurrentSortName = function()
	{
		return (itsCurrentSortName);
	};


	this.getCurrentIsDescending = function ()
	{
		return (itsCurrentIsDescending);
	};


	this.setCurrentSortName = function(inSortName)
	{
		itsCurrentSortName = inSortName;
	};


	this.setCurrentIsDescending = function (inIsDescending)
	{
		itsCurrentIsDescending = inIsDescending;
	};


	/*
	 * Sets up the state for the sorter after initialization. Parameters
	 * specify the cookie to save state, the current sort order and
	 * whether it's descending. Typically called after loading a page.
	 */
	this.initializeAndRestoreState = function(inCookie,inSortName,inIsDescending)
	{
		if (inIsDescending === undefined)
			inIsDescending = false;

		self.setCurrentSortName(inSortName);
		self.setCurrentIsDescending(inIsDescending);
		self.setStateCookie(inCookie);
		itsListeners.call(self,inSortName,inIsDescending);
		self.restoreState();
	};
}
