/*
 * todo: error notification function to be used universally, especially from async handler
 * todo: handler function that extracts the column data array from the data returned from the server, externalize JSON
 */
function ProductComparator()
{
	// Private

	var self = this;

	// Properties

	var itsIDToURLMappingFunction;
	var itsColumnInsertionPointIDPrefix;
	var itsStartRow = 1;
	var itsEndRow;
	var itsStateCookie;
	var itsChangeNotificationFunction;
	var itsMaximumColumnCount;
	var itsDataExtractionFunction;
    var isUsedProduct;

    // Transient properties

	var itsProducts = new Properties();
	var itsUnplacedProducts = new Array();
	var kProductPropertyName = "product";
	var kAddPropertyName = "add";
    var kProductIsUsed = "used";

    this.getColumnInsertionPointPrefix = function()
	{
		return (itsColumnInsertionPointIDPrefix);
	};


	this.setColumnInsertionPointIDPrefix = function(inColumnInsertionPointIDPrefix)
	{
		itsColumnInsertionPointIDPrefix = inColumnInsertionPointIDPrefix;
		return (self);
	};


	this.getStartRow = function()
	{
		return (itsStartRow);
	};


	this.setStartRow = function(inStartRow)
	{
		itsStartRow = inStartRow;
		return (self);
	};


	this.getEndRow = function()
	{
		return (itsEndRow);
	};


	this.setEndRow = function(inEndRow)
	{
		itsEndRow = inEndRow;
		return (self);
	};


	this.getStateCookie = function()
	{
		return (itsStateCookie);
	};


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

    this.isUsedProduct = function()
    {
        return (isUsedProduct);
    };


    this.setUsedProduct = function(inUsedProduct)
    {
        isUsedProduct = inUsedProduct;
        return (self);
    };

	this.getIDToURLMappingFunction = function()
	{
		return (itsIDToURLMappingFunction);
	};


	this.setIDToURLMappingFunction = function(inIDToURLMappingFunction)
	{
		itsIDToURLMappingFunction = inIDToURLMappingFunction;
		return (self);
	};


	this.getChangeNotificationFunction = function()
	{
		return (itsChangeNotificationFunction);
	};


	this.setChangeNotificationFunction = function(inChangeNotificationFunction)
	{
		itsChangeNotificationFunction = inChangeNotificationFunction;
		return (self);
	}


	this.getMaximumColumnCount = function()
	{
		return (itsMaximumColumnCount);
	}


	this.setMaximumColumnCount = function(inMaximumColumnCount)
	{
		itsMaximumColumnCount = inMaximumColumnCount;
		return (self);
	}


	this.getDataExtractionFunction = function()
	{
		return (itsDataExtractionFunction);
	}


	this.setDataExtractionFunction = function(inDataExtractionFunction)
	{
		itsDataExtractionFunction = inDataExtractionFunction;
		return (self);
	}

    this.getProducts = function()
    {
        return (itsProducts);
    };

	function removeColumn(inID)
	{
		var theCell;
		var theRow;

		inID = inID + ".";
		for (theRow = itsStartRow; itsEndRow == undefined || theRow <= itsEndRow; theRow++)
		{
			theCell = document.getElementById(inID + theRow);
			if (theCell == null)
				break;
			theCell.parentNode.removeChild(theCell);
		}

		return (self);
	}


	function addColumn(inContent)
	{
		var i;
		var theContainerParentNode;
		var theContainerNode;
		var theEndNode;
		var theNodes;
		var theNode;
		var j;
		var isIE;

		if (inContent.length == 0)
			return (self);

		/**
		 * Unintuitive behavior: the browser apparently fails to create the nodes using innerHTML if they do not
		 * represent strictly correct HTML in context.  So if the element in the text is <td> and the containing
		 * element is not <tr> then the <td> will not actually be created by the innerHTML assignment.
		 */

		isIE = navigator.appName.indexOf("Microsoft Internet Explorer") > -1;

		theContainerParentNode = document.createElement("table");
		theContainerNode = document.createElement("tr");
		theContainerParentNode.appendChild(theContainerNode);
		for (i = 0,theRow = itsStartRow; i < inContent.length && (itsEndRow == undefined || theRow <= itsEndRow); i++,theRow++)
		{
			if (inContent[i] != null)
			{
				theEndNode = document.getElementById(itsColumnInsertionPointIDPrefix + "." + theRow);
				if (theEndNode == null)
					break;

				if (isIE)
				{
					// todo: this is busted.  It assumes that there is only one element in the content.
					// maybe this assumption will hold... maybe not.
					theNode = document.createElement(inContent[i]);
					theContainerNode.appendChild(theNode);
					theNode.innerHTML = inContent[i];
				}
				else
					theContainerNode.innerHTML = inContent[i];

				theNodes = theContainerNode.childNodes;
				for (j = 0; j < theNodes.length; j++)
					theEndNode.parentNode.insertBefore(theNodes.item(j),theEndNode);
			}
		}

		return (self);
	}


	function createHTTPRequest()
	{
		var theRequest;

		if (window.XMLHttpRequest)
		{
			try
			{
				theRequest = new XMLHttpRequest();
			}
			catch (e)
			{
				theRequest = null;
			}
			// branch for IE/Windows ActiveX version
		}
		else if (window.ActiveXObject)
		{
			try
			{
				theRequest = new ActiveXObject("Msxml2.XMLHTTP");
			}
			catch (e)
			{
				try
				{
					theRequest = new ActiveXObject("Microsoft.XMLHTTP");
				}
				catch (e)
				{
					theRequest = null;
				}
			}
		}

		return (theRequest);
	}


	/*
	 * Extract from the properties passed a set of product IDs.  The presence of the property kAddPropertyName
	 * makes the new set additive to inIDSet.
	 */
	function extractProductIDs(inProperties,inIDSet)
	{
		var theID;
		var i;

		if (inIDSet === undefined || !inProperties.containsKey(kAddPropertyName))
			inIDSet = new Properties();
		theID = inProperties.getProperty(kProductPropertyName);
		if (theID !== undefined)
		{
			if (theID instanceof Array)
			{
				for (i = 0; i < theID.length; i++)
				{
					if (theID[i] != "")
						inIDSet.setProperty(theID[i],theID[i]);
				}
			}
			else
			{
				if (theID != "")
					inIDSet.setProperty(theID,theID);
			}
		}

		return (inIDSet);
	}


	function isFull(inSize)
	{
		if (itsMaximumColumnCount !== undefined && inSize >= itsMaximumColumnCount)
		{
			alert("You have added more columns than the display can accommodate.  Please delete columns to make room.");
			return (true);
		}

		return (false);
	}


	// privileged


	this.mapIDToURL = function(inID)
	{
		if (itsIDToURLMappingFunction === undefined)
			return (inID);
		return (itsIDToURLMappingFunction(inID));
	};


	this.addProduct = function(inID,inIgnoreColumnLimits)
	{
		var theRequest;
		var theURL;

		if (itsProducts.containsKey(inID))
			return (true);

		if (isFull(itsProducts.getSize()))
			return (false);

		theRequest = createHTTPRequest();
		if (theRequest == null)
		{
			alert("Please enable JavaScript in your browser to use the comparison tool.");
			return (false);
		}

		theRequest.onreadystatechange = function()
		{
			var theData;

			if (theRequest.readyState != 4)
				return;

			// todo: this is broken.  This line of code breaks in IE, but in IE which has a poor garbage collector
			// the absence of this line of code can produce memory leaks.
			//theRequest.onreadystatechange = null;

			if (theRequest.status != 200)
			{
				// todo: need an error handler function, since there is no context to catch
				// an exception here.
				alert("There was a problem retrieving the data for this product (" + inID + ": " + theRequest.status + " " + theRequest.statusText + ")");
				theRequest = null;
				return;
			}

			theData = theRequest.responseText;
			theRequest = null;
			if (itsProducts.containsKey(inID))
				return;
			itsProducts.setProperty(inID,theURL);
			if (itsDataExtractionFunction !== undefined)
				theData = itsDataExtractionFunction(theData);
			else
			{
				// todo: backward compatibility
				theData = eval(theData);
				theData = theData.data;
			}
			addColumn(theData);
			if (itsChangeNotificationFunction !== undefined)
				itsChangeNotificationFunction(inID,true);
			self.saveState();
		};

		theURL = mapIDToURL(inID);
		theRequest.open("GET",theURL,true);
		theRequest.send("");

		return (true);
	};


	this.removeProduct = function(inID,inDoNotRestore)
	{
		var theCurrentCount;

		if (itsProducts.containsKey(inID))
		{
			if (itsChangeNotificationFunction !== undefined)
				itsChangeNotificationFunction(inID,false);
			removeColumn(inID);
			itsProducts.removeProperty(inID);
			self.saveState();

			// This will pick up any products for which there previously was no room
			if (!inDoNotRestore)
			{
				theCurrentCount = itsProducts.getSize();
				while (itsUnplacedProducts.length > 0)
				{
					if (isFull(theCurrentCount) || !self.addProduct(itsUnplacedProducts[0]))
						break;
					itsUnplacedProducts.shift();
					theCurrentCount++;
				}
			}
		}

		return (self);
	};


	this.getProductCount = function()
	{
		return (itsProducts.getKeys().length);
	};


	this.saveState = function()
	{
		var theState;

		if (itsStateCookie != null)
		{
			theState = new Properties();
			theState.setProperty(kProductPropertyName,itsProducts.getKeys());
			itsStateCookie.setValue(theState.asQueryString());
		}

		return (self);
	};


	this.restoreState = function(inDeletedProduct)
	{
		var theState;
		var theCookieIDs;
		var theQueryStringIDs;
		var theRemoveIDs;
		var theQueryString;
		var i;
		var isDeletedProductInQueryString = false;
		var theURL;
		var theCurrentCount;

		if (itsStateCookie !== undefined)
		{
			theState = new Properties();
			theState.parseQueryString(itsStateCookie.getValue());
			theCookieIDs = extractProductIDs(theState);
		}

		theQueryString = document.location.search.substr(1);
		if (theQueryString != "")
		{
			theState.parseQueryString(theQueryString);
			theQueryStringIDs = extractProductIDs(theState);
            if (theState.containsKey(kProductIsUsed))
                this.setUsedProduct(theState.getProperty(kProductIsUsed));
			if (!theState.containsKey(kAddPropertyName))
				theCookieIDs = new Properties();
			if (theQueryStringIDs.containsKey(inDeletedProduct))
			{
				theQueryStringIDs.removeProperty(inDeletedProduct);
				isDeletedProductInQueryString = true;
			}
		}

		if (theCookieIDs !== undefined || theQueryStringIDs !== undefined)
		{
			theRemoveIDs = itsProducts.clone();
			theRemoveIDs.removeProperties(theCookieIDs);
			theRemoveIDs.removeProperties(theQueryStringIDs);
			theRemoveIDs = theRemoveIDs.getKeys();

			if (theCookieIDs !== undefined)
			{
				theCookieIDs.removeProperties(itsProducts);
				theCookieIDs = theCookieIDs.getKeys();
			}
			else
				theCookieIDs = [];

			if (theQueryStringIDs !== undefined)
			{
				theQueryStringIDs.removeProperties(itsProducts);
				theQueryStringIDs.removeKeys(theCookieIDs);
				theQueryStringIDs = theQueryStringIDs.getKeys();
			}
			else
				theQueryStringIDs = [];

			// Being careful even though there is no concurrency in most implementations
			theCurrentCount = itsProducts.getSize() - theRemoveIDs.length + theCookieIDs.length;

			for (i = 0; i < theRemoveIDs.length; i++)
				self.removeProduct(theRemoveIDs[i],true);
			for (i = 0; i < theCookieIDs.length; i++)
				self.addProduct(theCookieIDs[i],true);

			for (i = 0; i < theQueryStringIDs.length; i++,theCurrentCount++)
			{
				if (isFull(theCurrentCount) || !self.addProduct(theQueryStringIDs[i]))
					break;
			}

			for ( ; i < theQueryStringIDs.length; i++)
				itsUnplacedProducts.push(theQueryStringIDs[i]);
		}
	};
}
