/* ****************************** Subcat ****************************** */
function Subcat(jsName, domPrefix, subcatId, subcatName, mfrName, mainImageIsFlash, mainImage, swatchImage, zoomImage,
                noStockNoBuy, subcatAttributes, subcatProducts, useProdPix, prodPixReplaceableAttributeIndex,
                imageRotationData, doNotShowPrice, doNotAllowPurchase, callforPriceMfr, callForPriceMsg, secProdAltTitle, secProdExtTitle,
                addToCartForPrice, delayPrice, shaftPopupAvailable, hasGiftWrapProducts, ratingNames, rcpid,
                fullFlashURL, flashWidth, flashHeight, flashAttributesList)
{
  this.jsName = jsName;
  this.domPrefix = domPrefix;
  this.subcatId = subcatId;
  this.subcatName = subcatName;
  this.mfrName = mfrName;
  this.mainImageIsFlash = mainImageIsFlash;
  this.mainImage = mainImage;
  this.swatchImage = swatchImage;
  this.zoomImage = zoomImage;
  this.noStockNoBuy = noStockNoBuy;
  this.subcatAttributes = subcatAttributes;
  this.subcatProducts = subcatProducts;
  this.useProdPix = useProdPix;
  this.prodPixReplaceableAttributeIndex = prodPixReplaceableAttributeIndex;
  this.imageRotationData = imageRotationData;
  this.doNotShowPrice = doNotShowPrice;
  this.doNotAllowPurchase = doNotAllowPurchase;
  this.callforPriceMfr = callforPriceMfr;
  this.callForPriceMsg = callForPriceMsg;         // Unused
  this.secProdAltTitle = secProdAltTitle;
  this.secProdExtTitle = secProdExtTitle;
  this.addToCartForPrice = addToCartForPrice;
  this.delayPrice = delayPrice;
  this.shaftPopupAvailable = shaftPopupAvailable;
  this.hasGiftWrapProducts = hasGiftWrapProducts;
  this.ratingNames = ratingNames;
  this.rcpid = rcpid;
  this.fullFlashURL = fullFlashURL;
  this.flashWidth = flashWidth;
  this.flashHeight = flashHeight;
  this.flashAttributesList = flashAttributesList;

  if ((typeof mainImage) == 'string')
    this.mainImage = this.createParameterizedImage(mainImage);
  if ((typeof swatchImage) == 'string')
    this.swatchImage = this.createParameterizedImage(swatchImage);
  if ((typeof zoomImage) == 'string')
    this.zoomImage = this.createParameterizedImage(zoomImage);
  
  // Set parentage
  for (var i = 0; i < subcatAttributes.length; i++)
    this.subcatAttributes[i].parentSubcat = this;
  for (var i = 0; i < subcatProducts.length; i++)
    this.subcatProducts[i].parentSubcat = this;
  
  // Initialize state
  this.visibleAttributeEndIndex = 0;
  this.attributeVisibilityCallbacks = [];
  this.attributeChangeCallbacks = [];
  this.lastGridInfo = new GridInfo(this, 0, 0, 1);
  this.prodPixLoaded = false;
  this.lastProdPixValueIndex = -1;
  this.lastProdPixError = null;
  this.delayPriceClicked = false;
  this.giftWrapSelected = false;
  this.tempHideSelectData = null;
  
  if (this.imageRotationData != null)
    this.imageRotationData.init(this);

  // Register default callback
  this.addAttributeChangeCallback(displayStandardInfo);
  
  return;
}

Subcat.prototype.setTradeinPromoFlags = function(tradeinPromoFlags)
{
  this.hasTradeinPromo = true;
  this.tradeinPromoFlags = tradeinPromoFlags;
  return;
}

Subcat.prototype.setProductTourInfo = function(productTourAttributeIndex)
{
  this.hasProductTour = true;
  this.productTourAttributeIndex = productTourAttributeIndex;
  return;
}

Subcat.prototype.setComparisonData = function(comparisonData)
{
  this.comparisonData = comparisonData;
  comparisonData.subcatObj = this;
  return;
}

Subcat.prototype.setProductPageTabs = function(productPageTabs)
{
  this.productPageTabs = productPageTabs;
  productPageTabs.subcatObj = this;
  return;
}

Subcat.prototype.setPersonalizationProfile = function(personalizationProfile)
{
  this.personalizationProfile = personalizationProfile;
  personalizationProfile.subcatObj = this;
  return;
}

/* *************** Getters *************** */


Subcat.prototype.getNumAttributes = function()
{
  return this.subcatAttributes.length;
}

Subcat.prototype.getNumAttributeValues = function(attributeIndex)
{
  return this.subcatAttributes[attributeIndex].getNumValues();
}

Subcat.prototype.getSubcatAttributeByName = function(attributeName)
{
  var ind = this.getSubcatAttributeIndexByName(attributeName);
  if (ind < 0)
    return null;
  return this.subcatAttributes[ind];
}

Subcat.prototype.getSubcatAttributeIndexByName = function(attributeName)
{
  for (var i = 0; i < this.subcatAttributes.length; i++)
    if (this.subcatAttributes[i].attributeName == attributeName)
      return i;
  return -1;
}

Subcat.prototype.getProductIndexById = function(productId)
{
  for (var i = 0; i < this.subcatProducts.length; i++)
    if (this.subcatProducts[i].productId == productId)
      return i;
  return -1;
}

Subcat.prototype.getProductById = function(productId)
{
  var ind = this.getProductIndexById(productId);
  if (ind < 0)
    return null;
  return this.subcatProducts[ind];
}

Subcat.prototype.getProductByGridNum = function(gridNum)
{
  for (var i = 0; i < this.subcatProducts.length; i++)
    if (this.subcatProducts[i].gridNum == gridNum)
      return this.subcatProducts[i];
  return null;
}

Subcat.prototype.getSelectedProduct = function()
{
  if (this.lastGridInfo.isFullySelected())
    return this.getProductByGridNum(this.lastGridInfo.gridNumber);
  return null;
}

Subcat.prototype.getSelectedQty = function()
{
  var f = this.getAttributeForm();
  if (!f)
    return 1;
  var s = f[this.getDomName('QtySel')];
  if (!s)
    return 1;
  var si = s.selectedIndex;
  if ((si < 0) || (si >= s.length))
    return 1;
  var n = parseInt(s.options[si].value);
  if (n <= 0)
    return 1;
  return n;
}

Subcat.prototype.setSelectQtyRange = function(minQty, maxQty)
{
  var f = this.getAttributeForm();
  if (!f)
    return;
  var s = f[this.getDomName('QtySel')];
  if (!s)
    return;
  var curMinQty = parseInt(s.options[0].value);
  var curMaxQty = parseInt(s.options[s.length - 1].value);
  if ((curMinQty == minQty) && (curMaxQty == maxQty))
    return;
  if (minQty > maxQty)
    maxQty = minQty;

  var curQty = this.getSelectedQty();
  if (curQty < minQty)
    curQty = minQty;
  if (curQty > maxQty)
    curQty = maxQty;

  var numQtys = maxQty - minQty + 1;
  s.length = numQtys;
  for (var i = 0; i < numQtys; i++)
    s.options[i] = new Option('' + (minQty + i), '' + (minQty + i));
  s.selectedIndex = curQty - minQty;
  return;
}

Subcat.prototype.getDomName = function(id)
{
  return this.domPrefix + id;
}

Subcat.prototype.getSelectionGridInfo = function(maxAttributeEndIndex)
{
  if (maxAttributeEndIndex == null)
    maxAttributeEndIndex = this.visibleAttributeEndIndex;
  var attributeEndIndex = 0;
  var gridNumber = 0;
  for (attributeEndIndex = 0; attributeEndIndex < maxAttributeEndIndex; attributeEndIndex++)
  {
    var valIndex = this.getSelectedAttributeValueIndex(attributeEndIndex);
    if (valIndex < 0)
      break;
    gridNumber = gridNumber * this.getNumAttributeValues(attributeEndIndex) + valIndex;
  }
  
  var gridDivisor = 1;
  for (var i = attributeEndIndex; i < this.getNumAttributes(); i++)
    gridDivisor *= this.getNumAttributeValues(i);
  return new GridInfo(this, gridNumber, attributeEndIndex, gridDivisor);
}

Subcat.prototype.isImageRotationEnabled = function()
{
  return (this.imageRotationData != null) && this.imageRotationData.isEnabled();
}

/* *************** DOM Getters *************** */

Subcat.prototype.getAttributeForm = function()
{
  return document.forms[this.getDomName('AttributeForm')];
}

Subcat.prototype.getElementById = function(id)
{
  return getById(this.getDomName(id));
}

Subcat.prototype.setElementContent = function(id, elementContent)
{
  var d = this.getElementById(id);
  if (!d)
    return;
    
  if (typeof elementContent == 'string')
  {
    d.innerHTML = elementContent;
    elementContent = true;
  }
  d.style.visibility = elementContent ? 'inherit' : 'hidden';
  return;
}

Subcat.prototype.getAttributeSel = function(attributeIndex)
{
  var f = this.getAttributeForm();
  return f[this.getDomName('AttrSel' + attributeIndex)];
}

Subcat.prototype.getMainImageFlashObj = function()
{
  if (!this.mainImageIsFlash)
    return null;
  var t = document[this.getDomName('MainImage')];
  if (t && (t.length != null) && (t.length > 1) && t[1])
    return t[1];
  return t;
}

Subcat.prototype.isGiftWrapSelected = function()
{
  if (!this.hasGiftWrapProducts)
    return false;
  var d = this.getElementById('GiftWrapCheckBox');
  if (d == null)
    return false;
  return d.checked;
}

/* *************** Getting state *************** */

Subcat.prototype.getSelectedAttributeValueIndex = function(attributeIndex)
{
  if (attributeIndex >= this.visibleAttributeEndIndex)
    return -1;
  var s = this.getAttributeSel(attributeIndex);
  var si = s.selectedIndex;
  if (si < 0)
    return -1;
  return parseInt(s.options[si].value);
}

/* *************** Callbacks *************** */

Subcat.prototype.addAttributeVisibilityCallback = function(cb)
{
  this.attributeVisibilityCallbacks.push(cb);
  return;
}

Subcat.prototype.addAttributeChangeCallback = function(cb)
{
  this.attributeChangeCallbacks.push(cb);
  return;
}

Subcat.prototype.doCallbacks = function(origVisibleAttributeEndIndex, origGridInfo, forceCallbacks)
{
  if (forceCallbacks || (origVisibleAttributeEndIndex != this.visibleAttributeEndIndex))
    for (var i = 0; i < this.attributeVisibilityCallbacks.length; i++)
      this.attributeVisibilityCallbacks[i](this, origVisibleAttributeEndIndex);
  if (forceCallbacks || (origGridInfo == null) || !origGridInfo.equals(this.lastGridInfo))
    for (var i = 0; i < this.attributeChangeCallbacks.length; i++)
      this.attributeChangeCallbacks[i](this, origGridInfo);
  return;
}

/* *************** Standard callbacks *************** */

function displayStandardInfo(subcatObj, origGridInfo)
{
  var p = subcatObj.lastGridInfo.getSelectedProduct();
  var possibleProducts = subcatObj.lastGridInfo.getPossibleProductSelections();
  
  subcatObj.displaySRP(p);
  subcatObj.displayPrice(possibleProducts);
  subcatObj.displaySKU(p);
  subcatObj.displayAvailMsg(p);
  subcatObj.displayMainImage(possibleProducts);
  subcatObj.displaySwatch();
  subcatObj.displayTradeinPromoMessage(p);
  subcatObj.updateAddToCartButton(p);
  subcatObj.prodPixChangeAttribute(origGridInfo);
  subcatObj.updateShaftPopup(p);
  subcatObj.updateGiftWrapOffer(p);
  subcatObj.updateMinSelectQty(possibleProducts);
  return;
}

Subcat.prototype.displaySRP = function(p)
{
  if ((p != null) && (p.srpCents > 0))
  {
    this.setElementContent('SRPDisplay', '**Original SRP ' + formatPriceWithSign(p.srpCents));
    return;
  }
  this.setElementContent('SRPDisplay', false);
  return;
}

Subcat.prototype.displayPrice = function(possibleProducts)
{
  if (this.doNotShowPrice)
    return;
  if (possibleProducts.length == 0)
  {
    this.setElementContent('PriceDisplay', false);
    return;
  }
getPriceText:
  if (this.addToCartForPrice || (this.delayPrice && !this.delayPriceClicked))
  {
    if (possibleProducts.length == 1)
    {
      if (this.addToCartForPrice)
        t = '';
      else
        t = '<b><font face="Arial, Helvetica, sans-serif" size=2 color="#FF0000">Click <a href="javascript:' + this.jsName + '.revealPrice()">here</a> to display Price</font></b>';
    }
    else
      t = '<b><font face="Arial, Helvetica, sans-serif" size=2 color="#FF0000">Make Selection</font></b>';
  }
  else
  {
    var priceTierRanges = new PriceTierRanges();
    priceTierRanges.combineWithProductTiers(possibleProducts[0]);
    var minPriceCents = possibleProducts[0].priceCents;
    var maxPriceCents = possibleProducts[0].priceCents;
    var useStrikeThru = possibleProducts[0].strikeThruPrice;
    var minStrikeThruPriceCents = possibleProducts[0].strikeThruPriceCents;
    var maxStrikeThruPriceCents = possibleProducts[0].strikeThruPriceCents;
    
    var curDiscountedPriceCents = possibleProducts[0].discountedPriceCents;
    if (curDiscountedPriceCents < 0)
      curDiscountedPriceCents = possibleProducts[0].priceCents;
    if ((possibleProducts[0].mapPriceCents > 0) && (curDiscountedPriceCents < possibleProducts[0].mapPriceCents))
    {
      t = '';
      break getPriceText;
    }
    
    for (var i = 1; i < possibleProducts.length; i++)
    {
      var curProduct = possibleProducts[i];
      var curPriceCents = curProduct.priceCents;
      if (curProduct.strikeThruPrice)
      {
        useStrikeThruPrice =  true;
        var curStrikeThruPriceCents = curProduct.strikeThruPriceCents;
        // Strike-thru price is the original non-discounted price when present
        if (curStrikeThruPriceCents >= 0)
        {
          if ((minStrikeThruPriceCents < 0) || (curStrikeThruPriceCents < minStrikeThruPriceCents))
            minStrikeThruPriceCents = curStrikeThruPrice;
          if ((maxStrikeThruPriceCents < 0) || (curStrikeThruPriceCents > maxStrikeThruPriceCents))
            maxStrikeThruPriceCents = curStrikeThruPriceCents;
        }
        else
        {
          if ((minStrikeThruPriceCents < 0) || (curStrikeThruPriceCents < minStrikeThruPriceCents))
            minStrikeThruPriceCents = curStrikeThruPriceCents;
          if ((maxStrikeThruPriceCents < 0) || (curStrikeThruPriceCents > maxStrikeThruPriceCents))
            maxStrikeThruPriceCents = curStrikeThruPriceCents;
        }
      }
        
      if (curPriceCents < minPriceCents)
        minPriceCents = curPriceCents;
      if (curPriceCents > maxPriceCents)
        maxPriceCents = curPriceCents;
      curDiscountedPriceCents = curProduct.discountedPriceCents;
      if (curDiscountedPriceCents < 0)
        curDiscountedPriceCents = curPriceCents;
      if ((curProduct.mapPriceCents > 0) && (curDiscountedPriceCents < curProduct.mapPriceCents))
      {
        t = '';
        break getPriceText;
      }
      
      priceTierRanges.combineWithProductTiers(curProduct);
    }
    
    var priceStr = '';
    if (useStrikeThru)
    {
      // Strike-thru the price
      if ((minStrikeThruPriceCents >= 0) && (maxStrikeThruPriceCents >= 0))
      {
        // A strike-thru price range is present
        if ((minPriceCents == minStrikeThruPriceCents) && (maxPriceCents == maxStrikeThruPriceCents))
        {
          // Price ranges are the same; remove strike-thru price range
          useStrikeThru = false;
        }
      }
      else
      {
        // No special spike-thru price range, strike-thru given price
        minStrikeThruPriceCents = minPriceCents;
        maxStrikeThruPriceCents = maxPriceCents;
        minPriceCents = -1;
        maxPriceCents = -1;
      }
    }
    var t = '<b><font face="Arial, Helvetica, sans-serif" size="2" color="#FF0000">Our Price: ';
    if (useStrikeThru)
    {
      t += '<del>';
      if (minStrikeThruPriceCents < maxStrikeThruPriceCents)
        t += formatPriceWithSign(minStrikeThruPriceCents) + '-' + formatPriceWithSign(maxStrikeThruPriceCents);
      else
        t += formatPriceWithSign(minStrikeThruPriceCents);
      t += '</del>';
    }
    if (minPriceCents >= 0)
    {
      if (useStrikeThru)
        t += ' ';
      if (minPriceCents < maxPriceCents)
        t += formatPriceWithSign(minPriceCents) + '-' + formatPriceWithSign(maxPriceCents);
      else
        t += formatPriceWithSign(minPriceCents);
    }
    t += '</font></b>';
    
    // If more than one tier present, or only one tier with a minimum greater than 1, then show tiers
    if ((minPriceCents >= 0) && ((priceTierRanges.getNumTiers() > 1) || ((priceTierRanges.getNumTiers() == 1) && (priceTierRanges.getTierMinQty(0) > 1))))
    {
      t += '<br><table border=0 cellspacing=0 cellpadding=1 class="productform">'
         + '  <tr><td colspan=2 style="padding-left: 15px"><b><font color="black" size="2">Save More</font></b></td></tr>';
      for (var i = 0; i < priceTierRanges.getNumTiers(); i++)
      {
        t += '  <tr><td style="padding-left: 15px" valign="top"><b><font color="black">';
        var minQty = priceTierRanges.getTierMinQty(i);
        var nextTierMinQty = priceTierRanges.getNextTierMinQty(i);
        if (nextTierMinQty < 0)
          t += minQty + ' or more:';
        else
        {
          if (--nextTierMinQty == minQty)
            t += minQty;
          else
            t += minQty + '-' + nextTierMinQty;
        }
        t += '</font></b></td><td align="right" valign="top"><b><font color="black">';
        var minPrice = priceTierRanges.getTierMinPrice(i);
        var maxPrice = priceTierRanges.getTierMaxPrice(i);
        if (minPrice < maxPrice)
          t += formatPriceWithSign(minPrice) + '-' + formatPriceWithSign(maxPrice);
        else
          t += formatPriceWithSign(minPrice);
        t += '</font></b></td></tr>\n';
      }
      t += '</table>';
    }
  }
  this.setElementContent('PriceDisplay', t);
  return;
}

Subcat.prototype.revealPrice = function()
{
  var possibleProducts = this.lastGridInfo.getPossibleProductSelections();
  if (possibleProducts.length == 1)
  {
    this.delayPriceClicked = true;
    this.displayPrice(possibleProducts);
  }
  return;
}

Subcat.prototype.displaySKU = function(p)
{
  if ((p != null) && (p.sku.length > 0))
  {
    this.setElementContent('SKUDisplay', 'SKU# ' + p.sku);
    return;
  }
  this.setElementContent('SKUDisplay', false);
  return;
}

Subcat.prototype.displayAvailMsg = function(p)
{
  if (p == null)
  {
    this.setElementContent('AvailDisplay', false);
    return;
  }
  var t = p.availMsg;
  if (t.length > 0)
  {
    if (p.stat == 'T1')
      t = '<b>Shipped From Manufacturer</b>';
    else
      t = '<font color="#12008C"><b>' + t + '</b></font>';
  }
  else
    t = '<font color="#12008C"><b>In Stock</b></font>';
  this.setElementContent('AvailDisplay', t);
}

Subcat.prototype.displayMainImage = function(possibleProducts)
{
  if (this.mainImageIsFlash)
    return;
  if (possibleProducts.length == 0)
    return;
  this.displayImage(this.mainImage, 'MainImage', possibleProducts[0]);
  return;
}

Subcat.prototype.displayImage = function(pImage, imageName, p)
{
  if (pImage.isEmptyImage())
    return;
  var d = document.images[this.getDomName(imageName)];
  if (d)
    d.src = pImage.expandImage(p);
  return;
}

Subcat.prototype.updateAddToCartButton = function(p)
{
  var canAddToCart = false;
checkProd:
  if (p != null)
  {
    if (this.noStockNoBuy && p.noStock)
      break checkProd;
    if (p.doNotAllowPurchase)
      break checkProd;
    canAddToCart = true;
  }
  var img = document.images[this.getDomName('AddToCart')];
  if (img)
    changeImageButtonStatus(img, !canAddToCart);
  return;
}

Subcat.prototype.prodPixChangeAttribute = function(origGridInfo)
{
  if (!this.useProdPix)
    return;
  if (this.prodPixReplaceableAttributeIndex < 0)
    return;
//  var oldValueIndex = origGridInfo.getAttributeValueIndex(this.prodPixReplaceableAttributeIndex);
  var newValueIndex = this.lastGridInfo.getAttributeValueIndex(this.prodPixReplaceableAttributeIndex);
  if ((this.lastProdPixValueIndex == newValueIndex) || (newValueIndex < 0))
    return;
  this.prodPixChangeAttributeValueIndex(newValueIndex);
  this.lastProdPixValueIndex = newValueIndex;
  return;
}

Subcat.prototype.getProdPixAttributeValueByIndex = function(valueIndex)
{
  return removeBadIdChars(this.subcatAttributes[this.prodPixReplaceableAttributeIndex].attributeValues[valueIndex]).toLowerCase();
}

Subcat.prototype.prodPixChangeAttributeValueIndex = function(valueIndex)
{
  var prodPixObj = this.getMainImageFlashObj();
  if (prodPixObj == null)
    return;
  try
  {
    if (prodPixObj.PercentLoaded() < 100)
      throw 'Not loaded';
    if (!this.prodPixLoaded)
      throw 'Not loaded (2)';
  }
  catch (e)
  {
    this.lastProdPixError = e;
    var t = this.jsName + '.prodPixChangeAttributeValueIndex(' + valueIndex + ')';
    window.setTimeout(t, 100);
    return;
  }
  prodPixObj.SetVariable("col", this.getProdPixAttributeValueByIndex(valueIndex));
  prodPixObj.TPlay("_level0/setCol");
  return;
}

Subcat.prototype.displaySwatch = function()
{
  if (this.swatchImage.isEmptyImage())
    return;
  var swatchAttributeIndexes = this.swatchImage.getAttributeIndexes();
  var minAttributeIndex = -1;
  for (var i = 0; i < swatchAttributeIndexes.length; i++)
    if ((minAttributeIndex < 0) || (swatchAttributeIndexes[i] < minAttributeIndex))
      minAttributeIndex = swatchAttributeIndexes[i];
  var gridInfo = this.lastGridInfo;
  if (minAttributeIndex >= 0)
    while (gridInfo.attributeEndIndex > minAttributeIndex)
      gridInfo = gridInfo.removeLastAttribute();
  var possibleProducts = gridInfo.getPossibleProductSelections();

  var swatchImages = [];
  var swatchGridNums = [];
productLoop:
  for (var i = 0; i < possibleProducts.length; i++)
  {
    var p = possibleProducts[i];
    var gridNum = 0;
    var lastNumValues = 0;
    for (var j = 0; j < swatchAttributeIndexes.length; j++)
    {
      var attrIndex = swatchAttributeIndexes[j];
      gridNum = (gridNum * lastNumValues) + p.getValueIndex(attrIndex);
      lastNumValues = this.getNumAttributeValues(attrIndex);
    }
    var insertionIndex = 0;
    for (insertionIndex = 0; insertionIndex < swatchGridNums.length; insertionIndex++)
    {
      if (gridNum == swatchGridNums[insertionIndex])
        continue productLoop;
      if (gridNum < swatchGridNums[insertionIndex])
        break;
    }
    for (var j = swatchGridNums.length; j > insertionIndex; j--)
    {
      swatchGridNums[j] = swatchGridNums[j - 1];
      swatchImages[j] = swatchImages[j - 1];
    }
    swatchGridNums[insertionIndex] = gridNum;
    swatchImages[insertionIndex] = this.swatchImage.expandImage(p);
  }
  if (swatchImages.length == 0)
  {
    this.setElementContent('Swatch', false);
    return;
  }

  var t = '<table border="1" bordercolor="black" cellspacing="0" cellpadding="0">';
  var MAX_COLS = 10;
  for (var i = 0; i < swatchImages.length; i++)
  {
    if ((i % MAX_COLS) == 0)
      t += '<tr>\n';
    t += '<td><a href="javascript:' + this.jsName + '.selectSwatchGrid(' + swatchGridNums[i] + ')"><img width=30 border=0 src="' + swatchImages[i] + '" onError="' + this.jsName + '.swatchImageComingSoon(this)"></a></td>\n';
    if (((i + 1) % MAX_COLS) == 0)
      t += '</tr>\n';
  }
  if (swatchImages.length < MAX_COLS)
    t += '</tr>\n';
  else
  {
    var swatchColSpan = (swatchImages.length % MAX_COLS);
    if (swatchColSpan > 0)
      t += '<td colspan="' + swatchColSpan + '"></td>\n</tr>\n';
  }
  t += '</table>';
  this.setElementContent('Swatch', t);
  return;
}

Subcat.prototype.displayTradeinPromoMessage = function(p)
{
  if (!this.hasTradeinPromo)
    return;
  var prodIndex = (p == null) ? -1 : this.getProductIndexById(p.productId);
  if (prodIndex < 0)
    this.setElementContent('TradeinPromoMsg', false);
  else
    this.setElementContent('TradeinPromoMsg', this.tradeinPromoFlags[prodIndex] ? '<b><font color="red">Qualifies for trade-in bonus</font></b>' : '<b><font color="red">Does not qualify for trade-in bonus</font></b>');
  return;
}

Subcat.prototype.updateShaftPopup = function(p)
{
  if (!this.shaftPopupAvailable)
    return;
  var d = this.getElementById('ShaftPopup');
  if (!d)
    return;
  if ((p == null) || !p.shaftPopupAvailable)
    d.style.visibility = 'hidden';
  else
  {
    d.innerHTML = '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="javascript:' + this.jsName + '.displayShaftPopup(' + p.productId + ')" title="Shaft Specs"><font color="blue" style="font-size: 9pt; font-weight:bold">Learn More</font></a>';
    d.style.visibility = 'inherit';
  }
  return;
}

Subcat.prototype.displayShaftPopup = function(productId)
{
  window.open('/customer/category/showstats.jsp?PRODUCT_ID=' + productId, '_blank', 'resizable=yes,scrollbars=no,menubar=no,location=no,toolbar=no,width=500,height=500');
  return;
}

Subcat.prototype.updateGiftWrapOffer = function(p)
{
  if (!this.hasGiftWrapProducts)
    return;
  var d = this.getElementById('GiftWrapOffer');
  if (d == null)
    return;
  if ((p == null) || (p.giftWrapInfo == null))
    d.style.visibility = 'hidden';
  else
  {
    var t = '<a href="javascript:' + this.jsName + '.displayGiftWrapOffer()" title="Display Gift Wrapping"><img src="/images/gifticon.gif" alt="Check to have this product gift wrapped"></a>&nbsp;Gift Wrap for '
          + formatPriceWithSign(p.giftWrapInfo.priceCents)
          + '<input type="checkbox" id="' + this.getDomName('GiftWrapCheckBox') + '" value="Y" onChange="' + this.jsName + '.updateGiftWrapSelected(this)"';
    if (this.giftWrapSelected)
      t += ' CHECKED';
    t += '><br><br>';
    d.innerHTML = t;
    d.style.visibility = 'inherit';
  }
  return;
}

Subcat.prototype.displayGiftWrapOffer = function()
{
  var p = this.getSelectedProduct();
  if ((p == null) || (p.giftWrapInfo == null))
    return;
  window.open(p.giftWrapInfo.imageSrc, '_blank', 'resizable=no,scrollbars=no,menubar=no,location=no,toolbar=no,width=509,height=418');
  return;
}

Subcat.prototype.updateGiftWrapSelected = function(checkbox)
{
  if (checkbox)
    this.giftWrapSelected = checkbox.checked;
  return;
}

Subcat.prototype.updateMinSelectQty = function(possibleProducts)
{
  if (possibleProducts.length == 0)
    return 1;
  var minQty = possibleProducts[0].getMinPurchaseQty();
  var maxQty = possibleProducts[0].getMaxPurchaseQty();
  if (maxQty < 1)
    maxQty = 10;
  for (var i = 1; i < possibleProducts.length; i++)
  {
    var curMinQty = possibleProducts[i].getMinPurchaseQty();
    if (curMinQty < minQty)
      minQty = curMinQty;
    var curMaxQty = possibleProducts[i].getMaxPurchaseQty();
    if (curMaxQty < 1)
      curMaxQty = 10;
    if (curMaxQty > maxQty)
      maxQty = curMaxQty;
  }
  this.setSelectQtyRange(minQty, maxQty);
  return;
}


/* *************** Handling attribute selections *************** */

Subcat.prototype.initDisplay = function(productId)
{
  var p = null;
  if ((productId != null) && (productId > 0))
    p = this.getProductById(productId);
  var origGridInfo = this.lastGridInfo;
  var origVisibleAttributeEndIndex = this.visibleAttributeEndIndex;
  
  if (p != null)
  {
    var fullSelect = true;
    for (var i = 0; i < this.getNumAttributes(); i++)
    {
      var targetValIndex = p.getValueIndex(i);
      var valIndex = this.displayAttributeSelect(i, targetValIndex);
      if (valIndex != targetValIndex)
      {
        fullSelect = false;
        break;
      }
    }
    if (fullSelect)
    {
      this.updateLastGridInfo(this.getNumAttributes());
      if (this.isImageRotationEnabled())
        this.imageRotationData.stopRotation();
    }
  }
  else
    this.displayAttributesCascade(0);
  
  if (this.isImageRotationEnabled())
  {
    this.imageRotationData.parentDisplayInit = true;
    this.imageRotationData.displayCaption();
  }
  
  this.doCallbacks(origVisibleAttributeEndIndex, origGridInfo, true);
  if (this.doNotAllowPurchase)
    this.showCallForPrice();
  return;
}

Subcat.prototype.attributeSelectChanged = function(attributeIndex)
{
  if (attributeIndex >= this.visibleAttributeEndIndex)
    return;
  var valIndex = this.getSelectedAttributeValueIndex(attributeIndex);
  if (valIndex < 0)
    return;
  
  var origGridInfo = this.lastGridInfo;
  if (origGridInfo.getAttributeValueIndex(attributeIndex) == valIndex)
    return;

  if (this.isImageRotationEnabled())
    this.imageRotationData.stopRotation();
  
  var origVisibleAttributeEndIndex = this.visibleAttributeEndIndex;

  this.removeChooseOne(attributeIndex);
  this.displayAttributesCascade(attributeIndex + 1);

  this.doCallbacks(origVisibleAttributeEndIndex, origGridInfo);
  return;
}

Subcat.prototype.removeChooseOne = function(attributeIndex)
{
  var s = this.getAttributeSel(attributeIndex);
  if (s.length > 0)
  {
    if (parseInt(s.options[0].value) < 0)
    {
      var si = s.selectedIndex - 1;
      if (si < 0)
        si = 0;
      var newLength = s.length - 1;
      for (var i = 0; i < newLength; i++)
        s.options[i] = new Option(s.options[i + 1].text, s.options[i + 1].value);
      s.length = newLength;
      s.selectedIndex = si;
    }
  }
  return;
}

Subcat.prototype.updateLastGridInfo = function(attributeEndIndex)
{
  this.lastGridInfo = this.getSelectionGridInfo(attributeEndIndex);
  return;
}
  

Subcat.prototype.displayAttributesCascade = function(startAttributeIndex)
{
  while ((startAttributeIndex < this.getNumAttributes()) && (this.displayAttributeSelect(startAttributeIndex++) >= 0));
  // Update grid before leaving
  this.updateLastGridInfo(startAttributeIndex);
  return;
}

Subcat.prototype.displayAttributeSelectByName = function(attributeName, attributeValue)
{
  attributeName = attributeName.toLowerCase();
  attributeValue = attributeValue.toLowerCase();
  for (var i = 0; i < this.getNumAttributes(); i++)
  {
    var curAttribute = this.subcatAttributes[i];
    if (curAttribute.attributeName.toLowerCase() == attributeName)
    {
      for (var j = 0; j < curAttribute.attributeValues.length; j++)
      {
        if (curAttribute.attributeValues[j].toLowerCase() == attributeValue)
        {
          var returnVal = this.displayAttributeSelect(i, j);
          if (returnVal >= 0)
            this.displayAttributesCascade(i + 1);
          return returnVal;
        }
      }
    }
  }
  return null;
}

Subcat.prototype.displayAttributeSelect = function(attributeIndex, attributeValueIndex)
{
  if (attributeValueIndex == null)
    attributeValueIndex = -1;
  this.updateLastGridInfo(attributeIndex);
  if (this.lastGridInfo.attributeEndIndex < attributeIndex)
  {
    // Some of the attributes before attributeIndex are not selected
    return null;
  }
  
  var curAttribute = this.subcatAttributes[attributeIndex];
  var valueFlags = new Array(curAttribute.getNumValues());
  for (var i = 0; i < valueFlags.length; i++)
    valueFlags[i] = false;
  var numValueFlagsSet = 0;
  for (var i = 0; i < this.subcatProducts.length; i++)
  {
    var p = this.subcatProducts[i];
    if (!this.lastGridInfo.doesProductMatch(p))
      continue;
    var curValueIndex = p.getValueIndex(attributeIndex);
    if (!valueFlags[curValueIndex])
    {
      valueFlags[curValueIndex] = true;
      numValueFlagsSet++;
    }
  }
  if (numValueFlagsSet == 0)
  {
    // The previous attribute select lists contained values leading to an impossible selection!
    return null;
  }
  
  if (numValueFlagsSet == 1)
  {
    for (var i = 0; i < valueFlags.length; i++)
    {
      if (valueFlags[i])
      {
        attributeValueIndex = i;
        break;
      }
    }
  }
  else
    if ((attributeValueIndex >= 0) && !valueFlags[attributeValueIndex])
      attributeValueIndex = -1;
  
  var s = this.getAttributeSel(attributeIndex);
  s.length = numValueFlagsSet + ((attributeValueIndex < 0) ? 1 : 0);
  var curIndex = 0;
  if (attributeValueIndex < 0)
    s.options[curIndex++] = new Option('Choose One', '-1');
  var si = 0;
  for (var i = 0; i < valueFlags.length; i++)
  {
    if (valueFlags[i])
    {
      if (i == attributeValueIndex)
        si = curIndex;
      s.options[curIndex++] = new Option(curAttribute.attributeValues[i], i);
    }
  }
  s.selectedIndex = si;
  if (this.hasTempHideSelectData())
    this.tempHideSelectData[0][attributeIndex] = 'inherit';
  else
    this.setElementContent('AttrDiv' + attributeIndex, true);
  this.visibleAttributeEndIndex = ++attributeIndex;

  for (; attributeIndex < this.getNumAttributes(); attributeIndex++)
  {
    if (this.hasTempHideSelectData())
      this.tempHideSelectData[0][attributeIndex] = 'hidden';
    else
      this.setElementContent('AttrDiv' + attributeIndex, false);
  }
  
  return attributeValueIndex;
}

Subcat.prototype.selectSwatchGrid = function(swatchGridNum)
{

  if (this.swatchImage.isEmptyImage())
    return;
  if (this.isImageRotationEnabled())
    this.imageRotationData.stopRotation();
  var swatchAttributeIndexes = this.swatchImage.getAttributeIndexes();
  var swatchAttributeValueIndexes = new Array(swatchAttributeIndexes.length);
  var gridNum = swatchGridNum;
  for (var i = swatchAttributeIndexes.length - 1; i >= 0; i--)
  {
    var attrIndex = swatchAttributeIndexes[i];
    swatchAttributeValueIndexes[i] = gridNum % this.getNumAttributeValues(attrIndex);
    gridNum = Math.floor(gridNum / this.getNumAttributeValues(attrIndex));
  }

  var minAttributeIndex = -1;
  var maxAttributeIndex = -1;
  for (var i = 0; i < swatchAttributeIndexes.length; i++)
  {
    if ((minAttributeIndex < 0) || (swatchAttributeIndexes[i] < minAttributeIndex))
      minAttributeIndex = swatchAttributeIndexes[i];
    if (swatchAttributeIndexes[i] > maxAttributeIndex)
      maxAttributeIndex = swatchAttributeIndexes[i];
  }
  if (maxAttributeIndex < 0)
    return;

  var results = this.getSmallestPossibleProducts(this.lastGridInfo, swatchAttributeIndexes, swatchAttributeValueIndexes);
  if ((results[2] == null) || !results[0])
    return;
  
  this.setSelectionMatchProduct(results[2], maxAttributeIndex);
  return;
}

Subcat.prototype.getSmallestPossibleProducts = function(gridInfo, attributeIndexes, attributeValueIndexes)
{
  gridInfo = gridInfo.clone();
  var attributeValuesChanged = false;
  var maxAttributeIndex = -1;
  for (var i = 0; i < attributeIndexes.length; i++)
    if (attributeIndexes[i] > maxAttributeIndex)
      maxAttributeIndex = attributeIndexes[i];

  while (gridInfo.attributeEndIndex > (maxAttributeIndex + 1))
    gridInfo = gridInfo.removeLastAttribute();
  while (true)
  {
    // Check whether selected attributes match the requested ones, and whether the unselected attributes are possible.
    var possibleProducts = gridInfo.getPossibleProductSelections();
    for (var i = 0; i < attributeIndexes.length; i++)
    {
      var attrIndex = attributeIndexes[i];
      var attrValIndex = gridInfo.getAttributeValueIndex(attrIndex);
      if (attrValIndex < 0)
      {
        attributeValuesChanged = true;
        // Filter products to those that could match
        var newPossibleProductsLen = 0;
        for (var j = 0; j < possibleProducts.length; j++)
        {
          if (possibleProducts[j].getValueIndex(attrIndex) == attributeValueIndexes[i])
          {
            if (newPossibleProductsLen != j)
              possibleProducts[newPossibleProductsLen] = possibleProducts[j];
            newPossibleProductsLen++;
          }
        }
        possibleProducts.length = newPossibleProductsLen;
      }
      else
      {
        // Stop if invalid attribute value selected
        if (attrValIndex != attributeValueIndexes[i])
        {
          possibleProducts.length = 0;
          attributeValuesChanged = true;
          break;
        }
      }
    }
    // check possibleProducts
    if ((possibleProducts.length > 0) || (gridInfo.attributeEndIndex <= 0))
      return [attributeValuesChanged, possibleProducts, (possibleProducts.length == 0) ? null : possibleProducts[0]];
    gridInfo = gridInfo.removeLastAttribute();
  }
  return;
}

Subcat.prototype.setSelectionMatchProduct = function(p, maxAttributeIndex)
{
  var origVisibleAttributeEndIndex = this.visibleAttributeEndIndex;
  var origGridInfo = this.lastGridInfo;

  var curAttrIndex = 0;
  for (curAttrIndex = 0; curAttrIndex <= maxAttributeIndex; curAttrIndex++)
  {
    var curValIndex = this.getSelectedAttributeValueIndex(curAttrIndex);
    var targetValIndex = p.getValueIndex(curAttrIndex);
    if (targetValIndex == curValIndex)
      continue;
    var valIndex = this.displayAttributeSelect(curAttrIndex, targetValIndex);
    if (valIndex != targetValIndex)
      break;
  }

  this.displayAttributesCascade(curAttrIndex);

  this.doCallbacks(origVisibleAttributeEndIndex, origGridInfo);
  return;
}

Subcat.prototype.receiveFSCommand = function(command, args)
{
  if (args == 'false')		// some bs message the flash file sends
    return;
  if (!this.prodPixLoaded)
  {
    this.prodPixLoaded = true;
    return;
  }
  
  if (this.prodPixReplaceableAttributeIndex < 0)
    return;
  var prodPixAttr = this.subcatAttributes[this.prodPixReplaceableAttributeIndex];
  args = args.toLowerCase();
  var targetIndex = -1;
  for (var i = 0; i < prodPixAttr.getNumValues(); i++)
  {
    if (removeBadIdChars(prodPixAttr.attributeValues[i]).toLowerCase() == args)
    {
      targetIndex = i;
      break;
    }
  }
  this.lastProdPixValueIndex = targetIndex;
  if (targetIndex < 0)
    return;
  
  var results = this.getSmallestPossibleProducts(this.lastGridInfo, [this.prodPixReplaceableAttributeIndex], [targetIndex]);
  if ((results[2] == null) || !results[0])
    return;
  
  this.setSelectionMatchProduct(results[2], this.prodPixReplaceableAttributeIndex);
  return;
}

/* *************** Misc *************** */

Subcat.prototype.mainImageComingSoon = function(img)
{
  if (this.isImageRotationEnabled() && (img.src.indexOf('/images/products/coming-soon.jpg') < 0))
    if (this.imageRotationData.imageError())
      return;
  if (img.src.indexOf('/images/products/coming-soon.jpg') < 0)
    img.src = '/images/products/coming-soon.jpg';
  return;
}

Subcat.prototype.mainImageLoaded = function(img)
{
  if (this.isImageRotationEnabled() && (img.src.indexOf('/images/products/coming-soon.jpg') < 0))
    this.imageRotationData.imageLoaded();
  return;
}

Subcat.prototype.swatchImageComingSoon = function(img)
{
  if (img.src.indexOf('/images/products/coming-soon-icon.jpg') < 0)
    img.src = '/images/products/coming-soon-icon.jpg';
  return;
}

Subcat.prototype.createParameterizedImage = function(imageName)
{
  var imageNameParts = [];
  var startIndex = 0, endIndex = 0, lastIndex = 0;
  var len = imageName.length;
searchLoop:
  while (startIndex < len)
  {
    startIndex = imageName.indexOf('#', startIndex);
    if (startIndex < 0)
      break;
    endIndex = imageName.indexOf('#', startIndex + 1);
    if (endIndex < 0)
      break;
    var attrNameFiltered = imageName.substring(startIndex + 1, endIndex);
    for (var i = -1; i < this.getNumAttributes(); i++)
    {
      var curAttributeName = (i < 0) ? 'MERCHPN' : removeBadIdChars(this.subcatAttributes[i].attributeName);
      if (curAttributeName == attrNameFiltered)
      {
        if ((startIndex - lastIndex) > 0)
          imageNameParts.push(imageName.substring(lastIndex, startIndex));
        imageNameParts.push(i);
        lastIndex = endIndex + 1;
        startIndex = lastIndex;
        continue searchLoop;
      }
    }
    startIndex = endIndex;
  }
  if (lastIndex < len)
  {
    if (lastIndex == 0)
      imageNameParts.push(imageName);
    else
      imageNameParts.push(imageName.substring(lastIndex));
  }
  return new ParameterizedImage(imageNameParts);
}

Subcat.prototype.createFlashMainImage = function()
{
  var mainImageDomName = this.getDomName('MainImage');
  var newArgs = [this.getDomName('MainImageFlashDiv'),
    'allowScriptAccess','sameDomain',
    'movie', this.fullFlashURL,
    'name', mainImageDomName,
    'id', mainImageDomName,
    'quality', 'high',
    'bgcolor', '#ffffff',
    'wmode', 'transparent',
    'swliveconnect', 'true',
    'codebase', 'http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=8,0,0,0',
    'pluginspage', 'http://www.macromedia.com/go/getflashplayer',
    'width', '' + this.flashWidth,
    'height', '' + this.flashHeight
  ];
  if (this.flashAttributesList != null)
    for (var i = 0; i < this.flashAttributesList.length; i++)
      newArgs.push(this.flashAttributesList[i]);
  newArgs.push('src');
  newArgs.push(this.fullFlashURL);
  createFlashObjectWithHooks(this.getDomName('MainImageFlashDiv'), newArgs, mainImageDomName, this.jsName);
  return;
}

/* *************** Public functions *************** */

Subcat.prototype.showProductTour = function()
{
  if (!this.hasProductTour)
    return;
  var link = '/customer/category/productTour.jsp?id=' + this.subcatId;
  if (this.productTourAttributeIndex >= 0)
  {
    var ind = this.getSelectedAttributeValueIndex(this.productTourAttributeIndex);
    if (ind >= 0)
      link += '&attrval=' + escape(this.subcatAttributes[this.productTourAttributeIndex].attributeValues[ind]);
  }
  window.open(link, 'producttour', 'resizable=yes,scrollbars=yes,width=795,height=500');
  return;
}

Subcat.prototype.addToCart = function()
{
  var p = this.getSelectedProduct();
  if (p == null)
  {
    alert('Please finish selecting a product.');
    return;
  }
  if (this.noStockNoBuy && p.noStock)
  {
    alert('This product is currently not available.');
    return;
  }
  if (this.doNotAllowPurchase)
  {
    this.showCallForPrice();
    return;
  }
  
  if (this.personalizationProfile)
  {
    var personalSel = this.personalizationProfile.getSelection();
    if (this.personalizationProfile.requirePersonalization)
    {
      if (this.personalizationProfile.numLines > 0)
      {
        var hasLines = false;
        for (var i = 0; i < personalSel.selectionLines.length; i++)
        {
          if (personalSel.selectionLines[i].length > 0)
          {
            hasLines = true;
            break;
          }
        }
        if (!hasLines)
        {
          alert('This product must be personalized; please finish filling out your personalization options.');
          return;
        }
      }
    }
    floatCart.addPersonalizedProductToCart(p.productId, this.getSelectedQty(), this.personalizationProfile.profileId, this.rcpid, personalSel.getSerializedData(), this.isGiftWrapSelected(), null, p.getSubcatId());
    return;
  }
  floatCart.addToCart(p.productId, this.getSelectedQty(), this.isGiftWrapSelected(), null, p.getSubcatId());
  return;
}

Subcat.prototype.showCallForPrice = function()
{
  window.open('/customer/category/callForPrice.jsp?mfg=' + this.callforPriceMfr, '_blank', 'resizable=yes,width=530,height=260');
  return;
}

Subcat.prototype.showZoomImage = function()
{
  if (this.zoomImage.isEmptyImage())
    return;
  var possibleProducts = this.lastGridInfo.getPossibleProductSelections();
  this.displayImage(this.mainImage, 'MainImage', possibleProducts[0]);
  window.open('/customer/category/viewzoomimg.jsp?img=' + this.zoomImage.expandImage(possibleProducts[0]), 'zoomimg', 'resizable=yes,width=540,height=650');
  return;
}

Subcat.prototype.addToWishlist = function()
{
  // todo
}

Subcat.prototype.emailFriend = function()
{
  var p = this.getSelectedProduct();
  var t = '/customer/email_friend.jsp?SUBCATEGORY_ID=' + this.subcatId;
  if (p != null)
    t += '&prodid=' + p.productId;
  window.open(t, 'emailFriend', 'width=550,height=450,resizable=yes');
  return;
}

Subcat.prototype.hasTempHideSelectData = function()
{
  return (this.tempHideSelectData != null) && (this.tempHideSelectData.length >= 2);
}

Subcat.prototype.tempHideSelects = function()
{
  if (this.tempHideSelectData != null)
    return;
  this.tempHideSelectData = [];
  var attributeStateData = [];
  for (var i = 0; i < this.getNumAttributes(); i++)
  {
    var d = this.getElementById('AttrDiv' + i);
    attributeStateData.push(((d != null) && (d.style.visibility != null)) ? d.style.visibility : '');
    if (d)
      d.style.visibility = 'hidden';
  }
  this.tempHideSelectData.push(attributeStateData);
  var d = this.getElementById('QtySpan');
  this.tempHideSelectData.push(((d != null) && (d.style.visibility != null)) ? d.style.visibility : '');
  if (d)
    d.style.visibility = 'hidden';
  return;
}

Subcat.prototype.undoTempHideSelects = function()
{
  if (!this.hasTempHideSelectData())
    return;
  var attributeStateData = this.tempHideSelectData[0];
  for (var i = 0; (i < this.getNumAttributes()) && (i < attributeStateData.length); i++)
  {
    if (attributeStateData[i].length == 0)
      continue;
    var d = this.getElementById('AttrDiv' + i);
    if (d)
      d.style.visibility = attributeStateData[i];
  }
  var d = this.getElementById('QtySpan');
  if (d)
    d.style.visibility = this.tempHideSelectData[1];
  this.tempHideSelectData = null;
  return;
}

/* ****************************** SubcatAttribute ****************************** */

function SubcatAttribute(attributeName, attributeValues)
{
  this.attributeName = attributeName;
  this.attributeValues = attributeValues;
}

SubcatAttribute.prototype.getNumValues = function()
{
  return this.attributeValues.length;
}

/* ****************************** SubcatProduct ****************************** */

function SubcatProduct(productId, sku, gridNum, priceCents, srpCents, stat, availMsg, noStock, shaftPopupAvailable, minPurchaseQty, maxPurchaseQty, giftWrapInfo, strikeThruPrice, strikeThruPriceCents, tieredPricingData, mapPriceCents, discountedPriceCents)
{
  this.productId = productId;
  this.sku = sku;
  this.gridNum = gridNum;
  this.priceCents = priceCents;
  this.srpCents = srpCents;
  this.stat = stat;
  this.availMsg = availMsg;
  this.noStock = noStock;
  this.shaftPopupAvailable = shaftPopupAvailable;
  this.minPurchaseQty = minPurchaseQty;
  this.maxPurchaseQty = maxPurchaseQty;
  this.giftWrapInfo = giftWrapInfo;
  this.strikeThruPrice = strikeThruPrice;
  this.strikeThruPriceCents = strikeThruPriceCents;
  this.tieredPricingData = tieredPricingData;
  this.mapPriceCents = mapPriceCents;
  this.discountedPriceCents = discountedPriceCents;
}

SubcatProduct.prototype.getValueIndex = function(attributeIndex)
{
  var n = this.gridNum;
  for (var i = this.parentSubcat.getNumAttributes() - 1; i > attributeIndex; i--)
    n = Math.floor(n / this.parentSubcat.getNumAttributeValues(i));
  return n % this.parentSubcat.getNumAttributeValues(attributeIndex);
}

SubcatProduct.prototype.getAttributeValue = function(attributeIndex)
{
  return this.parentSubcat.subcatAttributes[attributeIndex].attributeValues[this.getValueIndex(attributeIndex)];
}

SubcatProduct.prototype.hasTieredPricing = function()
{
  return (this.tieredPricingData != null);
}

SubcatProduct.prototype.getTieredPricingCount = function()
{
  if (this.hasTieredPricing())
    return (this.tieredPricingData.length >>> 1);
  return 0;
}

SubcatProduct.prototype.getTierMinimumQty = function(tierIndex)
{
  return this.tieredPricingData[tierIndex << 1];
}

SubcatProduct.prototype.getNextTierMinimumQty = function(tierIndex)
{
  if (++tierIndex == this.getTieredPricingCount())
    return -1;
  return this.getTierMinimumQty(tierIndex);
}

SubcatProduct.prototype.getTierPriceCents = function(tierIndex)
{
  return this.tieredPricingData[(tierIndex << 1) + 1];
}

SubcatProduct.prototype.getMinPurchaseQty = function()
{
  if (this.minPurchaseQty > 1)
    return this.minPurchaseQty;
  return 1;
}

SubcatProduct.prototype.getMaxPurchaseQty = function()
{
  if (this.maxPurchaseQty < 1)
    return 0;
  return this.maxPurchaseQty;
}

SubcatProduct.prototype.getSubcatId = function()
{
  if (this.parentSubcat)
    return this.parentSubcat.subcatId;
  return 0;
}

/* ****************************** GridInfo ****************************** */

function GridInfo(parentSubcat, gridNumber, attributeEndIndex, gridDivisor)
{
  this.parentSubcat = parentSubcat;
  this.gridNumber = gridNumber;
  this.attributeEndIndex = attributeEndIndex;
  this.gridDivisor = gridDivisor;
}

GridInfo.prototype.clone = function()
{
  return new GridInfo(this.parentSubcat, this.gridNumber, this.attributeEndIndex, this.gridDivisor);
}

GridInfo.prototype.equals = function(o)
{
  return (this.gridNumber == o.gridNumber) && (this.attributeEndIndex == o.attributeEndIndex) && (this.gridDivisor == o.gridDivisor);
}

GridInfo.prototype.isFullySelected = function()
{
  return (this.attributeEndIndex == this.parentSubcat.getNumAttributes());
}

GridInfo.prototype.getSelectedProduct = function()
{
  if (this.isFullySelected())
    return this.parentSubcat.getProductByGridNum(this.gridNumber);
  return null;
}

GridInfo.prototype.doesProductMatch = function(p)
{
  return (Math.floor(p.gridNum / this.gridDivisor) == this.gridNumber);
}

GridInfo.prototype.getPossibleProductSelections = function()
{
  var possibleProducts = [];
  for (var i = 0; i < this.parentSubcat.subcatProducts.length; i++)
  {
    var p = this.parentSubcat.subcatProducts[i];
    if (this.doesProductMatch(p))
      possibleProducts.push(p);
  }
  return possibleProducts;
}

GridInfo.prototype.removeLastAttribute = function()
{
  if (this.attributeEndIndex == 0)
    return null;
  var numAttributeValues = this.parentSubcat.getNumAttributeValues(this.attributeEndIndex - 1);
  var newGridNumber = Math.floor(this.gridNumber / numAttributeValues);
  var newGridDivisor = this.gridDivisor * numAttributeValues;
  return new GridInfo(this.parentSubcat, newGridNumber, this.attributeEndIndex - 1, newGridDivisor);
}

GridInfo.prototype.getAttributeValueIndex = function(attributeIndex)
{
  if (attributeIndex >= this.attributeEndIndex)
    return -1;
  var n = this.gridNumber;
  for (var i = this.attributeEndIndex - 1; i > attributeIndex; i--)
    n = Math.floor(n / this.parentSubcat.getNumAttributeValues(i));
  return n % this.parentSubcat.getNumAttributeValues(attributeIndex);
}


/* ****************************** ParameterizedImage ****************************** */

function ParameterizedImage(imageComponents)
{
  if ((imageComponents != null) && ((typeof imageComponents) == 'object'))
  {
    this.imageComponents = imageComponents;
    return;
  }
  this.imageComponents = new Array(arguments.length);
  for (var i = 0; i < arguments.length; i++)
    this.imageComponents[i] = arguments[i];
  return;
}

ParameterizedImage.prototype.expandImage = function(p)
{
  var t = '';
  for (var i = 0; i < this.imageComponents.length; i++)
  {
    var component = this.imageComponents[i];
    if ((typeof component) == 'string')
      t += component;
    else
      t += removeBadIdChars((component < 0) ? p.sku : p.getAttributeValue(component)).toLowerCase();
  }
  return t;
}

ParameterizedImage.prototype.isEmptyImage = function()
{
  return (this.imageComponents.length == 0);
}

ParameterizedImage.prototype.getAttributeIndexes = function()
{
  var indexes = [];
  for (var i = 0; i < this.imageComponents.length; i++)
  {
    var component = this.imageComponents[i];
    if ((typeof component) != 'string')
      indexes.push(component);
  }
  return indexes;
}

/* ****************************** SubcatImageRotationData ****************************** */

function SubcatImageRotationData(intervalMillis, initialProductId)
{
  this.intervalMillis = intervalMillis;
  this.initialProductId = initialProductId;
  this.initFinished = false;
  this.parentDisplayInit = false;
  this.stoppedRotating = false;
  this.isScheduled = false;
}

SubcatImageRotationData.prototype.init = function(parentSubcat)
{
  this.parentSubcat = parentSubcat;
  this.currentIndex = -1;
  this.imageGridNums = [];
  this.imageList = [];
  this.captionList = [];
  if (parentSubcat.mainImage.isEmptyImage())
  {
    this.stopRotation();
    this.initFinished = true;
    return;
  }
  var attributeIndexes = parentSubcat.mainImage.getAttributeIndexes();
  for (var i = 0; i < parentSubcat.subcatProducts.length; i++)
  {
    var curProduct = parentSubcat.subcatProducts[i];
    var imageGridNum = 0;
    for (var j = 0; j < attributeIndexes.length; j++)
    {
      var attrIndex = attributeIndexes[j];
      if (attrIndex < 0)
        imageGridNum = imageGridNum * parentSubcat.subcatProducts.length + i;
      else
        imageGridNum = imageGridNum * parentSubcat.subcatAttributes[attrIndex].getNumValues() + curProduct.getValueIndex(attrIndex);
    }
    var matchingIndex = -1;
    for (var j = 0; j < this.imageGridNums.length; j++)
    {
      if (imageGridNum == this.imageGridNums[j])
      {
        matchingIndex = j;
        break;
      }
    }
    if (matchingIndex < 0)
    {
      matchingIndex = this.imageGridNums.length;
      var newCaption = '';
      for (var j = 0; j < attributeIndexes.length; j++)
      {
        var attrIndex = attributeIndexes[j];
        if (newCaption.length > 0)
          newCaption += ', ';
        if (attrIndex < 0)
          newCaption += 'PN: ' + curProduct.sku;
        else
          newCaption += parentSubcat.subcatAttributes[attrIndex].attributeName + ': ' + curProduct.getAttributeValue(attrIndex);
      }
      this.imageGridNums.push(imageGridNum);
      this.imageList.push(parentSubcat.mainImage.expandImage(curProduct));
      this.captionList.push(newCaption);
    }
    if (curProduct.productId == this.initialProductId)
      this.currentIndex = matchingIndex;
  }
  if (this.imageGridNums.length < 2)
    this.stopRotation();
  this.initFinished = true;
  return;
}

SubcatImageRotationData.prototype.isEnabled = function()
{
  return !this.stoppedRotating;
}

SubcatImageRotationData.prototype.stopRotation = function()
{
  this.stoppedRotating = true;
  var d = this.parentSubcat.getElementById('imageRotationDiv');
  if (d)
  {
    d.innerHTML = '';
    d.style.visibility = 'hidden';
  }
  return;
}

SubcatImageRotationData.prototype.imageLoaded = function()
{
  if (!this.isEnabled())
    return;
  this.scheduleRotation(this.intervalMillis);
  return;
}

SubcatImageRotationData.prototype.imageError = function()
{
  if (!this.isEnabled())
    return false;
  if (this.currentIndex >= 0)
  {
    for (var i = this.currentIndex + 1; i < this.imageGridNums.length; i++)
    {
      this.imageGridNums[i - 1] = this.imageGridNums[i];
      this.imageList[i - 1] = this.imageList[i];
      this.captionList[i - 1] = this.captionList[i];
    }
    this.imageGridNums.length--;
    this.imageList.length = this.imageGridNums.length;
    this.captionList.length = this.imageGridNums.length;
    this.currentIndex--;
  }
  return this.rotateImage();
}

SubcatImageRotationData.prototype.rotateImage = function()
{
  if (!this.isEnabled())
    return false;
  this.isScheduled = false;
  if (!this.initFinished || !this.parentDisplayInit)
  {
    this.scheduleRotation(this.intervalMillis >>> 3);
    return false;
  }
  if (this.currentIndex < 0)
    this.currentIndex = 0;
  else
    if (++this.currentIndex >= this.imageGridNums.length)
      this.currentIndex = 0;
  if (this.imageGridNums.length <= 0)
  {
    this.stopRotation();
    return false;
  }
  var d = document.images[this.parentSubcat.getDomName('MainImage')];
  if (d)
    d.src = this.imageList[this.currentIndex];
  this.displayCaption();
  if (this.imageGridNums.length == 1)
  {
    this.stopRotation();
    return true;
  }
  return true;
}

SubcatImageRotationData.prototype.scheduleRotation = function(numMillis)
{
  if (this.isScheduled)
    return;
  this.isScheduled = true;
  window.setTimeout(this.parentSubcat.jsName + '.imageRotationData.rotateImage()', numMillis);
  return;
}

SubcatImageRotationData.prototype.displayCaption = function()
{
  if (!this.isEnabled())
    return;
  var caption = (this.currentIndex < 0) ? this.parentSubcat.subcatName : this.captionList[this.currentIndex];
  var d = this.parentSubcat.getElementById('imageRotationDiv');
  if (d)
  {
    d.innerHTML = '<font color="#000099"><b>' + caption + '</b></font>';
    d.style.visibility = 'inherit';
  }
  return;
}

/* ****************************** GiftWrapInfo ****************************** */

function GiftWrapInfo(productId, priceCents, imageSrc)
{
  this.productId = productId;
  this.priceCents = priceCents;
  this.imageSrc = imageSrc;
}

/* ****************************** PriceTierRanges ****************************** */

function PriceTierRanges()
{
  this.data = [];
}

PriceTierRanges.prototype.getNumTiers = function()
{
  return (this.data.length / 3);
}

PriceTierRanges.prototype.getTierMinQty = function(tierIndex)
{
  return this.data[tierIndex * 3];
}

PriceTierRanges.prototype.getNextTierMinQty = function(tierIndex)
{
  if (++tierIndex == this.getNumTiers())
    return -1;
  return this.data[tierIndex * 3];
}

PriceTierRanges.prototype.getTierMinPrice = function(tierIndex)
{
  return this.data[(tierIndex * 3) + 1];
}

PriceTierRanges.prototype.getTierMaxPrice = function(tierIndex)
{
  return this.data[(tierIndex * 3) + 2];
}

PriceTierRanges.prototype.insertRawTierData = function(insertionIndex, minQty, minPrice, maxPrice)
{
  for (var i = this.data.length - 1; i >= (insertionIndex * 3); i--)
    this.data[i + 3] = this.data[i];
  this.setRawTierData(insertionIndex, minQty, minPrice, maxPrice);
  return;
}

PriceTierRanges.prototype.removeRawTierData = function(removalIndex)
{
  for (var i = (removalIndex + 1) * 3; i < this.data.length; i++)
    this.data[i - 3] = this.data[i];
  this.data.length -= 3;
  return;
}

PriceTierRanges.prototype.setRawTierData = function(tierIndex, minQty, minPrice, maxPrice)
{
  this.data[tierIndex * 3] = minQty;
  this.data[(tierIndex * 3) + 1] = minPrice;
  this.data[(tierIndex * 3) + 2] = maxPrice;
  return;
}

PriceTierRanges.prototype.consolidateTiers = function()
{
  for (var i = this.getNumTiers() - 2; i >= 0; i--)
  {
    if ((i + 1) == this.getNumTiers())
      continue;
    if ((this.getTierMinPrice(i) == this.getTierMinPrice(i + 1)) && (this.getTierMaxPrice(i) == this.getTierMaxPrice(i + 1)))
      this.removeRawTierData(i + 1);
  }
  return;
}

PriceTierRanges.prototype.combineWithProductTiers = function(product)
{
  if (product.hasTieredPricing())
  {
    for (var i = product.getTieredPricingCount() - 1; i >= 0; i--)
    {
      var newPriceCents = product.getTierPriceCents(i);
      if (newPriceCents < 0)
        newPriceCents = product.priceCents + newPriceCents;
      if (newPriceCents < 0)
        newPriceCents = 0;
      this.combineWithPriceRange(product.getTierMinimumQty(i), product.getNextTierMinimumQty(i), newPriceCents, newPriceCents);
    }
//    if (product.getTierMinimumQty(0) > 1)
//      this.combineWithPriceRange(1, product.getTierMinimumQty(0), product.priceCents, product.priceCents);
  }
  else
    this.combineWithPriceRange(1, -1, product.priceCents, product.priceCents);
  this.consolidateTiers();
  return;
}

PriceTierRanges.prototype.combineWithPriceRange = function(minQty, nextTierMinQty, minPrice, maxPrice)
{
  if (this.getNumTiers() == 0)
  {
    this.data.push(minQty);
    this.data.push(minPrice);
    this.data.push(maxPrice);
    return;
  }
  var firstAffectedTierIndex = -1;
  var lastAffectedTierIndex = -1;
  for (var i = this.getNumTiers() - 1; ((firstAffectedTierIndex < 0) || (lastAffectedTierIndex < 0)) && (i >= 0); i--)
  {
    var curMinQty = this.getTierMinQty(i);
    if (firstAffectedTierIndex < 0)
    {
      if (minQty >= curMinQty)
        firstAffectedTierIndex = i;
    }
    if (lastAffectedTierIndex < 0)
    {
      if (nextTierMinQty < 0)
        lastAffectedTierIndex = i;
      else
        if (nextTierMinQty > curMinQty)
          lastAffectedTierIndex = i;
    }
  }
  if (firstAffectedTierIndex < 0)
  {
    // Tier starts before the first tier
    this.insertRawTierData(0, minQty, minPrice, maxPrice);
    firstAffectedTierIndex++;
    lastAffectedTierIndex++;
  }
  for (var i = firstAffectedTierIndex; i <= lastAffectedTierIndex; i++)
  {
    var curMinQty = this.getTierMinQty(i);
    var curNextTierMinQty = this.getNextTierMinQty(i);
    var curMinPrice = this.getTierMinPrice(i);
    var curMaxPrice = this.getTierMaxPrice(i);
    // First check if the price range will be changed
    if ((curMinPrice <= minPrice) && (curMaxPrice >= maxPrice))
      continue;
    var newTierMinPrice = curMinPrice;
    var newTierMaxPrice = curMaxPrice;
    if (minPrice < newTierMinPrice)
      newTierMinPrice = minPrice;
    if (maxPrice > newTierMaxPrice)
      newTierMaxPrice = maxPrice;
    
    // Look for the different kinds of overlaps
    if (minQty > curMinQty)
    {
      // New tier starts within the current tier
      if ((nextTierMinQty > 0) && ((curNextTierMinQty < 0) || (nextTierMinQty < curNextTierMinQty)))
      {
        // New tier is contained within the current tier
        // Results in a three-way split
        this.insertRawTierData(++i, minQty, newTierMinPrice, newTierMaxPrice);
        this.insertRawTierData(++i, nextTierMinQty, curMinPrice, curMaxPrice);
      }
      else
      {
        // New tier starts within the current tier, and goes to the end of the current tier
        this.insertRawTierData(++i, minQty, newTierMinPrice, newTierMaxPrice);
      }
    }
    else
    {
      // New tier starts at or before the current tier
      if ((nextTierMinQty > 0) && ((curNextTierMinQty < 0) || (nextTierMinQty < curNextTierMinQty)))
      {
        // New tier is ends before the end of the current tier
        this.setRawTierData(i, curMinQty, newTierMinPrice, newTierMaxPrice);
        this.insertRawTierData(++i, nextTierMinQty, curMinPrice, curMaxPrice);
      }
      else
      {
        // New tier completely overlaps current tier
        this.setRawTierData(i, curMinQty, newTierMinPrice, newTierMaxPrice);
      }
    }
  }
  return;
}
