function ListBuilder(o) {
  this.dataStore = o.dataStore;
  this.grid = o.grid;
  this.useCustomLookupKey = o.useCustomLookupKey || false; //(Boolean) if true, store the lookup key for the grid in a custom attribute on the <option> element;
  this.elementsToDisable = o.elementsToDisable || []; //additional elements on the page to disable, an array of jquery selectors;
  this.filtered = null; //internal, an array of arrays, it stores all of the currently applicable options for a given list;
  this.breadcrumb = []; //internal, stores all of the previous possible options for a given list;
  this.duplicates = o.duplicates || false; //set to true if you want to append a unique number to the end of each <select> id, necessary if duplicates exist like in the new order page;
  this.retrofit = o.retrofit || false; //set to true if there are previously-selected list items that need to be selected (like after a validation error);
  this.counter = o.counter; //set the counter to a pre-determined number, else it will begin at zero;
}
ListBuilder.lastIndexOf = function (arr, vItem) {
  var i = arr.length;
  for (var j = i - 1; 0 <= j; j--) {
    if (arr[j] === vItem) {
      return j;
    }
  }
  return -1; //no match, return -1 (not found);
};
/*******************************************************************/
/*******************************************************************/
ListBuilder.setCounter = (function () {
  var i = 0;
  return function (iSalt) {
	if (iSalt) {
	  i = iSalt;
	  iSalt = null;
	}
    return i++;
  };
})();
/*******************************************************************/
/*******************************************************************/
ListBuilder.dataTemplate = function (arr) { //include the default template as a class method;
  var oOptions = {
    configurableLists: [],
    nonConfigurableLists: []
  };
  $.each(arr, function (n, a) {
	oOptions[a.optionListId] = {};
	if (a.isConfigurable) {
	  oOptions.configurableLists.push(a.optionListId);
	  $.each(a.map, function (n, b) {
	    for (var i in b) {
	      oOptions[a.optionListId][i] = b[i];
	    }
	  });
	} else {
	  oOptions.nonConfigurableLists.push(a.optionListId);
    }
  });
  return oOptions;
};
/*******************************************************************/
/*******************************************************************/
ListBuilder.prototype.createLists = function (oParent) {
  var that = this,
    oHashTable = this.hashTable,
    aConfigurableLists = oHashTable.configurableLists,
    aNonConfigurableLists = oHashTable.nonConfigurableLists,
    iCounter,
    aRetrofit = [];

  this.domContains = {}; //an object which holds the objects that represents each list;

  var fnUnique = function (a) {
    var aUnique = [];
    $.each(a, function (i, val) {
      if ($.inArray(val, aUnique) === -1) aUnique.push(val);    
    });
    return aUnique;
  };

  var fnGetKeys = function (i, sKey) {
    var arr = [];
    $.each(that.filtered, function (n, a) {
      if (a[i] === Number(sKey)) {
        arr.push(a[i + 1]);
      }
    });
    return fnUnique(arr);
  };

  var fnGetUniqueValues = function (arr, index) {
    var a = [];
    $.each(arr, function (j, n) {
      var i = n[index];
      a.push(i);
    });
    return fnUnique(a);
  };

  var fnDisable = function (bState) { //if there are any additional elements passed in the constructor to disable; this function toggles their state;
    if (that.elementsToDisable.length) {
      var sState = bState ? "disabled" : "";
      $.each(that.elementsToDisable, function (i, sSelector) {
        $(sSelector).attr("disabled", sState);
      });
    }
  };

  var fnRetrofit = function () {
    $.each(aConfigurableLists, function (i) {
      that.filtered = !that.filtered || !that.filtered.length? that.grid : that.filtered;
      that.filter(i, aRetrofit[i]); //each list is mapped to a breadcrumb;
    });

    for (var n = 1, iLen = aConfigurableLists.length; n < iLen; n++) {
      $.each(fnGetUniqueValues(that.breadcrumb[n], n), function (i, a) {
        var oClone = $(oHashTable[aConfigurableLists[n]][a]).clone();
        $("#" + aConfigurableLists[n]).append(oClone[0]);
      });
      $("#" + aConfigurableLists[n]).attr("disabled", "");
    }
  };

  $.each(this.dataStore, function (i, oList) {
    var sListName = oList.optionListId,
      sLabelName = oList.optionLabel,
      bConfigurable = oList.isConfigurable,
      sTemplate = "";

    if ($.inArray(sListName, aConfigurableLists) < 0) return;

    if ($("#" + sListName)) { //keep the list but remove its children;
      var oList = $("#" + sListName),
        oSelectList = oHashTable[sListName],
        sOptionValue = $(":selected", oList).val();

      $("option", oList).each(function () {
        if (that.useCustomLookupKey) {
          for (var prop in oSelectList) {
            if (oSelectList[prop] === this.value) {
              $(this).attr("lookupkey", prop);
              oSelectList[prop] = $(this).remove()[0];
            }
          }
        } else {
          if (oSelectList[this.value]) {
            oSelectList[this.value] = $(this).remove()[0];
          }
        }
      });

      /*
       * if there are dupes, the names of the ids as well as the object properties that map to those
       * ids need to be unique for the dom scripting behaviors to work;
       */
      if (that.duplicates) { //alter the id;
    	var sOldId = oList[0].id, sNewId;

    	iCounter = ListBuilder.setCounter();
        aConfigurableLists[i] = oList[0].id = sNewId = sOldId + "_" + iCounter; //retain the original id and add the current value of counter to the end;
        oHashTable[sNewId] = oHashTable[sOldId]; //put the old contents into the new property (which maps to the id of the <select>, which was also changed);
        delete oHashTable[sOldId]; //and delete the original property;
      }
      if (i > 0 && !that.retrofit) oList.attr("disabled", "disabled");
      oList.attr("isConfigurable", true); //set the isConfigurable attribute to facilitate referencing in the submit handler;

      if (that.retrofit) aRetrofit.push(sOptionValue); //we need to store the selected option value for each list here, else we won't have access to it later in fnRetrofit;
    }
  });

  $.each(aNonConfigurableLists, function (i, sList) {
    var sOldId = sList,
      sNewId = that.duplicates ?
        sOldId + "_" + iCounter : sOldId,
      oElem;

    if (that.duplicates) { //do the same here as with the nonconfig items;
      aNonConfigurableLists[i] = sNewId; //change the name in the array;
      oElem = document.getElementById(sOldId); //get a reference to the original element...;
      oElem.id = sNewId; //...and change its id to the new munged one;
    }
    if (!that.retrofit) $("#" + sNewId).attr("disabled", "disabled");
  });

  fnDisable(true);

  $.each(aConfigurableLists, function (i, sListName) { //bind the event handlers;
    $("#" + sListName).change(function () {
      var arr = [],
        oOpt = null,
        sNextList = aConfigurableLists[i + 1],
        sOptionValue = that.useCustomLookupKey ?
        	$("option:selected", this).attr("lookupkey") :
            this.value;

      var aTemp = aConfigurableLists.concat(aNonConfigurableLists);
      for (var n = i + 1, iLen = aTemp.length; n < iLen; n++) { //reset each list starting from the index + 1 to the end of the array;
        var sName = aTemp[n];
        if ($.inArray(sName, aConfigurableLists) > -1) $("#" + sName)[0].length = 1; //reset the lists (only the configurable lists);
        $("#" + sName).attr("disabled", "disabled");
        fnDisable(true); //disable any additional elements;
        if ($("#" + sName)[0].onchange) $("#" + sName)[0].onchange(); //fire any events that are attached to the disabled lists;
      }

      that.filter(i, sOptionValue); //each list is mapped to a breadcrumb;

      if (sOptionValue) {
    	if (ListBuilder.lastIndexOf(aConfigurableLists, this.id) === aConfigurableLists.length - 1) { //is it the last element in the configurableLists array?;
          $.each(aNonConfigurableLists, function (i, sList) {
            $("#" + sList).attr("disabled", "");
          });
          fnDisable(false); //enable any additional elements;
        } else {
          $.each(fnGetKeys(i, sOptionValue), function (i, a) {
            //oHashTable[sNextList][a].selected = false; //doesn't work in opera;
            var oClone = $(oHashTable[sNextList][a]).clone();
            oClone[0].selected = false; //defensive check; make sure that any option that was previously selected is not when presented again to the user (for instance, an option will have its selected property set to true if it was selected and then there was a validation error);
            $("#" + sNextList).append(oClone[0]);
          });
          $("#" + sNextList).attr("disabled", "");
        }
      }
    });
  });

  //initialize the first drop-down list with values;
  $.each(fnGetUniqueValues(this.grid, 0), function (i, a) {
    $("#" + aConfigurableLists[0]).append(oHashTable[oHashTable.configurableLists[0]][a]); //lookup from the first configurable list;
  });

  if (that.retrofit) fnRetrofit();

  this.filtered = this.grid; //initialize the filter;
   
  //bind the submit handler to the containing form;
  var oForm = $("#" + aConfigurableLists[0])[0].form; //get a reference to the form;
  $(oForm).submit(function (e) {
    var arr = [],
      bReturn = true;//,
      //sAttr = that.useCustomLookupKey ? "lookupkey" : "value";

    $("select[isConfigurable]", this).each(function () {
      if (this.value) {
        if (that.useCustomLookupKey) {
          arr.push($("option:selected", this).attr("lookupkey"));
        } else {
          arr.push(this.value);
        }
      } else {
        bReturn = false; //at least one of the lists doesn't have a valid selection so cancel the submit;
      }
    });
    $("input[name=configOptionKey]", this).val(arr.join()); //set the value for the hidden input;
    return bReturn;
  });

};

/*******************************************************************/
/*******************************************************************/
ListBuilder.prototype.filter = function (i, sKey) {
  if (!this.breadcrumb[i]) { //if no breadcrumb exists for the list...;
    this.breadcrumb[i] = []; //...create it...;
    this.breadcrumb[i] = this.filtered; //...and set it;
    this.breadcrumb[i].selection = sKey; //store the currently selected value; this is how we know to reset the breadcrumbs if the value is different;
  } else if (this.breadcrumb[i].selection !== sKey) { //a different value was selected...;
    this.filtered = this.breadcrumb[i]; //...persist the stored value from the breadcrumb...;
    this.breadcrumb[i].selection = sKey; //...store the new value;
    this.breadcrumb.splice(i, this.breadcrumb.length); //remove previous "breadcrumbs" b/c the selection has changed;
    this.breadcrumb[i] = this.filtered; //reset the breadcrumb with the new filter after the new value was selected;;
  }
  var arr = [];
  $.each(this.filtered, function (n, a) {
    if (a[i] === Number(sKey)) {
      arr.push(a);
    }
  });
  this.filtered = arr;
};
/*******************************************************************/
/*******************************************************************/
ListBuilder.prototype.transform = function (fnTemplate) { //pass in a custom template to transform the dataStore;
  var fn = fnTemplate || ListBuilder.dataTemplate;
  this.hashTable = fn(this.dataStore); //an object that holds the transformed data store;
};