/*global $ */
/*global showTooltip, hideAllTooltips, showNonStickyTooltip */
/*global activateCheckboxHierarchy */
/*global s, runOmnitureOnResults */
/*global window */
/*jslint browser: true, eqeqeq: true, undef: true */
/******************************************************************************
Lines above are for jslint, the JavaScript verifier.
http://www.jslint.com/
******************************************************************************/

/******************************************************************************
Other scripts required for this one to work:
	http://www.geappliances.com/scripts/gea_global.js
	http://www.geappliances.com/scripts/tooltip.js
	http://www.geappliances.com/scripts/jquery.js
	http://www.geappliances.com/scripts/jquery.corner.js
	http://www.geappliances.com/scripts/jquery.init.js
	http://www.geappliances.com/scripts/jquery.history.js
		A cross-browser implementation of implementing bookmarkable
		links and history without a full page refresh.
	http://www.geappliances.com/scripts/jquery.ajaxxml.js
		Implements $.ajaxXML, a wrapper for $.ajax that works on
		local files in Internet Explorer.
	http://www.geappliances.com/scripts/array.js
		Implements some array methods for browsers that don't have
		them built-in.
		These include forEach, filter, indexOf, and map.
******************************************************************************/

/******************************************************************************
Utility stuff not really specific to ResultsPage.
******************************************************************************/

$.NBSP		= "\u00a0";	// &nbsp;
$.EM_DASH	= "\u2014";	// &mdash;
$.EN_DASH	= "\u2013";	// &ndash;
$.ELLIPSIS	= "\u2026";	// &hellip;
$.EACUTE	= "\u00e9";     // &eacute;

// Handy shortcuts for setting certain attributes.
// Instead of: $(foo).attr("alt", text).attr("title", text);
// You can do: $(foo).alt(text);
// etc.
$.fn.extend({
	"alt": function (text) {
		return this.each(function () {
			$(this).attr("alt", text).attr("title", text);
		});
	},
	"src": function (value) {
		return this.each(function () {
			$(this).attr("src", value);
		});
	},
	"href": function (value) {
		return this.each(function () {
			$(this).attr("href", value);
		});
	},
	"size": function (width, height) {
		return this.each(function () {
			$(this).attr("width", width).attr("height", height);
		});
	}
});

if (!Array.prototype.joinAnd) {
	// ["1"].joinAnd()           returns "1"
	// ["1", "2"].joinAnd()      returns "1 and 2"
	// ["1", "2", "3"].joinAnd() returns "1, 2, and 3"
	// etc.
	// Used to generate link titles on "More Available Colors"
	// links, which show up as tooltips when mouse-hovering.
	Array.prototype.joinAnd = function () {
		var len = this.length;
		if (len <= 2) {
			return this.join(" and ");
		}
		else {
			return this.slice(0, len - 1).join(", ") +
				", and " + this[len - 1];
		}
	};
}

/******************************************************************************
ResultsPage begins.
******************************************************************************/

var ResultsPage = {};

/* "constants" */

ResultsPage.LIST_VIEW = 1;
ResultsPage.GRID_VIEW = 2;

ResultsPage.DEFAULT_RESULTS_PER_PAGE = 12;
ResultsPage.RESULTS_PER_PAGE_OPTIONS = [12, 24, 48, 72, 100];
// These must be multiples of four.

ResultsPage.GRID_VIEW_COLUMNS = 4;
ResultsPage.COMPARE_MAX_MODELS = 10;
ResultsPage.MAX_PAGES_TO_SHOW_ALL = 9;

ResultsPage.IS_ON_TEAMSITE =
	/^(inftw|teamsite)/.test(document.location.host);
ResultsPage.IS_USING_SAMPLE_DATA =
	/^(inftw|teamsite)/.test(document.location.host);
ResultsPage.IS_DEBUGGING =
	/^(inftw|teamsite)/.test(document.location.host) ||
	/GEAResultsTest/.test(document.location.pathname);
if (ResultsPage.IS_DEBUGGING) {
	ResultsPage.FILTER_CHECKBOX_DELAY = 1000;
}

ResultsPage.PHOTO_NOT_AVAILABLE_URL =
	"http://products.geappliances.com/MarketingObjectRetrieval/cache/not_avail_thumb.gif";

ResultsPage.META_KEYWORDS = [
	"GE Appliances",
	"kitchen appliances",
	"energy star appliances",
	"GE Profile",
	"GE Caf" + $.EACUTE,
	"kitchen design",
	"kitchen remodeling"
];
ResultsPage.META_DESCRIPTION_TEMPLATE =
	"GE Appliances offers a variety of high-quality [products].  " +
	"Choose [products] by color, price and brands like " + 
	"GE, GE Profile and GE Caf" + $.EACUTE + ".";

/******************************************************************************
Utility stuff specific to ResultsPage.
******************************************************************************/

/* Because on <label> elements the for attribute points to an ID, not a
DOM reference to a checkbox, this function generates and returns a
unique (for this page load) ID. */
ResultsPage.generateCheckboxID = function () {
	if (!this.checkboxID) {
		this.checkboxID = 0;
	}
	return "checkbox_" + (this.checkboxID += 1);
};

/******************************************************************************
Composition of image and link URLs.
******************************************************************************/

ResultsPage.supportFileURL = function (url) {
	/* Argument url starts with "/".  Does not specify protocol or
	hostname. */
	if (/^(inftw|teamsite)/.test(document.location.host)) {
		return url;
	}
	else if (/\bGEAResults\.htm\b/.test(document.location.href)) {
		return "http://www.geappliances.com" + url;
	}
	else {
		return "http://teamsite.indsys.ge.com/" + 
			"iw-mount/GECP/main/Internet/GEA_Redesign/" + 
			"WORKAREA/Common" + url;
	}
};

ResultsPage.geaThumbnailURL = function (image) {
	return "http://products.geappliances.com/" +
		"MarketingObjectRetrieval/Dispatcher" + 
		"?RequestType=Image&Name=" + encodeURIComponent(image) + 
		"&Variant=ThumbnailPhoto";
};

ResultsPage.geaSpecPageURL = function (sku) {
	return "/ApplProducts/Dispatcher" + 
		"?REQUEST=SpecPage&Sku=" + encodeURIComponent(sku);
};

ResultsPage.geaDocumentURL = function (doc) {
	return "http://products.geappliances.com/" + 
		"MarketingObjectRetrieval/Dispatcher" + 
		"?RequestType=PDF&Name=" + encodeURIComponent(doc);
};

ResultsPage.geaCompareURL = function (skus) {
	if (skus && skus.length) {
		return "/ApplProducts/Dispatcher" +
			"?REQUEST=Compare&" + 
			skus.map(function (sku) {
				return "sku=" + encodeURIComponent(sku);
			}).join("&");
		// Yes.  SpecPage uses "Sku="; Compare uses "sku=".
	}
	return null;
};

ResultsPage.geaColorSwatchURL = function (id) {
	return "http://products.geappliances.com/" + 
		"MarketingObjectRetrieval/Dispatcher" + 
		"?RequestType=Image&RecId=" + encodeURIComponent(id);
};

/******************************************************************************
Populating the Filters
******************************************************************************/

/* Populates a single filter category with a single value. */
/* It is called by the appendFilter method. */
/* filterGroup and checkboxContainer are created by the appendFilter method. */
/* value is a DOM or jQuery reference to a <Value> inside a <Filter>. */
ResultsPage.appendFilterValue = function (filterGroup, checkboxContainer,
					  value) {
	var that = this;
	var name, checked, enabled, checkboxDiv, checkbox, label,
		definition, tooltipClick, checkboxID;

	name = $(value).text();
	checked = $(value).attr("checked") === "true";
	enabled = $(value).attr("enabled") === "true";
	definition = $(value).attr("definition");

	checkboxDiv = $("<div class='checkboxDiv'></div>");
	if (!enabled) {
		$(checkboxDiv).addClass("disabledCheckboxDiv");
	}
	$(checkboxContainer).append(checkboxDiv);

	checkboxID = this.generateCheckboxID();

	checkbox = $("<input type='checkbox' class='checkbox' />");
	$(checkboxDiv).append(checkbox); 
	// ^^^ see note [1] at the end of this file

	$(checkbox).attr("id", checkboxID);
	$(checkbox).attr("name", "xxx");
	$(checkbox).attr("value", name);
	$(checkbox).get(0).disabled = !enabled;
	$(checkbox).get(0).defaultChecked = false;
	$(checkbox).get(0).checked = checked;
	if (ResultsPage.IS_DEBUGGING && ResultsPage.FILTER_CHECKBOX_DELAY) {
		$(checkbox).click(function () {
			if (that.checkboxTimeout) {
				window.clearTimeout(that.checkboxTimeout);
			}
			that.checkboxTimeout = window.setTimeout(function () {
				that.checkboxTimeout = null;
				that.pageNumber = 1;
				that.updateResults();
			}, ResultsPage.FILTER_CHECKBOX_DELAY);
		});
	}
	else {
		$(checkbox).click(function () {
			that.pageNumber = 1;
			that.updateResults();
		});
	}

	filterGroup.values.push({ name: name, checkbox: $(checkbox).get(0) });
	label = $("<label></label>").text(name);
	$(checkboxDiv).append(label);
	label.get(0).htmlFor = checkboxID;
	label.css("cursor", "pointer");

	if (definition) {
		tooltipClick = function (event) {
			$("#filterValueTooltipTitle").empty().append(name);
			$("#filterValueTooltipText").empty().append(definition);
			showTooltip("filterValueTooltip", 300, event, this);
			return false;
		};
		$(checkboxDiv).append(" ");
		$(checkboxDiv).append(
			$("<a href='javascript:void(0);'></a>").
			append($("<img />").
			       src(this.supportFileURL("/images/results/icon_help.gif")).
			       alt("What is this?").
			       size(12, 12)).
			click(tooltipClick));
	}
};

/* Populates the page with a filter category and its values. */
/* filter is a DOM or jQuery reference to a <Filter> in the filters XML. */
ResultsPage.appendFilter = function (filter) {
	var that = this;
	var name, filterBox, filterBox_inner, filterBox_inner2,
		checkboxContainer, filterGroup;
	
	name = $(filter).children("Name").eq(0).text(); // was DisplayName
	
	filterBox = $("<div class='filterBox'></div>");
	$("#filtersContainer").append(filterBox);
	filterBox_inner = $("<div class='filterBox_inner'></div>");
	$(filterBox).append(filterBox_inner);
	filterBox_inner2 = $("<div class='filterBox_inner2'></div>");
	$(filterBox_inner).append(filterBox_inner2);
	
	$(filterBox_inner2).append($("<h3 class='filterHeading'></h3>").
				   text(name));
	
	checkboxContainer = $("<div class='checkboxContainer'></div>");
	$(filterBox_inner2).append(checkboxContainer);

	filterGroup = { name: name, values: [] };
	this.filterGroups.push(filterGroup);
	this.filterGroupsByName[name] = filterGroup;

	$(filter).find("Values>Value").each(function (index, value) {
		that.appendFilterValue(filterGroup, checkboxContainer, value);
	});
};

ResultsPage.populateFromFiltersXML = function (xml) {
	var that = this;
	var resetURL;
	
	// do these BEFORE calling appendFilter.
	this.filterGroups = [];
	this.filterGroupsByName = {};
	
	$("#filtersContainer").empty();

	if ((!xml) || ($(xml).find("Filter").length < 1)) {
		$("#narrowBy").css("display", "none");
		return;
	}
	
	$("#narrowBy").css("display", "block");

	$(xml).find("Filter").each(function (index, filter) {
		// do these BEFORE calling buildHashString.
		that.appendFilter(filter);
	});

	resetURL = "#" + that.buildHashString({ clearFilters: true,
						pageNumber: 1 });

	$("#filtersContainer").append("<div style='clear: both;'></div>");
	$("a.resetAllLink").href(resetURL).unbind("click").click(function () {
		$("#resultsForm").get(0).reset();
		that.pageNumber = 1;
		that.updateResults();
		return false;
	});
};

/******************************************************************************
Populating the Results
******************************************************************************/

/* Adds a row of cells to the grid-view table.  They may or may not be
populated later. */
ResultsPage.addGridViewRow = function () {
	var i, cell;

	this.resultInfoRow = $("<tr></tr>");
	this.resultCompareRow = $("<tr></tr>");
	this.resultInfoCells = [];
	this.resultCompareCells = [];
	for (i = 0; i < ResultsPage.GRID_VIEW_COLUMNS; i += 1) {
		cell = $("<td class='resultInfoCell'>&nbsp;</td>");
		this.resultInfoCells.push(cell);
		this.resultInfoRow.append(cell);
		cell = $("<td class='resultCompareCell'>&nbsp;</td>");
		this.resultCompareCells.push(cell);
		this.resultCompareRow.append(cell);
	}
	this.resultsTable.append(this.resultInfoRow);
	this.resultsTable.append(this.resultCompareRow);
};

/* Called before displaying the rebates tooltip. */
/* rebates is an array of objects created by the appendResult method
from the results XML. */
ResultsPage.populateRebatesTooltip = function (rebates) {
	var ul;

	ul = $("<ul></ul>").addClass("spaced");
	$("#rebatesContainer").empty().append(ul);
	
	rebates.forEach(function (rebate) {
		ul.append($("<li></li>").
			  append($("<a></a>").
				 attr("target", "_blank").
				 href(rebate.url).
				 append(rebate.text)));
	});
};

/* Called before displaying the additional colors tooltip. */
/* additionalColors is an array of objects created by the appendResult
method from the results XML. */
ResultsPage.populateAdditionalColorsTooltip = function (additionalColors) {
	var table, tr, td1, td2, col, colorSwatchURL, specPageURL;

	table = $("<table border='0' cellspacing='0' cellpadding='0' " + 
		  "class='additionalColorsTable'>");
	
	col = 1;
	additionalColors.forEach(function (o) {
		colorSwatchURL = this.geaColorSwatchURL(o.image);
		specPageURL = this.geaSpecPageURL(o.sku);
		if (col === 1) {
			tr = $("<tr></tr>");
			table.append(tr);
		}
		td1 = $("<td class='swatch'></td>");
		td2 = $("<td class='info'></td>");
		td1.append($("<div></div>").append($("<img />").
						   src(colorSwatchURL).
						   alt(o.color)));
		td2.append($("<b></b>").append($("<a></a>").
					       href(specPageURL).
					       append(o.color)));
		td2.append("<br />" + o.sku + "<br />" +
			   "$" + o.price + " MSRP");
		tr.append(td1).append(td2);
		col = (col % 2) + 1;
	}, this);
	if (col === 2) {
		tr.append($("<td class='swatch'>&nbsp;</td>"));
		tr.append($("<td class='info'>&nbsp;</td>"));
	}
	
	$("#additionalColorsContainer").empty().append(table);
};

/* Appends a product result to the list-view or grid-view table. */
/* result is a DOM or jQuery reference to a <Product> in the results XML. */
ResultsPage.appendResult = function (result) {
	var that = this;
	var modelNumber, description, altText, isEnergyStar,
		features, hasMoreFeatures, price, priceInquiryText,
		additionalColors, additionalColorNames, rebates,
		images, documents, energyGuides,
		whereToBuyLinkURL, whereToBuyText,
		imageURL, linkURL, specPageURL,
		photoDiv, modelDiv, featuresDiv, iconsDiv,
		energyStarIcon, energyGuideIcon,
		priceDiv, whereToBuyDiv, whereToBuyButton,
		rebatesDiv, moreColorsDiv,
		layoutCol1, layoutCol2, layoutCol3,
		resultRow, infoCell, compareCell,
		compareCheckboxSpan, compareTextSpan,
		compareCheckbox,
		promoIcons,
		promoIconsDiv,
		o, ul, i, onchange, onclick;
	
	modelNumber = $(result).children("ModelNumber").eq(0).text();
	description = $(result).children("Description").eq(0).text();
	altText = description + " " + $.EM_DASH + " Model #: " + modelNumber;
	isEnergyStar = $(result).children("IsEnergyStar").eq(0).text() === "true";
	
	features = $(result).find("Feature").get().map(function (e) {
		return $(e).text();
	});
	hasMoreFeatures = 
		$(result).children("Features").attr("hasmorefeatures") === "true";

	price = $(result).children("Price").eq(0).text();
	priceInquiryText = $(result).children("priceInquiryText").eq(0).text();
	
	additionalColors = 
		($(result).find("AdditionalColors>AdditionalColor").get().
		 map(function (e) {
			 return {
				 sku:   $(e).children("SKU").eq(0).text(),
				 color: $(e).children("Color").eq(0).text(),
				 image: $(e).children("Image").eq(0).text(),
				 price: $(e).children("Price").eq(0).text()
			 };
		 }));
	rebates = 
		($(result).find("Rebates>Rebate").get().
		 map(function (e) {
			 return {
				 text: $(e).children("RebateTitle").eq(0).text(),
				 url:  $(e).children("RebateURL").eq(0).text()
			 };
		 }));
	images = $(result).find("Images>Image").get().map(function (e) {
		return { type: $(e).attr("type"), text: $(e).text() };
	});
	documents = $(result).find("Documents>Document").get().map(function (e) {
		return { type: $(e).attr("type"), text: $(e).text() };
	});
	energyGuides = documents.filter(function (d) {
		return d.type === "Energy_Guide"; 
	});
	
	whereToBuyLinkURL = $(result).find("WhereToBuy>Link").eq(0).text();
	whereToBuyText    = $(result).find("WhereToBuy>Text").eq(0).text();
	if (whereToBuyText === "") {
		whereToBuyText = "Where to Buy";
	}

	/*********************************************************************/

	var checked = this.isSelectedSKU(modelNumber);
	o = { sku: modelNumber, checked: checked };
	this.results.push(o);

	specPageURL = this.geaSpecPageURL(modelNumber);

	/*********************************************************************/

	if (images.length) {
		imageURL = this.geaThumbnailURL(images[0].text);
		photoDiv = ($("<div class='photoDiv'></div>").
			    append($("<a></a>").
				   href(specPageURL).
				   append($("<img class='doubleBorder' />").
					  alt(altText).
					  src(imageURL).
					  size(96, 100))));
	}
	else {
		photoDiv = ($("<div class='photoDiv'></div>").
			    append($("<a></a>").
				   href(specPageURL).
				   append($("<img class='doubleBorder' />").
					  alt(altText + " (Photo Not Available)").
					  src(ResultsPage.PHOTO_NOT_AVAILABLE_URL).
					  size(96, 100))));
	}

	modelDiv = ($("<div class='modelDiv'></div>").
		    append($("<div class='titleDiv'></div>").
			   append($("<a></a>").
				  href(specPageURL).
				  text(description))).
		    append($("<div class='skuDiv'></div>").
			   text("Model #: " + modelNumber)));
	
	if (features.length) {
		featuresDiv = $("<div class='featuresDiv'></div>");

		ul = $("<ul></ul>");
		$(featuresDiv).append(ul);

		features.forEach(function (feature) {
			$(ul).append($("<li></li>").text(feature));
		});
		$(featuresDiv).append("<div style='clear: both;'></div>");
		if (hasMoreFeatures) {
			featuresDiv.append($("<a></a>").
					   href(specPageURL).
					   text("more features"));
		}
	}

	if (isEnergyStar || energyGuides.length) {
		iconsDiv = $("<div class='iconsDiv'></div>");
	}
	if (isEnergyStar) {
		energyStarIcon =
			$("<span class='resultIcon energyStarIcon'></span>");
		energyStarIcon.append(
			$("<img />").
			src(this.supportFileURL("/images/results/icon_energy_star.jpg")).
			size(39, 39).
			alt("This appliance is ENERGY STAR Compliant."));
		iconsDiv.append(energyStarIcon);
		iconsDiv.append(" ");
	}
	if (energyGuides.length) {
		energyGuideIcon =
			$("<span class='resultIcon energyGuideIcon'></span>");
		linkURL = this.geaDocumentURL(energyGuides[0].text);
		energyGuideIcon.append(
			$("<a></a>").
			attr("target", "_blank").
			href(linkURL).
			append($("<img />").
			       src(this.supportFileURL("/images/results/icon_energy_guide.gif")).
			       size(43, 14).
			       alt("Download Energy Guide").
			       attr("border", 0)));
		iconsDiv.append(energyGuideIcon);
		iconsDiv.append(" ");
	}

	if (additionalColors.length) {
		additionalColorNames = additionalColors.map(function (o) { 
			return o.color;
		}).joinAnd();
		onclick = function (event) {
			that.populateAdditionalColorsTooltip(additionalColors);
			showTooltip("additionalColorsTooltip",
				    300, event, this);
			return false;
		};
		moreColorsDiv = $("<div class='moreColorsDiv'></div>");
		moreColorsDiv.append($("<p></p>").
				     append($("<a href='javascript:void(0);'></a>").
					    click(onclick).
					    text("More Colors Available").
					    attr("title",
						 "Also available in " +
						 additionalColorNames)));
	}
	
	if (price !== null && price !== "") {
		onclick = function (event) {
			showTooltip("estRetailPriceTooltip", 360, event, this);
			return false;
		};
		priceDiv = $("<div class='priceDiv'></div>");
		priceDiv.append("<div class='amount'>$" + price + "</div>");
		priceDiv.append($("<a href='javascript:void(0);'></a>").
				click(onclick).
				text("MSRP"));
	}
	else if (priceInquiryText !== null && priceInquiryText !== "") {
		priceDiv = $("<div class='priceDiv'></div>");
		priceDiv.html(priceInquiryText);
	}
	
	whereToBuyDiv = $("<div class='whereToBuyDiv'></div>");
	whereToBuyButton = $("<div class='button'></div>");
	if (whereToBuyLinkURL) {
		whereToBuyButton.append($("<a></a>").
					href(whereToBuyLinkURL).
					text(whereToBuyText));
		whereToBuyDiv.append(whereToBuyButton);
	}
	else {
		whereToBuyDiv.html(whereToBuyText);
	}
	
	if (rebates.length) {
		rebatesDiv = $("<div class='rebatesDiv'></div>");
		rebatesDiv.append($("<a href='javascript:void(0);'></a>").
				  click(function (event) {
					  that.populateRebatesTooltip(rebates);
					  showTooltip('rebatesTooltip', 
						      215, event, this);
					  return false;
				  }).
				  addClass("rebatesLink").
				  append($("<img />").
					 src(this.supportFileURL("/images/common/icon_rebates.gif")).
					 size(21, 16).
					 alt("Rebates").
					 css("vertical-align", "middle")).
				  append("Rebates"));
	}

	promoIcons = $(result).find("PromoIcon").get().map(function (e) {
		return {
			image:   $(e).children("Image").eq(0).text(),
			altText: $(e).children("AltText").eq(0).text()
		};
	});
	if (promoIcons.length) {
		promoIconsDiv = $("<div class='promoIconsDiv'></div>");
		promoIcons.forEach(function (promoIcon) {
			var image = promoIcon.image;
			var altText = promoIcon.altText;
			var div = $("<div class='promoIconDiv'></div>");
			div.append($("<img />").
				   src(image).
				   alt(altText));
			promoIconsDiv.append(div);
		});
	}

	if (this.viewType === ResultsPage.GRID_VIEW) {
		if ((this.resultCount % ResultsPage.GRID_VIEW_COLUMNS) === 0) {
			this.addGridViewRow();
		}
		infoCell = 
			this.resultInfoCells[this.resultCount %
					     ResultsPage.GRID_VIEW_COLUMNS];
		compareCell =
			this.resultCompareCells[this.resultCount % 
						ResultsPage.GRID_VIEW_COLUMNS];
		this.resultCount += 1;

		// clear out the non-breaking spaces.
		infoCell.empty();
		compareCell.empty();

		layoutCol1 = $("<div class='layoutCol1'></div>");
		layoutCol2 = $("<div class='layoutCol2'></div>");

		if (photoDiv) {
			infoCell.append(photoDiv);
		}
		if (modelDiv) {
			infoCell.append(modelDiv);
		}
		if (featuresDiv) {
			infoCell.append(featuresDiv);
		}
		if (priceDiv) {
			layoutCol1.append(priceDiv);
		}
		if (whereToBuyDiv) {
			layoutCol1.append(whereToBuyDiv);
		}
		if (rebatesDiv) {
			layoutCol1.append(rebatesDiv);
		}
		if (promoIconsDiv) {
			layoutCol1.append(promoIconsDiv);
		}
		if (moreColorsDiv) {
			layoutCol1.append(moreColorsDiv);
		}
		if (iconsDiv) {
			layoutCol2.append(iconsDiv);
		}

		infoCell.append(layoutCol1);
		infoCell.append(layoutCol2);
		infoCell.append("<div style='clear: both;'></div>");
	}
	else { /* ResultsPage.LIST_VIEW */
		resultRow = $("<tr></tr>");
		this.resultsTable.append(resultRow);

		infoCell = $("<td class='resultInfoCell'></td>");
		compareCell = $("<td class='resultCompareCell'></td>");
		
		resultRow.append(infoCell);
		resultRow.append(compareCell);

		layoutCol1 = $("<div class='layoutCol1'></div>");
		layoutCol2 = $("<div class='layoutCol2'></div>");
		layoutCol3 = $("<div class='layoutCol3'></div>");

		if (photoDiv) {
			layoutCol1.append(photoDiv);
		}
		if (modelDiv) {
			layoutCol2.append(modelDiv);
		}
		if (featuresDiv) {
			layoutCol2.append(featuresDiv);
		}
		if (iconsDiv) {
			layoutCol2.append(iconsDiv);
		}
		if (moreColorsDiv) {
			layoutCol2.append(moreColorsDiv);
		}
		if (priceDiv) {
			layoutCol3.append(priceDiv);
		}
		if (whereToBuyDiv) {
			layoutCol3.append(whereToBuyDiv);
		}
		if (rebatesDiv) {
			layoutCol3.append(rebatesDiv);
		}
		if (promoIconsDiv) {
			layoutCol3.append(promoIconsDiv);
		}

		infoCell.append(layoutCol1);
		infoCell.append(layoutCol2);
		infoCell.append(layoutCol3);
		infoCell.append("<div style='clear: both;'></div>");
	}

	compareTextSpan = $("<span class='text'></span>");
	var updateCompareTextSpan = function () {
		if (o.checked) {
			infoCell.addClass("selectedResultInfoCell");
			compareCell.addClass("selectedResultCompareCell");
			o.link = $("<a href='#' class='compareLink'></a>");
			o.link.append("COMPARE MODELS NOW");
			compareTextSpan.empty().append(o.link);
		}
		else {
			infoCell.removeClass("selectedResultInfoCell");
			compareCell.removeClass("selectedResultCompareCell");
			o.link = null;
			compareTextSpan.empty();
			compareTextSpan.append("select to COMPARE MODELS");
		}
	};

	onclick = function () {
		if (that.selectedSKUs.length >=
		    ResultsPage.COMPARE_MAX_MODELS && this.checked) {
			this.checked = false;
			window.alert("Sorry, you may only select up to " +
				     ResultsPage.COMPARE_MAX_MODELS +
				     " models to compare.");
			that.removeSelectedSKU(this.value);
			return false;
		}
		o.checked = this.checked;
		if (this.checked) {
			that.addSelectedSKU(this.value);
		}
		else {
			that.removeSelectedSKU(this.value);
		}
		updateCompareTextSpan();
		that.updateCompareLinks();
	};

	compareCheckbox = $("<input type='checkbox' />");
	$(compareCheckbox).attr("name", "compareSKU");
	$(compareCheckbox).attr("value", modelNumber);
	$(compareCheckbox).click(onclick);
	
	compareCheckboxSpan = $("<span class='checkbox'></span>");
	$(compareCheckboxSpan).append(compareCheckbox);

	compareCell.append(compareCheckboxSpan);
	updateCompareTextSpan();
	compareCell.append(compareTextSpan);

	$(compareCheckbox).get(0).checked = checked;
	$(compareCheckbox).get(0).defaultChecked = checked;
	// see Note [1] at the end of this file.
};

ResultsPage.addSelectedSKU = function (sku) {
	if (this.selectedSKUs.indexOf(sku) === -1) {
		this.selectedSKUs.push(sku);
	}
};

ResultsPage.removeSelectedSKU = function (sku) {
	var i;
	while (this.selectedSKUs.length &&
	       (i = this.selectedSKUs.indexOf(sku)) !== -1) {
		this.selectedSKUs.splice(i, 1);
	}
};

ResultsPage.isSelectedSKU = function (sku) {
	return (this.selectedSKUs.indexOf(sku) !== -1);
};

/* Called after one of the compare checkboxes is checked or unchecked
and some DOM manipulation is done, this function ONLY changes what
happens when you click on one of the "COMPARE MODELS NOW" links under
them. */
ResultsPage.updateCompareLinks = function () {
	var skus, url;
	
	skus = this.selectedSKUs;

	if (skus.length < 2) {
		$("a.compareLink").each(function () {
			$(this).href("#").unbind("click").click(function () {
				window.alert("To compare, please select " +
					     "at least 2 models");
				return false;
			}).attr("title",
				"You must select at least " + 
				"2 models to compare.");
		});
	}
	else {
		url = this.geaCompareURL(skus);
		$("a.compareLink").each(function () {
			$(this).href(url).unbind("click").click(function () {
				return true;
			}).attr("title", "Compare selected items.");
		});
	}
};

/* xml is a DOM or jQuery reference to the results XML document. */
ResultsPage.populateFromResultsXML = function (xml) {
	var that = this;
	var countDiv;

	$("#resultsContainer").empty();
	this.resultsDisplayed = 0;
	this.resultCount = 0;	// needed while populating grid view.

	if (xml) {
		$(".resultsHeader").show();
		$("#pagination1").show();
		$(".resultsBar").show();
		$("#resultsContainer").show();
		$("#pagination2").show();
		$(".resultsCompareLine").show();

		this.resultsTable = $("<table class='resultsTable' " +
				      "border='0' cellspacing='0' " + 
				      "cellpadding='0'>");
		$("#resultsContainer").append(this.resultsTable);
		if (this.viewType === ResultsPage.GRID_VIEW) {
			this.resultsTable.addClass("resultsGridViewTable");
		}
		else { /* ResultsPage.LIST_VIEW */
			this.resultsTable.addClass("resultsListViewTable");
		}
		this.startIndex   =
			parseInt($(xml).find("StartIndex"  ).eq(0).text(), 10);
		this.endIndex     =
			parseInt($(xml).find("EndIndex"    ).eq(0).text(), 10);
		this.totalResults =
			parseInt($(xml).find("TotalResults").eq(0).text(), 10);
		this.pageNumber   =
			parseInt($(xml).find("PageNumber"  ).eq(0).text(), 10);
		this.totalPages   =
			parseInt($(xml).find("TotalPages"  ).eq(0).text(), 10);
		this.populateResultsHeader();
		this.results = [];
		if (!this.carryOverSelectedSKUs) {
			this.selectedSKUs = [];
		}
		$(xml).find("Product").each(function (index, result) {
			that.appendResult(result);
		});
		this.updateCompareLinks();
		this.carryOverSelectedSKUs = false;
	}
	else {
		$(".resultsHeader").hide();
		$("#pagination1").hide();
		$(".resultsBar").hide();
		$("#resultsContainer").hide();
		$("#pagination2").hide();
		$(".resultsCompareLine").hide();

		this.resultsTable = null;
		this.startIndex   = 0;
		this.endIndex     = 0;
		this.totalResults = 0;
		this.pageNumber   = 0;
		this.totalPages   = 0;
		this.results      = [];
		that.carryOverSelectedSKUs = false;
	}
};

/* Populates the pagination and that bar above the list-view or
grid-view table. */
ResultsPage.populateResultsHeader = function () {
	var that = this;
	var selector, url;

	$("#startIndex").text(this.startIndex);
	$("#endIndex").text(this.endIndex);
	$("#totalResults").text(this.totalResults);
	$("#totalResults2").text(this.totalResults);

	$("#totalResultsText").text(this.totalResults +
				    ((this.totalResults === 1) ?
				     " Model" : " Models"));

	// The space before "match" or "matches" is an IE cosmetic bug
	// workaround.  The bug involves two dynamically populated
	// inline elements next to one another, where even though
	// there's a space between them in the HTML source, it's not
	// rendered.
	$("#totalResultsText2").text((this.totalResults === 1) ?
				     " matches your selections" :
				     " match your selections");

	$("#totalResultsText3").text((this.totalResults === 0) ?
				     "0 Models" : 
				     (((this.startIndex === this.endIndex) ?
				       ("Model " + this.startIndex ) :
				       ("Models " + this.startIndex +
					$.EN_DASH + this.endIndex)) +
				      " of " + this.totalResults));

	if (this.totalResults === 0) {
		$("#resultsError").show().
		text("No models match your selections.");
	}
	else {
		$("#resultsError").hide().empty();
	}
	
	this.populatePagination($("#pagination1"));
	this.populatePagination($("#pagination2"));

	if (this.viewType === ResultsPage.GRID_VIEW) {
		url = "#" + this.buildHashString({
			viewType: ResultsPage.LIST_VIEW
		});

		$("#resultViewOptionGrid").empty().addClass("selected").
		append($("<nobr>Grid</nobr>").
		       append($("<img></img>").
			      src(this.supportFileURL("/images/results/icon_grid_selected.gif")).
			      size(26, 22).
			      alt("Grid")));
		$("#resultViewOptionList").empty().removeClass("selected").
		append($("<nobr></nobr>").
		       append($("<a>List</a>").href(url).
			      click(this.createChangeViewTypeFunction(ResultsPage.LIST_VIEW)).
			      append($("<img />").
				     src(this.supportFileURL("/images/results/icon_list.gif")).
				     size(26, 22))));
	}
	else {
		url = "#" + this.buildHashString({
			viewType: ResultsPage.GRID_VIEW
		});

		$("#resultViewOptionList").empty().addClass("selected").
		append($("<nobr>List</nobr>").
		       append($("<img></img>").
			      src(this.supportFileURL("/images/results/icon_list_selected.gif")).
			      size(26, 22).
			      alt("List")));
		$("#resultViewOptionGrid").empty().removeClass("selected").
		append($("<nobr></nobr>").
		       append($("<a>Grid</a>").href(url).
			      click(this.createChangeViewTypeFunction(ResultsPage.GRID_VIEW)).
			      append($("<img />").
				     src(this.supportFileURL("/images/results/icon_grid.gif")).
				     size(26, 22))));
	}

	selector = document.getElementById("resultsPerPageSelector");
	if (selector) {
		$(selector).empty();
		ResultsPage.RESULTS_PER_PAGE_OPTIONS.forEach(
			function (n) {
				var selected, option;
				selected = (n === that.resultsPerPage);
				option = $("<option></option>");
				option.attr("value", n);
				option.append(n);
				if (selected) {
					option.attr("selected", "selected");
				}
				$(selector).append(option);
			}
		);
		selector.onchange = function () {
			var value;

			// in IE, changing a dropdown then scrolling the
			// mousewheel triggers yet another update to the
			// page.
			window.focus();
			this.blur();

			value = this[this.selectedIndex].value;
			value = (typeof(value) === "string") ? 
				parseInt(value, 10) : value;
			that.resultsPerPage = value;
			that.pageNumber = 1;
			that.updateResults({ 
				carryOverSelectedSKUs: true
			});
			return false;
		};
	}
};

/* Creates and returns an onclick handler for the "Grid" or "List" link to
change from one view to the other. */
ResultsPage.createChangeViewTypeFunction = function (viewType) {
	var that = this;

	return function () {
		that.viewType = viewType;
		that.updateResults({
			carryOverSelectedSKUs: true
		});
		return false;
	};
};

/******************************************************************************
Pagination
******************************************************************************/

/* Creates and returns an onclick handler for a page number, previous,
or next link. */
ResultsPage.createGotoPageFunction = function (pageNumber) {
	var that = this;

	return function () {
		hideAllTooltips(); /* incase pagination tooltip is open */
		that.pageNumber = pageNumber;
		that.updateResults({
			carryOverSelectedSKUs: true
		});
		return false;
	};
};

/* Populates the Additional Pagination "tooltip" with page numbers from
i to j, inclusive. */
ResultsPage.populateAdditionalPaginationTooltip = function (i, j) {
	var where;

	where = $("#additionalPaginationContainer");
	where.empty();
	for (; i <= j; i += 1) {
		this.appendPageLink(where, i);
	}
};

/* Appends a link to the page number specified by i to the DOM or jQuery
element specified by where. */
ResultsPage.appendPageLink = function (where, i) {
	var url;

	url = "#" + this.buildHashString({ pageNumber: i });
	if (i === this.pageNumber) {
		$(where).append($("<span class='box current'></span>").
				text(i));
	}
	else {
		$(where).append($("<a class='box'></a>").
				href(url).
				click(this.createGotoPageFunction(i)).
				text(i));
	}
	where.append(" ");
};

/* Appends a "..." style page number link to the DOM or jQuery element
specified by where.  Link opens a tooltip for page numbers i to j,
inclusive. */
ResultsPage.appendEllipsisLink = function (where, i, j) {
	var that = this;
	var link;
	
	link = $("<a class='box' href='javascript:void(0);'>" +
		 $.ELLIPSIS + "</a>");
	link.mouseover(function (event) {
		that.populateAdditionalPaginationTooltip(i, j);
		showNonStickyTooltip("additionalPaginationTooltip",
				     120, event, this);
		return false;
	});
	link.attr("title", "Pages " + i + $.EN_DASH + j);

	if (this.pageNumber >= i && this.pageNumber <= j) {
		link.addClass("current");
	}
	
	$(where).append(link);
	$(where).append(" ");
};

/* Appends a "Prev" link to the DOM or jQuery element specified by where.
Link opens page number i. */
ResultsPage.appendPreviousPageLink = function (where, i) {
	var url;

	url = "#" + this.buildHashString({ pageNumber: i });
	where.append($("<a class='box'></a>").
		     href(url).
		     click(this.createGotoPageFunction(i)).
		     text("<" + $.NBSP + "Previous"));
	where.append(" &nbsp; ");
};

/* Appends a "Next" link to the DOM or jQuery element specified by where.
Link opens page number i. */
ResultsPage.appendNextPageLink = function (where, i) {
	var url;

	url = "#" + this.buildHashString({ pageNumber: i });
	where.append("&nbsp; ");
	where.append($("<a class='box'></a>").
		     href(url).
		     click(this.createGotoPageFunction(i)).
		     text("Next" + $.NBSP + ">"));
};

/* Clears the DOM or jQuery element specified by where, and adds the
pagination links to it. */
ResultsPage.populatePagination = function (where) {
	var that = this;
	var i, showPageLink, showEllipsis, prevFunction, nextFunction;

	where.empty();
	if (this.totalPages === 1) {
		return;
	}
	
	if (this.pageNumber > 1) {
		this.appendPreviousPageLink(where, this.pageNumber - 1);
	}
	if (this.totalPages <= ResultsPage.MAX_PAGES_TO_SHOW_ALL) {
		for (i = 1; i <= this.totalPages; i += 1) {
			this.appendPageLink(where, i);
		}
	}
	else {
		for (i = 1; i <= ResultsPage.MAX_PAGES_TO_SHOW_ALL - 2;
		     i += 1) {
			this.appendPageLink(where, i);
		}
		this.appendEllipsisLink(where, 8, this.totalPages - 1);
		this.appendPageLink(where, this.totalPages);
	}
	if (this.pageNumber < this.totalPages) {
		this.appendNextPageLink(where, this.pageNumber + 1);
	}
};

/******************************************************************************
Building the Hash String
-------------------------------------------------------------------------------
We rely on the following assumptions:

- Category names never contain a tilde (~).

- Filter group names and values never contain an exclamation point (!)
or asterisk (*) or tilde (~).
******************************************************************************/

ResultsPage.getSelectedCategories = function () {
	var isChecked, getName;

	/* These work on both categories and subcategories. */
	isChecked = function (cat) { return cat.checked; };
	getName = function (cat) { return cat.name; };

	if (this.categories.every(isChecked)) {
		return [{ name: this.mainCategoryName,
			  displayName: this.mainCategoryDisplayName }];
	}
	return (this.categories.
		map(function (c) { 
			if (c.checked) {
				return [c];
			}
			else {
				return c.subCategories.filter(isChecked);
			}
		}).
		filter(function (a) { return a && (a.length !== 0); }).
		reduce(function (previous, current) {
			return previous.concat(current);
		}, []));
};

ResultsPage.getSelectedCategoryNames = function () {
	return this.getSelectedCategories().map(
		function (c) { return c.name; }
	);
};

/* This function is not actually used for building query strings or hash
strings.  It is used for Omniture. */
ResultsPage.getSelectedCategoryDisplayNames = function () {
	return this.getSelectedCategories().map(
		function (c) { return c.displayName; }
	);
};

/* This function is also not used for building query strings or hash
strings.  It is used by the setMetaTags method. */
ResultsPage.getSelectedLevel2CategoryDisplayNames = function () {
	var isChecked;

	/* This works on both categories and subcategories. */
	isChecked = function (cat) { return cat.checked; };

	if (this.categories.length < 1) {
		return [this.mainCategoryDisplayName];
	}
	return (this.categories.
		map(function (c) {
			if (c.checked) {
				return [c.displayName];
			}
			else if (c.subCategories.some(isChecked)) {
				return [c.displayName];
			}
		}).
		filter(function (a) { return a && (a.length !== 0); }).
		reduce(function (previous, current) {
			return previous.concat(current);
		}, []));
};

ResultsPage.buildCategoryQueryStringComponent = function () {
	var qs;

	qs = this.getSelectedCategoryNames().join("~");
	if (qs !== "") {
		qs = encodeURIComponent(qs);
		return "Category=" + qs;
	}
	return null;
};

ResultsPage.filterGroupToQueryStringComponent = function (group) {
	var values, isChecked, getName;

	isChecked = function (value) { return value.checkbox.checked; };
	getName = function (value) { return value.name; };

	values = group.values.filter(isChecked).map(getName);
	if (values.length) {
		return group.name + "!" + values.join("*");
	}
	else {
		return null;
	}
};

ResultsPage.buildFiltersQueryStringComponent = function () {
	var qs;
	
	qs = (this.filterGroups.
	      map(ResultsPage.filterGroupToQueryStringComponent).
	      filter(function (x) { return x !== null; }).
	      join("~"));
	if (qs !== "") {
		qs = encodeURIComponent(qs);
		return "Filters=" + qs;
	}
	return null;
};

/* This function is invoked in two types of scenarios:

1. When someone clicks a form control (or link) that changes something
to update the results, in which case no options argument is passed.
This function's return value is passed on to $.historyLoad().

2. When a link is created that changes something to update the results,
in which case an options argument is passed containing what needs to be
modified.  This function's return value is used to create a link URL.
We do this so that, when you middle-click on a link to open a new tab,
a new tab is opened with the correct result.

In both cases, a hash string is returned.  It is like a query string,
only it's in the hash component of a URL, and is encoded slightly
differently.  See the comments in this function below for why.

Since the controls used to change the product categories, the sort
field, and the sort order are not links, this function does not support
overriding those. */
ResultsPage.buildHashString = function (/* optional */ options) {
	var cqsc, fqsc, sfqsc, qs, hs, qa;
	var viewType, pageNumber, resultsPerPage;

	if (!options) {
		options = {};
	}

	cqsc  = this.buildCategoryQueryStringComponent();
	fqsc  = this.buildFiltersQueryStringComponent();
	if (this.specialFiltersQuery !== null) {
		sfqsc = "SpecialFilters=" + 
			encodeURIComponent(this.specialFiltersQuery);
	}
	else {
		sfqsc = null;
	}
	if (options.clearFilters) {
		fqsc = null;
	}
	if (options.clearSpecialFilters) {
		sfqsc = null;
	}

	viewType = "viewType" in options ?
		options.viewType :
		this.viewType;
	pageNumber = "pageNumber" in options ?
		options.pageNumber :
		this.pageNumber;
	resultsPerPage = "resultsPerPage" in options ?
		options.resultsPerPage :
		this.resultsPerPage;
	
	qa = [];
	if (viewType !== ResultsPage.LIST_VIEW) {
		qa.push("v=" + encodeURIComponent(viewType));
	}
	if (pageNumber !== 1) {
		qa.push("p=" + encodeURIComponent(pageNumber));
	}
	if (resultsPerPage !== ResultsPage.DEFAULT_RESULTS_PER_PAGE) {
		qa.push("r=" + encodeURIComponent(resultsPerPage));
	}
	if ((this.sortField !== "erp") ||
	    (this.sortOrder !== ((this.sortField === "erp") ? "d" : "a"))) {
		qa.push("s=" + encodeURIComponent(this.sortField));
		qa.push("ascdesc=" + encodeURIComponent(this.sortOrder));
	}
	qa.push(cqsc);
	qa.push(fqsc);
	qa.push(sfqsc);
	
	qs = qa.filter(function (x) { return x !== null; }).join("&");
	return qs.replace(/\%/g, "$"); // see also: parseHashString

	/* Running $.historyLoad with an argument containing URL-encoded
	data (or anything with a "%") causes the function passed via
	$.historyInit to be invoked twice in certain browsers (at least
	Firefox 3).  While we need to use URL encoding to construct
	query strings, we need to "escape" the "%" characters.  Since
	the encodeURIComponent() function encodes "$" as well, we can
	safely use *that* in a hash string.  I'm assuming. */
};

/*****************************************************************************/

/* Adds dynamic behavior to the sort-by control, and selects its default
value. */
ResultsPage.activateSortByDropdown = function () {
	var that = this;
	var selector, selectedValue, i, option;

	selector = document.getElementById("sortBySelector");
	if (!selector) {
		return;
	}
	selector.onchange = function () {
		var a;

		// in IE, changing a dropdown then scrolling the mousewheel
		// triggers yet another update to the page.
		window.focus();
		this.blur();

		a = this.value.split(",");
		that.sortField = a[0];
		that.sortOrder = a[1];
		that.pageNumber = 1;
		that.updateResults({
			carryOverSelectedSKUs: true
		});
	};

	selectedValue = this.sortField + "," + this.sortOrder;
	for (i = 0; i < selector.options.length; i += 1) {
		option = selector.options[i];
		if (option.value === selectedValue) {
			selector.selectedIndex = i;
		}
	}
};

/*****************************************************************************/

ResultsPage.updateResults = function (options) {
	this.carryOverSelectedSKUs = options && options.carryOverSelectedSKUs;
	$.historyLoad(this.buildHashString());
	// triggers a call to onHistoryLoad.
};

/******************************************************************************
The "Loading..." Screen
******************************************************************************/

ResultsPage.LOADING_IMAGE_WIDTH = 67;
ResultsPage.LOADING_IMAGE_HEIGHT = 78;
ResultsPage.LOADING_IMAGE_URL = "/images/results/spinner_loading.gif";

ResultsPage.showLoadingScreen = function () {
	var documentHeight, verticalScroll, viewportHeight, imageY;

	if (document.documentElement &&
	    document.documentElement.scrollHeight) { 
		// IE, Mozilla
		documentHeight = document.documentElement.scrollHeight;
	}
	else if (document.body.scrollHeight) {
		// Safari
		documentHeight = document.body.scrollHeight;
	}
	
	if (window.innerWidth) {
		// All browsers but IE
		verticalScroll = window.pageYOffset;
		viewportHeight = window.innerHeight;
	}
	else if (document.documentElement &&
		 document.documentElement.clientWidth) {
		// IE6 with DOCTYPE
		verticalScroll = document.documentElement.scrollTop;
		viewportHeight = document.documentElement.clientHeight;
	}
	else if (document.body.clientWidth) { // IE4, IE5, IE6 without DOCTYPE
		verticalScroll = document.body.scrollTop;
		viewportHeight = document.body.clientHeight;
	}

	imageY = Math.floor(verticalScroll +
			    viewportHeight / 2 -
			    ResultsPage.LOADING_IMAGE_HEIGHT / 2);

	if (!this.loadingImage || !this.blockingLayer) {
		this.blockingLayer = (
			$("<div id='blockingLayer'></div>").
			css("position", "absolute").
			css("width", "100%").
			css("left", "0px").
			css("z-index", "1000").
			css("background-color", "white").
			css("opacity", "0.82").
			append($.NBSP)
		);
		this.loadingImage = (
			$("<div id='loadingImage'></div>").
			css("position", "absolute").
			css("width", "100%").
			css("z-index", "1001").
			css("text-align", "center").
			css("left", "0px").
			append($("<img />").
			       src(this.supportFileURL(ResultsPage.LOADING_IMAGE_URL)).
			       size(ResultsPage.LOADING_IMAGE_WIDTH,
				    ResultsPage.LOADING_IMAGE_HEIGHT).
			       alt("Loading" + $.ELLIPSIS))
		);
		$(document.body).prepend(this.loadingImage);
		$(document.body).prepend(this.blockingLayer);
	}

	this.loadingImage.css("top", imageY + "px");
	this.blockingLayer.css("height", documentHeight + "px");

	$(this.blockingLayer).show();
	$(this.loadingImage).show();
};

ResultsPage.hideLoadingScreen = function () {
	$(this.blockingLayer).hide();
	$(this.loadingImage).hide();
};

ResultsPage.loadingCount = 0;
/* holds the number of AJAX requests in progress at any given point in time. */

ResultsPage.incrementLoadingCount = function () {
	if (this.loadingCount === 0) {
		this.showLoadingScreen();
	}
	this.loadingCount += 1;
};

ResultsPage.decrementLoadingCount = function () {
	this.loadingCount -= 1;
	if (this.loadingCount === 0) {
		this.hideLoadingScreen();
	}
};

/******************************************************************************
Populating the Categories
******************************************************************************/

ResultsPage.populateFromCategoriesXML = function (xml) {
	var that = this;
	var text, url, col, tr, td, div, meta_keywords, meta_description, name;

	$("#productCategoryListSpan").empty();

	/* used for Category= portion of hash string and XML request
	query strings when all categories are checked. */
	if (xml) {
		this.mainCategoryName =
			$(xml).find("Categories>Category>Name").eq(0).text();
		this.mainCategoryDisplayName =
			$(xml).find("Categories>Category>DisplayName").eq(0).text();
	}
	else {
		this.mainCategoryName = "";
		this.mainCategoryDisplayName = "";
	}

	this.categories = [];

	if ((!xml) || ($(xml).find("Category").length < 1)) {
		$("#productCategoryListHeader").css("display", "none");
		$("#categoryBreadcrumb").css("display", "none");
		$(".mainProductCategoryGoesHere").text("GE Products");
		return;
	}

	$("#productCategoryListHeader").css("display", "block");
	$("#categoryBreadcrumb").css("display", "inline");

	$(xml).find("Category[displayed='true']").each(function (index, category) {
		name = $(category).find("DisplayName").eq(0).text();
		if (index) {
			$("#productCategoryListSpan").append("&nbsp;&nbsp;|&nbsp; ");
		}
		$("#productCategoryListSpan").append(name);
	});
	
	url = $(xml).find("Categories>Category>URL").eq(0).text();

	$(".breadcrumb a.mainProductCategoryGoesHere").href(url);
	$(".mainProductCategoryGoesHere").text(this.mainCategoryDisplayName);
	document.title = this.mainCategoryDisplayName + " from GE Appliances";

	if ($(xml).find("Category[secondlevel='true']").length < 1) {
		return;
	}

	$(xml).find("Category[secondlevel='true']").each(function (index, categoryXML) {
		var name, displayName, checkbox, checked, category;

		name = $(categoryXML).children("Name").text();
		displayName = $(categoryXML).children("DisplayName").text();
		checked = $(categoryXML).attr("checked") === "true";

		category = {
			name: name,
			displayName: displayName,
			checked: checked,
			subCategories: []
		};
		that.categories.push(category);

		$(categoryXML).find("Category").each(function (subIndex, subCategoryXML) {
			var subName, subDisplayName, subCheckbox, subChecked;

			subName = $(subCategoryXML).children("Name").text();
			subDisplayName = $(subCategoryXML).children("DisplayName").text();
			subChecked = checked ||
				($(subCategoryXML).attr("checked") === "true");

			category.subCategories.push({
				name: subName,
				displayName: subDisplayName,
				checked: subChecked
			});
		});
	});
	this.setMetaTags();
};

/* We do this incase search engines start figuring out how to navigate
the JavaScript-required results page *and* extract the updated meta tags
after each update. */
ResultsPage.setMetaTags = function () {
	var meta_keywords, meta_description;

	meta_keywords = (this.getSelectedLevel2CategoryDisplayNames().
			 concat(ResultsPage.META_KEYWORDS).join(", "));
	$("meta[name='keywords']").attr("content", meta_keywords);

	meta_description = (ResultsPage.META_DESCRIPTION_TEMPLATE.
			    replace(/\[products\]/g, 
				    this.mainCategoryDisplayName.
				    toLowerCase()));
	$("meta[name='description']").attr("content", meta_description);
};

/******************************************************************************
Populate Special Filters area
******************************************************************************/

ResultsPage.populateFromSpecialFilters = function () {
	var that = this;
	var p, a, url;

	$("#clearSpecialFiltersContainer").empty();
	if (this.specialFiltersQuery === null) {
		return;
	}

	url = "#" + this.buildHashString({ clearSpecialFilters: true,
					   pageNumber: 1 });
	
	p = $("<p style='margin-top: 1.0em;'></p>");
	p.append(this.specialFiltersQuery.
		 replace(/\!/, ": ").
		 replace(/\*/, ", ").
		 replace(/\~/, "; ") + " &nbsp; ");

	a = $("<a></a>");
	a.append("remove");
	a.href(url);
	a.unbind("click");
	a.click(function () {
		that.specialFiltersQuery = null;
		that.pageNumber = 1;
		that.updateResults();
		return false;
	});

	p.append(a);
	$("#clearSpecialFiltersContainer").append(p);
};

/******************************************************************************
Composition of XML Request URLs
******************************************************************************/

ResultsPage.categoriesXMLURL = function () {
	if (ResultsPage.IS_USING_SAMPLE_DATA) {
		return "/_test/results_page/categories.xml";
	}
	else {
		return "/ApplProducts/Dispatcher" +
			"?REQUEST=CATEGORYHIERARCHY&Category=" +
			encodeURIComponent(this.categoryQuery);
	}
};

ResultsPage.filtersXMLURL = function () {
	if (ResultsPage.IS_USING_SAMPLE_DATA) {
		return "/_test/results_page/filters.xml";
	}
	else {
		return "/ApplProducts/Dispatcher" + 
			"?REQUEST=FILTEROPTIONS&" +
			this.xmlRequestQueryString;
	}
};

ResultsPage.resultsXMLURL = function () {
	if (ResultsPage.IS_USING_SAMPLE_DATA) {
		return "/_test/results_page/results.xml";
	}
	else {
		return "/ApplProducts/Dispatcher" + 
			"?REQUEST=MODELSELECTION&" +
			this.xmlRequestQueryString;
	}
};

/*****************************************************************************/

/* Loads all XML, and populates the page when all requests successfully
return. */
ResultsPage.load = function () {
	var that = this;
	var categoriesXML, 
		filtersXML, 
		resultsXML,
		categoriesRequestComplete, 
		filtersRequestComplete, 
		resultsRequestComplete,
		oncomplete, onerror, errors;

	errors = [];
	oncomplete = function () {
		/* called when each request is finished */
		if (categoriesRequestComplete &&
		    filtersRequestComplete &&
		    resultsRequestComplete) {
			if (errors.length) {
				window.alert("One or more unexpected errors " +
					     "happened while trying to " + 
					     "retrieve data.\n\nDetails:\n\n" +
					     errors.join("\n\n"));
			}
			that.populateFromCategoriesXML(categoriesXML);
			that.populateFromFiltersXML(filtersXML);
			that.populateFromResultsXML(resultsXML);
			that.populateFromSpecialFilters();
			if (that.categories.length ||
			    that.filterGroups.length ||
			    that.results.length ||
			    ResultsPage.IS_DEBUGGING) {
				that.runOmniture();
			}
		}
	};
	onerror = function (request, status, ex) {
		var message = "";
		message += "Error type: " + status + "\n";
		message += "Request URL: " + this.url + "\n";
		message += "Request Status: " + request.status +
			" " + request.statusText + "\n";
		if (ex) {
			message += "Exception: " + ex.name + " -- " + 
				ex.message + "\n";
		}
		message = message.replace(/\n$/, "");
		errors.push(message);
	};

	this.incrementLoadingCount();
	this.incrementLoadingCount();
	this.incrementLoadingCount();
	$.ajaxXML({
		url: this.categoriesXMLURL(),
		success: function (xml) {
			categoriesXML = xml;
		},
		error: onerror,
		complete: function (request, status) {
			/* called after success or error */
			that.decrementLoadingCount();
			if (ResultsPage.IS_DEBUGGING) {
				$("#debugCategoriesXML").empty().
				append($("<a>Categories XML</a>").
				       href(this.url));
			}
			categoriesRequestComplete = true;
			oncomplete();
		}
	});
	$.ajaxXML({
		url: this.filtersXMLURL(),
		success: function (xml) {
			filtersXML = xml;
		},
		error: onerror,
		complete: function (request, status) {
			/* called after success or error */
			that.decrementLoadingCount();
			if (ResultsPage.IS_DEBUGGING) {
				$("#debugFiltersXML").empty().
				append($("<a>Filters XML</a>").
				       href(this.url));
			}
			filtersRequestComplete = true;
			oncomplete();
		}
	});
	$.ajaxXML({
		url: this.resultsXMLURL(),
		success: function (xml) {
			resultsXML = xml;
		},
		error: onerror,
		complete: function (request, status) {
			/* called after success or error */
			that.decrementLoadingCount();
			if (ResultsPage.IS_DEBUGGING) {
				$("#debugResultsXML").empty().
				append($("<a>Results XML</a>").
				       href(this.url));
			}
			resultsRequestComplete = true;
			oncomplete();
		}
	});
};

/*****************************************************************************/

/* Returns a string containing the selected values for the specified
filter(s) with the specified name separated by "; ", or the empty
string. */
ResultsPage.getFilterValueListForOmniture = function (/* name [, ...] */) {
	var that = this;
	var isChecked, getName, names, filters, values;
	
	isChecked = function (value) { return value.checkbox.checked; };
	getName = function (value) { return value.name; };

	names = Array.prototype.splice.call(arguments, 0);
	// Copies a pseudo-array to a *real* array, at which point we
	// can actually use Array methods.  See:
	// http://www.mennovanslooten.nl/blog/post/59
	
	filters = names.filter(function (name) {
		return name in that.filterGroupsByName;
	}).map(function (name) {
		return that.filterGroupsByName[name];
	});
	
	values = filters.map(function (filter) {
		return filter.values;
	}).reduce(function (previous, current) {
		return previous.concat(current);
	}, []);

	return values.filter(isChecked).map(getName).join("; ");
};

ResultsPage.runOmniture = function () {
	var prop3append, prop4append, categoryQuery;

	if (this.categoryQuery) {
		categoryQuery = (this.categoryQuery.replace(/_/g, " ").
				 replace(/~/g, "; "));
	}
	else {
		categoryQuery = "";
	}
	prop3append = this.mainCategoryDisplayName ||
		categoryQuery;
	prop4append = this.getSelectedCategoryDisplayNames().join("; ") || 
		categoryQuery;

	//Page Names
	s.pageName = document.title;

	//Site Sections
	s.channel = "Products";

	//Site Section Level 2
	s.prop3 = s.channel + "|" + prop3append;

	//Site Section Level 3
	s.prop4 = s.prop3 + "|" + prop4append;

	//Site Section Level 4
	s.prop5 = s.prop4 + "|Product Results Page";

	//page Type
	s.prop16 = "Product Results";

	//Appliance Type being searched
	s.prop17 = s.prop4;

	s.prop18 = this.getFilterValueListForOmniture("BRAND");
	s.prop19 = this.getFilterValueListForOmniture("COLOR");
	s.prop20 = this.getFilterValueListForOmniture("PRICE");
	s.prop21 = this.getFilterValueListForOmniture("CAPACITY");
	s.prop22 = this.getFilterValueListForOmniture("INSTALLATION");
	s.prop23 = this.getFilterValueListForOmniture(
		"WASH CYCLES", /* dishwashers */
		"WASHER CYCLES" /* washers */);
	s.prop24 = this.getFilterValueListForOmniture("FEATURES");
	s.prop25 = this.getFilterValueListForOmniture(
		"SPECIAL CYCLES" /* dryers */);
	s.prop29 = this.getFilterValueListForOmniture("DISPENSER");
	s.prop30 = this.getFilterValueListForOmniture("ICEMAKER");
	s.prop32 = this.getFilterValueListForOmniture("FUEL TYPE");
	s.prop33 = this.getFilterValueListForOmniture("COOKING TECHNOLOGY");
	s.prop34 = this.getFilterValueListForOmniture("CONFIGURATION");
	s.prop35 = this.getFilterValueListForOmniture("COOKTOP BURNER TYPE");
	s.prop37 = this.getFilterValueListForOmniture("STYLE");
	s.prop38 = this.getFilterValueListForOmniture("CONTROL TYPE");
	s.prop39 = this.getFilterValueListForOmniture("COOKING MODES");
	s.prop42 = this.getFilterValueListForOmniture("VOLTAGE");
	s.prop44 = this.getFilterValueListForOmniture("UNIT CAPACITY");
	s.prop47 = this.getFilterValueListForOmniture("COOLING BTUH");
	s.prop48 = this.getFilterValueListForOmniture("UNIT TYPE");

	//Site Hierarchy
	s.hier1 = s.prop5;

	runOmnitureOnResults();
};

/*****************************************************************************/

/* invoked when the HTML page is loaded or when the hash string is changed. */
ResultsPage.parseHashString = function (hs) {
	var pairs, pair, i, idx, name, value, qs, query;

	query = {};
	if (hs !== null && hs !== undefined) {
		qs = hs.replace(/\$/g, "%"); // see also: buildHashString
		pairs = qs.split("&");
		pairs.forEach(function (pair) {
			idx = pair.indexOf("=");
			if (idx === -1) {
				name = pair;
				value = pair;
			}
			else {
				name = pair.substring(0, idx);
				value = pair.substring(idx + 1);
			}
			name = decodeURIComponent(name.replace(/\+/g, " "));
			value = decodeURIComponent(value.replace(/\+/g, " "));
			query[name] = value;
		});
	}

	this.viewType = ("v" in query) ? parseInt(query.v, 10) : 
		ResultsPage.LIST_VIEW;
	this.resultsPerPage = ("r" in query) ? parseInt(query.r, 10) :
		ResultsPage.DEFAULT_RESULTS_PER_PAGE;
	this.pageNumber = ("p" in query) ? parseInt(query.p, 10) : 1;
	this.sortField = ("s" in query) ? query.s : "erp";
	if ("ascdesc" in query) {
		this.sortOrder = query.ascdesc;
	}
	else {
		// default depends on sortField.
		this.sortOrder = (this.sortField === "erp") ? "d" : "a";
	}
	this.categoryQuery = ("Category" in query) ? query.Category : null;
	this.filtersQuery  = ("Filters" in query)  ? query.Filters  : null;
	this.specialFiltersQuery = ("SpecialFilters" in query) ? 
		query.SpecialFilters : null;
	
	// Kind of like the hash string, but we don't need to pass the
	// viewType, and it's URL-encoded without replacing % with $.
	this.xmlRequestQueryString = 
		["p="       + encodeURIComponent(this.pageNumber),
		 "r="       + encodeURIComponent(this.resultsPerPage),
		 "s="       + encodeURIComponent(this.sortField),
		 "ascdesc=" + encodeURIComponent(this.sortOrder),
		 ((this.categoryQuery !== null) ?
		  "Category=" + encodeURIComponent(this.categoryQuery) : null),
		 ((this.filtersQuery  !== null) ?
		  "Filters="  + encodeURIComponent(this.filtersQuery) : null),
		 ((this.specialFiltersQuery !== null) ?
		  "SpecialFilters=" + encodeURIComponent(this.specialFiltersQuery) : null)
		].filter(function (x) { return x !== null; }).join("&");
};

/* invoked when historyLoad or historyInit is called. */
ResultsPage.onHistoryLoad = function (hs) {
	this.parseHashString(hs);
	this.load();
	this.activateSortByDropdown();
};

/* invoked when the HTML page is loaded. */
ResultsPage.initialize = function () {
	var that = this;
	var debug;

	this.selectedSKUs = [];

	this.carryOverSelectedSKUs = false;
	// is temporarily set to true ONLY when doing one of the following:
	// - changing a page number
	// - switching between grid and list view
	// - changing sorting criteria
	// - changing the number of results per page

	if (ResultsPage.IS_DEBUGGING) {
		debug = $("<div id='debug'></div>");
		debug.css("background-color", "yellow");
		debug.css("color", "black");
		debug.append("<h1>Debugging</h1>");
		if (ResultsPage.FILTER_CHECKBOX_DELAY) {
			debug.append("<p>The secret " + 
				     "<b>filter checkbox delay feature</b> " +
				     "is turned on.<br />" +
				     "It allows you to click multiple " +
				     "checkboxes in rapid succession. " +
				     "After a " + 
				     ResultsPage.FILTER_CHECKBOX_DELAY +
				     "-millisecond delay, the results " +
				     "page will update.");
			debug.append("<p><b>This feature will not appear " +
				     "on production or in security review. " + 
				     "So don't worry.</b></p>");
		}
		debug.append("<div id='debugCategoriesXML' />");
		debug.append("<div id='debugFiltersXML' />");
		debug.append("<div id='debugResultsXML' />");
		debug.append($("<div id='debugMessages' />").css({
			"overflow": "scroll",
			"height": "8em",
			"background-color": "white",
			"border": "solid 1px black",
			"width": "50%",
			"text-align": "left",
			"margin": "1.0em auto 0 auto"
		}));
		$(document.body).prepend(debug);
	}
	$.historyInit(function (hs) {
		that.onHistoryLoad(hs);
	});
	
	// in IE, topnav (and other) links don't work until we do this.
	$("a[href*='GEAResults.htm'],a[href*='GEAResultsTest.htm']").
	click(function () {
		var hash = this.href.replace(/^.*\#/, "");
		$.historyLoad(hash); // triggers a call to onHistoryLoad.
		
		return false;
	});
};

/******************************************************************************
Notes:

[1] You must place a checkbox into the DOM before manipulating its
checked and defaultChecked properties.  Otherwise, when the checkbox is
inserted into the DOM, its checked property is reset to its
defaultChecked property in IE6 (and possibly other browsers).
******************************************************************************/


