/*jslint devel: false, browser: true, undef: true, unparam: true, sloppy: true, vars: true, white: true, nomen: true, plusplus: true, maxerr: 50, indent: 4 */
/*global $, $$, Event, Tx, TxEventObserver, Store, SSTouchScrollContext, MHintContext, MPoint, SSData, MProductImpressionTracker, Control, History, Element, Effect */

////////////////
// MArray
////////////////

/**
    MArray wraps a plain javascript array (actually newObject() since it seems to have less junk stored inside it)
    and keeps track of how many elements are in the array.  The size is different from the length of the array since
    that indicates the number of slots in the array, which isn't real useful when there can be gaps in the array's.
    numbering.

    Iteration is done with perform(function) and performRange(from, to, function).  The perform(..) function send all
    non-null elements to the function paired with the corresponding index.  The performRange(..) function sends ALL
    elements (including null values) within then range provide.
*/

var SSPan = {};
SSPan.viewerContexts = [];
SSPan.imageUrlsCache = {};
SSPan.disclosureOpened = true;

function MArray()
{
    this.array = {};
    this.size = 0;
}

MArray.prototype.set = function(index, value)
{
    if (this.array[index] === undefined) {
        this.size++;
    }
    this.array[index] = value;
};

MArray.prototype.get = function(index)
{
    return this.array[index];
};

MArray.prototype.remove = function(index)
{
    if (this.array[index] !== undefined) {
        this.size--;
        delete this.array[index];
    }
};

MArray.prototype.performRange = function(fromIndex, toIndex, functionToPerform)
{
    var index;
    for (index = fromIndex; index < toIndex; index++) {
        if (functionToPerform(index, this.array[index]) === MArray.breakPerformMarker) {
            break;
        }
    }
};

MArray.prototype.perform = function(functionToPerform)
{
    var index;
    for (index in this.array) {
        if (this.array.hasOwnProperty(index)) {
            if (index < 0 || functionToPerform(index, this.array[index]) === MArray.breakPerformMarker) {
                break;
            }
        }
    }
};

MArray.breakPerformMarker = { breakMarker:1 };

////////////////
// MCell
////////////////
function MCell(viewerContext, cellOrigin, index)
{
    this.viewerContext = viewerContext;
    this.index = index;
    this.div = document.createElement('div');

    var divStyle = this.div.style;
    divStyle.width = viewerContext.cellSize.width + 'px';
    divStyle.height = viewerContext.cellSize.height + 'px';
    this.setOrigin(cellOrigin);
    divStyle.position = 'absolute';

    viewerContext.contentDiv.appendChild(this.div);
    this.cellImgSrc = null;
}

MCell.prototype.mdescription = "MCell class";

MCell.prototype.setOrigin = function(cellOrigin)
{
    var divStyle = this.div.style;
    divStyle.left = cellOrigin.x + 'px';
    divStyle.top = cellOrigin.y + 'px';
};

MCell.prototype.release = function()
{
    this.viewerContext.contentDiv.removeChild(this.div);
    // Bug #398 -- this removes the details popups from the document.body when
    // their corresponding cell/div is removed from the DOM.
    var productCellDiv = Tx.findChildWithClass(this.div, 'rawCell');
    var detailsDiv = this.viewerContext.detailsContext.getDetailsDiv(productCellDiv);
    if (detailsDiv !== null) {
        document.body.removeChild(detailsDiv);
    }
};

MCell.prototype.scratchDiv = document.createElement('div');

MCell.prototype.setInnerHTML = function(string)
{
    this.scratchDiv.innerHTML = string.trim();
    var newDomElement = Tx.findFirstNontextChild(this.scratchDiv);
    this.moveImagesAside(newDomElement);

    this.div.appendChild(newDomElement);
};

MCell.prototype.moveImagesAside = function(element)
{
    var images = element.getElementsByTagName("IMG");
    var imagesCount = images.length;
    var index;
    for (index = 0; index < imagesCount; index++) {
        var image = images[index];
        var imageSrc = image.src;
        image.setAttribute('imageSrc', imageSrc);
        image.src = this.viewerContext.cleardotUrl;
    }
    this.requiresImgSwizzle = true;
};

MCell.prototype.swizzleImg = function()
{
    var index;
    if (this.requiresImgSwizzle) {
        var images = this.div.getElementsByTagName("IMG");
        var imagesCount = images.length;
        for (index = 0; index < imagesCount; index++) {
            var image = images[index];
            image.src = image.getAttribute('imageSrc');
        }
        this.requiresImgSwizzle = false;
    }
};

MCell.prototype.setChild = function(productCell)
{
    this.div.appendChild(productCell);
};

MCell.prototype.adjustTwoLineDivHeight = function()
{
    if (Tx.isIE6()) {
        var twoLineDiv = Tx.findChildTagWithId(this.div, 'DIV', 'twoLineDiv');
        if (twoLineDiv !== null) {
            twoLineDiv.style.height = "";
        }
    }
};

// Returns this cell's rawCell div
MCell.prototype.getCellDomElement = function()
{
    return Tx.findChildWithClass(this.div, 'rawCell');

};

//////////////////
// MViewerContext
//////////////////
function MViewerContext(viewerDiv, contentDiv, rawCellsUrl, cellSizeFunction, cellCount, detailsContext, isEmbedded,
                        minRowCount, maxRowCount, hintsEnabled, alwaysHint, conservativeFaulting, trackScrollOffsetInAnchor,
                        drawLeftBorder, sidebarDiv, cleardotUrl, widthOffset, sponsoredCount, mayHideArrows, useWindowWidth, tracksCTPositions)
{
    this.viewerName = viewerDiv.id;
    this.detailsContext = detailsContext;
    this.rawCellsUrl = rawCellsUrl;
    this.cellCount = cellCount;
    this.isEmbedded = isEmbedded;
    this.minRowCount = minRowCount;
    this.maxRowCount = maxRowCount;
    this.conservativeFaulting = conservativeFaulting;
    this.sidebarDiv = sidebarDiv;
    this.trackScrollOffsetInAnchor = trackScrollOffsetInAnchor;
    this.cleardotUrl = cleardotUrl;

    this.viewerDiv = viewerDiv;
    viewerDiv.mcontext = this;      // ptr from DOM to context - do this for all components?
    this.contentDiv = contentDiv;
    // This must be set in javascript for scrolling to work -- putting in stylesheet has no effect, aparently.
    // See bug 1165: this must be position:relative for the absTop/Left functions to work so that this div is in the offsetParent chain.
    this.contentDiv.style.position = 'relative';
    //this.contentDiv.style.border = '1px solid blue';

    this.cellSizeFunction = cellSizeFunction;
    this.cellSize = cellSizeFunction();
    this.cells = new MArray();
    this.animateInterval = null;
    this.widthOffset = widthOffset;
    this.useWindowWidth = useWindowWidth;
    this.tracksCTPositions = tracksCTPositions;
    this.sponsoredCount = sponsoredCount;
    this.mayHideArrows = mayHideArrows;

    // pagination display
    this.pageNumberDiv = $(this.viewerName+"PageNumber");
    this.pageCountDiv = $(this.viewerName+"PageCount");

    // scroll buttons
    this.scrollArrows = $(this.viewerName+"ScrollArrows");
    this.topScrollLeft = $(this.viewerName+"TopScrollLeft");
    this.topScrollRight = $(this.viewerName+"TopScrollRight");
    var viewerContext = this;
    this.topScrollLeft.onclick = function() {
        return viewerContext.scrollHorizontal(false);
    };
    this.topScrollRight.onclick = function() {
        return viewerContext.scrollHorizontal(true);
    };

    this.isLeftScrollEnabled = true;
    this.isRightScrollEnabled = true;

    var sliderContext = new MSliderContext(this);
    this.sliderContext = sliderContext.handle1 ? sliderContext : null;
    if (Tx.isTouch() && !this.isEmbedded) {
        this.touchScroll = new SSTouchScrollContext(this);
    }
    this.observers = new TxEventObserver();

    // Get our geometry initially set up - note postInit hackery below too
    this.handleResize(false, false);

    // add this viewerContext to the global list of active MViewerContexts
    // see defintion of window.onresize below for more on this.
    SSPan.viewerContexts.push(this);
    if (!document.focusedViewerContext) {
        document.focusedViewerContext = this;
    }

    if (!this.isEmbedded) {
        var mouseScrollEventName = (Tx.isIE() || Tx.isSafari() || Tx.isChrome()) ? "mousewheel" : "DOMMouseScroll";
        Event.observe(this.contentDiv, mouseScrollEventName, this.handleMouseWheel.bindAsEventListener(this), false);
    }
    this.keyPressHandler = this.handleKeyPress.bindAsEventListener(this);
    Event.observe(document.documentElement, "keydown", this.keyPressHandler, false);

    this.faultCellsTimeoutId = null;
    this.faultImagesTimeoutID = null;
    this.positionPreloadedCells();
    this.contentDiv.style.width = '100%';
    var productViewerWrapper = $(this.viewerName+'ProductViewerWrapper');
    if (!this.isEmbedded && drawLeftBorder) {
         productViewerWrapper.style.borderLeft = '1px ' + Store.borderColor + ' solid';
    }
    this.updateScrollButtonState();
    this.hintContext = (hintsEnabled && !isEmbedded) ? new MHintContext("hintBubble", alwaysHint, this) : null;

    if (this.tracksCTPositions) {
        document.detailsContext.observers.addObserver("show", this.fixupDetailsProductLinks.bind(this));
    }
}

MViewerContext.prototype.mdescription = "MViewerContext class";

/**
    The onresize function for the window must actually invoke the handleResize()
    method for each of the viewerContexts that are instantiated.  We store an array
    of the active MViewerContexts on the document and iterate through this array
    invoking the handleResize() function for each viewerContext.
*/
SSPan.onresize = function()
{
    var viewerContextCount = SSPan.viewerContexts.length;
    var index;
    for (index = 0; index < viewerContextCount; index++) {
        var currentViewerContext = SSPan.viewerContexts[index];
        currentViewerContext.handleResize(true, true);
    }
};

SSPan.addObserver = function(eventName, observer)
{
    var index;
    for (index = 0; index < SSPan.viewerContexts.length; index++) {
        SSPan.viewerContexts[index].observers.addObserver(eventName, observer);
    }
};

Tx.addEventListenerToElement(window, 'resize', SSPan.onresize, false);
if (Tx.isTouch()) {
    Tx.addEventListenerToElement(window, 'orientationchange', SSPan.onresize, false);
}

SSPan.initViewer = function(viewerName, rawCellsUrl, totalCellCount, isEmbedded, cellSizeFunction, minRowCount, maxRowCount,
                        hintsEnabled, alwaysHint, conservativeFaulting, trackScrollOffsetInAnchor, drawLeftBorder,
                        cleardotUrl, widthOffset, sponsoredCount, mayHideArrows, useWindowWidth, tracksCTPositions, startHidden)
{
    var viewerDiv = $(viewerName);
    var contentDiv = $(viewerName+'ContentDiv');
    if (startHidden) {
        contentDiv.style.visibility = 'hidden';
    }
    contentDiv.style.overflow = 'hidden';
    contentDiv.style.width = '0px';
    var cellSize = cellSizeFunction();
    var cellHeight = cellSize.height;
    var numRows = SSPan.computeNumRows(contentDiv, cellHeight, isEmbedded, minRowCount, maxRowCount);
    contentDiv.style.height = (numRows * cellHeight) + "px";

    // Note: document.totalCellCount is defined in an inline <script> tag in CellBrowser.html
    var cellCount = totalCellCount === undefined ? 100 : totalCellCount;
    var detailsContext = document.detailsContext;

    var sidebarDiv = null;
    var sidebarContainer = Tx.findChildWithId(viewerDiv, 'SidebarDiv');
    if (sidebarContainer) {
        sidebarDiv = Tx.findFirstNontextChild(sidebarContainer);
    }

    var viewerContext = new MViewerContext(viewerDiv, contentDiv, rawCellsUrl, cellSizeFunction, cellCount, detailsContext,
                                           isEmbedded, minRowCount, maxRowCount, hintsEnabled, alwaysHint, conservativeFaulting,
                                           trackScrollOffsetInAnchor, drawLeftBorder, sidebarDiv, cleardotUrl, widthOffset,
                                           sponsoredCount, mayHideArrows, useWindowWidth, tracksCTPositions);
    // Note: should pass this flag to MDetailsContext constructor
    detailsContext._isSmallWidget = isEmbedded && cellHeight === Store.smallProductCellSize().height;
    if (Tx.isIE6()) {
        viewerContext.cells.perform(function(index, cell) {
            cell.adjustTwoLineDivHeight();
        });
    }

    if (Tx.isIE()) {
        // See bug 983
        window.setTimeout(viewerContext.postInitIE.bind(viewerContext), 1);
    } else {
        // See bugs 453 and 1036.  We're doing a second handleResize, the first being
        // in the context constructor.  The first resize may have caused a vert scroller
        // to appear, so we may need to resize again to get rid of the horiz scroller.
        window.setTimeout(viewerContext.postInitBody.bind(viewerContext), 500);
    }

    return viewerContext;
};

MViewerContext.prototype.postInitIE = function()
{
    if (Tx.windowInnerWidth() === 0 || this.contentDiv.offsetWidth === 0) {
        // Not ready to calc final geometry yet
        if (this.postInitInvocationCount === undefined) {
            this.postInitInvocationCount = 0;
        }

        this.postInitInvocationCount++;

        // Max of 5 seconds before we just go for it.
        if (this.postInitInvocationCount < 50) {
            window.setTimeout(this.postInitIE.bind(this), 100);
            return;
        }
    }

    this.postInitBody();
};

MViewerContext.prototype.postInitBody = function()
{
    this.handleResize(false, true);
    this.contentDiv.style.visibility = 'visible';
    this.scrollArrows.style.visibility = (this.mayHideArrows && this.pageCount === 1) ? 'hidden' : 'visible';

    this.updateScrollPositionFromAnchor();
    this.observers.notify("init");
};

MViewerContext.prototype.positionPreloadedCells = function()
{
    // must first extract the children into separate array so
    // we're not modifying the childNodes as we process this loop.
    var children = $(this.contentDiv).select('.rawCell');
    var childrenLength = children.length;
    var cellIndex = 0;
    var childIndex;
    for (childIndex = 0; childIndex < childrenLength; childIndex++) {
        var child = children[childIndex];
        /*
        var img = $(child).down('.centerCellImg');
        if (img) {
            $(img).setStyle({visibility: 'hidden'});
        }
        */
        var cellOrigin = this.cellOriginForIndex(cellIndex);
        var newCell = new MCell(this, cellOrigin, cellIndex);
        this.cells.set(cellIndex, newCell);
        this.contentDiv.removeChild(child);
        newCell.setChild(child);
        Store.fixAjaxProductLinks(child);
        if (this.tracksCTPositions) {
            this._wrapProductLinks(child, child, null);
        }
        cellIndex++;
    }
};

// Wrap a function around the onlick of any product links that will record a set
// of params describing the row, col, etc of the click.  If cellDiv is passed, we'll
// figure out the row, col, etc.  If an explicitPosition is passed, we'll just use
// that string (used for the look sidear).
MViewerContext.prototype._wrapProductLinks = function(linkHolder, cellDiv, explicitPosition)
{
    var targets = linkHolder.select(".productLink");
    var i;
    for (i = targets.length-1; i >= 0; i--) {
        MViewerContext._wrapProductLink(this, targets[i], cellDiv, explicitPosition);
    }
};

// needs to be a separate function so we have a new context for our closures (vs being in the caller)
// http://stackoverflow.com/questions/643542/doesnt-javascript-support-closures-with-local-variables
MViewerContext._wrapProductLink = function(viewerContext, target, cellDiv, explicitPosition)
{
    var oldOnclick = target.onclick;
    if (oldOnclick && oldOnclick.isOurWrapper === undefined) {
        oldOnclick = oldOnclick.bindAsEventListener(target);
        target.onclick = function(currEvent) {
            // We tag the div with the extra params we'll need in openRetailerWindow().
            // Note "this" is the element getting clicked, in this func.
            if (cellDiv) {
                this.extraParams = viewerContext._getGeometryParams(cellDiv);
            } else {
                this.extraParams = "ctl=" + explicitPosition;
            }
            var result = oldOnclick(currEvent);
            this.extraParams = null;
            return result;
        };
        target.onclick.isOurWrapper = true;
    }
};

// returns a query param string with some position info about the cellDiv (row, col, etc)
MViewerContext.prototype._getGeometryParams = function(cellDiv)
{
    var cellSize = this.cellSize;
    // the cellDiv's parent is a "fault" wrapper div, which is the one that is actually positioned.
    var styleValue = cellDiv.parentNode.style.left;

    // col index, counting from the very leftmost product of the whole result set
    var absoluteCol = styleValue.substring(0, styleValue.length-2) / cellSize.width;
    // col index of first cell that is at least 80% visible visible
    var firstColShowing = Math.floor(this.contentDiv.scrollLeft / cellSize.width + 0.8);
    // visible column index - might even be -1 if they clicked a visible sliver at left
    var col = absoluteCol - firstColShowing;

    styleValue = cellDiv.parentNode.style.top;
    var row = styleValue.substring(0, styleValue.length-2) / cellSize.height;
    // note, these are all 1-based indexes
    return "ctr=" + (row+1) + "&ctc=" + (col+1) + "&ctn=" + this.numRows + "&ctp=" + this.getPageCountLabel();
};

// XXX move to MSliderContext section below
MSliderContext.prototype._resetTrackWidth = function()
{
    var minTrackWidth = 27;
    var maxTrackWidth = 220;

    var outerTrackVisibility = 'hidden';
    var maxCols = 1 + this.viewerContext.indexToCol(this.viewerContext.cellCount - 1);
    var trackWidth =  (minTrackWidth * maxCols) / this.viewerContext.numCols;
    if (trackWidth > minTrackWidth) {
        if (trackWidth > maxTrackWidth) {
            trackWidth = maxTrackWidth;
        }
        outerTrackVisibility = 'visible';
    }
    this.sliderContainer.style.visibility = outerTrackVisibility;
    // Bug #571 -- IE7 requires a background with some color for events to be recognized
    this.slider.track.style.width = trackWidth + "px";
    this.slider.trackLength = trackWidth;

    this._thinTrack.style.width = (trackWidth + 4) + "px";
    // Bug #571 -- IE7 requires a background with some color for events to be recognized
    this._trackBackground.style.width = this._outerTrack.style.width;
    // In IE, the handleLength isn't available until after the onload, so we must reset it here.
    this.slider.handleLength = parseInt(this.slider.handles[0].offsetWidth, 10);
};

MViewerContext.prototype._positionDummyDiv = function()
{
    // stretch out the div to full width
    if (!this.dummyDiv) {
        this.dummyDiv = document.createElement('div');
        this.dummyDiv.innerHTML = "&nbsp;";
        var divStyle = this.dummyDiv.style;
        divStyle.position = 'absolute';
        this.contentDiv.appendChild(this.dummyDiv);
    }
    this.dummyDiv.style.left = (this.indexToCol(this.cellCount) * this.cellSize.width) + 'px';
};

MViewerContext.prototype._updateScrollButtonState = function(topButton, direction, enabled)
{
    var oldClass;
    var newClass;
    var size = "";
    if (topButton.className.indexOf("Small") > 0) {
      size = "Small";
    }
    if (enabled) {
      oldClass = 'shopstyle_cellBrowser' + direction + 'Arrow' + size + 'Disabled';
      newClass = 'shopstyle_cellBrowser' + direction + 'Arrow' + size;
    }
    else {
      oldClass = 'shopstyle_cellBrowser' + direction + 'Arrow' + size;
      newClass = 'shopstyle_cellBrowser' + direction + 'Arrow' + size + 'Disabled';
    }

    $(topButton).removeClassName(oldClass);
    $(topButton).addClassName(newClass);
};

MViewerContext.prototype.updateScrollButtonState = function()
{
    var scrollLeft = this.contentDiv.scrollLeft;
    if (scrollLeft <= 0) {
        // make sure left arrows are disabled
        if (this.isLeftScrollEnabled) {
            this._updateScrollButtonState(this.topScrollLeft, 'Left', false);
            this.isLeftScrollEnabled = false;
        }
    }
    else if (!this.isLeftScrollEnabled) {
        // make sure left arrows are enabled
        this._updateScrollButtonState(this.topScrollLeft, 'Left', true);
        this.isLeftScrollEnabled = true;
    }

    if (scrollLeft >= this.maxScrollLeft) {
        // make sure right arrows are disabled
        if (this.isRightScrollEnabled) {
        this._updateScrollButtonState(this.topScrollRight, 'Right', false);
            this.isRightScrollEnabled = false;
        }
    }
    else if (!this.isRightScrollEnabled) {
        // make sure right arrows are enabled
        this._updateScrollButtonState(this.topScrollRight, 'Right', true);
        this.isRightScrollEnabled = true;
    }
    this.updatePaginationDisplay();
};

MViewerContext.prototype.canScroll = function()
{
    return this.isLeftScrollEnabled || this.isRightScrollEnabled;
};

// Returns the page count show to the user
MViewerContext.prototype.getPageCountLabel = function()
{
    if (this.contentDiv.scrollLeft >= this.maxScrollLeft) {
        return this.pageCount;
    } else {
        // Sort of quirky, because a scroll of just 1px beyond the initial state causes us to report
        // page 2.  The good side is that because of the look sidebar we often don't scroll a whole
        // page width on the first page scroll, and this definition of the page label means that
        // we'll always change the page count on the first page scroll, even if there were a sidebar.
        var firstColShowing = Math.floor(this.contentDiv.scrollLeft / this.cellSize.width);
        return Math.ceil((firstColShowing / this.numCols)) + 1;
    }
};

MViewerContext.prototype.updatePaginationDisplay = function()
{
    this.pageNumberDiv.innerHTML = this.getPageCountLabel();
};

// Actually move our content div by the given delta
MViewerContext.prototype.scrollBy = function(deltaX, deltaY)
{
    var scrollLeftOrig = this.contentDiv.scrollLeft;
    this.setScrollLeft(this.contentDiv.scrollLeft + deltaX);
    var didScroll = scrollLeftOrig !== this.contentDiv.scrollLeft;
    return didScroll;
};

MViewerContext.prototype.setScrollLeft = function(scrollLeft)
{
    this.contentDiv.scrollLeft = scrollLeft;
    document.focusedViewerContext = this;
};

MViewerContext.prototype.updateAnchor = function()
{
    if (this.trackScrollOffsetInAnchor &&
            window.location.pathname.startsWith('/browse')  /* tracking on product pages doesn't work */
            ) {
        var scrollLeft = this.contentDiv.scrollLeft;
        var minCol = Math.floor(scrollLeft / this.cellSize.width);
        var upperLeftCellIndex = this.numRows * minCol;
        var offset = Math.floor(scrollLeft) - minCol * this.cellSize.width;

        var newAnchor;
        if (upperLeftCellIndex >= 0 || offset >= 0) {
            newAnchor = upperLeftCellIndex + "_" + offset;
        }
        else {  // don't want this even for 0_0 as browser scrolls to top of page on blank anchor
            newAnchor = "";
        }

        if (document.browseContext && document.browseContext.ajaxEnabled && History.emulated.pushState) {
            // only need this for IE/html4, otherwise keep using anchor
            History.replaceState(null, null, Tx.replaceParam(History.getState().url, "pos", newAnchor));
        }
        else {
            Tx.setLocationAnchor(newAnchor);
        }
    }
};

MViewerContext.prototype.updateScrollPositionFromAnchor = function()
{
    if (this.trackScrollOffsetInAnchor) {
        var anchor = window.location.hash;
        if (Tx.getParameterFromUrl("pos", anchor)) {    // html4 pos encoded in anchor
            anchor = "#" + Tx.getParameterFromUrl("pos", anchor);
        }
        if (anchor && anchor.length > 1) {
            var scrollInfo = anchor.substring(1).split("_");
            if (scrollInfo.length === 2) {
                var leftCellIndex = parseInt(scrollInfo[0], 10);
                var offset = parseInt(scrollInfo[1], 10);

                if (leftCellIndex > 0 || offset > 0) {
                    return this._updateScrollPosition(leftCellIndex, offset);
                }
            }
        }
    }
    return false;
};

MViewerContext.prototype._updateScrollPosition = function(leftCellIndex, offset)
{
    var scrollLeft;
    var resetAnchor = false;

    // We respect the scroll position religiously UNLESS its a resize where we can fit all of the content on screen.
    if (this.totalContentWidth <= this.contentDiv.clientWidth) {
        scrollLeft = 0;
        // We must reset the anchor since we are essentially overriding the saved scroll pos.
        // Otherwise when the window shrinks again we might unexpectedly bounce them back to a scrolled
        // position
        resetAnchor = true;
    }
    else {
        scrollLeft = Math.floor(leftCellIndex / this.numRows) * this.cellSize.width + offset;
    }

    this.setScrollPosition(scrollLeft);

    if (this.sliderContext !== null) {
        var sliderValue = (scrollLeft - this.sliderContext.sliderControlOffset) / this.sliderContext.sliderControlWidth;
        this.sliderContext.positionSlider(sliderValue);
    }
    if (resetAnchor) {
        this.updateAnchor();
    }
    return true;
};

// The following functions named with '_' prefix are here to serve the "scrollHorizontal" function.

MViewerContext.prototype._clearAnimateInterval = function()
{
    // cancel any currently operating window.setInterval functions.
    if (this.animateInterval) {
        // need to add code here to 'complete' the current scrolling.
        window.clearInterval(this.animateInterval);
        this.animateInterval = null;
        this.cancelFaultCellsTimeout();
        this.cancelFaultImagesTimeout();
        this.scheduleFaultCells(150);
        this.scheduleFaultImages(150);
        this.updateScrollButtonState();
        // Update the slider with the new position
        if (this.sliderContext !== null && this.sliderContext.isSpringySlider) {
            var sliderValue = (this.contentDiv.scrollLeft - this.sliderContext.sliderControlOffset) / this.sliderContext.sliderControlWidth;
            this.sliderContext.positionSlider(sliderValue);
        }
        this.updateAnchor();
    }
};

MViewerContext.prototype._getNumColsRespectingSidebar = function()
{
    if (this.sidebarDiv && this.sidebarDiv.parentNode.style.visibility !== "hidden") {
        // We may move one or two fewer items if there is a sidebar showing.  We only do this if going right, since
        // we assume the sidebar closes on scroll.
        return this.computeNumCols(this.getMaxWidth() - this.sidebarDiv.parentNode.clientWidth, this.cellSize.width);
    } else {
        return this.numCols;
    }
};

/**
    The targetScrollLeft is meaningful while animating the scrolling.
    When scrolling is complete, contentDiv.scrollLeft and targetScrollLeft will be the same.
    This exists because when a user double clicks on the scroll arrow, we need to update the
    targetScrollLeft with a new value so that no matter which thread/request end up handling the scrolling
    it will end up in the right place.
*/
MViewerContext.prototype._updateTargetScrollLeft = function(isRight)
{
    var numColsToMove = isRight ? this._getNumColsRespectingSidebar() : this.numCols;
    var deltaScrollLeft = numColsToMove * this.cellSize.width;
    var initialScrollLeft = this.targetScrollLeft;
    var desiredScrollLeft;
    if (isRight) {
        desiredScrollLeft = initialScrollLeft + deltaScrollLeft;
        this.targetScrollLeft = Math.min(this.maxScrollLeft, desiredScrollLeft);
    }
    else {
        desiredScrollLeft = initialScrollLeft - deltaScrollLeft;
        this.targetScrollLeft = Math.max(0, desiredScrollLeft);
    }
    this.targetScrollDistance = initialScrollLeft - this.targetScrollLeft;
};

MViewerContext.prototype.computeStepSize = function(isRight)
{
    if (!this.isEmbedded && !Tx.isTouch()) {
        var currentTime = (new Date()).getTime();
        var deltaTime = currentTime - this.animationStartTime;
        var desiredDeltaPosition = this.desiredSpeed * deltaTime;
        var previousDeltaPosition = Math.abs(this.contentDiv.scrollLeft - this.animationStartPosition);
        var stepSize = Math.min(this.cellSize.width * 0.5, desiredDeltaPosition - previousDeltaPosition);
        var weightFactor = 8;
        this.scrollStepSize = ((weightFactor * this.scrollStepSize) + stepSize) / (weightFactor + 1);
    }
    var scrollStepSize = isRight ? this.scrollStepSize : -this.scrollStepSize;
    return scrollStepSize;
};

MViewerContext.prototype._animateScrollStep = function(isRight)
{
    var didScroll = false;
    var displacement =  this.targetScrollLeft - this.contentDiv.scrollLeft;
    if (displacement !== 0) {
        var currentStepSize = this.computeStepSize(isRight);
        currentStepSize = isRight ? Math.min(displacement, currentStepSize) : Math.max(displacement, currentStepSize);
        didScroll = this.scrollBy(currentStepSize, 0);
    }

    if (!didScroll) {
        // we did no scrolling, that's the signal that the animation is done
        this.targetScrollLeft = this.contentDiv.scrollLeft;
        this._clearAnimateInterval();
        this.notifyScrollListeners();
    }
};

MViewerContext.prototype._scrollHorizontal = function(isRight)
{
    if ((!isRight && !this.isLeftScrollEnabled) || (isRight && !this.isRightScrollEnabled)) {
        return;
    }

    var delayMillis = 32;
    this._clearAnimateInterval();

    if (this.sliderContext !== null) {
        var sliderValue = this.sliderContext.slider.value;
        var delta = this.sliderContext.isSpringySlider ? (1 / this.sliderContext.sliderControlPageWidth) : (1 / (this.fractionalPageCount - 1));
        this.sliderContext.prepositionSlider(isRight, isRight ? sliderValue + delta : sliderValue - delta);
    }

    // Compute the new targetScrollLeft which is where we want the content view to
    // be when this scrolling stops.  We store this on the viewerContext (as targetScrollLeft)
    // so that, even if this is interrupted by another click, we can pick up where we left off.
    // When scrolling is complete, the actual scrollLeft and the targetScrollLeft will be the same.

    this._updateTargetScrollLeft(isRight);
    var viewerContext = this;
    // creating a function here captures the current environment so isRight, etc are accessible
    // from within the function when called from window.setInterval.
    var windowIntervalFunction = function() {
        viewerContext._animateScrollStep(isRight);
    };
    this.animationStartTime = (new Date()).getTime();
    this.animationStartPosition = this.contentDiv.scrollLeft;
    // windowSetInterval calls a function repeatedly, with a fixed time delay between each call to that function
    viewerContext.animateInterval = window.setInterval(windowIntervalFunction, delayMillis);

    this.didScroll();

    this.emitAnalyticsPageScroll(isRight);

    return false;
};

MViewerContext.prototype.scrollHorizontal = function(isRight)
{
    return this._scrollHorizontal(isRight);
};

MViewerContext.prototype.getNumRows = function()
{
    return this.numRows;
};

MViewerContext.prototype.colRowToIndex = function(col, row)
{
    return col * this.numRows + row;
};

// We generally use the 'traditional' ShopStyle column major ordering, except when
// we have some sponsored products being promoted to the tops of some columns, in
// which case we go column major for those first N rows.
MViewerContext.prototype.indexToCol = function(index)
{
    if (index >= this.sponsoredCount * this.numRows) {
        return Math.floor(index / this.numRows);
    } else {
        return index % this.sponsoredCount;
    }
};

MViewerContext.prototype.indexToRow = function(index)
{
    if (index >= this.sponsoredCount * this.numRows) {
        return index % this.numRows;
    } else {
        return Math.floor(index / this.sponsoredCount);
    }
};

MViewerContext.prototype.cellOriginForIndex = function(index)
{
    var cellSize = this.cellSize;
    var x = this.indexToCol(index) * cellSize.width;
    var y = this.indexToRow(index) * cellSize.height;
    return new MPoint(x, y);
};

MViewerContext.prototype._pruneCells = function(startIndex)
{
    var cells = this.cells;
    if (cells.size > this.pruneThreshold || startIndex === 0) {
        var minPrune = Math.max(0, startIndex - this.pruneWindowSize);
        var maxPrune = startIndex + this.pruneWindowSize;
        var pruneFunction = function(index, cell) {
            if (index < minPrune || index > maxPrune) {
                cell.release();
                cells.remove(index);
            }
        };
        cells.perform(pruneFunction);
    }
};

MViewerContext.prototype.discardAndReload = function()
{
    this.detailsContext.hideCurrentDetails();
    this.discardAllCells();
    this.reload();
};

MViewerContext.prototype.discardAllCells = function()
{
    var cells = this.cells;
    cells.perform(function(index, cell) {
        cell.release();
        cells.remove(index);
    });
};

MViewerContext.prototype.getFirstVisibleCellIndex = function()
{
    var cellSize = this.cellSize;
    var scrollLeft = this.contentDiv.scrollLeft;
    var firstCol = Math.floor(scrollLeft / cellSize.width);
    return this.colRowToIndex(firstCol, 0);
};

MViewerContext.prototype.getLastVisibleCellIndex = function()
{
    var cellSize = this.cellSize;
    var numRows = this.numRows;

    var numCols = this._getNumColsRespectingSidebar();
    var scrollLeft = this.contentDiv.scrollLeft;
    var firstCol = Math.floor(scrollLeft / cellSize.width);
    var endIndex = this.colRowToIndex(firstCol + numCols - 1, numRows - 1);
    return Math.min(this.cellCount - 1, endIndex);
};

MViewerContext.prototype._fetchCells = function(expandSide)
{
    var cellSize = this.cellSize;
    var numRows = this.numRows;
    var numCols = this.numCols;
    var contentDiv = this.contentDiv;

    var scrollLeft = contentDiv.scrollLeft;
    var minCol = Math.floor(scrollLeft / cellSize.width);
    var numPages = this.useConservativeFaulting() ? 1 : 2;
    var maxCol = minCol + (numPages * this.visibleCols) - 1;

    var minRow = 0;
    var maxRow = numRows - 1;
    if (expandSide === -1) {
        maxCol = minCol;
        minCol -= numCols;
    }
    if (expandSide === 1) {
        minCol += numCols;
        maxCol += (3 * this.visibleCols);
    }
    minCol = Math.max(0, minCol);
    minRow = Math.max(0, minRow);

    var startIndex = this.colRowToIndex(minCol, minRow);
    var endIndex = this.colRowToIndex(maxCol, maxRow);

    endIndex = Math.min(this.cellCount - 1, endIndex);
    startIndex = Math.max(0, startIndex);

    var indicesToFault = null;
    var addFaultIndicesFunction = function(cellIndex, cell) {
        if (cell === undefined) {
            if (indicesToFault === null) {
                indicesToFault = [];
            }
            indicesToFault.push(cellIndex);
        }
    };
    this.cells.performRange(startIndex, endIndex + 1, addFaultIndicesFunction);
    if (indicesToFault === null) {
        return;
    }

    this._pruneCells(startIndex);
    if (indicesToFault.length > 0) {
        var cellFaults = [];
        var minIndex = 1000000;
        var index;
        for (index = 0; index < indicesToFault.length; index++) {
            var faultIndex = indicesToFault[index];
            var cellOrigin = this.cellOriginForIndex(faultIndex);
            var newCell = new MCell(this, cellOrigin, faultIndex);
            this.cells.set(faultIndex, newCell);
            cellFaults.push(newCell);
            if (faultIndex < minIndex) {
                minIndex = faultIndex;
            }
        }
        if (cellFaults.length > 0) {
            // Bug #780 -- reset the cookie for each request as its possible for the
            // user to have multiple windows open at different sizes and each window
            // needs to update the cookie before each request rather than on resize (since
            // resize may not actually occur, but there will be different sizes for each request).
            if (!this.isEmbedded) {
                Tx.resetWindowSizeCookie(SSData.cookieDomain);
            }
            var url = this.rawCellsUrl;
            url += '&min=' + minIndex + '&count=' + indicesToFault.length;

            var extraParam = MProductImpressionTracker.getImpressionsQueryString();
            if (extraParam) {
                url += '&' + extraParam;
            }

            var viewerContext = this;
            var options = {
                method: 'get',
                onSuccess: function (xmlhttp) {
                    var text = Tx.getResponseText(xmlhttp);
                    if (!Tx.handleAjaxExceptionPage(text)) {
                        viewerContext.parseTextIntoCellFaults(viewerContext, cellFaults, text);
                        viewerContext.scheduleFaultImages(200);
                        viewerContext.observers.notify("fault");
                    }
                }
            };
            Tx.ajax(url, options);
        }
    }
};

MViewerContext.prototype.useConservativeFaulting = function()
{
    return this.conservativeFaulting && this.contentDiv.scrollLeft === 0;
};

// fetch any cells we need, schedule additional propsective faulting
MViewerContext.prototype._faultCells = function(expandSide)
{
    this._fetchCells(expandSide);
    this.cancelFaultCellsTimeout();
    if (!this.useConservativeFaulting()) {
        var viewerContext = this;
        if (expandSide === 0 && !this.isEmbedded) {
            var faultRightSideCellsFunction = function() {
                viewerContext._faultCells(1);
            };
            // Keeping this short makes it more likely that the rawProducts that results will carry the
            // first batch of impressions data back (vs us needing a dedicated request).
            this.faultExtraCellsTimeoutId = window.setTimeout(faultRightSideCellsFunction, 1500);
        }
        else if (expandSide === 1) {
            var faultLeftSideCellsFunction = function() {
                viewerContext._faultCells(-1);
            };
            this.faultExtraCellsTimeoutId = window.setTimeout(faultLeftSideCellsFunction, 4000);
        }
    }
};

MViewerContext.prototype.scheduleFaultCells = function(delay)
{
    if (this.faultCellsTimeoutId === null) {
        var viewerContext = this;
        var faultCellsFunction = function() {
            viewerContext._faultCells(0);
        };
        this.faultCellsTimeoutId = window.setTimeout(faultCellsFunction, delay);
    }
};

MViewerContext.prototype.cancelFaultCellsTimeout = function()
{
    if (this.faultCellsTimeoutId !== null) {
        window.clearTimeout(this.faultCellsTimeoutId);
        this.faultCellsTimeoutId = null;
    }
    this.cancelFaultExtraCellsTimeout();
};

MViewerContext.prototype.cancelFaultExtraCellsTimeout = function()
{
    if (this.faultExtraCellsTimeoutId !== null) {
        window.clearTimeout(this.faultExtraCellsTimeoutId);
        this.faultExtraCellsTimeoutId = null;
    }
};

MViewerContext.prototype.faultImages = function()
{
    var contentDiv = this.contentDiv;
    var cellSize = this.cellSize;
    var scrollLeft = contentDiv.scrollLeft;
    var minCol = Math.floor(scrollLeft / cellSize.width);
    var firstCellIndex = minCol * this.numRows;
    firstCellIndex = Math.max(0, firstCellIndex);
    var visibleCellCount = this.visibleCols * this.numRows;
    // In the embedded case, we only want to load the images
    // for the cells which are actually visible.  Once the
    // user scrolls, the other images wil be loaded.
    var useConservativeFaulting = this.useConservativeFaulting();
    var numPages = useConservativeFaulting ? 1 : 2;
    var lastCellIndex = Math.floor(firstCellIndex + (numPages * visibleCellCount));
    lastCellIndex = Math.min(lastCellIndex, this.cellCount);

    var viewerContext = this;
    var swizzleCellImageFunction = function(index, cell) {
        if (cell && viewerContext.faultImagesTimeoutID === null) {
            cell.swizzleImg();
        }
    };
    this.cells.performRange(firstCellIndex, lastCellIndex, swizzleCellImageFunction);
    if (!useConservativeFaulting) {
        this.cells.performRange(Math.max(0, firstCellIndex - visibleCellCount), firstCellIndex, swizzleCellImageFunction);
    }
};

MViewerContext.prototype.cancelFaultImagesTimeout = function()
{
    if (this.faultImagesTimeoutID !== null) {
        window.clearTimeout(this.faultImagesTimeoutID);
        this.faultImagesTimeoutID = null;
    }
};

MViewerContext.prototype.scheduleFaultImages = function(delay)
{
    this.cancelFaultImagesTimeout();
    var viewerContext = this;
    var faultImagesFunction = function () {
        viewerContext.faultImagesTimeoutID = null;
        viewerContext.faultImages();
    };
    this.faultImagesTimeoutID = window.setTimeout(faultImagesFunction, delay);
};

MViewerContext.prototype.setOnDidScroll = function(handler)
{
    this.onDidScroll = handler;
};

MViewerContext.prototype.didScroll = function()
{
    if (this.hintContext !== null) {
        this.hintContext.didScroll();
    }

    if (this.onDidScroll) {
        this.onDidScroll(this);
    }
};

MViewerContext.prototype.notifyScrollListeners = function()
{
    var lastScrollLeft = this.lastScrollLeft || 0;
    var scrollDistance = Math.abs(lastScrollLeft - this.contentDiv.scrollLeft);
    var isBigScroll = scrollDistance > (this.contentDiv.clientWidth * 0.50);

    this.observers.notify("scroll", isBigScroll);

    if (isBigScroll) {
        this.lastScrollLeft = this.contentDiv.scrollLeft;
    }
};

MViewerContext.prototype.parseTextIntoCellFaults = function(viewerContext, cellFaults, text)
{
    var previousIndex = 0;
    var cellIndex = 0;
    var index = 0;
    // This must be kept in sync with identical string in RawProducts.java
    var marker = '<!-- @-@-@-@-@-@-@-@ -->';
    var markerLength = marker.length;
    while ((index = text.indexOf(marker, previousIndex)) >= 0) {
        var part = text.substring(previousIndex, index);
        var cell = cellFaults[cellIndex];
        if (cell) {
            cell.setInnerHTML(part);
            if (viewerContext.tracksCTPositions) {
                var rawCellDiv = $(cell.div).down(".rawCell");
                viewerContext._wrapProductLinks(rawCellDiv, rawCellDiv, null);
                /*
                var img = $(rawCellDiv).down('.centerCellImg');
                if (img) {
                    $(img).setStyle({visibility: 'hidden'});
                }
                */
            }
        }
        previousIndex = index + markerLength;
        cellIndex++;
    }
};

SSPan.computeNumRows = function(contentDiv, cellHeight, isEmbedded, minRowCount, maxRowCount)
{
    // Bug #758 -- Unfortunately, the getAbsTop(...) returns different values for IE at different
    // times during the early processing of a page (Safari and FF don't seem to do this).
    // If we're close to a boundary where we might display 3 rows, this will sometimes result in
    // three rows being displayed and then when the absTop changes, it will go back to two rows.
    // To avoid this, I empirically determined that 115 px is a good number for IE (as well as FF and Safari).
    // So I hard-coded it with the Tx.isIE() conditional, but decided that it would be better to have it
    // fail in all browsers if someone changes something that makes 115 invalid.
    // The hard-coded 44 comes from Mike (for his embedded browser extensions).
    //var absTopContentDiv = Tx.isIE() ? 115 : getAbsTop(contentDiv);
    var absTopContentDiv = isEmbedded ? 44 : Store.topPadForRowsCalc;
    var maxHeight = Tx.windowSizeForCookie() - absTopContentDiv - 5;
    // 0.06 fudge is so we try a little harder to fit more rows in, even it you lose a tiny slice of the last one
    // var rowCount = Math.max(minRowCount, Math.floor(0.06 + maxHeight / cellHeight))
    var rowCount = Math.max(minRowCount, Math.floor(maxHeight / cellHeight));
    return maxRowCount < minRowCount ? rowCount : Math.min(rowCount, maxRowCount);
};

MViewerContext.prototype.computeNumCols = function(totalWidth, cellWidth)
{
    // 0.1 fudge is so we don't reshow the same col for the sake of a tiny slice - Seeing 90% is good enough.
    var minCols = this.isEmbedded ? 1 : 2;
    return Math.max(minCols, Math.floor(0.1 + totalWidth / cellWidth));
};

MViewerContext.prototype.getMaxWidth = function()
{
    var maxWidth;
    if (this.useWindowWidth) {
        // document.documentElement.scrollWidth vs Tx.windowInnerWidth would include the
        // entire doc width (perhaps wider than the window when scrolling).  To make use
        // of that, we need to get rid of setting our own width in the resize callout
        // and just get sized like any other div, otherwise our width never shrinks.
        maxWidth = Tx.windowInnerWidth() - SSPan.getAbsLeft(this.contentDiv) - this.widthOffset;
        // Negative widthOffset means set a fixed width.
        if (this.widthOffset < 0) {
            maxWidth = - this.widthOffset;
        }

        if (Tx.isFirefox() || Tx.isSafari() || Tx.isChrome()) {
            //see bug 445
            maxWidth -= 1;
        }
    }
    else {
        // simply use the width of our containing div
        maxWidth = $(this.viewerName).getWidth();
    }

    return maxWidth;
};

// Called upon window resize, also once as the page is getting loaded, once the geometry is stable.
MViewerContext.prototype.handleResize = function(isWindowResize, doReload)
{
    var cellSize = this.cellSizeFunction();
    var didCellSizeChange = cellSize.height !== this.cellSize.height;

    // Remove need for r=1 redirect. If the height of the cell from the server
    // is too far off the height calculated by the client, presume the cookie
    // is out of date and refetch all cells from server.
    if (!didCellSizeChange) {
        var firstRawCell = Tx.findChildWithClass(this.viewerDiv, 'rawCell');
        if (firstRawCell !== null) {
            var productCell = Tx.findFirstNontextChild(firstRawCell);
            if (productCell !== null) {
                if (Math.abs(productCell.offsetHeight - cellSize.height) > 30) {
                    didCellSizeChange = true;
                }
            }
        }
    }
    if (didCellSizeChange) {
        this.discardAllCells();
    }
    this.cellSize = cellSize;
    var maxWidth = this.getMaxWidth();

    this.numRows = SSPan.computeNumRows(this.contentDiv, this.cellSize.height, this.isEmbedded, this.minRowCount, this.maxRowCount);
    // Number of fully visible cols (or at least almost fully visible) that we are managing.  Determines page-scroll quantity.
    this.numCols = this.computeNumCols(maxWidth, this.cellSize.width);
    // Number of visible cols, even if you can only see a tiny bit of the last cells.
    this.visibleCols = Math.ceil(this.contentDiv.clientWidth / this.cellSize.width);
   if (this.visibleCols > 15) {
        // sanity, in case browser is reporting whacky size
        this.visibleCols = 15;
    }

    this.pruneWindowSize = (Tx.isIE6() ? 7 : 30) * this.numRows * this.numCols;
    this.pruneThreshold = 2 * this.pruneWindowSize;

    // desiredSpeed is in units of pixels per millisecond (one screen width per 750 millis)
    this.desiredSpeed = (this.visibleCols * this.cellSize.width) / 750;
    this.targetScrollLeft = 0;
    this.scrollStepSize = Tx.isIE() ? 93 : 46; // 185/5 == 37  (each cell is 185 pixels wide)
    if (this.isEmbedded) {
        this.scrollStepSize = 26;
    }
    else if (Tx.isTouch()) {
        this.scrollStepSize = 120;
    }

    // total number of pages we have to show
    this.pageCount = Math.ceil(this.cellCount / (this.numCols * this.numRows));
    this.pageCountDiv.innerHTML = this.pageCount;
    this.scrollArrows.style.visibility = (this.mayHideArrows && this.pageCount === 1) ? 'hidden' : 'visible';

    this.fractionalPageCount = (this.indexToCol(this.cellCount - 1) + 1) / this.numCols;

    // avoid resize in widget to resolve bugs 797, 809, 811, 916
    // don't resize if width is negative (could happen if we size by our container, and the container isn't findable) - IE6 croaks
    if (!this.isEmbedded && maxWidth > 0) {
        this.contentDiv.style.width = maxWidth + 'px';
    }
    var height = this.numRows * this.cellSize.height;
    this.contentDiv.style.height = height + 'px';

    // maxScrollLeft depends on numCols -- if that changes, need to recalc
    this.totalContentWidth = (this.indexToCol(this.cellCount - 1) + 1) * this.cellSize.width;
    this.maxScrollLeft = Math.max(0, this.totalContentWidth - this.contentDiv.clientWidth);
    if (this.sliderContext !== null) {
        this.sliderContext.handleResize();
    }

    if (isWindowResize && this.updateScrollPositionFromAnchor()) {
        this.repositionCells();
    }
    else {
        this.contentDiv.scrollLeft = 0;
        this.targetScrollLeft = 0;
        if (doReload) {
            this.reload();
        }
        this.updateScrollButtonState();
    }

    this._positionDummyDiv();

    SSPan.doSidebarClipping(this.sidebarDiv, this.contentDiv);

    if (isWindowResize) {
        // notify resize Observers
        this.observers.notify("resize");
    }
};

SSPan.imageLoaded = function(img)
{
    // mark the image as loaded (because ssCentered will be defined), but not yet centered
    img.ssCentered = false;
    // note that some browsers may call the onload multiple times - and the first onload may give us an image or html structure that is bad
    // then a subsequent load will mark the image as not centered, again, and we'll center with proper image
};

// the window interval to run image centering forever
SSPan.centerInterval = null;

// center all product images on the page (that are marked to be centered)
SSPan.beginCentering = function()
{
    if (SSPan.centerInterval === null) {
        SSPan.centerInterval = window.setInterval(function() {SSPan.centerImages(0);}, 100);
    }
};

SSPan.centerImages = function(count)
{
    this.centeringTimeout = null;
    var images = $$('.centerCellImg');
    var imageDone = false;

    var centeredCount = 0;
    images.each(function(img){
        // if img.ssCentered is undefined, then the image isn't loaded yet
        // if it is false then the image has been loaded (or reloaded) but not yet centered
        if (img.ssCentered !== undefined && !img.ssCentered) {
            // loaded but not centered
            imageDone = false;
            var dim = img.getDimensions();
            if (dim.width > 1) {
                var parent = img.up('div');
                if (parent) {
                    SSPan._centerImage(parent, img);
                    centeredCount++;
                }
            }
        }

        if (centeredCount > 12) {
            // allow the browser to display this much DOM
            // we'll do the rest on the next iteration
            return;
        }
    });
};

// internal work for centering an image inside a parent
SSPan._centerImage = function(parent, img)
{
    var divSize = $(parent).getDimensions();

    var divWidth = divSize.width;
    var divHeight = divSize.height;
    img.ssCentered = true;
    var imgSize = img.getDimensions();
    var left = (divWidth-imgSize.width)/2;
    $(img).setStyle({left: (left+'px'), top: ((divHeight-imgSize.height)/2+'px'), margin: 0, visibility: 'visible'});
};

SSPan.doSidebarClipping = function(sidebarDiv, contentDiv)
{
    if (!sidebarDiv) {
        return;
    }

    // This div clips what we can see of the sidebar, which may be arbitrarily long
    var sidebarContainer = sidebarDiv.parentNode;
    if (sidebarContainer.style.overflow === "scroll" || sidebarContainer.style.overflowY === "scroll") {
        // the parent allows scrolling, so we don't clip
        return;
    }

    if (Tx.isIE6()) {
        // Hack out of left field to size the sidebar to the bottom of the content area.
        sidebarContainer.style.height = (contentDiv.offsetHeight + 16 - sidebarContainer.offsetTop) + 'px';
    }

    var firstPieceToHide = null;
    var containerLimit = sidebarContainer.offsetHeight;
    //trace("containerLimit =  " + containerLimit);

    // cycle through the child elements of the sidebar from the top on down
    var child = sidebarDiv.firstChild;
    var firstPiece = child;
    while (true) {
        // if child is a separator...
        if (!child || (child.id && child.id.endsWith("separator"))) {
            // ...reveal all DIVs above the separator
            //trace("found sep " + child)
            while (firstPiece !== child) {
                if (firstPiece.tagName === "DIV") {
                    firstPiece.style.visibility = 'inherit';
                    //trace("showing " + firstPiece)
                }
                firstPiece = firstPiece.nextSibling;
            }
            // firstPiece is now == child, who is the start of a new block of content
        } else {
            // if child not separator but is a DIV and hangs off bottom of sidebar, mark as first piece to hide and get out of loop
            //trace('testing div at = ' + (child.offsetTop + child.offsetHeight));
            if (child.tagName === "DIV" && child.offsetTop + child.offsetHeight > containerLimit) {
                firstPieceToHide = firstPiece;
                //trace('start hiding at = ' + (child.offsetTop + child.offsetHeight));
                break;
            }
        }

        // advance to next sibling (to iterate the loop)
        if (child) {
            child = child.nextSibling;
        } else {
            break;
        }
    }

    // hide the DIV that hangs off the sidebar, plus every DIV below it
    while (firstPieceToHide) {
        if (firstPieceToHide.tagName === "DIV") {
            firstPieceToHide.style.visibility = 'hidden';
        }
        firstPieceToHide = firstPieceToHide.nextSibling;
    }
};

MViewerContext.prototype.repositionCells = function()
{
    var viewerContext = this;
    var repositionFunction = function(index, cell) {
        var cellOrigin = viewerContext.cellOriginForIndex(cell.index);
        cell.setOrigin(cellOrigin);
    };
    this.cells.perform(repositionFunction);
};

MViewerContext.prototype.reload = function()
{
    this.cancelFaultCellsTimeout();
    // prune ALL cells (0 forces all cells to be pruned)
    this._pruneCells(0);
    this.repositionCells();
    this.scheduleFaultCells(100);
};

// XXX move to MSliderContext section below
MSliderContext.prototype._updateFromSlider = function(sliderValue)
{
    this.viewerContext.detailsContext.hideCurrentDetails();
    var scrollPosition = this.sliderControlOffset + Math.ceil(this.sliderControlWidth * sliderValue);
    this.viewerContext.setScrollPosition(scrollPosition);
};

MViewerContext.prototype.setScrollPosition = function(scrollPosition)
{
    this.setScrollLeft(scrollPosition);
    this.targetScrollLeft = scrollPosition;
    this.updateScrollButtonState();
    this.scheduleFaultCells(300);
    this.scheduleFaultImages(300);
};

MViewerContext.prototype.emitAnalyticsPageScroll = function(isRight)
{
    if (Store.isAjaxTrackingEnabled()) {
        var doEmit = function() {
            if (this.animateInterval) {
                //trace('omni DELAYED');
                this.emitAnalyticsPageScroll(isRight);
            } else {
                //trace('omni send');
                Store.emitOmnitureAjaxPacket(isRight ? this.topScrollRight : this.topScrollLeft, "Product Page Scroll");
            }
        };
        doEmit.bind(this);
        //trace('omni schedule');

        // This causes a huge torrent of events to be sent to Omniture.
        // Commenting it out to make it harder to turn on by accident
        // window.setTimeout(doEmit, 5000);
    }
};

MViewerContext.prototype.fixupDetailsProductLinks = function()
{
    // When details div is coming up, tweak the product links to send us position stats
    var detailsFetcherDiv = document.detailsContext._currentDetailsFetcherDiv;
    var detailsDiv = document.detailsContext.getDetailsDiv(detailsFetcherDiv);
    var rawCellDiv = $(detailsFetcherDiv).up('.rawCell');
    if (rawCellDiv) {
        this._wrapProductLinks(detailsDiv, rawCellDiv, null);
    } else if (this.sidebarDiv && $(detailsFetcherDiv).descendantOf(this.sidebarDiv)) {
        this._wrapProductLinks(detailsDiv, null, "sidebar");
    }
};

// XXX move to MSliderContext section below
// Called when the mouseup happens on the slider (or for a single tick of the scrollwheel)
MSliderContext.prototype.handleSliderChanged = function(sliderValue)
{
    this.cancelAnimateSliderInterval();
    // We must decouple these statements with a setTimeout so that we can see the results of the prepositionSlider call.
    var sliderContext = this;
    var updateFunction = function() {
        sliderContext._updateFromSlider(sliderValue);
        sliderContext.positionSlider(sliderValue);
        sliderContext.handleSliderChangedTimeoutId = null;
        sliderContext.viewerContext.updateAnchor();
    };
    if (this.handleSliderChangedTimeoutId === null) {
        var isRight = sliderValue > this.previousSliderValue || sliderValue === 1.0;
        this.previousSliderValue = sliderValue;
        this.prepositionSlider(isRight, sliderValue);
        this.handleSliderChangedTimeoutId = window.setTimeout(updateFunction, 30);
    }
    this.viewerContext.didScroll();
    this.viewerContext.notifyScrollListeners();

    // This causes a huge torrent of events to be sent to Omniture.
    // Commenting it out to make it harder to turn on by accident
    //if (Store.isAjaxTrackingEnabled()) {
    //    Store.emitOmnitureAjaxPacket(this.handle1, "Product Slider Scroll");
    //}
};

// XXX move to MSliderContext section below
// Called when the mousedrag happens on the slider
MSliderContext.prototype.handleSliderMoved = function(sliderValue, slider)
{
    this.cancelAnimateSliderInterval();
    if (this.isSpringySlider && sliderValue >= 0.999) {
        this.animateSliderAtExtreme(slider, sliderValue, true);
    }
    // Need to keep track of direction of slider/mouse movement and use 0.01 here rather than 0.00.
    else if (this.isSpringySlider && sliderValue <= 0.001) {
        this.animateSliderAtExtreme(slider, sliderValue, false);
    }
    else {
        this._updateFromSlider(sliderValue);
    }
};

//////////////////
// MSliderContext
//////////////////

function MSliderContext(viewerContext)
{
    this.viewerContext = viewerContext;
    this.sliderContainer = $(this.viewerContext.viewerName+'SliderContainer');
    this.handle1 = $(this.viewerContext.viewerName+'Handle1');
    if (this.handle1 === null) {
        // If the slider isn't present, bail out.
        return;
    }
    this._outerTrack = $(this.viewerContext.viewerName+'Track1');
    this._thinTrack = $(this.viewerContext.viewerName+'Track2');
    this.slider = new Control.Slider(this.handle1, this._outerTrack, {
        onChange: function(sliderValue) { viewerContext.sliderContext.handleSliderChanged(sliderValue); },
        onSlide: function(sliderValue, slider) { viewerContext.sliderContext.handleSliderMoved(sliderValue, slider); }
    });
    // Bug #571 -- IE7 requires a background with some color for events to be recognized
    this._trackBackground = $(this.viewerContext.viewerName+'TrackBackground');

    this.sliderControlPageWidth = 10;
    this.sliderSpringBackLowEnd = 0.15;
    this.sliderSpringBackHighEnd = 1 - this.sliderSpringBackLowEnd;
    this.previousSliderValue = 0;
    this.handleSliderChangedTimeoutId = null;
}

MSliderContext.prototype.handleResize = function()
{
    // Slider control values are used to limit sensitivity of the slider -- only show 20 pages at a time.
    // sliderControlOffset is the amount of scrollLeft to add to the amount of scroll determined by the slider position.
    // sliderControlWidth is  numPagesInScollChunk * pixel-width of each page.
    this.sliderControlOffset = 0;
    this.sliderControlWidth = Math.min(this.viewerContext.maxScrollLeft, (this.sliderControlPageWidth * this.viewerContext.numCols * this.viewerContext.cellSize.width));
    this.sliderControlOffsetMax = this.viewerContext.totalContentWidth - this.sliderControlWidth;
    this.isSpringySlider = this.sliderControlWidth < this.viewerContext.maxScrollLeft;
    this._resetTrackWidth();
    this.positionSlider(0);
};

MSliderContext.prototype.setSliderControlOffset = function(value)
{
    this.sliderControlOffset = Math.max(0, Math.min(this.sliderControlOffsetMax, value));
};

MSliderContext.prototype.positionSlider = function(sliderValue)
{
    if (this.isSpringySlider) {
        var sliderControlOffset;
        if (sliderValue >= this.sliderSpringBackHighEnd &&
          (this.viewerContext.contentDiv.scrollLeft < (this.viewerContext.maxScrollLeft - (this.sliderSpringBackLowEnd * this.sliderControlWidth)))) {
            sliderControlOffset = this.sliderControlOffset + ((sliderValue - this.sliderSpringBackHighEnd) * this.sliderControlWidth);
            this.setSliderControlOffset(sliderControlOffset);
            sliderValue = this.sliderSpringBackHighEnd;
        }
        else if (sliderValue <= this.sliderSpringBackLowEnd && (this.sliderControlOffset > 0)) {
            sliderControlOffset = this.sliderControlOffset + ((sliderValue - this.sliderSpringBackLowEnd) * this.sliderControlWidth);
            this.setSliderControlOffset(sliderControlOffset);
            if (sliderControlOffset > 0) {
                sliderValue = this.sliderSpringBackLowEnd;
            }
        }
    }
    sliderValue = Math.max(Math.min(sliderValue, 1.0), 0);
    this.slider.value = sliderValue;
    this.slider.handles[0].style.left =  this.slider.translateToPx(sliderValue);
};

MSliderContext.prototype.prepositionSlider = function(isRight, sliderValue)
{
    sliderValue = isRight ? Math.min(sliderValue, 1) : Math.max(sliderValue, 0);
    if (!this.isSpringySlider) {
        this.slider.value = sliderValue;
    }
    this.slider.handles[0].style.left = this.slider.translateToPx(sliderValue);
};

MSliderContext.prototype.cancelAnimateSliderInterval = function()
{
    if (this.animateSliderIntervalId) {
        window.clearTimeout(this.animateSliderIntervalId);
        this.animateSliderIntervalId = null;
    }
};

MSliderContext.prototype.stepSliderAtExtreme = function(sliderValue, currentStepSize)
{
    this.setSliderControlOffset(this.sliderControlOffset + currentStepSize);
    this._updateFromSlider(sliderValue);
};

MSliderContext.prototype.animateSliderAtExtreme = function(slider, sliderValue, isRight)
{
    var initialScrollZone = 20;
    var currentStepSize = this.viewerContext.scrollStepSize / 8;
    var eventPointerX = Event.pointerX(slider.event);
    var handleX = Tx.absoluteLeft(this.handle1);
    var deltaX = eventPointerX - handleX;
    if (isRight && deltaX <= initialScrollZone) {
        deltaX = initialScrollZone;
    }
    else if (!isRight && deltaX >= -initialScrollZone) {
        deltaX = -initialScrollZone;
    }
    var ratio = deltaX  / initialScrollZone;
    currentStepSize *= ratio;

    var sliderContext = this;
    var animateSliderFunction = function() {
        sliderContext.stepSliderAtExtreme(sliderValue, currentStepSize);
        var scrollLeft = sliderContext.viewerContext.contentDiv.scrollLeft;
        if (scrollLeft === 0 || scrollLeft >= sliderContext.viewerContext.maxScrollLeft) {
            sliderContext.cancelAnimateSliderInterval();
        }
    };
    this.animateSliderIntervalId = window.setInterval(animateSliderFunction, 20);
};

function SSTouchScrollContext(viewerContext)
{
    this.viewerContext = viewerContext;
    this.contentDiv = $(this.viewerContext.contentDiv);
    // identifier of our particular touch event stream
    this.touchIdentifier = null;
    // array of all identifiers associated with the gesture, so we can tell when gesture is done
    this.gestureIdentifiers = [];
    this.isGesture = false;
    this.beginTimer = null;
    this.savedStartEvent = null;

    this.moveBind = this.move.bind(this);
    this.endBind = this.end.bind(this);

    this.touchStart = 0;
    this.scrollStart = 0;
    this.didScroll = false;

    this.contentDiv.observe('touchstart', this.start.bind(this));
}

// some touch has started
SSTouchScrollContext.prototype.start = function(event)
{
    // if our beginTimer is running, then this is another start soon after the first - let all through
    // if the touchstart has multiple changedTouches - this is a multitouch & let it through
    // if we have a touchidentifier, then this is a touch that started after we started handling onefinger - ignore it
    // otherwise, this is a brand new start of a touch, which might be a single touch, or a gesture
    this.didScroll = false;

    if (this.gestureIdentifiers.length === 0) {
        // beginning of some gesture - install our event observers
        $(this.contentDiv).observe('touchmove', this.moveBind);
        $(this.contentDiv).observe('touchend', this.endBind);
    }

    if (this.beginTimer !== null || this.isGesture || event.touches.length > 1) {
        // this whole touch sequence is a multi-touch gesture - we want all the events to go through
        // deliver the saved start event, if any left
        if (this.savedStartEvent !== null) {
            this.savedStartEvent.target.fireEvent("touchstart", this.savedStartEvent);
            this.savedStartEvent = null;
        }
        this.isGesture = true;
        if (this.beginTimer !== null) {
            window.clearTimeout(this.beginTimer);
            this.beginTimer = null;
        }
        var ii;
        for (ii=0; ii<event.changedTouches.length; ii++) {
            this.gestureIdentifiers[this.gestureIdentifiers.length] = event.changedTouches[ii].identifier;
        }
    }
    else if (this.touchIdentifier !== null) {
        // late start of another touch - ignore
        Event.stop(event);
        this.gestureIdentifiers[this.gestureIdentifiers.length] = event.changedTouches[0].identifier;
    }
    else {
        this.touchIdentifier = event.changedTouches[0].identifier;
        // brand new touch
        var context = this;
        this.beginTimer = window.setTimeout(function() {context.oneFingerStart(event);}, 350);
        this.gestureIdentifiers[0] = this.touchIdentifier;
        // note that we let event through, because this might be the beginning of a gesture

        this.savedStartEvent = event;
        Event.stop(event);

    }
};

// beginning of a one-finger touch that we handle
SSTouchScrollContext.prototype.oneFingerStart = function(event)
{
    this.beginTimer = null;
    this.savedStartEvent = null;

    // the scroll at the beginning of the touch
    this.scrollStart = this.contentDiv.scrollLeft;
    // where we started touching
    this.touchStart = event.changedTouches[0].pageX;
    //trace("ssts start " + this.touchStart + " " + this.scrollStart);

};

SSTouchScrollContext.prototype.move = function(event)
{
    if (this.beginTimer !== null) {
        // moves happening before timer up.  clear timer and treat as non-gesture
        window.clearTimeout(this.beginTimer);
        this.oneFingerStart(event);
    }
    if (!this.isGesture) {
        Event.stop(event);

        // scroll
        var change = event.changedTouches[0].pageX - this.touchStart;
        //trace("ssts move " + event.touches[0].pageX + " " + change);
        if (this.didScroll || change > 3 || change < 3) {
            this.didScroll = true;
        }
        else {
            // haven't moved much, don't scroll yet (may be letting touchend through)
            return;
        }
        this.viewerContext.detailsContext.hideCurrentDetails();
        this.viewerContext.setScrollPosition(this.scrollStart - change);
        this.viewerContext.didScroll();
    }
};

SSTouchScrollContext.prototype.end = function(event)
{
    if (this.beginTimer !== null) {
        // end happening before timer up.  clear timer and treat as non-gesture
        window.clearTimeout(this.beginTimer);
        this.beginTimer = null;
        this.savedStartEvent = null;
    }
    if (!this.isGesture) {
        if (event !== null) {
            Event.stop(event);
        }
        this.touchIdentifier = null;
    }

    // remove any touch sequences that are ending now
    var ii;
    for (ii=0; ii<event.changedTouches.length; ii++) {
        var ct = event.changedTouches[ii];
        this.gestureIdentifiers = this.gestureIdentifiers.without(ct.identifier);
    }

    if (this.gestureIdentifiers.length === 0) {
        // we're all done
        $(this.contentDiv).stopObserving('touchmove', this.moveBind);
        $(this.contentDiv).stopObserving('touchend', this.endBind);
        this.isGesture = false;
        this.touchIdentifier = null;
    }

    this.didScroll = false;
};

/*
* Orginal: http://adomas.org/javascript-mouse-wheel/
* prototype extension by "Frank Monnerjahn" themonnie @gmail.com
*/
Object.extend(Event, {
    wheel:function (event){
        var delta = 0;
        if (!event) {
            event = window.event;
        }
        if (event.wheelDelta) {
                delta = event.wheelDelta;
                if (window.opera) {
                    delta = -delta;
                }
                if (Tx.isSafari()) {
                    delta *= 0.1;
                }
                else if (Tx.isIE()) {
                    delta *= 0.25;
                }
        }
        else if (event.detail) {
            delta = -event.detail * 12;
        }
        return Math.round(delta); //Safari Round
    }
});

MViewerContext.prototype.handleMouseWheel = function(event)
{
    var delta = Event.wheel(event);
    if ((delta > 0 && this.isLeftScrollEnabled) || (delta < 0 && this.isRightScrollEnabled)) {
        this.detailsContext.hideCurrentDetails();
        this.detailsContext._extraDelay = 3000;
        if (this.sliderContext !== null) {
            var sliderValueDelta = (3.0 * delta) / this.sliderContext.sliderControlWidth;
            var newSliderValue = this.sliderContext.slider.value - sliderValueDelta;
            newSliderValue = (newSliderValue < 0) ? 0 : (newSliderValue > 1 ? 1 : newSliderValue);
            this.sliderContext.handleSliderChanged(newSliderValue);
        }
        else {
            if (Tx.isIE()) {
                delta *= 2;
            }
            var scrollPosition = this.contentDiv.scrollLeft - delta;
            this.setScrollPosition(scrollPosition);
        }
    }
    Event.stop(event);
};

MViewerContext.prototype._handleKeyPress = function(event, isRight)
{
    this.detailsContext.hideCurrentDetails();
    this.detailsContext._extraDelay = 3000;
    this._scrollHorizontal(isRight);
    Event.stop(event);
};

MViewerContext.prototype.handleKeyPress = function(event)
{
    var targetTagName = Event.element(event).tagName;
    if (targetTagName !== 'INPUT' && targetTagName !== 'TEXTAREA' && document.focusedViewerContext === this) {
        if (event.keyCode === Event.KEY_RIGHT) {
            this._handleKeyPress(event, true);
        }
        else if (event.keyCode === Event.KEY_LEFT) {
            this._handleKeyPress(event, false);
        }
    }
};

MViewerContext.prototype.prepareToClose = function()
{
    document.focusedViewerContext = null;
    this.detailsContext.closeDetailsPopup(true);

    if (this.keyPressHandler) {
        Event.stopObserving(document.documentElement, "keydown", this.keyPressHandler);
    }
};

//////////////////////
// Util
//////////////////////

// similar, but not the same as, Tx.absoluteLeft
SSPan.getAbsLeft = function(o)
{
    var iY = 0;
    while(o.offsetParent){
        iY += o.offsetLeft;
        o = o.offsetParent;
    }
    return iY;
};

//similar, but not the same as, Tx.absoluteTop
SSPan.getAbsTop = function(o)
{
    var iX = 0;
    while(o.offsetParent){
        iX += o.offsetTop;
        o = o.offsetParent;
    }
    return iX;
};

/////////////////////
// Image Url Caching
/////////////////////

SSPan.getStoreImageUrl = function(imageElement, imageName, useFilter)
{
    var imageUrl = SSPan.imageUrlsCache[imageName];
    if (!imageUrl) {
        var prototypeImageUrl = imageElement.src;
        var imageUrlPrefix = prototypeImageUrl.substring(0, prototypeImageUrl.lastIndexOf('/') + 1);

        imageUrl = imageUrlPrefix + imageName;
        if (useFilter) {
            imageUrl = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + imageUrl + "', sizingMethod='scale')";
        }
        SSPan.imageUrlsCache[imageName] = imageUrl;
    }
    return imageUrl;
};

///////////////////////
// Disclosure Triangles
///////////////////////

function MDisclosure(button, target, opened)
{
    this.observer = new TxEventObserver();
    button.onclick = this.buttonClicked.bindAsEventListener(this);
    if (button.tagName === "IMG") {
        this.triangleImage = button;
    } else {
        this.triangleImage = button.getElementsByTagName("IMG")[0];
    }
    this.targetDiv = target;
    this.setState(opened);
    this.didManualGesture = false;
}

MDisclosure.prototype.buttonClicked = function(event)
{
    var currentState = (this.triangleImage.src.match(/disclose-down\.gif$/) !== null);
    this.setState(!currentState);
    if (!this.didManualGesture) {
        Store.emitOmnitureAjaxPacket(this.triangleImage, currentState ? "Sidebar Closed" : "Sidebar Opened");
        this.didManualGesture = true;
    }
    SSPan.disclosureOpened = !currentState;
};

MDisclosure.prototype.setState = function(opened)
{
    if (!opened) {
        this.triangleImage.src = SSPan.getStoreImageUrl(this.triangleImage, 'disclose-right.gif', false);
        this.targetDiv.style.visibility = "hidden";
    } else {
        this.triangleImage.src = SSPan.getStoreImageUrl(this.triangleImage, 'disclose-down.gif', false);
        this.targetDiv.style.visibility = "inherit";
    }
    this.observer.notify('stateChanged', this);
};

MDisclosure.prototype.isOpen = function()
{
    return this.targetDiv.style.visibility !== "hidden";
};

SSPan.initDisclosure = function(startsOpen)
{
    var button = $('CrossLinksSidebarButton');
    var body = $('SidebarDiv');
    if (button && body) {
        var disclosure = new MDisclosure(button, body, startsOpen && SSPan.disclosureOpened !== false);
        button.disclosure = disclosure; // so we can get at this later
        var scrollHandler = function(context) {
            disclosure.setState(false);
        };
        if ($('browsePage1')) {
            $('browsePage1').mcontext.setOnDidScroll(scrollHandler);
        }
    }
};

// MBrowseContext

SSPan.update = function(link)
{
    if (document.browseContext) {
        return document.browseContext.updateContent(link.href);
    }
    return true;
};

function MBrowseContext(ajaxEnabled)
{
    this.ajaxEnabled = ajaxEnabled;
    this.currentQuery = Tx.encodeUriIfNeeded(window.location.pathname + window.location.search);
    this.pendingUrl = null;
    this.viewerContext = null;  // these two set by CellBrowser
    this.pageTitle = document.title;

    this.updateContainer = "browserContent";
    this.fadeContainer = "browserContentCell";
    this.observers = new TxEventObserver();
    this.bindToBrowserHistoryChanges();
}

MBrowseContext.prototype.updateContent = function(url)
{
    if (!this.ajaxEnabled) {
        return true;
    }
    if (!window.location.pathname.startsWith('/browse')) {
        // product pages don't get ajax filters, as they don't refresh properly on back/fwd
        return true;
    }

    url = this.prepareLink(url); // need to include /browse for history URL

    if (url !== Tx.removeParam(this.currentQuery, 'pos')) {    // clicking current category is a no-op
        var fadingBackground = Tx.createCoverDiv(null, 20, true);
        $(this.fadeContainer).appendChild(fadingBackground);
        new Effect.Opacity(fadingBackground, {duration: 0.2, from:0.0, to:0.5});

        // History.js method uses html5 history mechanism or falls back to html4 with hash.
        // Let the statechange callback in BrowsePage call updateFromHistory.
        History.pushState(null, null, url);
    }

    return false;
};

MBrowseContext.prototype.updateFromHistory = function(state)
{
    var url = this.prepareLink(state.url);
    var context = this;
    var options = {
        method: 'get',
        onSuccess: function(xmlhttp) {
            // Make sure this response matches the most recent request, in case several are fired quickly
            var incomingUrl = Tx.removeParam(xmlhttp.request.url, "ssAjax");
            if (context.pendingUrl === incomingUrl) {
                var contents = Tx.getResponseText(xmlhttp);
                if (contents.length > 0 && !Tx.handleAjaxExceptionPage(contents)) {
                    Element.update(context.updateContainer, contents);
                    Tx.findAndEvalJavascript(contents);

                    History.setTitle(context.pageTitle);
                    context.observers.notify("ajaxLoaded");
                }
            }
        }
    };
    // Firefox blank title bug workaround: make sure the current page title is in the history
    // in place of null passed to pushState; then in onSuccess we update with the new title.
    if (Tx.isFirefox) {
        History.setTitle(context.pageTitle);
    }

    // do ajax update if target url is a browse url (not a product page),
    // and url differs from current browser url (otherwise this is a
    // scroll update to the same page: only anchor or 'pos' param differs)
    if (!state.same && url.startsWith('/browse') &&
            Tx.removeParam(url, 'pos') !== this.currentQuery) {
        this.viewerContext.prepareToClose();

        // we don't encode the url so only browse/xx is the url param, and others like fl and fts are separate params
        this.currentQuery = url;
        this.pendingUrl = '/action/rawBrowser?url=' + url.replace('?', '&');
        Tx.ajax(this.pendingUrl, options);
    }
};

MBrowseContext.prototype.prepareLink = function(url)
{
    var start = url.indexOf('/browse');
    // make sure JP chars are escaped but not separators like / ? &
    // this is done conditionally as IE is not consistent with other browsers as to escaping
    return Tx.encodeUriIfNeeded((start === -1) ? url : url.substring(start));
};

MBrowseContext.prototype.bindToBrowserHistoryChanges = function()
{
    var context = this;
    if (this.ajaxEnabled) {
        History.Adapter.bind(window, 'statechange', function() {
            context.updateFromHistory(History.getState());
        });
    }
};



// kick off image centering
//SSPan.beginCentering();


