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 Array();}
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];
			}
		}
		
		if (url.indexOf("?")>=0){
			var splits=url.split("?");
			this.queryString=splits[1];
			url=splits[0];
		}
		
		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;
}

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

    this.visibleMenu.setStyle({position: "absolute"});
    this.visibleMenu.setStyle({left: (parent.offsetLeft+"px")});
    this.visibleMenu.setStyle({visibility: "visible"});

    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.detailPath="SKU/SKU_detail/main_variation_default_view_1_WIDTHxHEIGHT.jpg";
    this.otherPath="SKU/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);

    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);
    			}
    		},
    		
    		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){
				    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));
				        
				        if (varUtils.isDef(this.maxItemsPerPage) && pages<=this.maxItemsPerPage){
				            var allLink=jQuery("<a></a>").prependTo(container);
				            allLink.attr("href","#");
				            allLink.bind("click",function(e){jQuery("#resultSetSize").val(-1);filterSearchResults(0);});
				            allLink.text("View All");
				        } else {
				            this.renderResultSetSizeSelector(container,this.itemsPerPage,false);
				        }
				    } 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.
     */
    Parameter.prototype.isPhrase=function(value){
//    	var isPhrase=/^(\w+\s+)+\w+$/.test(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]();
}

/**
 * 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){
	
	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.getQueryString(encode);//queryString;
}

/**
 * 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);
    }
}

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);
		params=(new URLUtils(this.url+'?'+query)).getParameters();
	}
	
    try {
        jQuery.get(this.url,params,callback,this.writerType);
    } 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;
	}
	
	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();
	        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;
    };
    
	var formatItem=function(row, i, max, term){
    	if (varUtils.hasValue(row.image_url)){
    		var imageUtils=new ImageUtils(contextPath,staticImgPath,fluidBaseItemURL);
    		var imageTag=imageUtils.getProductImgTag(row.sku,"small",row.name,false,(row.pattern && row.image_url=="images/placeholder100x100.jpg"),false);
    		//term=imageTag.concat("<br/>".concat(term));
    		term=imageTag.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"]={solrQuery:encodeURIComponent(solrQueryUtils.getQueryString(0,100,false,true))};
	
	jQuery(targetElement).autocomplete(proxy,params);
}


/**
 * 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();