////////////////
// 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.
*/
function MArray()
{
    this.array = new Object();
    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)
{
    for (var index = fromIndex; index < toIndex; index++) {
        functionToPerform(index, this.array[index]);
    }
}

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

////////////////
// 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 = findChildTagWithId(this.div, '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 = findFirstNontextChild(this.scratchDiv);
    this.moveImagesAside(newDomElement);

    this.div.appendChild(newDomElement);
}

MCell.prototype.moveImagesAside = function(element)
{
    var images = element.getElementsByTagName("IMG");
    var imagesCount = images.length;
    for (var 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()
{
    if (this.requiresImgSwizzle) {
        var images = this.div.getElementsByTagName("IMG");
        var imagesCount = images.length;
        for (var index = 0; index < imagesCount; index++) {
            var image = images[index];
            var imageSrc = image.getAttribute('imageSrc');
            image.src = image.getAttribute('imageSrc');
        }
        this.requiresImgSwizzle = false;
    }
}

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

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

//////////////////
// MViewerContext
//////////////////
function MViewerContext(viewerDiv, contentDiv, rawCellsUrl, cellSizeFunction, cellCount, detailsContext, isEmbedded,
                        minRowCount, maxRowCount, hintsEnabled, alwaysHint, conservativeFaulting, trackScrollOffsetInAnchor,
                        drawLeftBorder, sidebarDiv, cleardotUrl, widthOffset, sponsoredCount, mayHideArrows)
{
    //alert("MViewer cons " + contentDiv + " " + (contentDiv instanceof HTMLElement));
    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.sponsoredCount = sponsoredCount;
    this.mayHideArrows = mayHideArrows;

    // pagination display
    this.pageNumberDiv = this.getElementById("pageNumber");
    this.pageCountDiv = this.getElementById("pageCount");

    // scroll buttons
    this.scrollArrows = this.getElementById("scrollArrows");
    this.topScrollLeft = this.getElementById("topScrollLeft");
    this.topScrollRight = this.getElementById("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);
    if (sliderContext.handle1 != null) {
        this.sliderContext = sliderContext;
    }
    this.observers = new TxEventObserver();

    // Get our geometry initially set up - note postInit hackery below too
    this.handleResize(false);
    
    // add this viewerContext to the global list of active MViewerContexts
    // see defintion of window.onresize below for more on this.
    document.viewerContexts.push(this);
    if (document.focusedViewerContext == null) {
        document.focusedViewerContext = this;
    }
    
    if (!this.isEmbedded) {
        var mouseScrollEventName = (isIE() || tx_isSafari()) ? "mousewheel" : "DOMMouseScroll";
        Event.observe(this.contentDiv, mouseScrollEventName, this.handleMouseWheel.bindAsEventListener(this), false);
    }
    Event.observe(document.documentElement, "keydown", this.handleKeyPress.bindAsEventListener(this), false);
    
    this.faultCellsTimeoutId = null;
    this.faultImagesTimeoutID = null;
    this.positionPreloadedCells();
    this.contentDiv.style.width = '100%';
    var productViewerWrapper = this.getElementById('productViewerWrapper');
    if (!this.isEmbedded && drawLeftBorder) {
         productViewerWrapper.style.borderLeft = '1px ' + store_borderColor + ' solid';
    }
    this.updateScrollButtonState();
    this.hintContext = hintsEnabled && !isEmbedded ? new MHintContext("hintBubble", alwaysHint, this) : null;
}

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.
*/
function pan_onresize() {
    var viewerContexts = document.viewerContexts;
    var viewerContextCount = viewerContexts.length;
    for (var index = 0; index < viewerContextCount; index++) {
        var currentViewerContext = viewerContexts[index];
        currentViewerContext.handleResize(true);
    }
}

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

tx_addEventListenerToElement(window, 'resize', pan_onresize, false);

// create an empty array and stash it in the function object defined above.
document.viewerContexts = [];

function pan_initViewer(viewerName, rawCellsUrl, totalCellCount, isEmbedded, cellSizeFunction, minRowCount, maxRowCount,
                        hintsEnabled, alwaysHint, conservativeFaulting, trackScrollOffsetInAnchor, drawLeftBorder,
                        cleardotUrl, widthOffset, sponsoredCount, mayHideArrows)
{
    var viewerDiv = document.getElementById(viewerName);
    var contentDiv = findChildWithId(viewerDiv, 'contentDiv');
    contentDiv.style.visibility = 'hidden';
    contentDiv.style.overflow = 'hidden';
    contentDiv.style.width = '0px';
    var cellSize = cellSizeFunction();
    var cellHeight = cellSize.height;
    var numRows = pan_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 = findChildWithId(viewerDiv, 'SidebarDiv');
    if (sidebarContainer) {
        sidebarDiv = 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);
    // Note: should pass this flag to MDetailsContext constructor
    detailsContext._isEmbedded = isEmbedded;
    detailsContext._disabled = isEmbedded && cellSize.height > document.smallProductCellSize.height;
    if (isIE6()) {
        viewerContext.cells.perform(function(index, cell) {
            cell.adjustTwoLineDivHeight();
        });
    }
 
    if (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 (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(arguments.callee.bind(this), 100);
            return;
        }
    }

    this.postInitBody();
}

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

MViewerContext.prototype.getElementById = function(targetId)
{
    var targetChild = findChildWithId(this.viewerDiv, targetId);
    return targetChild;
}

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 = findNontextChildren(this.contentDiv);
    var childrenLength = children.length;
    var cellIndex = 0;
    for (var childIndex = 0; childIndex < childrenLength; childIndex++) {
        var child = children[childIndex];
        if (child.id == "rawCell") {
            var cellOrigin = this.cellOriginForIndex(cellIndex);
            var newCell = new MCell(this, cellOrigin, cellIndex);
            this.cells.set(cellIndex, newCell);
            this.contentDiv.removeChild(child);
            newCell.setChild(child);
            cellIndex++;
        }
    }
}

// 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';
    }
    if (tx_isSafari()) {
        var sliderAssembly = this.viewerContext.getElementById('sliderAssembly');
        var sliderAssemblyLeft = this.sliderContainer.clientWidth / 2 + 85;
        sliderAssembly.style.left = sliderAssemblyLeft + "px";
    }
    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);
}

MViewerContext.prototype._positionDummyDiv = function()
{
    // stretch out the div to full width
    if (this.dummyDiv == null) {
        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;
}

MViewerContext.prototype.updatePaginationDisplay = function()
{
    var minCol = Math.floor(this.contentDiv.scrollLeft / this.cellSize.width);
    var pageNumber = Math.ceil((minCol / this.numCols)) + 1;    
    if (this.contentDiv.scrollLeft >= this.maxScrollLeft) {
        pageNumber = this.pageCount;
    }
    this.pageNumberDiv.innerHTML = pageNumber;
}

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) {
        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 {
            newAnchor = "";
        }
        
        tx_setLocationAnchor(newAnchor);
    }
}

MViewerContext.prototype.updateScrollPositionFromAnchor = function()
{
    if (this.trackScrollOffsetInAnchor) {
        var anchor = window.location.hash;
        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);
                }
            }
        }
    }
    // Special case to allow client to pass an 'ins_page' parameter to auto-scroll to given page
    else if (this.isEmbedded) {
        var page = tx_getParameter('ins_page');
        if (page != null && page > 1) {
            var cellsWide = Math.floor(this.contentDiv.clientWidth / this.cellSize.width);
            return this._updateScrollPosition((page - 1) * cellsWide, 0);
        }
    }
    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 != null) {
        // 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();
    }
}

/**
    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;
    if (isRight && this.sidebarDiv && this.sidebarDiv.parentNode.style.visibility != "hidden") {
        // We may move one less item if there is a sidebar showing.  We only do this if going right, since
        // we assume the sidebar closes on scroll.
        numColsToMove = pan_computeNumCols(this.getMaxWidth() - this.sidebarDiv.parentNode.clientWidth, this.cellSize.width);
    } else {
        numColsToMove = this.numCols;
    }
    var deltaScrollLeft = numColsToMove * this.cellSize.width;
    var initialScrollLeft = this.targetScrollLeft;
    if (isRight) {
        var desiredScrollLeft = initialScrollLeft + deltaScrollLeft;
        this.targetScrollLeft = Math.min(this.maxScrollLeft, desiredScrollLeft);
    }
    else {
        var desiredScrollLeft = initialScrollLeft - deltaScrollLeft;
        this.targetScrollLeft = Math.max(0, desiredScrollLeft);
    }
    this.targetScrollDistance = initialScrollLeft - this.targetScrollLeft;
}

MViewerContext.prototype.computeStepSize = function(isRight)
{
    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;
    if (!this.isEmbedded) {
        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) {
        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.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._faultCells = 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) {
            if (indicesToFault == null) {
                indicesToFault = new Array();
            }
            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;
        for (var 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(store_cookieDomain);
                }
                // These parameter names must stay in sync with definitions in UserState.java
                var url = this.rawCellsUrl;
                url += '&min=' + minIndex + '&count=' + indicesToFault.length;
                
                var viewerContext = this;
                var xmlhttp = xmlHttpObject();
                xmlhttp.onreadystatechange = function() {
                    if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
                        var text = getResponseText(xmlhttp);
                        if (!handleAjaxExceptionPage(text)) {
                           //text = tx_stripHeadTagFromAjaxResponse(text);
                           parseTextIntoCellFaults(viewerContext, cellFaults, text);
                           viewerContext.scheduleFaultImages(200);
                        }
                    }
                }
                xmlhttp.open("GET", url, true);
                xmlhttp.send(null);
        }
    }
}

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

MViewerContext.prototype.faultCells = function(expandSide)
{
    this._faultCells(expandSide);
    this.cancelFaultCellsTimeout();
    if (!this.useConservativeFaulting()) {
        var viewerContext = this;
        if (expandSide == 0 && !this.isEmbedded) {
            var faultRightSideCellsFunction = function() {
                viewerContext.faultCells(1);
            };
            this.faultExtraCellsTimeoutId = window.setTimeout(faultRightSideCellsFunction, 2000);
        }
        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 swizzleCellImageFunction = function(index, cell) {
        if (cell != null && this.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 ? this.lastScrollLeft : 0;
    var scrollDistance = Math.abs(lastScrollLeft - this.contentDiv.scrollLeft);
    var isBigScroll = scrollDistance > (this.contentDiv.clientWidth * .50);

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

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

function parseTextIntoCellFaults(viewerContext, cellFaults, text)
{
    var previousIndex = 0;
    var cellIndex = 0;
    var index = 0;
    // This must be kept in sync with idenitcal 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 != null) {
            cell.setInnerHTML(part);
        }
        previousIndex = index + markerLength;
        cellIndex++;
    }
}

function pan_computeNumRows(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 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 = 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);
}

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

MViewerContext.prototype.getMaxWidth = function()
{
    // document.documentElement.scrollWidth vs 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.
    var maxWidth = windowInnerWidth() - getAbsLeft(this.contentDiv) - this.widthOffset;
    // Negative widthOffset means set a fixed width.
    if (this.widthOffset < 0)
        maxWidth = - this.widthOffset;
    
    if (isFirefox() || tx_isSafari()) {
        //see bug 445
        maxWidth -= 1;
    }
    return maxWidth;
}

// XXX This is doing more than handleResize as evidenced by the fact its not called ONLY from handleResize
// XXX perhaps we just need a better name.
MViewerContext.prototype.handleResize = function(isWindowResize)
{
    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 = this.getElementById('rawCell');
        if (firstRawCell != null) {
            var productCell = 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();

    var oldNumRows = this.numRows;
    var oldNumCols = this.numCols;
    this.numRows = pan_computeNumRows(this.contentDiv, this.cellSize.height, this.isEmbedded, this.minRowCount, this.maxRowCount);
    this.numCols = pan_computeNumCols(maxWidth, this.cellSize.width);
    this.visibleCols = Math.ceil(this.contentDiv.clientWidth / this.cellSize.width);
    
    this.pruneWindowSize = (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 = isIE() ? 93 : 46; // 185/5 == 37  (each cell is 185 pixels wide)
    if (this.isEmbedded) {
        this.scrollStepSize = 26;
    }

    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;

    if (!this.isEmbedded) {     // avoid resize in widget to resolve bugs 797, 809, 811, 916
        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;
        this.reload();
        this.updateScrollButtonState();
    }
    
    this._positionDummyDiv();
    
    doSidebarClipping(this.sidebarDiv, this.contentDiv);
}

// One day this might belong in its own context object
function doSidebarClipping(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 (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);

    var child = sidebarDiv.firstChild;
    var firstPiece = child;
    while (1) {
        if (!child || child.id == "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 {
            //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;
            }
        }
        
        if (child) {
            child = child.nextSibling;
        } else {
            break;
        }
    }
    
    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()
{
    // prune ALL cells (-1 max forces all cells to be pruned)
    this.cancelFaultCellsTimeout();
    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 != null) {
                //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');
        window.setTimeout(doEmit, 5000);
    }
}

// XXX move to MSliderContext section below
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();

    if (store_isAjaxTrackingEnabled()) {
        store_emitOmnitureAjaxPacket(this.handle1, "Product Slider Scroll");
    }
}

// XXX move to MSliderContext section below
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 = viewerContext.getElementById('sliderContainer');
    this.handle1 = viewerContext.getElementById('handle1');
    if (this.handle1 == null) {
        // If the slider isn't present, bail out.
        return;
    }
    this._outerTrack = viewerContext.getElementById("track1");
    this._thinTrack = viewerContext.getElementById("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 = viewerContext.getElementById("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) {
        if (sliderValue >= this.sliderSpringBackHighEnd && 
          (this.viewerContext.contentDiv.scrollLeft < (this.viewerContext.maxScrollLeft - (this.sliderSpringBackLowEnd * this.sliderControlWidth)))) {
            var sliderControlOffset = this.sliderControlOffset + ((sliderValue - this.sliderSpringBackHighEnd) * this.sliderControlWidth);
            this.setSliderControlOffset(sliderControlOffset);
            sliderValue = this.sliderSpringBackHighEnd;
        }
        else if (sliderValue <= this.sliderSpringBackLowEnd && (this.sliderControlOffset > 0)) {
            var 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 != null) {
        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 = 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);
}


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

/////////////////////////////
// Compatibility
// This section should get
// replaced by Prototype.js
/////////////////////////////

function xmlHttpObject() {
    return window.innerWidth === undefined ? new ActiveXObject("Microsoft.XMLHTTP") : new XMLHttpRequest();
}

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

function debug(m)
{
    var debugElement = document.getElementById("debug");
    if (debugElement != null) {
        debugElement.innerHTML = m;
    }
}

function getAbsLeft(o)
{
    var iY = 0;
    while(o.offsetParent){
        iY += o.offsetLeft;
        o = o.offsetParent;
    }
    return iY;
}

function getAbsTop(o)
{
    var iX = 0;
    while(o.offsetParent){
        iX += o.offsetTop;
        o = o.offsetParent;
    }
    return iX;
}

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

document.imageUrlsCache = {};

document.getStoreImageUrl = function(imageElement, imageName, useFilter)
{
    var imageUrl = document.imageUrlsCache[imageName];
    if (imageUrl == null) {
        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')";
        }
        document.imageUrlsCache[imageName] = imageUrl;
    }
    return imageUrl;
}

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

function MDisclosure(button, target, opened)
{
    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;
    }
}

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