function VarUtils() {
}

/**
 * Returns true if a value is not undefined or null.
 */
VarUtils.prototype.isDef = function(value) {
	return (typeof value != "undefined") && value != null;
}

/**
 * Returns true if value is has a value other than 'null' or the empty string.
 */
VarUtils.prototype.hasValue = function(value) {
	return this.isDef(value)
			&& (typeof value == "number" || !("" === (value.replace(/^\s*/, "")
					.replace(/\s*$/, ""))));
}

/**
 * Returns true if (string paramter) value is a number otherwise false.
 */
VarUtils.prototype.isNumber = function(value) {
	return (typeof value == "string")
			&& !value.replace(/\$,\./, value).match(/[^0-9]+/);
}

/**
 * Returns -1 if a &lt; b, 0 if a=b, 1 if a &gt; b.
 */
VarUtils.prototype.compare = function(a, b) {
	var results = 0;
	if (this.isNumber(a) && this.isNumber(b)) {
		results = a - b;
	} else {
		results += (a < b ? -1 : (a > b ? 1 : 0));
	}

	return results;
}

/**
 * Returns true if <code>value</code> is between <code>low</code> and <code>high</code>.
 * The default operation is inclusive, set <code>exclusive</code> to <code>true</code> if
 * you want value to match either <code>low</code> or <code>high</code>.
 */
VarUtils.prototype.isBetween = function(value, low, high, exclusive) {
	var results = false;
	if (varUtils.isDef(exclusive) && exclusive) {
		results = (value > low && value < high);
	} else {
		results = (value >= low && value <= high);
	}
	return results;
}

/**
 * Returns a numeric value formatted as currency, e.g. 1000.00->1,000.00. A currency symbol is not
 * added. You can specify a normalized factor and a precision to aid in converting between denominations.
 * 
 * For example to convert 100,000 pennies to dollars set the normalizationfactor=100 and the precision=2 (if you want
 * to maintain a fractional (cents) component).
 * 
 * Precision defaults to 2 if not specified.
 */
VarUtils.prototype.formatCurrency = function(value, normalizationfactor,
		precision) {
	var formatted = value;
	if (varUtils.isDef(value) && !isNaN(value)) {
		if (varUtils.isDef(normalizationfactor) && varUtils.isNumber(value)) {
			value = new Number(parseInt(value) / normalizationfactor).toFixed(
					(varUtils.isDef(precision) ? precision : 2)).toString();
		}

		var dollarsAndCents = value.split(/\./);
		var dollar = dollarsAndCents[0];
		formatted = dollar;
		if (dollar.length > 3) {
			var splitPoint = dollar.length - 3;
			var lastThreeDigits = dollar.slice(splitPoint);
			formatted = this.formatCurrency(dollar.slice(0, splitPoint)) + ","
					+ lastThreeDigits
		}
		formatted += (dollarsAndCents.length > 1 ? "." + dollarsAndCents[1]
				: "");
	}

	return formatted;
}

/**
 * Performs a binary search over <code>sortedArray</code> and returns either the index
 * at which the element (primatives only) appears or null.  
 */
VarUtils.prototype.search = function(sortedArray, element, lower, upper,
		comparator) {
	var index = null;
	lower = this.isDef(lower) ? lower : 0;
	upper = this.isDef(upper) ? upper : sortedArray.length;

	var defaultComparator = function(a, b) {
		var results = 0;
		if (a > b) {
			results = 1;
		} else if (a < b) {
			results = -1;
		}

		return results;
	}

	var comparator = varUtils.isDef(comparator) ? comparator
			: defaultComparator;

	if (upper >= lower) {
		if (upper == lower) {
			index = (comparator(sortedArray[upper], element) == 0) ? upper
					: null;
		} else {
			var midpoint = Math.floor((upper - lower) / 2);
			var match = comparator(sortedArray[midpoint], element);
			if (match == 0) {
				index = midpoint;
			} else if (match < 0) {
				index = this.search(sortedArray, element, lower, midpoint,
						comparator);
			} else {
				index = this.search(sortedArray, element, midpoint, upper,
						comparator);
			}
		}
	}

	return index;
}

/**
 * Returns true if and only if <code>value</code> is defined and has a boolean value of <code>true</code>.
 */
VarUtils.prototype.isTrue = function(value) {
	return this.isDef(value)
			&& (value === true || (typeof value == "string" && value
					.toLowerCase() === "true"));
}

/**
 * Converts <code>string</code> to a <code>json</code>object. 
 */
VarUtils.prototype.stringToJSON = function(string) {
	var json = string;
	if (typeof json == "string") {
		json = this.isDef(string.evalJSON) ? json.evalJSON() : eval("(" + json
				+ ")");
	}

	return json;
}

function URLBuilder(url) {
	this.urlUtils = new URLUtils(url);

	this.protocol = this.urlUtils.getProtocol();
	this.user = this.urlUtils.getUser();
	this.password = this.urlUtils.getPassword();
	this.host = this.urlUtils.getHost();
	this.path = this.urlUtils.getPath();
	this.fragment = this.urlUtils.getFragment();
	this.parameters = this.urlUtils.getParameters();
	if (!varUtils.isDef(this.parameters)) {
		this.parameters = new Object();
	}
}

URLBuilder.prototype.setPath = function(path) {
	this.path = path;
}
URLBuilder.prototype.setProtocol = function(protocol) {
	this.protocol = protocol;
}
URLBuilder.prototype.setUser = function(user) {
	this.user = user;
}
URLBuilder.prototype.setPassword = function(password) {
	this.password = password;
}
URLBuilder.prototype.clearParameters = function() {
	delete this.parameters;
	this.parameters = new Object();
}
URLBuilder.prototype.getParameters = function() {
	return this.parameters;
}
URLBuilder.prototype.addParameter = function(name, value) {
	if (!varUtils.isDef(this.parameters[name])) {
		this.parameters[name] = new Array();
	}
	this.parameters[name].push(value)
}

URLBuilder.prototype.getQueryString = function(encode) {
	encode = varUtils.isDef(encode) ? encode : true;

	var count = 0
	var parameters = "";
	for ( var parameter in this.parameters) {
		var values = this.parameters[parameter];
		for ( var i = 0; i < values.length; ++i) {
			if (count > 0) {
				parameters = parameters.concat("&");
			}
			parameters = parameters.concat(parameter.concat("=",
					encode ? encodeURIComponent(values[i]) : values[i]));
			++count;
		}
	}

	return parameters;
}

URLBuilder.prototype.getURL = function() {
	var url = varUtils.hasValue(this.protocol) ? (this.protocol + "://") : "";

	if (varUtils.hasValue(this.user)) {
		url = url.concat(this.user,
				varUtils.hasValue(this.password) ? (":" + this.password) : "",
				"@");
	}

	url = varUtils.hasValue(this.host) ? (url.concat("/", this.host)) : "";
	url = varUtils.hasValue(this.path) ? (url.concat("/", this.path)) : "";

	// return
	// url.concat(varUtils.hasValue(parameters)?"?".concat(parameters):"");
	return url.concat("?".concat(this.getQueryString()));
}

/**
 * Helper functions for URL manipulation. Breaks up a URL into it's constituent parts plus any query string parameters.
 */
function URLUtils(url) {
	this.protocol = "";
	this.user = "";
	this.password = "";
	this.host = "";
	this.port = "";
	this.path = "";
	this.fragment = "";
	this.queryString = "";
	this.parameters = null;
	this.init(url);
}

/**
 * Initialization routine, <code>url</code> is separated into protocol, host, port, user, password,path,fragment and
 * any query string parameters. If <code>url</code> is not provided this information is taken from the browsers <code>window</code>
 * object. 
 */
URLUtils.prototype.init = function(url) {
	if (!varUtils.hasValue(url)) {
		if (varUtils.isDef(window.location)) {
			this.protocol = window.location.protocol.replace(/:/, "");
			this.host = window.location.hostname;
			this.port = window.location.port;
			this.path = window.location.pathname;
			this.fragment = window.location.hash;
			this.queryString = window.location.search;
			if (varUtils.hasValue(this.queryString)) {
				this.queryString = this.queryString.replace(/\?/, "");
			}
		}
	} else {
		if (url.indexOf("//") >= 0) {
			var splits = url.split("//");
			url = splits[1];
			var hostPort = splits[0].split(":");
			this.host = splits[0];
			this.port = splits[1];
		}

		if (url.indexOf("@") >= 0) {
			var splits = url.split("@");
			url = splits[1];
			var userPasswod = splits[0].split(":");
			this.user = userPassword[0];
			if (userPassword.length > 1) {
				this.password = userPassword[1];
			}
		}

		var queryStrBegin = url.indexOf("?");
		if (queryStrBegin >= 0) {
			this.queryString = url.substr(queryStrBegin + 1);
			url = url.substr(0, queryStrBegin);
			// alert("URL: "+url+"\nQuery string: "+this.queryString);
		}

		if (url.indexOf("#") >= 0) {
			var splits = url.split("#");
			this.fragment = splits[1];
			url = splits[0];
		}

		var hostDelimiter = "/";
		var hostEnd = url.indexOf(hostDelimiter);
		if (hostEnd >= 0) {
			this.host = url.substring(0, hostEnd);
			url = url.substring(hostEnd + hostDelimiter.length);
		}

		this.path = url;
	}

	this.initParams();
}

URLUtils.prototype.initParams = function() {
	if (!varUtils.isDef(this.parameters)) {
		this.parameters = new Object();
	}

	if (varUtils.hasValue(this.queryString)) {
		var paramValues = this.queryString.split(/\&/);
		for ( var i = 0; i < paramValues.length; i++) {
			var paramValue = paramValues[i].split(/=/);
			if (varUtils.isDef(this.parameters[paramValue[0]])) {
				this.parameters[paramValue[0]].push(paramValue[1])
			} else {
				this.parameters[paramValue[0]] = new Array();
				this.parameters[paramValue[0]].push(paramValue[1])
			}
		}
	}
}

URLUtils.prototype.getProtocol = function() {
	return this.protocol;
}
URLUtils.prototype.getUser = function() {
	return this.user;
}
URLUtils.prototype.getPassword = function() {
	return this.password;
}
URLUtils.prototype.getHost = function() {
	return this.host;
}
URLUtils.prototype.getPort = function() {
	return this.port;
}
URLUtils.prototype.getPath = function() {
	return this.path;
}
URLUtils.prototype.getFragment = function() {
	return this.fragment;
}
URLUtils.prototype.getQueryString = function() {
	return this.queryString;
}
URLUtils.prototype.getParameters = function() {
	return this.parameters;
}

/**
 * Returns the path portion of the url, everything after the host:port
 * up to the query string including any fragments (the bit between '#' and '?').
 */
URLUtils.prototype.getPath = function() {
	return this.path;
}

/**
 * Returns everything after the question mark.
 */
URLUtils.prototype.getQueryString = function() {
	return this.queryString;
}
/**
 * Returns the entire set of query string parameters as an associative array.
 */
URLUtils.prototype.getQueryStringParams = function() {
	if (!varUtils.isDef(this.parameters)) {
		this.initParams();
	}

	return this.parameters;
}

/**
 * Returns the value (only) of a single query string parameter.
 */
URLUtils.prototype.getQueryStringParam = function(param) {
	if (!varUtils.isDef(this.parameters)) {
		this.initParams();
	}

	var paramValue = this.parameters[param];
	if (varUtils.isDef(paramValue) && paramValue.length > 0) {
		paramValue = (paramValue.length > 0) ? (paramValue.length > 1 ? paramValue
				: paramValue[0])
				: null;
	}
	return paramValue;
}

URLUtils.prototype.getReferrer = function() {
	return document.referrer;
}

function CookieUtils() {
}

/**
 * Creates a cookie, name, with value, value. Cookie will expire
 * in days, um, days from the moment it is set. Leave days blank or negative
 * if the cookie should expire at the end of the session or 0 if it
 * should be deleted.
 */
CookieUtils.prototype.createCookie = function(name, value, days) {
	if (varUtils.isDef(days)) {
		var date = new Date();
		date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
		var expires = "; expires=" + date.toGMTString();
	} else
		var expires = "";
	document.cookie = name + "=" + value + expires + "; path=/";
}

/**
 * returns the cookie with name, name.
 */
CookieUtils.prototype.readCookie = function(name) {
	var nameEQ = name + "=";
	var ca = document.cookie.split(";");
	for ( var i = 0; i < ca.length; i++) {
		var c = ca[i];
		while (c.charAt(0) == " ")
			c = c.substring(1, c.length);
		if (c.indexOf(nameEQ) == 0)
			return c.substring(nameEQ.length, c.length);
	}
	return null;
}

/**
 * Deletes the cookie with name 'name'.
 */
CookieUtils.prototype.eraseCookie = function(name) {
	createCookie(name, "", 0);
}

/**
 * Routines to help deal with referrals.
 * @return
 */
function ReferrerUtils() {
	this.cookieUtils = new CookieUtils();
}

ReferrerUtils.prototype.setWeddingChannelCookie = function() {
	var cookieName = "referrer-mcf";
	var params = urlUtils.getQueryStringParams();
	var sessionId = params.wc_sid;
	var wcRef = params.wcref;
	var registryId = (urlUtils.getPath().indexOf("registry") >= 0 && varUtils
			.isDef(params.id)) ? params.id : null;

	if (varUtils.isDef(sessionId) || varUtils.isDef(wcRef)) {
		this.cookieUtils.createCookie(cookieName, "weddingchannel|"
				+ registryId
				+ "|"
				+ (varUtils.isDef(sessionId) ? sessionId : "null")
				+ "|{referrer:'weddingchannel',referrerData:{sessionId:'"
				+ (varUtils.isDef(sessionId) ? sessionId : "null")
				+ "',registryId:"
				+ registryId
				+ ",utm_source:"
				+ (varUtils.isDef(params.utm_source) ? "'" + params.utm_source
						+ "'" : null)
				+ ",utm_medium:"
				+ (varUtils.isDef(params.utm_medium) ? "'" + params.utm_medium
						+ "'" : null)
				+ ",utm_campaign:"
				+ (varUtils.isDef(params.utm_campaign) ? "'"
						+ params.utm_campaign + "'" : null) + "}}");
	}

}

ReferrerUtils.prototype.setNextJumpCookie = function() {
	var cookieName = "referrer-nxj";
	var params = urlUtils.getQueryStringParams();

	var u1 = params.u1;

	if (varUtils.isDef(u1) /* && urlUtils.getReferrer()=='www.nextjump.com' */) {
		this.cookieUtils.createCookie(cookieName, "nextjump|"
				+ u1
				+ "|{referrer:'nextjump',referrerData:{u1:'"
				+ (varUtils.isDef(u1) ? u1 : "null")
				+ "',id:"
				+ (varUtils.isDef(params.ID) ? "'" + params.ID + "'" : null)
				+ ",categoryId:"
				+ (varUtils.isDef(params.CategoryID) ? "'" + params.CategoryID
						+ "'" : null) + "}}");
	}
}

function MenuUtils() {
	this.visibleMenu = null;
	this.menuSource = null;
	this.timer = null;
	this.visibleDuration = (3 * 1000);
	this.context = null;
	this.offsetLeft = 0;
	this.position = "absolute";
	this.paddingLeft = 0;
}

MenuUtils.prototype.setPaddingLeft = function(paddingLeft) {
	this.paddingLeft = paddingLeft;
}

MenuUtils.prototype.setPosition = function(position) {
	this.position = position;
}

MenuUtils.prototype.setOffsetLeft = function(offset) {
	this.offsetLeft = offset;
}

MenuUtils.prototype.showMenu = function(parent, menu) {
	this.visibleMenu = menu.cloneNode(true);

	this.visibleMenu.setStyle( {
		position : this.position
	});
	
	/*in ie, parent.offsetLeft always returns 0*/
	var offsetLeft = parent.offsetLeft;
	if(this.offsetLeft != 0){
		offsetLeft = this.offsetLeft;
	}
	
	this.visibleMenu.setStyle( {
		left : (offsetLeft + "px")
	});
	this.visibleMenu.setStyle( {
		visibility : "visible"
	});
	this.visibleMenu.setStyle( {
        display : "block"
	});

	this.visibleMenu.setStyle( {
		paddingLeft : (this.paddingLeft + "px")
	});
	
	menu.parentNode.appendChild(this.visibleMenu);
	this.menuSource = menu;
}

MenuUtils.prototype.hideMenu = function(menu) {
	if (varUtils.isDef(this.timer)) {
		clearTimeout(this.timer);
	}

	menu.setStyle( {
		visibility : "hidden"
	});
	menu.setStyle( {
		position : "static"
	});
	menu.parentNode.removeChild(menu);
	this.visibleMenu = null;
	this.menuSource = null;
}

MenuUtils.prototype.toggleVisibility = function(parent, menu) {
	if (varUtils.isDef(menu)) {
		if (varUtils.isDef(this.visibleMenu)) {
			this.hideMenu(this.visibleMenu);
		}

		this.showMenu(parent, menu);
	}
}

MenuUtils.prototype.startHideTimer = function(menu) {
	if (varUtils.isDef(this.menuSource) && this.menuSource === menu) {
		//this.timer=setTimeout("menutils.hideMenu(menutils.visibleMenu)",this.visibleDuration);
	}
}

MenuUtils.prototype.getUrl = function(url) {
	return this.context + url;
}

function ImageUtils(appContext, staticImgRoot, productImgRoot) {
	this.appContext = appContext;
	this.staticImgRoot = staticImgRoot;
	this.fullPath = productImgRoot;

	// this is the "detail" fluid image
	this.detailPath = "EXPLODEDPATH/SKU_detail/main_variation_default_view_1_WIDTHxHEIGHT.jpg";

	// this is the "generated" fluid image
	this.otherPath = "EXPLODEDPATH/generated/SKU_default_1_WIDTHxHEIGHT.jpg";

	this.productSizeMapping = {
		detail : {
			placeholder : staticImgRoot + "/placeholder100x100.jpg",
			file : {
				width : 450,
				height : 500
			},
			web : {
				width : 450,
				height : 500
			}
		},
		large : {
			placeholder : staticImgRoot + "/placeholder100x100.jpg",
			file : {
				width : 250,
				height : 250
			},
			web : {
				width : 250,
				height : 250
			}
		},
		medium : {
			placeholder : staticImgRoot + "/placeholder100x100.jpg",
			file : {
				width : 130,
				height : 130
			},
			web : {
				width : 120,
				height : 120
			}
		},
		smallmedium : {
			placeholder : staticImgRoot + "/placeholder100x100.jpg",
			file : {
				width : 100,
				height : 100
			},
			web : {
				width : 88,
				height : 88
			}
		},
		small : {
			placeholder : staticImgRoot + "/placeholder100x100.jpg",
			file : {
				width : 50,
				height : 50
			},
			web : {
				width : 48,
				height : 48
			}
		}
	}
}

/**
 * Returns the path to the image for a given sku. This is based on the set
 * of images for the fluid displays and there is no check to ensure that the
 * returned path actually points to something. If there is no image the path will
 * ultimately return a 404
 *
 * sku - Product sku
 * sizeKey - One of: small, smallmedium, medium, large. Sizes return images:
 * 48x48, 88x88, 120x120, and 250x250 respectively
 *
 * isDetail - set to true if you want the larger size image that is normally displayed
 * when viewing a single product.
 *
 */
ImageUtils.prototype.getProductImgPath = function(sku, size, isDetail) {
	var path = null;

	if (varUtils.isDef(isDetail) && isDetail) {
		path = this.fullPath + this.detailPath;
	} else {
		path = this.fullPath + this.otherPath;
	}

	path = path.replace(/SKU/g, sku);
	path = path.replace(/EXPLODEDPATH/g,
			split_sku_code_for_fluid_image_path(sku));

	if (varUtils.isDef(size)) {
		if (typeof size == "string") {
			size = this.productSizeMapping[size];
		}
	} else {
		size = this.productSizeMapping["medium"];
	}

	path = path.replace(/HEIGHT/, size.file.height);
	path = path.replace(/WIDTH/, size.file.width);
	return path;
}

/**
 * Returns a fully formatted image tag for a given product (sku).
 *
 * sku - Product sku
 * sizeKey - One of: small, smallmedium, medium, large. Sizes return images:
 * 48x48, 88x88, 120x120, and 250x250 respectively
 *
 * alt - value for the alt attribute.
 *
 * isDetail - set to true if you want the larger size image that is normally displayed
 * when viewing a single product.
 *
 * forcePlaceholder - The current method of rendering an image tag does not confirm whether or not
 * an image is available for the product unless test is true. If you have an alternate means of
 * testing for the existence of an image and need to use the place holder set this to true. The correct
 * sized place holder will be returned.
 *
 * test - will check to see if the file exists; if not the returned tag references
 * the appropriate place holder.
 */
ImageUtils.prototype.getProductImgTag = function(sku, size, alt, isDetail,
		forcePlaceholder, test) {
	var tag = "<img ID HEIGHT WIDTH SRC ALT/>";
	var size = this.productSizeMapping[size];
	var imgPath = forcePlaceholder ? size.placeholder : this.getProductImgPath(
			sku, size, isDetail);
	var idPrefix = "pimg-";

	if (varUtils.isDef(test) && test) {
		var callback = function(data, textStatus) {
			if (!data.exists || textStatus != "success") {
				jQuery("#" + idPrefix + data.sku).attr("src", size.placeholder);
			}
		}
		jQuery.get(this.appContext + "/util/file_test.jsp", {
			file : imgPath,
			psku : sku
		}, callback, "json");
	}

	tag = tag.replace(/ID/, "id='" + idPrefix + sku + "'").replace(/HEIGHT/,
			"height='" + size.web.height + "'").replace(/WIDTH/,
			"width='" + size.web.width + "'").replace(/SRC/,
			"src='" + imgPath + "'").replace(/ALT/, "ALT='" + alt + "'");

	return tag;
}

ImageUtils.prototype.setDetailPath = function(path) {
	this.detailPath = path;
}

ImageUtils.prototype.setOtherPath = function(path) {
	this.otherPath = path;
}

ImageUtils.prototype.setPlacerholderPath = function(size, path, webWidth,
		webHeight) {
	this.productSizeMapping[size].placeholder = path;
	this.productSizeMapping[size].web.width = varUtils.isDef(webWidth) ? webWidth
			: this.productSizeMapping[size].web.width;
	this.productSizeMapping[size].web.height = varUtils.isDef(webHeight) ? webHeight
			: this.productSizeMapping[size].web.height;
}

/**
 * Helper function to build the dom objects used by the filter, product, pagination, and autocomplete routines.
 */
function DomUtils() {
}

/**
 * Helper function to build the dom objects used by the filter, product, pagination, and autocomplete routines.
 * This doesn't actually create a DOM node it just packages up the meta data for use by other components.
 * @param attributes - Control attributes.
 * @param events - Events and their handlers.
 * @param data - Control data.
 * @param value - Value of the filter control.
 * @param callbacks - Routines that will be invoke pre, at, and post render..
 */
DomUtils.prototype.buildDomObject = function(attributes, events, data, value,
		callbacks) {
	var domObject = new Object();
	domObject["attributes"] = attributes;
	domObject["events"] = events;
	domObject["data"] = data;
	domObject["value"] = value;
	domObject["callbacks"] = callbacks;
	return domObject;
}

DomUtils.prototype.buildLabelObject = function(term, label, count) {
	var labelObject = new Object();
	labelObject["term"] = term;
	labelObject["label"] = label;
	labelObject["count"] = count;

	return labelObject;
}

/**
 * Maps values returned from the solr calls to human readable values.
 *
 * Solr response conversions (see db: product_spec_option, option_paradigm, option_paradigm_value)
 */
function SolrFacetUtils(translationMap) {
	this.domUtils = new DomUtils();
	this.translationMap = translationMap;
	this.formatters = {
		formatPrice : function(price) {
			var value = price.replace(/\"/, "");
			var highLow = value
					.replace(
							/price:\[(\d+(\.\d+)?) TO ((\d+(\.\d+)?)|(\*))\]/g,
							"$1,$3").split(/,/);
			var label = (highLow[1] == "*" ? "" : "$")
					+ varUtils.formatCurrency(highLow[0], 100, 0) + " TO $"
					+ varUtils.formatCurrency(highLow[1], 100, 0);
			if ((highLow[0] + 0) == 0) {
				label = "Under $" + varUtils.formatCurrency(highLow[1], 100, 0);
			}
			if (highLow[1] == "*") {
				label = "$" + varUtils.formatCurrency(highLow[0], 100, 0)
						+ " and above";
			}

			return label;
		}
	};

	/**
	 * The <code>filter</code> are called before (pre), during (render), and
	 * after (post) each filter and each filter element. You can replace any
	 * with your own implementation.
	 */
	this.filterCallbacks = {
		preItem : function(option, filter, filterItem, index) {
		},
		postItem : function(option, filter, filterItem, index) {
		},
		itemRender : function(container, filter, filterItem, index) {
			var option = jQuery("<option></option>").appendTo(container);
			option.attr("value", encodeURIComponent(filterItem.term));
			option.text(filterItem.label);
			if (varUtils.hasValue(filter.value)
					&& filterItem.term == filter.value) {
				option.attr("selected", true);
			}

			//added so dropdown appear properly in safari and chrome
			jQuery(container).hide();
			jQuery(container).show();
		},

		pre : function(container, filters) {
		},
		post : function(container, filters) {
		},
		//Default filter render - creates a select element and defers to individual callbacks to render each option.
		render : function(container, filter) {
			var selectControl = jQuery("<select></select>").appendTo(container);
			if (varUtils.isDef(filter.attributes)) {
				for ( var attribute in filter.attributes) {
					selectControl.attr(attribute, filter.attributes[attribute]);
				}
			}

			if (varUtils.isDef(filter.events)) {
				for ( var i = 0; i < filter.events.length; ++i) {
					var event = filter.events[i];
					selectControl.bind(event.name, event.data, event.handler);
				}
			}

			var callbackStrategy = new Object();
			var callbacks = filter.callbacks;
			callbackStrategy["pre"] = (varUtils.isDef(callbacks) && varUtils
					.isDef(callbacks["pre"])) ? callbacks["pre"]
					: this.filterCallbacks.preItem;
			callbackStrategy["post"] = (varUtils.isDef(callbacks) && varUtils
					.isDef(callbacks["post"])) ? callbacks["post"]
					: this.filterCallbacks.postItem;
			callbackStrategy["render"] = (varUtils.isDef(callbacks) && varUtils
					.isDef(callbacks["render"])) ? callbacks["render"]
					: this.filterCallbacks.itemRender;
			var thisArg = (varUtils.isDef(callbacks) && varUtils
					.isDef(callbacks["thisArg"])) ? callbacks["thisArg"] : this;

			for ( var i = 0; i < filter.data.length; ++i) {
				//We need to create the option element in the item method as options are only rendered if translation is available
				// var
				// option=jQuery("<option></option>").appendTo(selectControl);
				callbackStrategy["pre"].call(thisArg, selectControl, filter,
						filter.data[i], i);
				callbackStrategy["render"].call(thisArg, selectControl, filter,
						filter.data[i], i);
				callbackStrategy["post"].call(thisArg, selectControl, filter,
						filter.data[i], i);
			}
		}
	};

}

/**
 * Renders search filters from a set of Solr facet counts.
 * 
 * @param container - the jQuery wrapped parent HTML element where the containers
 * should be appended.
 * @param filters - An array of filter objects
 * <p>Example:<br/>
 * <code>
 * [{attributes:[{name:&lt;name&gt;,value:&lt;value&gt;}..],events:[{name:&lt;name&gt;,handler:&lt;function&gt;,data:[]}],data:[],value:&lt;value&gt;,callbacks:{thisArg:&lt;this argument&gt;,pre:&lt;function&gt;post:&lt;function&gt;render:&lt;function&gt;}},...,{...}]
 * </code>
 * <br/>In this case, the "pre" function is rendered prior to rendering any filters, the post after, and the "render" is handed control for each iteration.
 * <br/>For the default implementation data is the the array of term,value objects returned from <code>convertFacetsToObjects</code>.
 * </p>
 * 
 * @param callbacks - As above <code>callbacks:{thisArg:&lt;this argument&gt;,pre:"function",post:"function",render:"function"}</code><br/>Callbacks should
 * have the following signatures: <ul><li><code>pre:function(&lt;jQuery wrapped container element&gt;,&lt;filter object&gt;)</code></li>
 * <li><code>post:function(&lt;jQuery wrapped container element&gt;,&lt;filter object&gt;)</code></li>
 * <li><code>render:function(&lt;jQuery wrapped container element&gt;,&lt;filter object&gt;)</code></li></ul>
 * Callbacks are invoked in the following order: <ol><li>pre</li><li>render</li><li>post</li></ol> for each filter.
 * <p><span style="font-weight:bold">NOTE WELL:</span> If the <code>thisArg</code> member of the callbacks argument is undefined or null it
 * will be set to the active instance of <code>SolrFacetUtils</code>.</p>
 */
SolrFacetUtils.prototype.renderFilters = function(container, filters, callbacks) {
	if (varUtils.isDef(filters) && filters.length > 0) {
		var callbackStrategy = new Object();
		callbackStrategy["pre"] = (varUtils.isDef(callbacks) && varUtils
				.isDef(callbacks["pre"])) ? callbacks["pre"]
				: this.filterCallbacks.pre;
		callbackStrategy["post"] = (varUtils.isDef(callbacks) && varUtils
				.isDef(callbacks["post"])) ? callbacks["post"]
				: this.filterCallbacks.post;
		callbackStrategy["render"] = (varUtils.isDef(callbacks) && varUtils
				.isDef(callbacks["render"])) ? callbacks["render"]
				: this.filterCallbacks.render;

		var thisArg = (varUtils.isDef(callbacks) && varUtils
				.isDef(callbacks["thisArg"])) ? callbacks["thisArg"] : this;

		for ( var i = 0; i < filters.length; ++i) {
			callbackStrategy.pre.call(thisArg, container, filters[i]);
			callbackStrategy.render.call(thisArg, container, filters[i]);
			callbackStrategy.post.call(thisArg, container, filters[i]);
		}
	}
}

/**
 * Copies the facet portion of a JSON Solr response to an object. The object has the following structure:
 * <p>
 * <code>
 * {<br/>
 * fields:{field:[{term:&lt;term&gt;,label:&lt;term&gt;,count:&lt;count&gt;},...,{term:&lt;term&gt;,label:&lt;term&gt;,count:&lt;count&gt;},...,field:[...]}
 * <br/>}
 * </code>
 * </p>
 * <p>All facets are returned - when count is equal or greater to <code>minCount</code>. - so you should be prepared for that.
 * If <code>minCount</code> is undefined it is assigned a value of zero.<br/>You'll note that the label is the same as term. You can update this
 * later if you want to apply some special formatting (for currency for example).</p> 
 */
SolrFacetUtils.prototype.convertFacetsToObjects = function(solrResponse,
		minCount) {
	var facets = new Object();
	facets["facetFields"] = new Object();

	minCount = varUtils.isDef(minCount) ? minCount : 0;

	if (varUtils.isDef(solrResponse)
			&& varUtils.isDef(solrResponse.facet_counts)) {
		//assemble the facet queries
		if (varUtils.isDef(solrResponse.facet_counts.facet_queries)) {
			var queries = solrResponse.facet_counts.facet_queries;
			for ( var queryTerm in queries) {
				var fieldName = queryTerm.split(":")[0];
				var field = facets["facetFields"][fieldName];

				if (!varUtils.isDef(field)) {
					field = new Array();
					facets["facetFields"][fieldName] = field;
				}

				var count = queries[queryTerm];
				if (count >= minCount) {
					field.push(this.domUtils.buildLabelObject(queryTerm,
							queryTerm, queries[queryTerm]));
				}
			}
		}

		//assemble the facet fields
		var facetFields = solrResponse.facet_counts.facet_fields;
		if (varUtils.isDef(facetFields)) {
			for ( var facetField in facetFields) {
				var termsAndCounts = facetFields[facetField];
				var field = facets["facetFields"][facetField];
				if (!varUtils.isDef(field)) {
					field = new Array();
					facets["facetFields"][facetField] = field;
				}
				for ( var i = 0; i < termsAndCounts.length; i += 2) {
					var count = termsAndCounts[i + 1];
					if (count >= minCount) {
						var term = termsAndCounts[i];
						field.push(this.domUtils.buildLabelObject(term, term,
								count));
					}
				}
			}
		}
	}
	return facets;
}

/**
 * Handler for change events on filters. Just submits the parent form.
 */
SolrFacetUtils.prototype.filterSearchResults = function(form, page) {
	var currentPageInputField = jQuery("#current_page_id");
	if (varUtils.isDef(currentPageInputField)) {
		currentPageInputField.val(page);
	}
	form.submit();
}

/**
 * On change handler for filters - submits a query and updates all filters with the results. This allows
 * the filter values to dynamically change based on the results of the most recent query.
 */
SolrFacetUtils.prototype.updateFilters = function(form) {
	var searchAction = contextPath + "/sitesearch/search_do";
	var params = new Object();
	params["search_type"] = "filter_refresh";
	params["primary_search_type"] = form.search_type.value;
	var selectBoxes = jQuery("select[name^='filter_by']");
	for ( var i = 0; i < selectBoxes.length; ++i) {
		var value = selectBoxes[i].value;
		if (!varUtils.hasValue(value)) {
			value = "";
		}
		params[selectBoxes[i].name] = value;
	}
	jQuery.getJSON(searchAction, params, function(responseData) {
		data = responseData;
		initializeData();
		for ( var i in params) {
			if (i.match(/^filter_by/)) {
				var temp = params[i];
				for ( var j = 0; dropDownSets[dropDownSet][j].name != i; ++j) {
				}
				if (dropDownSets[dropDownSet][j].name == i) {
					dropDownSets[dropDownSet][j].selectedValue = params[i]
							.replace(/\%5C/, "'");
				}
			}
		}
		SolrFacetUtils.renderDropDowns(dropDownSets[dropDownSet], form.id
				+ "Controls", true, "footer");
	});
}

/**
 * Basic helper class to manipulate the documents returned from a solr query. There are some default routines to render
 * documents in a grid format. You can inject your own document render methods if you wish.
 */
function SolrDocumentUtils() {
	this.domUtils = new DomUtils();
	this.callbacks = {
		document : {
			pre : function(container, documents, document, index) {
			},
			post : function(container, documents, document, index) {
			},
			render : function(container, documents, document, index) {
				var listItem = jQuery("<li></li>").appendTo(container);
				var anchor = jQuery("<a></a>").appendTo(listItem);

				var link = document[linkType].replace(/^null/, contextPath);
				anchor.attr("href", "/" + link);
				anchor.attr("title", document.name + " By: " + document.brand);

				var imageUtils = new ImageUtils(contextPath, staticImgPath,
						fluidBaseItemURL);
				var imageTag = imageUtils
						.getProductImgTag(
								document.sku,
								imageSize,
								document.name,
								false,
								(!varUtils.isDef(document.image_url) || (document.pattern && document.image_url == "images/placeholder100x100.jpg")),
								false);
				jQuery(imageTag).appendTo(anchor);
				jQuery("<br/>").appendTo(anchor);
				(jQuery("<span></span>").appendTo(anchor)).text(document.name);
				jQuery("<br/>").appendTo(anchor);
				(jQuery("<span></span>").appendTo(anchor)).text("By: "
						+ document.brand);

				var unformatted = parseInt(document.price * 1);
				var price = varUtils.formatCurrency(
						convert_to_dollars(unformatted), 100, 0);
				jQuery(
						"<br/>" + ((unformatted + 0) > 0 ? "$" + price : "")
								+ "<br/>").appendTo(listItem);
			}
		},
		pre : function(container, documents) {
		},
		post : function(container, documents) {
		},
		//Default filter render - creates a select element and defers to individual callbacks to render each option.
		render : function(container, documents) {
			var documentContainer = jQuery("<ul></ul>").appendTo(container)
					.attr("class", "prodlist").attr("id", "prodlist");

			var callbackStrategy = new Object();
			var callbacks = documents.callbacks;
			callbackStrategy["pre"] = (varUtils.isDef(callbacks) && varUtils
					.isDef(callbacks["pre"])) ? callbacks["pre"]
					: this.callbacks.document.pre;
			callbackStrategy["post"] = (varUtils.isDef(callbacks) && varUtils
					.isDef(callbacks["post"])) ? callbacks["post"]
					: this.callbacks.document.post;
			callbackStrategy["render"] = (varUtils.isDef(callbacks) && varUtils
					.isDef(callbacks["render"])) ? callbacks["render"]
					: this.callbacks.document.render;
			var thisArg = (varUtils.isDef(callbacks) && varUtils
					.isDef(callbacks["thisArg"])) ? callbacks["thisArg"] : this;

			for ( var i = 0; i < documents.data.length; ++i) {
				callbackStrategy["pre"].call(thisArg, documentContainer,
						documents, documents.data[i], i);
				callbackStrategy["render"].call(thisArg, documentContainer,
						documents, documents.data[i], i);
				callbackStrategy["post"].call(thisArg, documentContainer,
						documents, documents.data[i], i);
			}
		}
	};
}

SolrDocumentUtils.prototype.convertDocumentsToObjects = function(solrResponse,
		fields) {
	var documents = new Array();
	if (varUtils.isDef(solrResponse.response)) {
		var docs = solrResponse.response.docs;
		for ( var i = 0; i < docs.length; ++i) {
			var document = docs[i];
			for ( var j = 0; j < fields.length; ++j) {
				var field = fields[j];
				documents.push(this.domUtils.buildLabelObject(document[field],
						document[field]));
			}
		}
	}
	return documents;
}

/**
 * Renders each document in <code>documents</code>, the JSON object returned by a Solr search.
 * 
 *  @param container - the jQuery wrapped container to put render the documents
 *  @param documents - JSON object returned from solr search
 *  @param callbacks - callback functions for pre, post, and render routines.
 */
SolrDocumentUtils.prototype.renderDocuments = function(container, documents,
		callbacks) {
	if (varUtils.isDef(documents) && documents.data.length > 0) {
		var callbackStrategy = new Object();
		callbackStrategy["pre"] = (varUtils.isDef(callbacks) && varUtils
				.isDef(callbacks["pre"])) ? callbacks["pre"]
				: this.callbacks.pre;
		callbackStrategy["post"] = (varUtils.isDef(callbacks) && varUtils
				.isDef(callbacks["post"])) ? callbacks["post"]
				: this.callbacks.post;
		callbackStrategy["render"] = (varUtils.isDef(callbacks) && varUtils
				.isDef(callbacks["render"])) ? callbacks["render"]
				: this.callbacks.render;

		var thisArg = (varUtils.isDef(callbacks) && varUtils
				.isDef(callbacks["thisArg"])) ? callbacks["thisArg"] : this;

		callbackStrategy.pre.call(thisArg, container, documents);
		callbackStrategy.render.call(thisArg, container, documents);
		callbackStrategy.post.call(thisArg, container, documents);
	}
}

/**
 * Paginates the json result set from a solr filter call. This class expects a certain object structure so if you supply
 * it anything other than the default json rendering from solr you'll need to override the existing methods.
 *
 * @param searchType - The search type, i.e. cat_product_search.
 * @param itemsPerPage - number of result documents to be displayed on any page. <code>totalItems</code> divided by this value plus the modulus
 * gives the total number of pages. Setting this value to negative indicates that all results should be displayed - i.e. 'view all'; the direct page links will
 * not be available.
 * @param maxItemsPerPage - Maximum number of items that can appear on a page before the pagination is forced. <code>maxItemsPerPage</code> works in tandem
 * with itemsPerPage in that even if the user want's to view all (sets itemsPerPage<=0) a hard upper limit of <code>maxItemsPerPage</code> will be set. Set this
 * to null or undefined if you want true view all.
 * @param numPageLinks - Number of page links to display. The links are windowed so that the current page is more or less centered in the listing.
 * @param linkAction - The action that each pagination link should point to
 * @param currentPage - The current page.
 *
 */
function SolrDocumentPaginator(itemsPerPage, maxItemsPerPage, numPageLinks,
		linkAction, currentPage, pageSizeOptions) {
	this.currentPage = currentPage;
	this.maxItemsPerPage = maxItemsPerPage;
	this.itemsPerPage = itemsPerPage;
	this.numberOfDirectLinks = numPageLinks;
	this.pageSizeOptions = varUtils.isDef(pageSizeOptions) ? pageSizeOptions
			: [ 12, 24, 36, 48 ];
	this.upperPaginationContainer = "<p class='paging' id='upperPaginationContainer'></p>";
	this.lowerPaginationContainer = "<p class='paging' id='lowerPaginationContainer'></p>";
	this.searchAction = contextPath + "/sitesearch/search_do";
	this.nextIcon = staticImgPath + "/next_page_arrow.gif";
	this.prevIcon = staticImgPath + "/prev_page_arrow.gif";
	this.spacer = staticImgPath + "/placeHolder_large.gif";

	this.callbacks = {
		document : {
			pre : function(container, pages, document, index) {
			},
			post : function(container, pages, document, index) {
			},
			render : function(container, pages, document, index) {
				/*
				var imageUtils=new ImageUtils(contextPath,staticImgPath,fluidBaseItemURL);
				var link=document[linkType].replace(/^null/,contextPath);
				var unformatted=parseInt(document.price * 100);
				var price=varUtils.formatCurrency(convert_to_dollars(unformatted));
				
				var listItem=jQuery("<li></li>").appendTo(container);
				var anchor=jQuery("<a></a>").appendTo(listItem);
				anchor.attr("href","/"+link);
				anchor.attr("title",document.name+" By: " + document.brand);
				
				var imageTag=imageUtils.getProductImgTag(document.sku,imageSize,document.name,false,(!varUtils.isDef(document.image_url) || (document.pattern && document.image_url=="images/placeholder100x100.jpg")),false);
				jQuery(imageTag).appendTo(anchor);
				jQuery("<br/>"+document.name+ "<br/>").appendTo(anchor);
				jQuery("By: " + document.brand).appendTo(anchor);
				jQuery("<br/>").appendTo(listItem);
				jQuery(((unformatted+0)>0?"$" + price:"") +"<br/>").appendTo(listItem);
				 */
			}
		},

		pre : function(container, pages) {
		},
		post : function(container, pages) {
		},
		/**
		 * Renders the pagination controls; each control delegates to the 'filterSearchResults' function.
		 * parentElement - The dom element the controls will be appended to
		 * totalItems - Total number of results documents to be displayed
		 *
		 */
		render : function(container, pages) {
			// "pages" is the number of TOTAL products (regardless of pagination).
		// "this.itemsPerPage" is the number of products to show for this page.

		if (varUtils.isDef(this.maxItemsPerPage)) {
			if (this.itemsPerPage <= 0 && pages > this.maxItemsPerPage) {
				this.itemsPerPage = this.maxItemsPerPage;
			}
		} else if (this.itemsPerPage <= 0) {
			this.itemsPerPage = this.pages;
		}

		var totalPages = (this.itemsPerPage <= 0 ? 1 : (Math.floor(pages
				/ this.itemsPerPage) + (pages % this.itemsPerPage > 0 ? 1 : 0)));
		if (totalPages <= this.numberOfDirectLinks) {
			this.numberOfDirectLinks = totalPages;
		}

		if (totalPages > 1) {
			var startOffset = this.currentPage
					- Math.floor((this.numberOfDirectLinks - 1) / 2);
			if ((startOffset + this.numberOfDirectLinks) > totalPages) {
				startOffset = totalPages - this.numberOfDirectLinks;
			}
			if (startOffset < 0) {
				startOffset = 0;
			}

			if (this.currentPage > 0) {
				var prevLink = jQuery("<a></a>").appendTo(container);

				prevLink.bind("click", {
					page : (currentPage - 1)
				}, function(e) {
					filterSearchResults(e.data.page)
				});

				prevLink.attr("href", "#");
				var image = jQuery("<img/>").appendTo(prevLink);
				image.attr("alt", "previous page");
				image.attr("src", this.prevIcon);
			}

			var links = "";
			for ( var i = 0; i < this.numberOfDirectLinks; ++i) {
				var page = (startOffset + i);
				var pageContainer = null;
				if (page != this.currentPage) {
					var pageContainer = jQuery("<a></a>").appendTo(container);
					pageContainer.attr("href", "#");
					pageContainer.bind("click", {
						"page" : page
					}, function(e) {
						filterSearchResults(e.data.page);
					});
				} else {
					var pageContainer = jQuery("<span></span>").appendTo(
							container);
					pageContainer.css("font-weight", "bold");
					pageContainer.css("font-size", "110%");
				}
				pageContainer.css("margin", "0px 3px");
				pageContainer.text(page + 1);
			}

			if (this.currentPage < (totalPages - 1)) {
				var nextLink = jQuery("<a></a>").appendTo(container);
				nextLink.attr("href", "#");
				nextLink.bind("click", {
					page : (currentPage + 1)
				}, function(e) {
					filterSearchResults(e.data.page);
				});

				var image = jQuery("<img/>").appendTo(nextLink);
				image.attr("alt", "next page");
				image.attr("src", this.nextIcon);
			}

			var template = " Page START of END";
			jQuery("<span></span>").appendTo(container).text(
					template.replace(/START/, (this.currentPage + 1)).replace(
							/END/, totalPages));
	        this.renderResultSetSizeSelector(container, this.itemsPerPage,
					false);

			if (varUtils.isDef(this.itemsPerPage) && pages > this.itemsPerPage) {
				var allLink = jQuery("<a></a>").prependTo(container);
				allLink.attr("href", "#");
				allLink.bind("click", function(e) {
					jQuery("#resultSetSize").val(100000);
					filterSearchResults(0);
				});
				allLink.text("View All");
				allLink.css("padding-right", "3px");
			}
		} else if (this.itemsPerPage <= 0) {
			this
					.renderResultSetSizeSelector(container, this.itemsPerPage,
							true);
		}
	}
	}
}

SolrDocumentPaginator.prototype.setCurrentPage = function(currentPage) {
	this.currentPage = currentPage;
}

SolrDocumentPaginator.prototype.setUpperPaginationContainer = function(
		container) {
	this.upperPaginationContainer = container;
}

SolrDocumentPaginator.prototype.setMaxItemsPerPage = function(maxItemsPerPage) {
	this.maxItemsPerPage = maxItemsPerPage;
}

SolrDocumentPaginator.prototype.setItemsPerPage = function(itemsPerPage) {
	this.itemsPerPage = itemsPerPage;
}

SolrDocumentPaginator.prototype.setNumDirectLinks = function(numPageLinks) {
	this.numberOfDirectLinks = numPageLinks;
}

SolrDocumentPaginator.prototype.setSearchAction = function(action) {
	this.searchAction = action;
}

/**
 * @param nextIcon - url to an image to represent the 'forward' control
 */
SolrDocumentPaginator.prototype.setNextIcon = function(path) {
	this.nextIcon = path;
}

/**
 * @param prevIcon - url to an image to represent the 'back' control
 */
SolrDocumentPaginator.prototype.setPrevIcon = function(path) {
	this.prevIcon = path;
}

SolrDocumentPaginator.prototype.renderResultSetSizeSelector = function(
		container, itemsPerPage, viewAll) {
	var dropDown = jQuery("<select></select>").prependTo(container).attr(
			"name", "resultSetSizeSelector")
			.attr("id", "resultSetSizeSelector");
	dropDown.bind("change", function(e) {
		jQuery("#resultSetSize").val(this.value > 0 ? this.value : 12);
		filterSearchResults(0);
	});

	if (varUtils.isTrue(viewAll)) {
		jQuery("<option></option").appendTo(dropDown).attr("value", -1).text(
				"View All");
	}

	for ( var i = 0; i < this.pageSizeOptions.length; ++i) {
		var count = this.pageSizeOptions[i];
		var option = jQuery("<option></option").appendTo(dropDown).attr(
				"value", count);
		option.text(count + " items/page");
		if (this.itemsPerPage == count) {
			option.attr("selected", true);
		}
		option.appendTo(dropDown);
	}
	/*
	var count=8;
	var option=jQuery("<option></option").appendTo(dropDown).attr("value",count);
	option.text(count+" items/page");
	if (this.itemsPerPage==count){
		option.attr("selected",true);
	}
	
	for (var i=1;i<=4;++i){
		var count=i*12;
		var option=jQuery("<option></option").appendTo(dropDown).attr("value",count);
		option.text(count+" items/page");
		if (this.itemsPerPage==count){
			option.attr("selected",true);
		}
		option.appendTo(dropDown);
	}
	 */
}

SolrDocumentPaginator.prototype.renderPagination = function(container, pages,
		callbacks) {
	if (varUtils.isDef(pages)) {
		var callbackStrategy = new Object();
		callbackStrategy["pre"] = (varUtils.isDef(callbacks) && varUtils
				.isDef(callbacks["pre"])) ? callbacks["pre"]
				: this.callbacks.pre;
		callbackStrategy["post"] = (varUtils.isDef(callbacks) && varUtils
				.isDef(callbacks["post"])) ? callbacks["post"]
				: this.callbacks.post;
		callbackStrategy["render"] = (varUtils.isDef(callbacks) && varUtils
				.isDef(callbacks["render"])) ? callbacks["render"]
				: this.callbacks.render;

		var thisArg = (varUtils.isDef(callbacks) && varUtils
				.isDef(callbacks["thisArg"])) ? callbacks["thisArg"] : this;

		callbackStrategy.pre.call(thisArg, container, pages);
		callbackStrategy.render.call(thisArg, container, pages);
		callbackStrategy.post.call(thisArg, container, pages);
	}
}

/**
 * Routines for offering possible corrections for misspelled search terms.
 */
SolrTermSuggest = function() {
}

/**
 * The Solr spellcheck handler will return a list of potential corrections for each field
 * that a term is queried against... and not collate them. This means that if you query against
 * 5 fields you'll get five corrections all of which might be the same. This routine
 * attempts to collate the suggestions. 
 * 
 * The returned value is a map of the original terms and an array of their corrections, i.e.
 * orginal term='selt' returned map would be {selt:[salt,...]}
 * 
 * @param suggestions - The suggestion portion of the Solr query results. Naturally this assumes
 * that your solr write type is json; you'll have to write your own parser for XML.
 */
SolrTermSuggest.prototype.collateTermSuggestions = function(suggestions) {
	var collated = new Object();
	for ( var i = 0; i < suggestions.length; i += 2) {
		var term = suggestions[i];
		var corrections = suggestions[i + 1].suggestion;
		if (!varUtils.isDef(collated[term])) {
			collated[term] = corrections.sort();
		} else {
			for (j = 0; j < corrections.length; ++j) {
				if (varUtils.search(collated[term], corrections[j]) == null) {
					collated[term].push(corrections[j]);
					collated[term] = collated[term].sort();
				}
			}
		}
	}
	return collated;
}

/**
 * If you've got the spell check handler setup on your Solr instance you can use this routine to render the results. Writer type
 * must be json.
 * 
 * @param container - The html <span style="font-weight:bold">element</span> where the results should be inserted.
 * @param doc - Entire solr response doc (as JSON).
 * @param preRenderCallback - Function that will be called before rendering begins (use this to set up an containing elements for example).
 * <p>The call back should take the following params: <ul><li>HTML element (container)</li><li>Suggestions (map of original terms to suggestions/corrections)</li>
 * <li>callbackParams</li></ul>
 * @param postRenderCallback - Function that will be after before rendering completes.
 * @param renderCallback - Function that will handle actual rendering.
 * @param callbackParams - This will be passed to your call back functions as is (use for passing along environment information for example).
 */
SolrTermSuggest.prototype.renderTermSuggestions = function(container,
		searchAction, doc, callbacks) {
	if (varUtils.isDef(doc.spellcheck)
			&& varUtils.isDef(doc.spellcheck.suggestions)
			&& doc.spellcheck.suggestions.length > 0) {
		var suggestions = this
				.collateTermSuggestions(doc.spellcheck.suggestions);
		if (varUtils.isDef(callbacks) && varUtils.isDef(callbacks["pre"])) {
			callbacks.pre.call(thisArg, container, suggestions);
		}

		if (varUtils.isDef(callbacks) && varUtils.isDef(callbacks["render"])) {
			callbacks.render.call(thisArg, container, suggestions);
		} else {
			var urlBuilder = new URLBuilder(searchAction);
			jQuery("<p>Did you mean:</p>").appendTo(container);
			var termList = jQuery("<ul id='misspellingCont'><ul>").appendTo(
					container);
			for ( var i in suggestions) {
				if (varUtils.isDef(callbacks)
						&& varUtils.isDef(callbacks["preItem"])) {
					callbacks.preItem.call(thisArg, container, suggestions,
							suggestions[i], i);
				}
				if (varUtils.isDef(callbacks)
						&& varUtils.isDef(callbacks["renderItem"])) {
					callbacks.renderItem.call(thisArg, termList, suggestions,
							suggestions[i], i);
				} else {
					for ( var j = 0; j < suggestions[i].length; ++j) {
						var action = jQuery("<a></a>").appendTo(
								jQuery("<li></li>").appendTo(termList));
						action.text(suggestions[i][j]);

						urlBuilder.clearParameters();
						urlBuilder.addParameter("search_term",
								suggestions[i][j]);
						urlBuilder.addParameter("search_type",
								"general_site_search");

						action.attr("href", urlBuilder.getURL());

					}
				}
				if (varUtils.isDef(callbacks)
						&& varUtils.isDef(callbacks["postItem"])) {
					callbacks.postItem.call(thisArg, container, suggestions,
							suggestions[i], i);
				}
			}
		}
		if (varUtils.isDef(callbacks) && varUtils.isDef(callbacks["post"])) {
			callbacks.post.call(thisArg, container, suggestions);
		}
	}
}

/**
 * Function to handle querying the Solr server via AJAX. Use this object in conjunction with the filter utils
 * for a complete JavaScript Solr solution.
 * @param url - From the scheme through the path, i.e. http://your.solr.server/your/solr/search-handler
 * @param writerType - One of XML,JSON
 * @param fieldList - Array of fields that make up each returned document, '*'=all fields
 * @param exclusions - a JSON object of field/value combinations that should not be included in the search results.
 * This value should alread be coded Lucene/Solr format, i.e.:{price:"[0 TO 0]"}. Separate values with a comma if you
 * would like to exclude multiple values per field.
 * @param facetParams - JSON object of facet params, e.g. {facet:true,"facet.method":"enum"} (double quote your property names
 * if they contain js symbols).
 * @see http://wiki.apache.org/solr/SimpleFacetParameters
 * @return
 */
function SolrQueryUtils(url, writerType, fieldList, exclusions, facetParams) {
	this.fieldList = (varUtils.isDef(fieldList) && fieldList.length > 0) ? fieldList
			: [ "*" ];
	this.setWriterType(writerType);
	this.url = url;
	this.init();

	// Faceting parameters:
	this.facetParams = new Object();
	this.facetParams["facet"] = (varUtils.isDef(facetParams) && varUtils
			.isDef(facetParams.facet)) ? facetParams.facet : true;
	this.facetParams["facet.method"] = (varUtils.isDef(facetParams) && varUtils
			.isDef(facetParams["facet.method"])) ? facetParams["facet.method"]
			: "enum";
	this.facetParams["facet.mincount"] = (varUtils.isDef(facetParams) && varUtils
			.isDef(facetParams["facet.mincount"])) ? facetParams["facet.mincount"]
			: 1;
	this.facetParams["facet.sort"] = (varUtils.isDef(facetParams) && varUtils
			.isDef(facetParams["facet.sort"])) ? facetParams["facet.sort"]
			: false;
}

/**
 * Sets the query generator back to a know state.
 */
SolrQueryUtils.prototype.init = function() {
	this.sort = {
		asc : new Object(),
		desc : new Object()
	};
	this.facetQueries = new Array();
	this.facetFields = new Array();
	this.queryParams = new Object();
	this.filter = new Object();
}

/**
 * Reset functions.
 * Clear out the various elements of a solr request without having to rebuild the whole thing.
 */
SolrQueryUtils.prototype.resetSort = function() {
	this.sort = {
		asc : new Object(),
		desc : new Object()
	};
}
SolrQueryUtils.prototype.resetFacetQueries = function() {
	this.facetQueries = new Array();
}
SolrQueryUtils.prototype.resetFacetFields = function() {
	this.facetFields = new Array();
}
SolrQueryUtils.prototype.resetQuery = function() {
	this.queryParams = new Object();
}
SolrQueryUtils.prototype.resetFilter = function() {
	this.filter = new Object();
}
SolrQueryUtils.prototype.addFacetParam = function(name, value) {
	this.facetParams[name] = value;
}

/**
 * Parameter factory function - creates a basic parameter from which all other parameter types inherit.
 * @param inclusive - true to allow documents with this value to appear in the results, false to prohibit
 * @param field - field name
 * @param value - The value, strings that include spaces will be wrapped with double quotes indicated that solar
 * should treat them as phrases.
 * @boost boost - Boost value, documents with a higher boost are given greater weight in the search results.
 */
SolrQueryUtils.prototype.makeParameter = function(required, inclusive, field,
		value, boost) {
	function Parameter(required, inclusive, field, value, boost) {
		this.required = (required === true);
		this.inclusive = (inclusive === true);
		this.field = field;
		this.value = value;
		this.boost = boost;
		this.type = "parameter";
	}

	/**
	 * Returns true if <code>value</value> starts and ends with
	 * something other than whitespace but has whitespace somewhere
	 * in the middle.  also handles the period so that "A. Jaffe" is considered
	 * a phrase.
	 */
	Parameter.prototype.isPhrase = function(value) {
		var isPhrase = /([\w.]+\s+)+\w+/.test(value);
		return isPhrase;
	}

	Parameter.prototype.compare = function(a, b) {
		return varUtils.compare(a, b);
	}

	Parameter.prototype.renderValue = function(value, boost) {
		var template = "VALUEBOOST";
		var results = "";
		var isPhrase = false;
		if (varUtils.hasValue(value)) {
			// value=(typeof value == "string")?value.trim():value;
			value = (typeof value == "string") ? value.replace(new RegExp(
					/(\s*$|^\s*)/g), "") : value;
			value = (this.isPhrase(value) ? ('"' + value + '"') : value);
			results = template.replace(/VALUE/, value).replace(/BOOST/,
					(varUtils.isDef(boost) && boost > 1) ? ("^" + boost) : "");
		}
		return results;
	}
	/**
	 * Returns this parameter type.
	 */
	Parameter.prototype.getType = function() {
		return this.type;
	}

	/**
	 * Attempts to merge two parameters into one. This does nothing but raise an exception if you
	 * attempt to merge the values of two parameters from different fields - that's a no-no.
	 */
	Parameter.prototype.merge = function(param) {
		if (param.field != this.field) {
			throw "You are attempting to merge two parameters for two different fields types: "
					+ this.field + " and " + param.field;
		}
	}

	/**
	 * Renders the parameter for the query string. Override this method if you need something beyond the default rendering
	 * scheme.
	 */
	Parameter.prototype.render = function() {
		alert("Render not implemented for this parameter type!");
	}

	return new Parameter(required, inclusive, field, value, boost);
}
/**
 * Single value parameter factory method.
 * @param inclusive - true to allow documents with this value to appear in the results, false to prohibit
 * @param field - field name
 * @param value - The value, strings that include spaces will be wrapped with double quotes indicated that solar
 * should treat them as phrases.
 * @boost boost - Boost value, documents with a higher boost are given greater weight in the search results.
 */
SolrQueryUtils.prototype.makeSingleValueParameter = function(required,
		inclusive, field, value, boost) {
	function SingleValueParameter() {
		this.type = "single";
	}
	;
	SingleValueParameter.prototype = this.makeParameter(required, inclusive,
			field, value, boost);
	SingleValueParameter.prototype.mergeParameter = SingleValueParameter.prototype.merge;
	SingleValueParameter.prototype.merge = function(param) {
		var compatibleTypes = [ "single", "multi" ];
		var type = param.getType();
		var merged = null;

		this.mergeParameter.call(this, param);
		if (!compatibleTypes.some( function(element, index, array) {
			return element == type;
		})) {
			throw "You are attempting to merge two incompatible types: " + type
					+ " and " + this.getType();
		} else {
			var values = this.value.concat(param.value);
			merged = SolrQueryUtils.prototype.makeMultiValueParameter(
					this.required, this.inclusive, this.field, values,
					this.boost);
		}

		return merged;
	}

	SingleValueParameter.prototype.render = function() {
		var template = "INCLUSIVEFIELD:VALUE";
		return template.replace(/INCLUSIVE/,
				(this.required ? "+" : (this.inclusive ? "" : "-"))).replace(
				/FIELD/, this.field).replace(/VALUE/,
				this.renderValue(this.value, this.boost));
	}

	return new SingleValueParameter();
}
/**
 * Multi value parameter factory method - applies grouping when rendered.
 * @param inclusive - true to allow documents with this value to appear in the results, false to prohibit
 * @param field - field name
 * @param values - Array or comma delimited string of values
 * @boost boost - Boost value, documents with a higher boost are given greater weight in the search results.
 */
SolrQueryUtils.prototype.makeMultiValueParameter = function(required,
		inclusive, field, values, boost) {
	if (typeof values == "string") {
		values = values.split(",");
	}

	function MultiValueParameter() {
		this.type = "multi";
	}
	;
	MultiValueParameter.prototype = this.makeSingleValueParameter(required,
			inclusive, field, values, boost);
	MultiValueParameter.prototype.render = function() {
		var template = "INCLUSIVEFIELD:(VALUES)";
		var values = "";
		for ( var i = 0; i < this.value.length; ++i) {
			values += (this.renderValue(this.value[i]) + (i < this.value.length - 1 ? " OR "
					: ""));
		}

		return template.replace(/INCLUSIVE/,
				(this.required ? "+" : (this.inclusive ? "" : "-"))).replace(
				/FIELD/, this.field).replace(/VALUES/, values);
	}

	return new MultiValueParameter();
}
/**
 * Range parameter factory method.
 * @param inclusive - true to allow documents with this range to appear in the results, false to prohibit
 * @param rangeInclusive - true of the solr should include values at the extreme ends of the range.
 * @param field - field name
 * @param values - Array of values. The array should contain an even number of values with the odd elements as the
 * low end of the range and the even elements the high end.
 * @boost boost - Boost value, documents with a higher boost are given greater weight in the search results.
 */
SolrQueryUtils.prototype.makeRangeParameter = function(required, inclusive,
		rangeRequired, rangeInclusive, field, values, boost) {
	function RangeParameter(rangeRequired, rangeInclusive, rangeBoost, values) {
		this.rangeRequired = varUtils.isDef(rangeRequired) ? rangeRequired
				: false;
		this.rangeInclusive = varUtils.isDef(rangeInclusive) ? rangeInclusive
				: true;
		this.rangeBoost = rangeBoost;
		this.type = "range";

		if (values.length % 2 != 0) {
			throw "The values array for a range parameter must have an even number of values";
		}
	}

	RangeParameter.prototype = this.makeParameter(required, inclusive, field,
			values);
	RangeParameter.prototype.mergeParameter = RangeParameter.prototype.merge;

	RangeParameter.prototype.isOverlap = function(range) {
		return (varUtils.isBetween(this.values[0], range.values[0],
				range.values[1]) || varUtils.isBetween(this.values[1],
				range.values[0], range.values[1]));
	}

	RangeParameter.prototype.merge = function(param) {
		this.mergeParameter.call(this, param);
		var merged = null;
		if (!param.getType() == this.getType()) {
			throw "You are attempting to merge two incompatible types: "
					+ param.getType() + " and " + this.getType();
		} else {
			merged = SolrQueryUtils.prototype.makeRangeParameter(this.required,
					this.inclusive, this.rangeRequired, this.rangeInclusive,
					this.field, this.value.concat(param.value).sort(
							this.compare), this.boost);
		}

		return merged;
	}
	RangeParameter.prototype.render = function() {
		var template = "INCLUSIVEFIELD:GROUPSRANGEGROUPE";
		var range = "OPENLOW TO HIGHCLOSEBOOST";

		var ranges = "";
		for ( var i = 0; i < this.value.length; i += 2) {
			ranges += range
					.replace(/OPEN/, (this.rangeInclusive ? "[" : "{"))
					.replace(/LOW/,
							this.value[i] == -Infinity ? "*" : this.value[i])
					.replace(
							/HIGH/,
							this.value[i + 1] == Infinity ? "*"
									: this.value[i + 1])
					.replace(/CLOSE/, (this.rangeInclusive ? "]" : "}"))
					.replace(
							/BOOST/,
							(varUtils.isDef(this.rangeBoost) && this.rangeBoost > 1) ? ("^" + this.rangeBoost)
									: "");
			if (i + 2 < this.value.length) {
				ranges += " OR ";
			}
		}

		var groupStart = "";
		var groupEnd = "";
		if ((this.value.length / 2) > 1) {
			groupStart = "(";
			groupEnd = ")";
		}

		return template.replace(/INCLUSIVE/,
				(this.required ? "+" : (this.inclusive ? "" : "-"))).replace(
				/FIELD/, this.field).replace(/GROUPS/, groupStart).replace(
				/RANGE/, ranges).replace(/GROUPE/, groupEnd);
	}

	return new RangeParameter(rangeRequired, rangeInclusive, boost, values);
}

/**
 * Attempts to merge two parameters together. Single value+multi or single+single value returns a multi value,
 * range+range will either return a array containing the two range parameters or include new ranges
 * where there is overlap. Range parameters cannot be combined with any other parameter type.
 *
 * The assumption here is that the parameters are for the same field - an exception is thrown if parameters
 * for two different fields are merged or if two incompatible fields are merged.
 */

SolrQueryUtils.prototype.setWriterType = function(writerType) {
	this.writerType = (varUtils.isDef(writerType) && varUtils.hasValue( {
		xml : "xml",
		json : "json"
	}[writerType])) ? writerType : "xml";
}

/**
 * Adds a field sort to the query results
 */
SolrQueryUtils.prototype.addSort = function(field, direction) {
	direction = (varUtils.isDef(direction) && varUtils.hasValue( {
		asc : "asc",
		desc : "desc"
	}[direction])) ? direction : "asc";
}

/**
 * Adds a parameter to the search query.
 */
SolrQueryUtils.prototype.addQueryParameter = function(parameter) {
	if (varUtils.isDef(this.queryParams[parameter.field])) {
		this.queryParams[parameter.field] = this.queryParams[parameter.field]
				.merge(parameter);
	} else {
		this.queryParams[parameter.field] = parameter;
	}
}

/**
 * Adds a parameter to the filter query.
 */
SolrQueryUtils.prototype.addFilterParameter = function(parameter) {
	if (varUtils.isDef(this.filter[parameter.field])) {
		this.filter[parameter.field] = this.filter[parameter.field]
				.merge(parameter);
	} else {
		this.filter[parameter.field] = parameter;
	}
}

/**
 * Exclusions are a way of globally excluding documents with the stated values from the results. They
 * will be included in every query.
 */
SolrQueryUtils.prototype.addExclusion = function(field, value) {

}

/**
 * Adds a facet query - Values can be a single value, an array, multiple values separated by commas (in which
 * case it will be converted to an array), or an array of JSON objects containing ranges, i.e. [{low:<value>, high:<value>},...]
 */
SolrQueryUtils.prototype.addFacetQuery = function(facetQuery) {
	this.facetQueries.push(facetQuery);
}

/**
 * Adds fields for faceting. Faceting provides a count for each document in the result count
 * set that contains that term in it's index. Facets work best on fields that don't have an
 * extra processing on them such as stemming.
 */
SolrQueryUtils.prototype.addFacetField = function(field) {

	if (!this.facetFields.some( function(element, index, array) {
		return element == field;
	})) {
		this.facetFields.push(field);
	}
}

/**
 * Helper method to add a field to the query string (correctly delimited,etc.).
 * This won't be used if you use the <code>executeQuery</code> method.
 */
/*
 * SolrQueryUtils.prototype.addFieldToQuery=function(query, field, delimiter){
 * var param=varUtils.isDef(field.render)?field.render():field; return
 * varUtils.hasValue(query)?(query+delimiter+param):param; }
 */
/**
 * Returns any FQ parameters (breaking this up now to provide more flexibility
 * to the client).
 * 
 * @param encoded -
 *            set to <code>true</code> if the parameters should be URL
 *            encoded.
 */
/*
 * SolrQueryUtils.prototype.getQueryFilterComponent=function(encode){ var
 * filter=""; for (param in this.filter){
 * filter=this.addFieldToQuery(filter,this.filter[param]," "); }
 * 
 * if (varUtils.hasValue(filter)){ filter="fq="+(varUtils.isTrue(encode) ?
 * encodeURIComponent(filter):filter); }
 * 
 * return filter; }
 */
/**
 * Returns any facet fields (breaking this up now to provide more flexibility to
 * the client).
 * 
 * @param encoded -
 *            set to <code>true</code> if the parameters should be URL
 *            encoded.
 */
/*
 * SolrQueryUtils.prototype.getQueryFacetParamsComponent=function(encode){ var
 * facetParams=""; for (var facetParam in this.facetParams){
 * facetParams=this.addFieldToQuery(facetParams,facetParam+"="+this.facetParams[facetParam],"&"); }
 * return facetParams; }
 */
/**
 * Returns any facet params, i.e.
 * <ul>
 * <li>facet.method=enum</li>
 * </ul>
 * (breaking this up now to provide more flexibility to the client).
 * 
 * @param encoded -
 *            set to <code>true</code> if the parameters should be URL
 *            encoded.
 */
/*
 * SolrQueryUtils.prototype.getQueryFacetFieldsComponent=function(encode){ var
 * facetFields=""; for (var i=0;i<this.facetFields.length;++i){
 * facetFields=this.addFieldToQuery(facetFields,"facet.field="+this.facetFields[i],"&"); }
 * 
 * return facetFields; }
 */
/**
 * Returns any facet queries, i.e.
 * <ul>
 * <li>&lt;facet field&gt;=&lt;value&gt;</li>
 * </ul>
 * (breaking this up now to provide more flexibility to the client).
 * 
 * @param encoded -
 *            set to <code>true</code> if the parameters should be URL
 *            encoded.
 */
/*
 * SolrQueryUtils.prototype.getQueryFacetQueryComponent=function(encode){ var
 * facetQuery=""; for (var i=0;i<this.facetQueries.length;++i){ var
 * encoded=(varUtils.isDef(encode) && encode) ?
 * encodeURIComponent(this.facetQueries[i].render()):this.facetQueries[i].render();
 * facetQuery=this.addFieldToQuery(facetQuery,"facet.query="+encoded,"&"); }
 * 
 * return facetQuery; }
 */
/**
 * Returns any query component, i.e.
 * <ul>
 * <li>q=&lt;value&gt;</li>
 * </ul>
 * (breaking this up now to provide more flexibility to the client).
 * 
 * @param encoded -
 *            set to <code>true</code> if the parameters should be URL
 *            encoded.
 */
/*
 * SolrQueryUtils.prototype.getQueryComponent=function(encode){ var query="";
 * for (param in this.queryParams){
 * query=this.addFieldToQuery(query,this.queryParams[param]," "); }
 * 
 * if(varUtils.hasValue(query)){ query="q="+(varUtils.isTrue(encode) ?
 * encodeURIComponent(query):query); }
 * 
 * return query; }
 */

/**
 * Converts the elements in a array or fields in an object to a string list.
 * Each term will be separated by <code>delimiter</code>.
 */
SolrQueryUtils.prototype.stringifyList = function(list, delimiter) {
	var stringifyers = new Object();
	stringifyers["array"] = function() {
		var string = "";
		for ( var i = 0; i < list.length; ++i) {
			if (i > 0) {
				string = string.concat(delimiter);
			}
			string = string.concat(list[i]);
		}
		return string;
	};

	stringifyers["object"] = function() {
		var string = "";
		var isFirst = true;
		for ( var field in list) {
			if (!isFirst) {
				string = string.concat(delimiter);
			}
			string = string
					.concat(varUtils.isDef(list[field].render) ? list[field]
							.render() : list[field]);
			isFirst = false;
		}
		return string;
	};

	var type = (list instanceof Array) ? "array" : (typeof list);
	return stringifyers[type]();
}

SolrQueryUtils.prototype.buildQueryString = function(startingDoc,
		resultSetSize, mlt) {
	var urlBuilder = new URLBuilder(this.url);
	urlBuilder.addParameter("wt", this.writerType);
	var param = this.stringifyList(this.fieldList, ",");
	if (varUtils.hasValue(param)) {
		urlBuilder.addParameter("fl", param);
	}
	param = this.stringifyList(this.filter, " ");
	if (varUtils.hasValue(param)) {
		urlBuilder.addParameter("fq", param);
	}
	urlBuilder.addParameter("rows", resultSetSize);
	urlBuilder.addParameter("start", startingDoc);

	for ( var i = 0; i < this.facetQueries.length; ++i) {
		urlBuilder.addParameter("facet.query", this.facetQueries[i].render());
	}
	for ( var i = 0; i < this.facetFields.length; ++i) {
		urlBuilder.addParameter("facet.field", this.facetFields[i]);
	}
	for ( var facetParam in this.facetParams) {
		urlBuilder.addParameter(facetParam, this.facetParams[facetParam]);
	}
	param = this.stringifyList(this.queryParams, " ");
	if (varUtils.hasValue(param)) {
		urlBuilder.addParameter("q", param);
	}

	return urlBuilder
}

/**
 * Returns the generated query string. This is mostly for debugging purposes as you can paste this
 * into a browser address bar and check your results. That said it should work perfectly as the
 * target of an AJAX call or whatever.
 * 
 * @param startingDoc - First document in the result set to return (for pagination)
 * @param resultSetSize - Number of documents to return (set this to zero if you only want facets)
 * @param mlt - More like this
 * @param encode - True if you want values to be encoded.
 */
SolrQueryUtils.prototype.getQueryString = function(startingDoc, resultSetSize,
		mlt, encode) {
	return this.buildQueryString(startingDoc, resultSetSize, mlt)
			.getQueryString(encode);// queryString;
}
/**
 * Returns the Solr query as a JavasScript object - parameter names are properties, values are values - for extra
 * processing or debugging.
 */
SolrQueryUtils.prototype.getQueryStringAsObject = function(startingDoc,
		resultSetSize, mlt) {
	return this.buildQueryString(startingDoc, resultSetSize, mlt)
			.getParameters();
}
/**
 * Executes the query by sending a AJAX call to the server. This method will use the AJAX
 * functions of jQuery to put together the qs parameters so it won't exactly match what you'll
 * get back from getQueryString but functionally they'll be identical.
 *
 * If the writer type (result format type) is xml any xslt passed in will be applied otherwise it will
 * be ignored.
 */
SolrQueryUtils.prototype.executeQuery = function(startingDoc, resultSetSize,
		mlt, callback) {
	var params = new Object();

	params["wt"] = this.writerType;
	params["fl"] = this.fieldList;
	params["rows"] = resultSetSize;
	params["start"] = startingDoc;

	if (this.facetFields.length > 0 || this.facetQueries.length > 0) {
		for ( var facetParam in this.facetParam) {
			facet = this.addFieldToQuery(facet, facetParam + "="
					+ this.facetParams[facetParam], "&");
		}
		params["facet.field"] = this.facetFields;
		params["facet.query"] = new Array();
		for ( var i = 0; i < this.facetQueries.length; ++i) {
			params["facet.query"].push(this.facetQueries[i].render());
		}
	}

	var query = null;
	for (param in this.queryParams) {
		query = this.addFieldToQuery(query, this.queryParams[param], " ");
	}
	params["q"] = query;

	var filter = null;
	for (param in this.filter) {
		filter = this.addFieldToQuery(filter, this.filter[param], " ");
	}

	if (varUtils.hasValue(filter)) {
		params["fq"] = filter;
	}

	try {
		jQuery.get(this.url, params, callback, this.writerType);
	} catch (e) {
		alert(e);
	}
}

/**
 * @param glomAndEncodeParams - If true will encode the entire query string and set it as the value of
 * parameter 'solrQuery', i.e. q=catalog_id:2 product_name:"Gold Tray"->solrQuery=q%3Dcatalog_id%3A%20product_name%3A"Gold%20Tray"
 */
SolrQueryUtils.prototype.forwardToProxy = function(startingDoc, resultSetSize,
		mlt, callback, glomAndEncodeParams) {
	var query = null;
	var params = null;
	if (varUtils.isTrue(glomAndEncodeParams)) {
		params = new Object();
		query = this.getQueryString(startingDoc, resultSetSize, mlt, true);
		params["solrQuery"] = encodeURIComponent(query);
	} else {
		query = this.getQueryString(startingDoc, resultSetSize, mlt, false);
		var delimiter = (this.url.indexOf("?") < 0 ? "?" : "&");
		params = (new URLUtils(this.url + delimiter + query)).getParameters();
	}

	try {
		params['randid'] = Math.random();
        jQuery.ajax({
                url : this.url,
                data : params,
                dataType : 'jsonp',
                jsonpCallback : '?',
                success : callback
        });
	} catch (e) {
		alert(e);
	}
}

/**
 * Autocomplete helper
 */
SolrAutocomplete = function() {
}

/**
 * Installs autocomplete on <code>targetElement</code>
 * 
 * @param proxy - the action to forward the solr query to.
 * @param targetElement - jQuery wrapped dom node to apply auto complete to.
 * @param facetFields - Solr facet fields to include in the response.
 * @param docFields - Document fields to include in the response.
 */
SolrAutocomplete.prototype.installAutocomplete = function(proxy, targetElement,
		facetFields, docFields) {
	var solrQueryUtils = new SolrQueryUtils(proxy, "json", docFields);

	for ( var i = 0; i < facetFields.length; ++i) {
		solrQueryUtils.addFacetField(facetFields[i]);
	}

	var comparator = function(a, b) {
		var result = 0;

		var aVal = a.value.toLowerCase();
		var bVal = b.value.toLowerCase();
		if (aVal < bVal) {
			--result;
		} else if (aVal > bVal) {
			++result;
		}

		return result;
	}

	//Autocomplete passes the results of the search query to this function whose
	// primary responsibility is to merge the facet data with the document data
	// so that the appear in the correct sequence in the autocomplete list.
	// This is not documented on the JQuery site but rather here:
	// http://www.barneyb.com/barneyblog/2008/11/22/jquerys-autocompletes-undocumented-source-option/
	var iterator = function(data) {
		var values = null;
		var searchTerm = targetElement.val();

		if (varUtils.hasValue(searchTerm) && varUtils.isDef(data)) {
			searchTerm = searchTerm.replace(/[^a-zA-Z0-9]/, "").toLowerCase();
			values = new Array();

			// The if clauses in the for loops attempted to filter out results
			// that weren't
			// prefixed with the search time. I've removed it.
			var items = data.facet_counts.facet_fields.facet_brand;
			for ( var i = 0; i < items.length; i += 2) {
				var value = items[i].replace(/[^a-zA-Z0-9]/, "").toLowerCase();
				if (value.indexOf(searchTerm) == 0) {
					value = items[i].replace(/\'\"/, "");
					var item = new Object();
					item["data"] = value;
					item["result"] = value;
					item["value"] = value;
					values.push(item);
				}
			}

			items = data.response.docs;
			for ( var i = 0; i < items.length; ++i) {
				var value = items[i].name.replace(/[^a-zA-Z0-9]/, "")
						.toLowerCase();
				// if (value.indexOf(searchTerm)==0){
				value = items[i].name.replace(/[\'\"]/, "");
				var item = new Object();
				item["value"] = value;
				item["data"] = items[i];
				item["result"] = value;
				item["row"] = items[i];
				values.push(item);
				// }
			}
			values = values.sort(comparator);

			var collated = new Array();
			for (i = 0; i < values.length; ++i) {
				if (collated.length <= 0
						|| (collated[collated.length - 1].value != values[i].value)) {
					collated.push(values[i]);
				}
			}
			values = collated;
		}
		return values;
	};

	/**
	 * Autocomplete plugin hands control back to formatItem to render the row in
	 * the select box.
	 * 
	 * Autocomplete attempts to suggest missing characters with the search term
	 * starting from the number of characters already typed in. If 'term' is not
	 * prefixed with what the customer enters you'll get some unfortunate
	 * results. This will happen if you search for a substring match rather than
	 * a strict prefix match.
	 * 
	 * Check the request method in the modified plugin file to see how the
	 * search is being performed and against what fields.
	 * 
	 * @param row -
	 *            row from the result set (either a facet value or a document)
	 * @param i -
	 *            row index
	 * @param max -
	 *            total items in result set.
	 * @param term -
	 *            Search term
	 * @return
	 */
	var formatItem = function(row, i, max, term) {
		//term.concat("<span>by&nbsp;"+row.brand+"</span>");
		var imageTag = undefined;
		if (varUtils.hasValue(row.image_url)) {
			var imageUtils = new ImageUtils(contextPath, staticImgPath,
					fluidBaseItemURL);
			imageTag = imageUtils
					.getProductImgTag(
							row.sku,
							"small",
							row.name,
							false,
							(row.pattern && row.image_url == "images/placeholder100x100.jpg"),
							false);
			// term=imageTag.concat(term);
		}

		// To turn off options beneath the items in the autocomplete list comment out this code.
		// var urlProperty = "static_url";
		// var context = "";
		// if (varUtils.hasValue(staticPath)) {
		// urlProperty = "dynamic_url";
		// context = contextPath;
		// }

		// if (varUtils.hasValue(row[urlProperty])) {
		// term = term.concat("<br/><span><a style=\"color:#4D4D4F\" href=\""
		// + context + row[urlProperty]
		// + "\" onclick=\"window.location='" + context
		// + row[urlProperty] + "'\">go to product</a>");
		// term = term
		// .concat("&nbsp;|&nbsp;<a style=\"color:#4D4D4F\"
		// href=\"javascript:void(0)\">search for similar</a></span>");
		// }
		// term = "<div>" + term + "</div>";

		// if (varUtils.hasValue(imageTag)) {
		// term = ("<div style=\"float:left\">" + imageTag + "</div>")
		// .concat(term);
		// }

		return term;
	};

	var formatResult = function(row, i, total) {
		return row.searchTerm;
	};

	var params = new Object();
	params["dataType"] = "json";
	params["parse"] = iterator;
	params["formatItem"] = formatItem;
	params["selectFirst"] = false;
	params["minChars"] = 3;
	params["mustMatch"] = false;
	params["autoFill"] = true;
	params["cacheLength"] = 100;
	params["width"] = 250;
	params["extraParams"] = solrQueryUtils
			.getQueryStringAsObject(0, 100, false);
	// params["extraParams"]={solrQuery:encodeURIComponent(solrQueryUtils.getQueryString(0,100,false,true))};

	jQuery(targetElement).autocomplete(proxy, params);

	// var autocompleter=jQuery.Autocompleter;
	// var lastword=autocompleter.lastWord("hello world!")
	// alert(lastword);
}

/**
 * WindowUtils wraps the ui functions in jQuery isolating the client from any underlying implemention details. In practice
 * this is probably just unnecessary an confusing and the client should just directly interact with their library of choice
 * (Google, YUI, Prototype? You decide!).
 * @return
 */
function WindowUtils() {
	var dialogs = null;
}

WindowUtils.prototype.getMaxZIndex = function() {
	var zElements = jQuery("*[style*='z-index']");
	var maxIndex = 0;

	for ( var i = 0; i < zElements.length; ++i) {
		var element = jQuery(zElements[i]);
		var zIndex = (element.css("z-index") * 1);
		if (zIndex > maxIndex) {
			maxIndex = zIndex;
		}
	}

	return maxIndex;
}

WindowUtils.prototype.createDialog = function(containingElement, id, title,
		contents) {
	//jQuery("<div id='"+id+"' style='visibility:hidden'></div>").appendTo(containingElement);
	jQuery("<div id='" + id + "'></div>").appendTo(containingElement);
	var dialog = jQuery("#" + id);
	dialog.html(contents);
	dialog.show();

	var config = new Object();
	config["title"] = title;
	config["stack"] = true;
	config["draggable"] = true;
	config["zIndex"] = this.getMaxZIndex() + 2000;
	jQuery(dialog).dialog(config);

	return jQuery(dialog).dialog(config);
}

WindowUtils.prototype.destroyDialog = function(dialog) {
}

WindowUtils.prototype.createOverlay = function(color) {
}

WindowUtils.prototype.destroyOverlay = function(overlay) {
}

WindowUtils.prototype.createLightbox = function(height, width, position) {
}

WindowUtils.prototype.destroyLightbox = function(lightbox) {
}

/**
 * Email Utility functions.
 */
function EmailUtils(parentElementId, formElementId, counterElementId, varName) {
	this.windowUtils = new WindowUtils();
	this.additionFriendDialog = null;
	this.parentElementId = parentElementId;
	this.formElementId = formElementId;
	this.counterElementId = counterElementId;
	this.varName = "emailUtils";
}

EmailUtils.prototype.showFriendDialog = function() {
	this.additionFriendDialog = this.windowUtils.createDialog(jQuery("#"
			+ this.parentElementId), "friendDialog",
			"Message will be sent to the following:", this.getTipText());
}

EmailUtils.prototype.validateParams = function(name, email) {
	var isValid = varUtils.isDef(name) && varUtils.hasValue(name.val())
			&& varUtils.isDef(email) && varUtils.hasValue(email.val());
	return isValid;
}

EmailUtils.prototype.getTipText = function() {
	var additionalFriends = jQuery("input[additionalFriend='true']");
	var template = "<li id='ID'><button onclick='" + this.varName
			+ ".removeFriend(jQuery(this).parent())'>x</button>NAME</li>";
	var tipText = "";
	if (additionalFriends.length > 0) {
		tipText = "<ul>";// "<p>Your message will be sent to the following
		// friends:</p><ul>";
		for ( var i = 0; i < additionalFriends.length; ++i) {
			var nameEmail = additionalFriends[i].value.split(/;/);
			tipText += template.replace(/ID/,
					(nameEmail[0] + ";" + nameEmail[1])).replace(/NAME/,
					nameEmail[0]);
		}
		/*
		tipText+=additionalFriends.reduce(function(previousValue, currentValue, index, array){
		    var friend=previousValue;
		    return friend+"<li>"+currentValue.val().split(/;/)[0]+"</li>";
		},"");
		 */
		tipText += "</ul>";
	}

	return tipText;
}

EmailUtils.prototype.removeFriend = function(friend) {
	jQuery("input[value='" + friend.attr("id") + "']").remove();
	jQuery(friend).remove();
	this.updateCounter();
}

EmailUtils.prototype.updateCounter = function() {
	if (varUtils.isDef(this.counterElementId)) {
		var additionalFriends = jQuery("input[additionalFriend='true']");
		var additionalFriendCount = 0;
		if (varUtils.isDef(additionalFriends)) {
			additionalFriendCount = additionalFriends.length;
			if (additionalFriendCount > 0) {
				var name = null;
				if (additionalFriendCount > 1) {
					name = additionalFriends[additionalFriendCount - 1].value
							.split(/;/)[0]
				} else {
					name = additionalFriends.val().split(/;/)[0]
				}
				var counter = "<span id='additionFriendsMessage'>Message will be sent to NAMEADDITIONAL.</span>";
				counter = counter
						.replace(/NAME/, name)
						.replace(
								/ADDITIONAL/,
								(additionalFriendCount > 1 ? (" plus "
										+ (additionalFriendCount - 1)
										+ " other <a onclick='" + this.varName + ".showFriendDialog()' href='javascript:void(0);'>friend(s)</a>")
										: ""));
				jQuery("#" + this.counterElementId).html(counter);
			} else {
				jQuery("#" + this.counterElementId).empty();
				if (varUtils.isDef(this.additionFriendDialog)) {
					this.additionFriendDialog.dialog("destroy");
				}
			}
		}
	}
}

EmailUtils.prototype.addFriend = function(name, email) {
	if (this.validateParams(name, email)) {
		var template = "<input type='hidden' name='NAME' value='VALUE' additionalFriend='true'>";
		template = template.replace(/NAME/, "friend").replace(/VALUE/,
				name.val() + ";" + email.val());
		jQuery(template).appendTo("#" + this.formElementId);
		this.updateCounter();
		// name.val("");
		// email.val("");

	}
}

EmailUtils.prototype.getFriendValue = function(name, email) {
	return name.val() + ";" + email.val();
}

//Globals
var varUtils = new VarUtils();
var urlUtils = new URLUtils();
