// This file depends on localized strings and functions currently located in:
// resources/new/<locale>/js/properties.js

var ALL_VALUE = "";
var ALL_VALUES_RE = /^(All||Any)?$/i;
var DEFAULT_ALL_OPTION = new Option(INV_ALLTEXT, ALL_VALUE, true, true);
var IS_FORM_DISABLED = false;
// Chained basic search criteria: used for dropdown names, HTML div names, and InvSearch property names
var LOCATION_NAME = "location";
var LOCATION_ROW = LOCATION_NAME + "Row";
var MAKE_NAME = "make";
var MAKE_ROW = MAKE_NAME + "Row";
var MODEL_NAME = "model";
var MODEL_ROW = MODEL_NAME + "Row";
var TRIM_NAME = "trim";
var TRIM_ROW = TRIM_NAME + "Row";
var VEHICLETYPE_NAME = "body_type";
var VEHICLETYPE_ROW = VEHICLETYPE_NAME + "Row";
// Other basic search criteria: used for dropdown names, HTML div names, and InvSearch property names
var CERTIFIED_NAME = "certified";
var MAXODOMETER_NAME = "miles";
var MAXPRICE_NAME = "maxPrice";
var MAXYEAR_NAME = "maxYear";
var MINYEAR_NAME = "minYear";
// Advanced search criteria: used for dropdown names, HTML div names, and InvSearch property names
var COLOREXTERIOR_NAME = "colorExterior";
var COLORINTERIOR_NAME = "colorInterior";
var COLOROEM_NAME = "colorOEM";
var CYLINDERS_NAME = "cylinders";
var DRIVEWHEELS_NAME = "driveWheels";
var ENGINE_NAME = "engine";
var FUELTYPE_NAME = "fuelType";
var LOCATIONSTATE_NAME = "locationState";
var STOCKNUMBER_NAME = "stockNumber";
var TRANSMISSION_NAME = "transmission";
var TRANSMISSIONSPEEDS_NAME = "transmissionSpeeds";
var VIN_NAME = "vin";
// Handy collections of fields
var CHAINED_FIELDS = [LOCATION_NAME, VEHICLETYPE_NAME, MAKE_NAME, MODEL_NAME, TRIM_NAME];
var RANGE_FIELDS = [MAXODOMETER_NAME, MAXPRICE_NAME, MINYEAR_NAME, MAXYEAR_NAME];
var ADVANCED_FIELDS = [COLOREXTERIOR_NAME, COLORINTERIOR_NAME, COLOROEM_NAME, CYLINDERS_NAME, DRIVEWHEELS_NAME, ENGINE_NAME, FUELTYPE_NAME, LOCATIONSTATE_NAME, TRANSMISSION_NAME, TRANSMISSIONSPEEDS_NAME];
var allFields = null;

/** Encapsulates everything public in the search API. **/
function InvSearch(jsDataObj) {
	this.jsdata = jsDataObj;
	this.searchForm = null;
	this.paramsForm = null;
	this.advancedFactory = null;
	this.isAdvancedDisplayed = false;
	this.hasAdvancedSearch = false;
}
InvSearch.prototype.init = function(searchForm, priceType, paramsForm) {
	this.searchForm = (searchForm) ? searchForm : document.forms[0];
	this.paramsForm = (paramsForm) ? paramsForm : ((document.forms.searchParamsForm) ? document.forms.searchParamsForm : null);
	this.advancedFactory = new AdvancedValueFactory(this, this.searchForm, priceType);
	// verify the jsdata object has data
	var hasData = false;
	for (var prop in this.jsdata){
		hasData = true;
		break;
	}
	// if jsdo has no data, disable all fields and display "no inventory" message on search pages
	if (!hasData) {
		this.jsdata = new Object();
		if (document.getElementById("noInventoryTextContainer")) document.getElementById("noInventoryTextContainer").style.display = "block";
		this.disableFields();
		return;
	}
	// register presence of advanced search
	this.advancedFactory.hasAdvancedSearch = (document.getElementById("advancedSearchContainer")) ? true : false;
	// initialize search criteria
	this.initCriteria();
}
InvSearch.prototype.initCriteria = function() {
	// initialize allFields to have advanced search criteria if necessary
	allFields = (this.advancedFactory.hasAdvancedSearch) ? CHAINED_FIELDS.concat(RANGE_FIELDS.concat(ADVANCED_FIELDS)) : CHAINED_FIELDS.concat(RANGE_FIELDS);
	// populate properties with any preselected values, or "all"
	this.criteriaList = new CriteriaList();
	for (var i=0; i<allFields.length; i++) {
		if (i < CHAINED_FIELDS.length) this.criteriaList.add(new InvCriteria(this, allFields[i], this.getFieldValue(allFields[i]), i));
		else this.criteriaList.add(new InvAdvancedCriteria(this, allFields[i], this.getFieldValue(allFields[i]), i));
	}
	// set up chained criteria customized "All" options
	this.criteriaList[LOCATION_NAME].setAllOption(new Option(INV_ALLLOCATIONS, ALL_VALUE, true, true));
	this.criteriaList[VEHICLETYPE_NAME].setAllOption(new Option(INV_ALLVEHICLETYPES, ALL_VALUE, true, true));
	this.criteriaList[MAKE_NAME].setAllOption(new Option(INV_ALLMAKES, ALL_VALUE, true, true));
	this.criteriaList[MODEL_NAME].setAllOption(new Option(INV_ALLMODELS, ALL_VALUE, true, true));
	this.criteriaList[TRIM_NAME].setAllOption(new Option(INV_ALLTRIMS, ALL_VALUE, true, true));
	// set up customized sorting rules
	for (var j=0; j<RANGE_FIELDS.length; j++) this.criteriaList[RANGE_FIELDS[j]].setIsPreSorted(true);
	if (this.advancedFactory.hasAdvancedSearch) {
		this.criteriaList[TRANSMISSIONSPEEDS_NAME].setComparator(numberComparator);
		this.criteriaList[CYLINDERS_NAME].setComparator(numberComparator);
	}
	// hide the location dropdown if there is only one location
	if (document.getElementById(LOCATION_ROW)) {
		var locationValues = toArray(this.jsdata);
		if (locationValues.length <= 1) document.getElementById(LOCATION_ROW).style.display = "none";
	}
	// set up initial location values, and kick off the chained population
	var isPreselect = (this.paramsForm) ? true : false;
	this.criteriaList[LOCATION_NAME].nodeList = [this.jsdata];
	this.criteriaList[LOCATION_NAME].addToValueMap(this.jsdata);
	this.criteriaList[LOCATION_NAME].resetNodeLists(true);
	this.criteriaList[LOCATION_NAME].set(this.searchForm, new Option(INV_ALLLOCATIONS, ALL_VALUE, true, true));
}

/* Call in the onClick of the location dropdown */
InvSearch.prototype.setLocation = function(newValue) {
	this.criteriaList[LOCATION_NAME].value = newValue;
	this.criteriaList[LOCATION_NAME].resetNodeLists(false);
	this.criteriaList[VEHICLETYPE_NAME].set(this.searchForm);
}
/* Call in the onClick of the vehicleType dropdown */
InvSearch.prototype.setVehicleType = function(newValue) {
	this.criteriaList[VEHICLETYPE_NAME].value = newValue;
	this.criteriaList[VEHICLETYPE_NAME].resetNodeLists(false);
	this.criteriaList[MAKE_NAME].set(this.searchForm);
}
/* Call in the onClick of the make dropdown */
InvSearch.prototype.setMake = function(newValue) {
	this.criteriaList[MAKE_NAME].value = newValue;
	this.criteriaList[MAKE_NAME].resetNodeLists(false);
	this.criteriaList[MODEL_NAME].set(this.searchForm);
}
/* Call in the onClick of the model dropdown */
InvSearch.prototype.setModel = function(newValue) {
	this.criteriaList[MODEL_NAME].value = newValue;
	this.criteriaList[MODEL_NAME].resetNodeLists(false);
	this.criteriaList[TRIM_NAME].set(this.searchForm);
}
/* Call in the onClick of the trim dropdown */
InvSearch.prototype.setTrim = function(newValue) {
	this.criteriaList[TRIM_NAME].value = newValue;
	this.criteriaList[TRIM_NAME].resetNodeLists(false);
	this.advancedFactory.setAll();
}
/* Call this in the onClick of the minYear dropdown */
InvSearch.prototype.setMinYear = function(newValue) {
	this.criteriaList[MINYEAR_NAME].value = newValue;
	var maxYearCriteria = this.criteriaList[MAXYEAR_NAME];
	maxYearCriteria.valueMap = {};
	var startingIndex = (this.searchForm[MINYEAR_NAME].selectedIndex == 0) ? this.searchForm[MINYEAR_NAME].options.length - 1 : this.searchForm[MINYEAR_NAME].selectedIndex;
	for (var i=1; i<=startingIndex; i++) maxYearCriteria.valueMap[this.searchForm[MINYEAR_NAME].options[i].value] = this.searchForm[MINYEAR_NAME].options[i].text;
	if ((maxYearCriteria.value != ALL_VALUE) && !maxYearCriteria.valueMap[maxYearCriteria.value]) maxYearCriteria.value = ALL_VALUE;
	maxYearCriteria.setOptions(this.searchForm[MAXYEAR_NAME], maxYearCriteria.getOptions(), new Option(INV_ALLTEXT, ALL_VALUE, true, true));
}
/** Submits the search form. 
	Adds data to the URL to support Traffic Reporter data gathering.
	POST data is not available in the Apache log, so we need to add it here. **/
InvSearch.prototype.submitForm = function(){
	if(IS_FORM_DISABLED){ return; }
	var makeParam = "&make=" + escape(this.criteriaList[MAKE_NAME].value);
	var modelParam = "&model=" + escape(this.criteriaList[MODEL_NAME].value);
	var trimParam = "&trim=" + escape(this.criteriaList[TRIM_NAME].value);	
	var searchTypeParam;
	if(this.searchForm.search.type == "hidden"){
		searchTypeParam= "&searchVal=" + this.searchForm.search.value;
	}else{
		searchTypeParam = "&searchVal=" + getSelected(this.searchForm.search).value;
	}
	//this variable is used to track new searches as opposed to paging through results
	var searchFreshParam = "&searchFresh=yes"
	this.searchForm.action = this.searchForm.action + makeParam + modelParam + trimParam + searchTypeParam + searchFreshParam;
	this.searchForm.submit();
}
/** Disable all the fields in the search form. **/
InvSearch.prototype.disableFields = function() {
	IS_FORM_DISABLED = true;
	for (var i=0; i<this.searchForm.elements.length; i++) { this.searchForm.elements[i].disabled = true; }
}
/** Get the initial value for a field: returns a preselected value if one exists,
	ALL_VALUE if the preselected value matches one of the known "all" values,
	and ALL_VALUE as a last resort. **/
InvSearch.prototype.getFieldValue = function(fieldName) {
	if (!this.paramsForm) return ALL_VALUE;
	if (!this.paramsForm[fieldName]) return ALL_VALUE;
	if (ALL_VALUES_RE.test(this.paramsForm[fieldName].value)) return ALL_VALUE;
	return this.paramsForm[fieldName].value;
}

/** Encapsulates one criteria in the inventory search dropdown. **/
function InvCriteria(invSearch, name, value, index) {
	this.invSearch = invSearch;
	this.name = name;
	this.value = value;
	this.index = index;
	this.option = null;
	this.valueMap = {};
	this.nodeList = [];
	this.isPreSorted = false;
	return this;
}
/** Add values in an array to this criteria's valueMap. **/
InvCriteria.prototype.addToValueMap = function(node) {
	for (var key in node) this.valueMap[key] = node[key][0];
}
/** Do not populate model/trim dropdowns till a make has been selected. **/
InvCriteria.prototype.doPopulate = function() {
	return (!(((this.name == MODEL_NAME) && (this.invSearch.criteriaList[MAKE_NAME].value == ALL_VALUE)) || ((this.name == TRIM_NAME) && (this.invSearch.criteriaList[MODEL_NAME].value == ALL_VALUE))));
}
InvCriteria.prototype.doTruncate = function(optionsLength) { return (((this.name == MAKE_NAME) || (this.name == MODEL_NAME)) && (optionsLength == 1)); }
/** Create list of Options for this criteria, from a key/value map. **/
InvCriteria.prototype.getOptions = function() {
	var optionList = new Array();
	for (var key in this.valueMap) {
		if (key.trim()) optionList[optionList.length] = new Option(this.valueMap[key], key);
	}
	return optionList;
}
/** Reset all fields holding selection and state values. **/
InvCriteria.prototype.reset = function() {
	this.nodeList = [];
	this.valueMap = {};
	this.value = ALL_VALUE;
}
/** Call this once per user click, for the criteria selected.  Initiate the node recursion. **/
InvCriteria.prototype.resetNodeLists = function(isPreselect) {
	// reset the nodeList of every criteria below the one being set (the one the user clicked).
	if (!isPreselect) this.invSearch.criteriaList.resetCriteria(allFields[this.index + 1]);
	this.invSearch.advancedFactory.clearAggregateMaps();
	// start recursing through each node in the nodeList, to set nodeLists down the food chain.
	for (var j=0; j<this.nodeList.length; j++) {
		this.recurseNodes(this.nodeList[j], (this.index + 1));
	}
	this.invSearch.advancedFactory.setCalculatedMaps();
}
/** Create a list of nodes for each criteria following the one clicked, filtered by selected values.
	Optionally populate valueMaps with the node data.  Also initiate advanced criteria population. **/
InvCriteria.prototype.recurseNodes = function(node, nextIndex) {
	var nextCriteria = this.invSearch.criteriaList[CHAINED_FIELDS[nextIndex]];
	if (this.value == ALL_VALUE) {
		for (var key in node) {
			if (nextCriteria) {
				nextCriteria.nodeList[nextCriteria.nodeList.length] = node[key][1];
				if (nextCriteria.doPopulate()) nextCriteria.addToValueMap(node[key][1]);
				nextCriteria.recurseNodes(node[key][1], (nextIndex + 1));
			} else {
				this.invSearch.advancedFactory.addToValueMaps(node[key]);
			}
		}
	} else if (node[this.value]) {
		if (nextCriteria) {
			nextCriteria.nodeList[nextCriteria.nodeList.length] = node[this.value][1];
			if (nextCriteria.doPopulate()) nextCriteria.addToValueMap(node[this.value][1]);
			nextCriteria.recurseNodes(node[this.value][1], (nextIndex + 1));
		} else {
			this.invSearch.advancedFactory.addToValueMaps(node[this.value]);
		}
	}
}
/** Populate this criteria's dropdown with the values in its valueMap; call set for the next criteria in the chain. **/
InvCriteria.prototype.set = function(someForm) {
	// the requested value doesn't exist in the list of options (as from a bogus preselect)
	if ((this.value != ALL_VALUE) && !this.valueMap[this.value]) {
		this.value = ALL_VALUE;
		// reset all criteria under this one to ALL
		if (this.invSearch.criteriaList.resetCriteria(allFields[this.index + 1]));
	}
	// retrieve the list of dropdown options
	var options = (this.isPreSorted) ? this.getOptions() : this.getOptions().sort(optionComparator);
	// make and model should not have "all" option if list is length 1
	var allOption = (this.doTruncate(options.length)) ? null : this.option;
	if (this.doTruncate(options.length)) {
		this.value = options[0].value;
		// manually create next criteria's valueMap from its nodeList, as this will not have been done in recurseNodes()
		var nextCriteria = this.invSearch.criteriaList[CHAINED_FIELDS[this.index + 1]];
		for (var i=0; i<nextCriteria.nodeList.length; i++) nextCriteria.addToValueMap(nextCriteria.nodeList[i]);
	}
	// put this criteria's options in the appropriate dropdown
	this.setOptions(someForm[this.name], options, allOption);
	// set the next dropdown in the chained hierarchy, except trim which sets all advanced search criteria
	if ((this.index + 1) < CHAINED_FIELDS.length) this.invSearch.criteriaList[CHAINED_FIELDS[this.index + 1]].set(someForm);
	else this.invSearch.advancedFactory.setAll();
}
/** Set a custom "All" option for this criteria. **/
InvCriteria.prototype.setAllOption = function(option) { this.option = option; }
/** Set to true when valueMap is already in desired sort order (e.g. year). **/
InvCriteria.prototype.setIsPreSorted = function(isPreSorted) { this.isPreSorted = isPreSorted; }
/** Set Options for criteria's dropdown.  Optionally pass an "all" option if required. **/
InvCriteria.prototype.setOptions = function(dropdown, options, allOption) {
	if (!dropdown) return;
	dropdown.length = 0;
	var selectedIndex = 0;
	if (allOption) dropdown.options[0] = new Option(allOption.text, allOption.value);
	if (options && options.length) {
		for (var i=0; i<options.length; i++) {
			if (options[i].value == this.value) selectedIndex = dropdown.options.length;
			dropdown.options[dropdown.options.length] = options[i];
		}
	} else if (dropdown.options[0].value != ALL_VALUE) {
		dropdown.options[0] = new Option(INV_ALLTEXT, ALL_VALUE, true, true);
	}
	dropdown.options[selectedIndex].selected = true;
}

/** Subclass of InvCriteria, for calculated and advanced search criteria. **/
function InvAdvancedCriteria(invSearch, name, value, index) {
	this.invSearch = invSearch;
	this.name = name;
	this.value = value;
	this.index = index;
	this.valueMap = {};
	this.isPreSorted = false;
	this.comparator = alphaComparator;
	return this;
}
InvAdvancedCriteria.prototype = new InvCriteria;
/** Add values in an array to this criteria's valueMap. **/
InvAdvancedCriteria.prototype.addToValueMap = function(criteriaArray) {
	var valueArray = criteriaArray[AdvancedValueFactory.DATA_INDEXES[this.name]];
	for (var i=0; i<valueArray.length; i++) this.valueMap[valueArray[i]] = valueArray[i];
}
/** Create list of Options for this criteria, from a key/value map. **/
InvAdvancedCriteria.prototype.getOptions = function() {
	var optionList = new Array();
	if (!this.isPreSorted) {
		var valueList = toArray(this.valueMap).sort(this.comparator);
		for (var i=0; i<valueList.length; i++) {
			if (valueList[i].trim()) optionList[optionList.length] = new Option(valueList[i], valueList[i]);
		}
	} else {
		for (var key in this.valueMap) {
			if (key.trim()) optionList[optionList.length] = new Option(this.valueMap[key], key);
		}
	}
	return optionList;
}
/** Populate this criteria's dropdown with the values in its valueMap **/
InvAdvancedCriteria.prototype.set = function(someForm) {
	this.setOptions(someForm[this.name], this.getOptions(), new Option(INV_ALLTEXT, ALL_VALUE, true, true));
}
/** Set a comparator to use when sorting; default is alpha. **/
InvAdvancedCriteria.prototype.setComparator = function(comparator) { this.comparator = comparator; }

/** Parses the jsdo's calculated and advanced value arrays, and populates the corresponding Criteria valueMaps. **/
function AdvancedValueFactory(invSearch, searchForm, priceType) {
	this.invSearch = invSearch;
	this.searchForm = searchForm;
	this.priceType = priceType;
	this.trimNodesArray = [];
	this.hasAdvancedSearch = false;
	this.priceType = (priceType) ? priceType : "priceRetail";
}
// dataIndexes is used to access value data from the array passed in the jsdata object.
// Some of the prices are double mapped, as the config can save 2 different keys for the same data.
AdvancedValueFactory.DATA_INDEXES = {
	"priceDiscountMin":1, "price2Min":1,
	"priceDiscountMax":2, "price2Max":2,
	"priceInternetMin":3, "internet_priceMin":3,
	"priceInternetMax":4, "internet_priceMax":4,
	"priceInvoiceMin":5, "invoice_priceMin":5,
	"priceInvoiceMax":6, "invoice_priceMax":6,
	"priceMsrpMin":7, "msrpMin":7,
	"priceMsrpMax":8, "msrpMax":8,
	"priceRetailMin":9, "priceMin":9,
	"priceRetailMax":10, "priceMax":10,
	"priceWholesaleMin":11,
	"priceWholesaleMax":12,
	"minYear":13,
	"maxYear":14,
	"minOdometer":15, 
	"maxOdometer":16,
	"engine":17,
	"transmission":18,
	"transmissionSpeeds":19,
	"driveWheels":20,
	"colorOEM":21,
	"colorExterior":22,
	"colorInterior":23,
	"fuelType":24,
	"cylinders":25,
	"locationState":26
}
/** Collect unique year, odometer and price values while recursing through data nodes. **/
AdvancedValueFactory.prototype.addToAggregateMap = function(sourceValueArray, targetValueMap) {
	if (sourceValueArray && sourceValueArray.length) targetValueMap[sourceValueArray[0]] = sourceValueArray[0];
}
/** Called from InvCriteria.recurseNodes(): parses an array of criteria values, adds them to the correct criteria maps. **/
AdvancedValueFactory.prototype.addToValueMaps = function(criteriaArray) {
	if (!criteriaArray) return;
	if (this.searchForm[MINYEAR_NAME] && this.searchForm[MAXYEAR_NAME]) {
		this.addToAggregateMap(criteriaArray[AdvancedValueFactory.DATA_INDEXES["minYear"]], this.years);
		this.addToAggregateMap(criteriaArray[AdvancedValueFactory.DATA_INDEXES["maxYear"]], this.years);
	}
	if (this.searchForm[MAXODOMETER_NAME]) {
		this.addToAggregateMap(criteriaArray[AdvancedValueFactory.DATA_INDEXES["minOdometer"]], this.odometers);
		this.addToAggregateMap(criteriaArray[AdvancedValueFactory.DATA_INDEXES["maxOdometer"]], this.odometers);
	}
	if (this.searchForm[MAXPRICE_NAME]) {
		this.addToAggregateMap(criteriaArray[AdvancedValueFactory.DATA_INDEXES[this.priceType + "Min"]], this.prices);
		this.addToAggregateMap(criteriaArray[AdvancedValueFactory.DATA_INDEXES[this.priceType + "Max"]], this.prices);
	}
	if (this.hasAdvancedSearch) {
		for (var i=0; i<ADVANCED_FIELDS.length; i++) this.invSearch.criteriaList[ADVANCED_FIELDS[i]].addToValueMap(criteriaArray);
	}
}
/** Reset maps containing unique year, odometer and price values. **/
AdvancedValueFactory.prototype.clearAggregateMaps = function() {
	this.years = {};
	this.odometers = {};
	this.prices = {};
}
/** Set options for calculated and advanced search criteria. **/
AdvancedValueFactory.prototype.setAll = function() {
	for (var i=CHAINED_FIELDS.length; i<allFields.length; i++) this.invSearch.criteriaList[allFields[i]].set(this.searchForm);
}
/** Execute year, odometer and price population **/
AdvancedValueFactory.prototype.setCalculatedMaps = function() {
	if (this.searchForm[MINYEAR_NAME] && this.searchForm[MAXYEAR_NAME]) this.setYearMaps();
	if (this.searchForm[MAXODOMETER_NAME]) this.setMilesMap();
	if (this.searchForm[MAXPRICE_NAME]) this.setPriceMap();
}
/** Populate the miles criteria's valueMap **/
AdvancedValueFactory.prototype.setMilesMap = function() {
	var allOdometerValues = toArray(this.odometers).sort(numberComparator);
	// empty values will be sorted to the start of the array; we need to get rid of them.
	var trueStartIndex = 0;
	for (trueStartIndex; trueStartIndex<allOdometerValues.length; trueStartIndex++) {
		if (allOdometerValues[trueStartIndex].trim()) break;
	}
	var odometerValues = allOdometerValues.slice(trueStartIndex, allOdometerValues.length);
	var increment = 5000;
	var milesCriteria = this.invSearch.criteriaList[MAXODOMETER_NAME];
	// when no miles data, or minMiles is less than the increment amount, use 0
	var minMiles = (odometerValues.length && (parseInt(odometerValues[0]) >= increment)) ? parseInt(odometerValues[0]) : 0;
	// when no miles data, use increment * 9; when maxMiles is less than minMiles, use minMiles
	var maxMiles = (odometerValues.length) ? ((parseInt(odometerValues[odometerValues.length - 1]) < minMiles) ? minMiles : parseInt(odometerValues[odometerValues.length - 1])) : increment * 9;
	var firstOptionMiles =  (Math.floor(minMiles / increment) * increment) + increment;
	var lastOptionMiles = (Math.floor(maxMiles / increment) * increment) + increment;
	for (var miles=firstOptionMiles; miles<=lastOptionMiles; miles+=increment) milesCriteria.valueMap[miles] = INV_MAXODOMETER_PREFIX + getFormattedNumber(miles);
}
/** Populate the price criteria's valueMap **/
AdvancedValueFactory.prototype.setPriceMap = function() {
	var allPriceValues = toArray(this.prices).sort(numberComparator);
	// empty values will be sorted to the start of the array; we need to get rid of them.
	var trueStartIndex = 0;
	for (trueStartIndex; trueStartIndex<allPriceValues.length; trueStartIndex++) {
		if (allPriceValues[trueStartIndex].trim()) break;
	}
	var priceValues = allPriceValues.slice(trueStartIndex, allPriceValues.length);
	var increment = 5000;
	var priceCriteria = this.invSearch.criteriaList[MAXPRICE_NAME];
	// when minPrice is less than the increment amount, use 0
	var minPrice = (priceValues.length && (parseInt(priceValues[0]) >= increment)) ? parseInt(priceValues[0]) : 0;
	// when no maxPrice data, use increment * 11; when maxPrice is less than minPrice, use minPrice
	var maxPrice = (priceValues.length) ? ((parseInt(priceValues[priceValues.length - 1]) < minPrice) ? minPrice : parseInt(priceValues[priceValues.length - 1])) : increment * 19;
	var firstOptionPrice =  (Math.floor(minPrice / increment) * increment) + increment;
	var lastOptionPrice = (Math.floor(maxPrice / increment) * increment) + increment;
	for (var price=firstOptionPrice; price<=lastOptionPrice; price+=increment) priceCriteria.valueMap[price] = INV_MAXPRICE_PREFIX + getFormattedPrice(price);
}
/** Populate the year criterias' valueMaps **/
AdvancedValueFactory.prototype.setYearMaps = function() {
	var allYearValues = toArray(this.years).sort(numberComparator);
	// empty values will be sorted to the start of the array; we need to get rid of them.
	var trueStartIndex = 0;
	for (trueStartIndex; trueStartIndex<allYearValues.length; trueStartIndex++) {
		if (allYearValues[trueStartIndex].trim()) break;
	}
	var yearValues = allYearValues.slice(trueStartIndex, allYearValues.length);
	var minYear = (yearValues.length) ? parseInt(yearValues[0]) : 0;
	var maxYear = (yearValues.length) ? parseInt(yearValues[yearValues.length - 1]) : 0;
	var minYearCriteria = this.invSearch.criteriaList[MINYEAR_NAME];
	var maxYearCriteria = this.invSearch.criteriaList[MAXYEAR_NAME];
	if (!minYear || !maxYear) {
		minYearCriteria.setOptions(this.searchForm[MINYEAR_NAME], null, new Option(INV_ALLTEXT, ALL_VALUE, true, true));
		this.invSearch.criteriaList[MAXYEAR_NAME].setOptions(this.searchForm[MAXYEAR_NAME], null, new Option(INV_ALLTEXT, ALL_VALUE, true, true));
		return;
	}
	for (var year=maxYear; year>=minYear; year--) {
		minYearCriteria.valueMap[year] = year;
		maxYearCriteria.valueMap[year] = year;
	}
}

/** Vehicle photo urls, used by taxonomy data */
VehiclePhotoUtility = {
	thumbnailUrlIndex : 1,
	photoUrlIndex : 2,
	jsdata : null,
	trimNode : null,
	vehicleSearch : null,
	init : function(vehicleSearch) {
		VehiclePhotoUtility.vehicleSearch = vehicleSearch;
	},
	getTrimNode : function() {
		if (!VehiclePhotoUtility.vehicleSearch || !VehiclePhotoUtility.vehicleSearch.jsdata) return null;
		var dataNode = VehiclePhotoUtility.vehicleSearch.jsdata[""][1][""][1];
		var make = VehiclePhotoUtility.vehicleSearch.criteriaList[MAKE_NAME].value;
		var model = VehiclePhotoUtility.vehicleSearch.criteriaList[MODEL_NAME].value;
		var trim = VehiclePhotoUtility.vehicleSearch.criteriaList[TRIM_NAME].value;
		if (!make || !model || !trim) return null;
		if (!dataNode[make] || !dataNode[make][1][model] || !dataNode[make][1][model][1][trim]) return null;
		return dataNode[make][1][model][1][trim];
	},
	getPhotoUrl : function() {
		var trimNode = VehiclePhotoUtility.getTrimNode();
		if (!trimNode || !trimNode[VehiclePhotoUtility.photoUrlIndex]) return "";
		return trimNode[VehiclePhotoUtility.photoUrlIndex];
	},
	getThumbnailUrl : function() {
		var trimNode = VehiclePhotoUtility.getTrimNode();
		if (!trimNode || !trimNode[VehiclePhotoUtility.thumbnailUrlIndex]) return "";
		return trimNode[VehiclePhotoUtility.thumbnailUrlIndex];
	}
}

/** Associative array of all criteria in the search form. **/
function CriteriaList() {}
/** Add a criteria to the list. **/
CriteriaList.prototype.add = function(criteria) {
	if (!criteria.name) return;
	this[criteria.name] = criteria;
}
/** Cycle through each criteria in the list and reset its values **/
CriteriaList.prototype.resetCriteria = function(startName) {
	for (var i=this[startName].index; i<allFields.length; i++) this[allFields[i]].reset();
}

/** Case-insensitive alpha sort, e.g. White, white, Yellow **/
function alphaComparator(a, b) {
	if (a.toLowerCase() > b.toLowerCase()) return 1;
	else if (a.toLowerCase() < b.toLowerCase()) return -1;
	else return 0;
}
/** Numeric sort, e.g. 1, 2, 10 **/
function numberComparator(n1, n2) { return n1 - n2; }
/** Case-insensitive alpha sort on Options **/
function optionComparator(a, b) { return alphaComparator(a.text, b.text); }
/** Trim whitespace off the start and end of a string **/
String.prototype.trim = function() { return this.replace(/^\s*([^\s]*)\s$/g, "$1"); }
/** Return an array of keys in a map **/
function toArray(map) {
	var array = [];
	for (var key in map) array[array.length] = key;
	return array;
}

if (!window.getSelected) {
	function getSelected(thiselement) {
			if (thiselement.type == "select-one") {
			var ind = thiselement.selectedIndex;
			if (ind >= 0) return {"index":ind, "value":thiselement.options[ind].value}
		}
		if (thiselement.length) {
			var isCheckbox = (thiselement[0].type == "checkbox") ? true : false;
			var values = new Array();
			var indexes = new Array();
			for (var i=0; i<thiselement.length; i++) {
				if (thiselement[i].checked && !isCheckbox) return {"index":i, "value":thiselement[i].value};
				if (thiselement[i].checked && isCheckbox) {
					values[values.length] = thiselement[i].value;
					indexes[indexes.length] = i;
				}
			}
			if (isCheckbox) {
				if (indexes.length) return {"index":indexes, "value":values};
				else return {"index":null, "value":null};
			}
		} else {
			if (thiselement.checked) return {"index":0, "value":thiselement.value};
		}
		return false;
	}
}
