/// <reference path="../ScriptReference/jquery-1.7.1-vsdoc.js" />
/// <reference path="../Scripts/Mediasite.Geometry.js" />
if (!Mediasite) {
	var Mediasite = {};
}
if (!Mediasite.Player) {
	Mediasite.Player = {};
}

// SlideNav
// +==================================================================
// Events: seek, beginPreview, endPreview
// ===================================================================
Mediasite.Player.SlideNav = function (element, slideImageUrlFactory) {

    var _element = $(element);
    var _slides = []; // Actually SlideItem instances.
    var _thumbsInUse = [];
    var _duration = 0.0;
    var _normalizedPosition = 0.0;
    var _thumbPool = [];
    var _slideAspect = 4 / 3;
    var _boxAspect = 16 / 9;
    var _thumbHeight = _element.height();
    var _thumbWidth = Math.floor(_thumbHeight * _boxAspect);
    var _spaceBetweenThumbsFraction = 0.17;
    var _spaceBetweenThumbs = Math.round(_thumbWidth * _spaceBetweenThumbsFraction);

    var _dragAnchor = 0;
    var _disablePositionUpdate = false;
    var _trajectory = 0.0;
    var _zeroTime = new Date(0, 0, 1);
    var _lastDragTime = _zeroTime;
    var _lastDragDelta = 0.0;
    var _dampeningForce = 9.0; // slides per second persecond * 1.0 unit mass
    var _maxVelocity = 16.0; // slides per second
    var _disablePositionUpdateUntil = _zeroTime;
    var _updateDisableAfterDrag = 3.5 * 1000; // in milliseconds
    var _updateDisableLagAfterMouseOffThumb = 3.5 * 1000; // in milliseconds
    var _ignoreNextClick = false;
    var _disableSeek = false; // TODO: needs accessors.
    var _slideEpsilon = 0.0005;
    var _slideImageUrlFactory = slideImageUrlFactory;
    var _cssTransofrmRulePrefixes = ['-webkit-', '-moz-', '-o-', '-ms-'];
    var _isIos = Mediasite.BrowserDetect.IOS();
    var _previewEngine = new Mediasite.Player.SlidePreviewEngine(_element);
    var _isSlidePreviewEnabled = !Mediasite.Player.Support.touch;
    var _previewSlide = null;
    var _previewThumbArea = new Mediasite.Geometry.Rect(0, 0, 0, 0);
    _previewEngine.setIsEnabled(_isSlidePreviewEnabled);
    _element.bind({
        'previewEngine-beginPreview': previewEngine_beginPreview,
        'previewEngine-endPreview': previewEngine_endPreview
    });

    var _layoutCalculator = new Mediasite.Player.SlideNavLayoutCalculator();
    _layoutCalculator.setHorizontalMargin(_spaceBetweenThumbs);
    _layoutCalculator.setLogicalScrollPosition(0);
    _layoutCalculator.setThumbWidth(_thumbWidth);
    _layoutCalculator.setSlideCount(0);

    var _thumbContainer = $('<div />').css({
        'width': '10px',
        'height': '100%',
        'position': 'absolute',
        'top': '0px'
    });
    _element.append(_thumbContainer);

    updateSlides();

    $(window).resize(function () {
        updateSlides();
    });


    // Add a css class so that our styles can branch for iOS devices.
    if (_isIos) {
        _element.addClass('ios');
    } else {
        _element.addClass('not-ios');
    }


    this.getElement = function () { return _element; };

    this.getSlides = function () { return _slides; };
    this.setSlides = function (value) {
        _slides = value;
        _layoutCalculator.setSlideCount(_slides.length);
        updateSlides();
    };

    this.getLogicalScrollPosition = function () { return _layoutCalculator.getLogicalScrollPosition(); };
    function setLogicalScrollPosition(value) {
        _layoutCalculator.setLogicalScrollPosition(value);
        updateSlides();
    }

    this.getDuration = function () { return _duration; };
    this.setDuration = function (value) { 
        _normalizedPosition = (_normalizedPosition * _duration / value) || 0; 

        _duration = value; 
    };

    this.getNormalizedPosition = function () { return _normalizedPosition; };
    this.setNormalizedPosition = function (value) {
        _normalizedPosition = value;
        if (isPositionUpdateEnabled()) {
            updateScrollPosition();
        } else {
            updateSlides();
        }
    };

    
    this.setUpdateDisableDelays = function(afterDrag, afterMouseOffThumb) { 
        if (typeof afterDrag != "undefined") _updateDisableAfterDrag = afterDrag;
        if (typeof afterMouseOffThumb != "undefined") _updateDisableLagAfterMouseOffThumb = afterMouseOffThumb;
    };

    this.getIsSlidePreviewEnabled = function () { return _isSlidePreviewEnabled; };
    this.setIsSlidePreviewEnabled = function (value) {
        _isSlidePreviewEnabled = value && !_isTouchDevice;
        _previewEngine.setIsEnabled(_isSlidePreviewEnabled);
    }

    this.getPreviewSlide = function () { return _previewSlide; };

    this.getPreviewThumbArea = function () { return _previewThumbArea; };

    this.getSlideThumbElement = function (slideIndex) {
        var element;

        var slideThumbs = $.map(getLivingThumbElements(), function(element, index) {
            return Mediasite.Player.SlideThumb.instanceFor(element);
        });
        slideThumbs.sort(function(a, b) {
            return a.getSlideIndex() - b.getSlideIndex();
        });
        
        if (slideThumbs.length && slideThumbs[0].getSlideIndex() <= slideIndex) {

            for (var i = 0, length = slideThumbs.length; i < length; i++) {
                var slideThumb = slideThumbs[i];
                if (slideThumb.getSlideIndex() == slideIndex) {
                    element = slideThumb.getElement();
                    break;
                } else if (slideThumb.getSlideIndex() > slideIndex) {
                    element = slideThumbs[i - 1].getElement();
                    break;
                }
            }
            // TODO: select elements.last() if its stack contains the slideIndex
        }

        

        return element;
    }


    function updateScrollPosition() {
        var logicalScrollPosition = 0.0;

        if (_slides != null && _slides.length > 0) {
            var currentIndex = getCurrentSlideIndex();
            logicalScrollPosition = currentIndex;

            if (currentIndex < 0) {
                logicalScrollPosition = 0;
            }
            else if (currentIndex < _slides.length - 1) {
                var currentSlideTime = _slides[currentIndex].getFirstSlide().Time;
                var nextSlideTime;
                if (currentIndex + 1 < _slides.length) {
                    nextSlideTime = _slides[currentIndex + 1].getFirstSlide().Time;
                } else {
                    nextSlideTime = _duration;
                }

                if (nextSlideTime - currentSlideTime > 0) {
                    var time = _normalizedPosition * _duration;
                    logicalScrollPosition += (time - currentSlideTime) / (nextSlideTime - currentSlideTime);
                }
            }
        }

        _thumbContainer.stop();
        setLogicalScrollPosition(logicalScrollPosition);
    }


    function getCurrentSlideIndex() {
        if (!_slides || _slides.length < 1) {
            return -1;
        }

        var time = _normalizedPosition * _duration + _slideEpsilon;
        var count = _slides.length;

        var ix;
        for (ix = 0; ix < count; ix++) {
            var slide = _slides[ix].getFirstSlide();
            if (slide.Time > time) {
                return ix - 1;
            }
        }

        return count - 1;
    }

    function updateSlides() { //SreenathP 8/4/2011: fixed condition to OR, instead of AND. if EITHER ONE is 0 or negative (an invalid value) then there is no use running layoutcalc on it so return
        var elementWidth = _element.width() || _element.parent().width(); //IE7 needs parent width sometimes

        if (elementWidth <= 0 || _element.height() <= 0) {
            return;
        }

        _layoutCalculator.setElementWidth(elementWidth);

        var layout = _layoutCalculator.calculate();

        removeInactiveElements(layout);
        addActiveElements(layout);
        updateThumbStates();

        _thumbContainer.css(getThumbContainerScrollCssMap(layout.getScrollOffset()));

        firePositionChanged(layout);
    }

    function firePositionChanged(layout) {
        var properties = { 
            getSlides: getBoundingSlideIndexes.bind(this, layout)
        };

        _element.trigger($.Event('scroll', properties));
    }

    function getBoundingSlideIndexes(layout) {
        var indexes = { };

        /* 
        var currentSlideItem = _slides[getCurrentSlideIndex()];
        if (currentSlideItem && currentSlideItem !=) {
            indexes.current = currentSlideItem && currentSlideItem.getIndex();
        }
        */

        var visibleElements = getVisibleThumbElements(layout);

        if (visibleElements.length) {
            var logicalThumbs = visibleElements.map(function(index, element) {
                return Mediasite.Player.SlideThumb.instanceFor(element);
            });
            logicalThumbs.sort(function(a, b) { return a.getSlideIndex() - b.getSlideIndex(); });

            
            indexes.first = logicalThumbs[0].getSlideIndex();
            indexes.last = logicalThumbs[logicalThumbs.length - 1].getSlideIndex();
        }
        
        return indexes;
    }

    function getThumbContainerScrollCssMap(scrollOffset) {
        if (areTransformsAvailable()) {
            var map = {};
            var i;
            for (i = 0; i < _cssTransofrmRulePrefixes.length; i++) {
                var prefix = _cssTransofrmRulePrefixes[i];
                map[prefix + 'transform-origin'] = 'top left';
                map[prefix + 'transform'] = 'translate3d(' + scrollOffset + 'px, 0, 0)';
            }
            return map;
        } else {
            return { 'left': scrollOffset };
        }
    }

    function areTransformsAvailable() {
        return Mediasite.Player.Support.csstransforms3d;
    }

    function updateThumbStates() {
        var currentSlide = getCurrentSlideIndex();

        var centerToCenterWidth = _layoutCalculator.getThumbWidth() + _layoutCalculator.getHorizontalMargin();
        var logicalScrollPosition = _layoutCalculator.getLogicalScrollPosition();

        var elements = getLivingThumbElements();
        for (var i = 0, length = elements.length; i < length; i++) {
            var logic = Mediasite.Player.SlideThumb.instanceFor(elements[i]);
            var index = logic.getLogicalIndex();

            logic.setHasSelection(currentSlide == index);

            var slide = _slides[index];

            var stckVis = Mediasite.Player.SlideThumb.StackVisibility;
            if (slide.getIsExpanded()) {
                logic.setStackState(stckVis.Expanded);
            } else if (slide.getIsExpandable()) {
                logic.setStackState(stckVis.Visible);
            } else {
                logic.setStackState(stckVis.Hidden);
            }

            var distanceFromCenter = (index - logicalScrollPosition) * centerToCenterWidth;
            logic.setPerspectiveFactor(distanceFromCenter / (_element.width() / 2));
        }
    }

    function removeInactiveElements(layout) {
        getLivingThumbElements().each(function () {
            var element = $(this);
            var index = Mediasite.Player.SlideThumb.instanceFor(element).getLogicalIndex();
            if (index < layout.getMinIndex() || index > layout.getMaxIndex()) {
                releaseThumbInstance(element);
            }
        });
    }

    function getLivingThumbElements() {
        return $('.alive', _thumbContainer);
    }

    var partialVisibility = false;
    function getVisibleThumbElements(layout) {
        var containerLeft = 0 - layout.getScrollOffset();
        var containerRight = _layoutCalculator.getElementWidth() + containerLeft;

        var thumbWidth = _layoutCalculator.getThumbWidth() + _layoutCalculator.getHorizontalMargin();

        var living = getLivingThumbElements();
        var visible = living.filter(function(index) {
            var element = this;
            var index = Mediasite.Player.SlideThumb.instanceFor(element).getLogicalIndex();
            var thumbLeft = layout.getThumbLeft(index);
            var thumbRight = thumbLeft + thumbWidth;

            var visible = partialVisibility 
                ? containerLeft < thumbRight && thumbLeft < containerRight
                :  containerLeft <= thumbLeft && thumbRight <= containerRight;
            return visible;
        });

        return visible;
    }

    function releaseThumbInstance(instance) {
        instance = instance;
        logic = Mediasite.Player.SlideThumb.instanceFor(instance);
        logic.resetStates();
        logic.setSource(undefined);
        logic.setCaption(undefined);
        logic.setLogicalIndex(-1);
        logic.setSlideIndex(-1);

        instance.removeClass('alive').addClass('dead');
    }

    function addActiveElements(layout) {
        var hugeNumber = 1000000000;
        var ixMinExisting = hugeNumber;
        var ixMaxExisting = -hugeNumber;

        var existingThumbElements = getLivingThumbElements();
        if (existingThumbElements.length > 0) {
            var existing = [];
            existingThumbElements.each(function () {
                existing.push(Mediasite.Player.SlideThumb.instanceFor(this));
            });
            existing.sort(function (a, b) { return a.getLogicalIndex() - b.getLogicalIndex(); });

            ixMinExisting = existing[0].getLogicalIndex();
            ixMaxExisting = existing[existing.length - 1].getLogicalIndex();
        }

        var thumbScaleFactor = 1.5;
        var lastPrepended = null;

        var ix;
        for (ix = layout.getMinIndex(); ix <= layout.getMaxIndex(); ix++) {

            var slide = _slides[ix].getFirstSlide();

            var thumbLogic;
            if (ix >= ixMinExisting && ix <= ixMaxExisting) {
                thumbLogic = existing[ix - ixMinExisting];
            }
            else {
                thumbLogic = createSlideThumb();
                thumbLogic.setLogicalIndex(ix);
                thumbLogic.getElement().css('left', layout.getThumbLeft(ix));
            }

            thumbLogic.setSlideIndex(slide.Number);
            thumbLogic.setToolTip(slide.Text);
            thumbLogic.setCaption(slide.Caption);

            var imageUrl = getImageUrl(slide.Number);
            if (!imageUrl || !imageUrl.resolve) {
                imageUrl = $.Deferred().resolve(imageUrl);
            }
            imageUrl.done((function(thumbLogic, imageUrl) {
                if (thumbLogic.getSource() != imageUrl) {
                    thumbLogic.setSource(imageUrl);
                }
            }).bind(this, thumbLogic));

        }
    }

    function getImageUrl(slideNumber) {
        return _slideImageUrlFactory(slideNumber, getImageWidth(), getImageHeight());
    }

    function getImageWidth() {
        return Math.round(_thumbHeight * _slideAspect);
    }

    function getImageHeight() {
        return getThumbHeight();
    }

    function getThumbWidth() {
        return Math.floor(_layoutCalculator.getThumbWidth());
    }

    function getThumbHeight() {
        return Math.floor(_thumbHeight);
    }

    function createSlideThumb() {
        var instance = resurrectSlideThumb(); // If we can find a zombie thumb, we'll use that.

        if (!instance) {
            // make new instance.
            instance = new Mediasite.Player.SlideThumb(getThumbWidth(), getThumbHeight());

            // wire up events.
            instance.getElement().bind({
                'thumb-mousedown.slideNav': thumb_mousedown,
                'thumb-mouseup.slideNav': thumb_mouseup,
                'thumb-begindrag.slideNav': thumb_begindrag,
                'thumb-drag.slideNav': thumb_drag,
                'thumb-enddrag.slideNav': thumb_enddrag,
                'thumb-click.slideNav': thumb_click,
                'thumb-stackexpand': thumb_stackexpand,
                'thumb-stackcollapse': thumb_stackcollapse,
                'mouseenter': thumb_mouseenter,
                'mouseleave': thumb_mouseleave
            });

            instance.getElement().addClass('alive').appendTo(_thumbContainer);
        }

        return instance;
    }

    function resurrectSlideThumb() {
        var corpses = jQuery('.dead', _thumbContainer);
        if (corpses.length > 0) {
            var element = corpses.first().removeClass('dead').addClass('alive');
            return Mediasite.Player.SlideThumb.instanceFor(element);
        } else {
            return null;
        }
    }

    function isPositionUpdateEnabled() {
        return (!_disablePositionUpdate && (new Date()).getTime() > _disablePositionUpdateUntil.getTime());
    }

    function expandCollapseSlide(logicalIndex, expand) {
        try {
            _slides[logicalIndex].setIsExpanded(expand);
        } catch (err) { /* Oops! */ }
    }


    // thumb event handlers
    // +===================
    function thumb_mousedown(e) {
        _thumbContainer.stop();

        _ignoreNextClick = false;

        if (_disablePositionUpdateUntil != _zeroTime) {
            var dragEnd = _disablePositionUpdateUntil.getTime() - _updateDisableAfterDrag;
            if ((new Date()).getTime() < dragEnd) {
                // We "caught" the clides in motion, so ignore the next click event.
                _ignoreNextClick = true;
            }
        }

        var thisThumb = Mediasite.Player.SlideThumb.instanceFor($(this));
        _lastThumbMouseDown = thisThumb;
    }

    var _lastThumbMouseDown = null;

    function thumb_mouseup(e) {
        var thisThumb = Mediasite.Player.SlideThumb.instanceFor($(this));

        if (thisThumb && _lastThumbMouseDown &&
            _lastThumbMouseDown.getLogicalIndex() != thisThumb.getLogicalIndex()) {

            _lastThumbMouseDown.forceReleaseMouse();
        }

        _lastThumbMouseDown = thisThumb;
    }

    function thumb_begindrag(e) {
        _dragAnchor = _layoutCalculator.getLogicalScrollPosition();

        _disablePositionUpdate = true;

        _trajectory = 0.0;
        _lastDragTime = _zeroTime;
    }

    function thumb_drag(e, data) {
        var logicalDelta = data.delta / (_layoutCalculator.getThumbWidth() + _layoutCalculator.getHorizontalMargin());
        var now = new Date();
        if (_lastDragTime == _zeroTime) {
            _lastDragDelta = data.delta;
            _lastDragTime = now;
        } else {
            var elapsed = (now.getTime() - _lastDragTime.getTime()) / 1000.0; // in milliseconds

            if (elapsed > 0.001) {
                var delta = (data.delta - _lastDragDelta);

                // add a primitive low pass filter on trajectory
                // with more weight given to longer interval (to account for drag...stop actions)100
                //
                // ~10 samples per second
                //

                var blend = Math.min(elapsed * 10.0, 1.0);
                _trajectory = _trajectory * (1.0 - blend) + (delta / elapsed) * (blend);

                _lastDragDelta = data.delta;
                _lastDragTime = now;
            }
        }

        var newPosition = Math.max(Math.min(_dragAnchor - logicalDelta, _slides.length), 0);
        setLogicalScrollPosition(newPosition);
    }

    function thumb_enddrag(e) {
        var from = _layoutCalculator.getLogicalScrollPosition();
        var logicalTrajectory =
			-_trajectory / (_layoutCalculator.getThumbWidth() + _layoutCalculator.getHorizontalMargin());
        logicalTrajectory = Math.min(Math.max(logicalTrajectory, -_maxVelocity), _maxVelocity);

        var interpolator = new Mediasite.Player.InertialInterpolator(from, 0.0, _slides.length, logicalTrajectory, _dampeningForce);
        var animationDuration = interpolator.getCalculatedDuration() * 1000;
        var parts = 1000;
        _thumbContainer.css({ 'fake-css-style': 0 });
        _thumbContainer.animate(
			{
			    'fake-css-style': parts
			},
			{
			    duration: animationDuration,
			    easing: 'linear',
			    step: function (now) {
			        var progress = now / parts;
			        var logicalPosition = interpolator.interpolate(progress);
			        setLogicalScrollPosition(logicalPosition);
			    }
			});

        _disablePositionUpdate = false;
        _disablePositionUpdateUntil = new Date((new Date()).getTime() + animationDuration + _updateDisableAfterDrag);
    }

    function thumb_click(e) {
        if (_ignoreNextClick) {
            return;
        }

        var thumb = $(this);
        var thumbLogic = Mediasite.Player.SlideThumb.instanceFor(thumb);
        var logicalIndex = thumbLogic.getLogicalIndex();

        if (logicalIndex < 0 || logicalIndex >= _slides.length) {
            return; // Ok, this thumb is already dead!
        }

        var parts = 1000;
        var from = _layoutCalculator.getLogicalScrollPosition();
        var delta = logicalIndex - from;

        _thumbContainer.css({ 'fake-css-rule': 0 });
        _thumbContainer.animate({
            'fake-css-rule': parts
        },
		{
		    duration: 0.5 * 1000,
		    easing: 'swing',
		    step: function (now) {
		        var progress = now / parts;
		        var newValue = from + delta * progress;
		        setLogicalScrollPosition(newValue);
		    }
		});

        if (_disableSeek) {
            _disablePositionUpdateUntil = new Date(new Date().getTime() + _updateDisableAfterDrag);
        } else {
            // disable scroll position update (since we're updating it ourselves in the animation above... but we still
            // need to update the dependency property so that external listeners can get the notifications right away
            //
            _disablePositionUpdate = true;

            var slideTime = _slides[logicalIndex].getFirstSlide().Time;
            var duration = _duration;
            var norm = (duration > 0.0) ? Math.min(Math.max((slideTime + _slideEpsilon) / duration, 0.0), 1.0) : 0.0;
            _normalizedPosition = norm;
            onSeek(slideTime, norm, slideTime);

            _disablePositionUpdate = false;
            _disablePositionUpdateUntil = _zeroTime;
        }
    }

    function thumb_stackexpand(e, data) {
        expandCollapseSlide(data.logicalIndex, true);
    }

    function thumb_stackcollapse(e, data) {
        expandCollapseSlide(data.logicalIndex, false);
    }

    function thumb_mouseenter(e) {
        _previewEngine.registerMouseEnter(Mediasite.Player.SlideThumb.instanceFor($(e.currentTarget)));

        // When the user has recently previewed a slide or dragged the fimstrip, 
        // we want to give him/her a chance to interact with another slide 
        // without the scroll position updating.
        if (!isPositionUpdateEnabled()) {
            _disablePositionUpdate = true;
        }
    }

    function thumb_mouseleave(e) {
        // When the user has recently previewed a slide or dragged the fimstrip, 
        // we want to give him/her a chance to interact with another slide 
        // without the scroll position updating.
        if (!isPositionUpdateEnabled()) {
            _disablePositionUpdate = false;
            _disablePositionUpdateUntil = new Date(new Date().getTime() + _updateDisableLagAfterMouseOffThumb);
        }
    }
    // -=====

    function previewEngine_beginPreview(e, data) {
        if (!data || !data.thumb) {
            return;
        }

        var index = data.thumb.getLogicalIndex();

        if (index >= _slides.length || index < 0) {
            onPreviewSlideChanged(null);
            return;
        }

        var slideItem = _slides[index];

        if (!slideItem) {
            return;
        }

        var thumbElement = $(data.thumb.getElement());
        var jqOffset = thumbElement.offset();


        _previewThumbArea = new Mediasite.Geometry.Rect(jqOffset.left, jqOffset.top, thumbElement.width(), thumbElement.height());
        _previewSlide = slideItem.getFirstSlide();

        onBeginPreview();

        _disablePositionUpdate = true;
    }

    function previewEngine_endPreview(e) {
        _previewSlide = null;
        onEndPreview()
    }

    function onBeginPreview() {
        _element.trigger('beginPreview');
    }

    function onEndPreview() {
        _element.trigger('endPreview');
    }

    function onSeek(position, normalizedPosition, slideNumber) {
        _element.trigger('seek', { position: position, normalizedPosition: normalizedPosition, slideNumber: slideNumber });
    }
};
// -=====


// InertialInterpolator
// +===================
Mediasite.Player.InertialInterpolator = function (from, min, max, trajectory, dampeningForce) {

	var _trajectory = trajectory;
	var _min = min;
	var _max = max;
	var _from = from;

	// for simplicity sake, dampening force is defined as: abs(acceleration due to friction) * 1.0 (mass).
	// this is so that we don't have to come up with an imaginary mass and coefficient of friction
	// (they all resolve to a constant scalar value anyway)
	//

	var _acc = (_trajectory > 0.0) ? -dampeningForce : dampeningForce;

	// equations from highschool physics:
	//
	// v^2 = v_0^2 + 2a(delta_x);
	// delta_x = (v^2 - v_0^2) / (2a);

	var delta = (0.0 - _trajectory * _trajectory) / (2.0 * _acc);

	var _target = Math.max(Math.min(from + delta, _max), _min);

	// v_f = v_0 + at
	// t = v_f - v_0 / a;

	var _duration = 0.0 - _trajectory / _acc;


	this.getCalculatedDuration = function () { return _duration; };
	this.getCalculatedTarget = function () { return _target; };

	this.interpolate = function (t) {
		var time = t * _duration;

		// v_f = v_0 + at
		var velocity = _trajectory + _acc * time;

		// delta_x = (v^2 - v_0^2) / (2a);
		var delta = (velocity * velocity - _trajectory * _trajectory) / (2.0 * _acc);

		return Math.max(Math.min(_from + delta, _max), _min);
	};
};

// -=====


// SlideNavLayoutCalculator
// +=========================
Mediasite.Player.SlideNavLayoutCalculator = function () {

    var _elementWidth = 600;
    var _slideCount = 0;
    var _thumbWidth = 95;
    var _horizontalMargin = 10;
    var _logicalScrollPosition = 0.0;


    this.getElementWidth = function () { return _elementWidth; };
    this.setElementWidth = function (value) { _elementWidth = value; };

    this.getSlideCount = function () { return _slideCount; };
    this.setSlideCount = function (value) { _slideCount = value; };

    this.getThumbWidth = function () { return _thumbWidth; };
    this.setThumbWidth = function (value) { _thumbWidth = value; };

    this.getHorizontalMargin = function () { return _horizontalMargin; };
    this.setHorizontalMargin = function (value) { _horizontalMargin = value; };

    this.getLogicalScrollPosition = function () { return _logicalScrollPosition; };
    this.setLogicalScrollPosition = function (value) { _logicalScrollPosition = value; };



    this.calculate = function () {
        if (_elementWidth <= 0) { //SreenathP 8/8/2011: replace with _elementWidth to revert; temporary fix for IE7.
            return new Mediasite.Player.SlideNavLayout(0, 0, 0, 0);
        }

        // The number of slides shown in the viewport is not just a matter of dividing widths.
        // Since we calculate based on the current slide being smack dab in the center, 
        // the number of slides at least partially visible should be odd. As a slide 
        // scrolls left one entire slide width during it's tenure as current slide, we need 
        // to add one more, resulting in an even number. So...
        // 
        // 1. Determine the number of slides that mathematically fit and round up.
        // 2. If that number is odd, add one. If it's even, add two.
        var visibleElements = Math.ceil(_elementWidth / (_thumbWidth + _horizontalMargin));
        visibleElements = visibleElements + 2 - visibleElements % 2;


        var ixPos = Math.floor(_logicalScrollPosition);
        var firstVisible = ixPos + 1 - (visibleElements / 2);

        var ixMin = Math.max(firstVisible - Math.ceil(visibleElements * (1 / 3)), 0);                    // 1/3 to left.
        var ixMax = Math.min(firstVisible + Math.ceil(visibleElements * (5 / 3)) - 1, _slideCount - 1);  // 2/3 to right.

        var fractionalProgress = _logicalScrollPosition - ixPos;

        var margins = Math.max(0, ixPos - 1);
        //SreenathP 8/8 2011: hate to do this but no matter what i try IE7 sees width of slide-navigator as zero.
        //slide-navigator is defined as width:100% of the parent body, which IE7 doesn't recognize for some bizarre reason
        //thus i am temporarily using body width since it is same as the defined slide-nav width anyway. 
        //not really scalable for future changes however (if you want nav to be smaller).
        //replaced case _elementWidth with _element.width()
        var scrollOffset =
			(_elementWidth - _thumbWidth) / 2                         // starting point
			- ixPos * (_thumbWidth + _horizontalMargin)                // offset to current slide
			- (_thumbWidth + _horizontalMargin) * fractionalProgress; // progress in current slide

        return new Mediasite.Player.SlideNavLayout(ixMin, ixMax, scrollOffset, _thumbWidth + _horizontalMargin);
    };
};


Mediasite.Player.SlideNavLayout = function (minIndex, maxIndex, scrollOffset, slideToSlideWidth) {
    this.getMinIndex = function () { return minIndex; };
    this.getMaxIndex = function () { return maxIndex; };
    this.getScrollOffset = function () { return scrollOffset; };

    this.getThumbLeft = function (slideIndex) {
        return slideIndex * slideToSlideWidth;
    };
};
// -=====


// SlideThumb
// +====================================================================
// Events: thumb-mousedown, thumb-begindrag, thumb-drag, thumb-enddrag, 
//         thumb-click, thumb-stackexpand, thumb-stackcollapse
// =====================================================================
Mediasite.Player.SlideThumb = function (width, height) {

    var MouseState = {
        Inactive: "Inactive",
        MouseDown: "MouseDown",
        MouseDownOnStack: "MouseDownOnStack",
        InDrag: "InDrag"
    };

    var MouseVisualState = {
        Normal: 'Normal',
        MouseOver: 'MouseOver',
        Pressed: 'Pressed',
        MouseOverStack: 'MouseOverStack',
        StackPressed: 'StackPressed'
    };

    var SelectionVisualState = {
        Unselected: { name: 'Unselected', scale: 1.0, frameOpacity: 0.5 },
        Selected: { name: 'Selected', scale: 1.1, frameOpacity: 1.0 }
    };


    var _element, _innerWrapper, _imgbox, _img, _imgFrame, _stack; // elements
    var _stackFrames = [];
    var _width = width;
    var _height = height;
    var _logicalIndex = -1;
    var _slideIndex = -1;
    var _hasSelection = false;
    var _dragAnchor = { x: 0, y: 0 };
    var _dragSlop = 3;
    var _mouseState = MouseState.Inactive;
    var _mouseVisualState = MouseVisualState.Normal;
    var _selectionVisualState = SelectionVisualState.Unselected;
    var _scale = 1.0;
    var _frameAspect = _width / _height;
    var _slideAspect = 4 / 3; // TODO: send this value in.
    var _slideWidthPercentage = 100 * _slideAspect / _frameAspect;
    var _stackState = Mediasite.Player.SlideThumb.StackVisibility.Hidden;
    var _perspectiveFactor = 0;
    var _isIos = Mediasite.BrowserDetect.IOS();
    var _isTouchDevice = Mediasite.Player.Support.touch;
    var _usePerspective = !_isTouchDevice && Mediasite.Player.Support.csstransforms3d;
    var _caption;

    _element = $('<div class="player-slide-thumb" ></div>')
		.width(_width)
		.height(_height)
		.css({
		    'position': 'absolute',
		    'fake-css-rule': '0px'
		});

    _element.data(Mediasite.Player.SlideThumb.instanceDataKey, this);

    _innerWrapper = $('<div class="player-slide-thumb-inner"></div>').appendTo(_element);


    _stack = $('<div class="player-slide-thumb-stack"></div>').appendTo(_innerWrapper);

    makeStackFrames();

    _imgbox = $('<div class="player-slide-thumb-img-box" ></div>').appendTo(_innerWrapper);
    _img = $('<img />').css({
        'position': 'absolute',
        'z-index': '1',
        'width': _slideWidthPercentage + '%',
        'height': '100%',
        'left': ((100 - _slideWidthPercentage) / 2) + '%',
        'fake-css-rule': '0px',
        '-moz-user-focus': 'ignore',
        '-moz-user-select': 'none',
        '-khtml-user-focus': 'ignore',
        '-khtml-user-select': 'none'
    }).bind('dragstart', function (e) { e.preventDefault(); }); // stop browser stupidity.
    _imgbox.append(_img);

    _imgFrame = $('<div class="player-slide-thumb-img-frame" ></div>').css({
        'width': '100%',
        'height': '100%'
    });
    _imgFrame.appendTo(_imgbox);


    _element.bind({
        'mousemove': element_mousemove
    });
    _imgbox.bind({
        'mouseup': imgbox_mouseup,
        'mousedown': imgbox_mousedown,
        'mouseenter': imgbox_mouseenter,
        'mouseleave': imgbox_mouseleave
    });
    _stack.bind({
        'mouseup': stack_mouseup,
        'mousedown': stack_mousedown,
        'mouseenter': stack_mouseenter,
        'mouseleave': stack_mouseleave
    });

    updateVisualState(0);



    // Public Accessors

    this.getElement = function () { return _element; };

    this.getLogicalIndex = function () { return _logicalIndex; };
    this.setLogicalIndex = function (value) { _logicalIndex = value; };

    this.getSlideIndex = function () { return _slideIndex; };
    this.setSlideIndex = function (value) { _slideIndex = value; };

    this.getToolTip = function () { return _element.attr('title'); };
    this.setToolTip = function (value) { _element.attr('title', value); };

    this.getLeft = function () { return _element.css('left'); };
    this.setLeft = function (value) { _element.css('left', value); };

    this.getSource = function () { return _img.attr('src'); };
    this.setSource = function (value) {
        if (!value) {
            //inline transparent gif
            value = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
        }
        _img.attr('src', value);
    };

    this.getCaption = function() {
        return _caption && _caption.text();
    };
    this.setCaption = function(value) {
        if (value && !_caption) {
            _caption = $('<div class="player-slide-thumb-caption"></div>').appendTo(_element);
        }

        if (_caption) _caption.html(value);
    }

    this.getHasSelection = function () { return _hasSelection; };
    this.setHasSelection = function (value) {
        _hasSelection = value;
        updateSelectionVisualState(value ? SelectionVisualState.Selected : SelectionVisualState.Unselected);
    };

    this.getStackState = function () { return _stackState; };
    this.setStackState = function (value) {
        updateStackVisualState(value);
        updateStackPerspective(); // Do this after updating the visual state.
        setStackTooltip(_stackState)
    };

    this.getPerspectiveFactor = function () { return _perspectiveFactor; };
    this.setPerspectiveFactor = function (value) {
        _perspectiveFactor = value;
        updateStackPerspective();
    };


    this.resetStates = function () {

    }

    this.forceReleaseMouse = function () {
        releaseMouseCapture();

        if (_mouseState === MouseState.InDrag) {
            OnThumbEndDrag();
        }

        _mouseState = MouseState.Inactive;
        updateVisualState(0);
    }


    // event handlers

    function imgbox_mouseup(e) {
        handleMouseUp(e.pageX, MouseVisualState.MouseOver);
    }
    function imgbox_mousedown(e) {
        _mouseState = MouseState.MouseDown;

        handleMouseDown(e);
        captureMouse();

        updateMouseVisualState(MouseVisualState.Pressed);
    }
    function imgbox_mouseenter(e) {
        updateMouseVisualState(MouseVisualState.MouseOver);
    }
    function imgbox_mouseleave(e) {
        updateMouseVisualState(MouseVisualState.Normal);
    }


    function document_mouseup(e) {
        handleMouseUp(e.pageX, MouseVisualState.Normal);
    }
    function document_mousemove(e) {
        element_mousemove(e);
    }
    function element_mousemove(e) {
        if (_mouseState === MouseState.InDrag) {
            OnThumbDrag(e.pageX - _dragAnchor.x);
        } else if (_mouseState === MouseState.MouseDown || _mouseState === MouseState.MouseDownOnStack) {
            var deltaX = e.pageX - _dragAnchor.x;
            var deltaY = e.pageY - _dragAnchor.y;
            if (Math.abs(deltaX) > _dragSlop || Math.abs(deltaY) > _dragSlop) {
                _mouseState = MouseState.InDrag;
                OnThumbBeginDrag();
            }
        }
    }


    function stack_mouseup(e) {
        handleMouseUp(e.pageX, MouseVisualState.MouseOverStack);
    }
    function stack_mousedown(e) {
        if (!canDragStack()) {
            return;
        }

        _mouseState = MouseState.MouseDownOnStack;

        handleMouseDown(e);
        captureMouse();

        updateMouseVisualState(MouseVisualState.StackPressed);
    }
    function stack_mouseenter(e) {
        updateMouseVisualState(MouseVisualState.MouseOverStack);
    }
    function stack_mouseleave(e) {
        updateMouseVisualState(MouseVisualState.Normal);
    }


    // Just some private methods

    function handleMouseUp(pageX, targetMouseVisualState) {
        _element.trigger('thumb-mouseup');

        releaseMouseCapture();

        if (_mouseState === MouseState.Inactive) {
            return;
        }

        if (_mouseState === MouseState.MouseDown) {
            OnThumbClick();
        }

        if (_mouseState === MouseState.MouseDownOnStack) {
            if (_stackState === Mediasite.Player.SlideThumb.StackVisibility.Visible) {
                OnStackExpand();
            } else if (_stackState === Mediasite.Player.SlideThumb.StackVisibility.Expanded) { // Expanded
                OnStackCollapse();
            }
        }

        if (_mouseState === MouseState.InDrag) {
            // Final drag
            var delta = pageX - _dragAnchor.x;
            OnThumbDrag(delta);
            OnThumbEndDrag();
            _mouseState = MouseState.Inactive;
        }

        _mouseState = MouseState.Inactive;

        updateMouseVisualState(targetMouseVisualState);
    }

    function handleMouseDown(e, moveHandler, upHandler) {
        _dragAnchor.x = e.pageX;
        _dragAnchor.y = e.pageY;

        OnThumbMouseDown();
    }

    function canDragStack() {
        var values = Mediasite.Player.SlideThumb.StackVisibility;
        return _stackState === values.Visible || _stackState === values.Expanded;
    }

    function setStackTooltip(stackVis) {
        var visNames = Mediasite.Player.SlideThumb.StackVisibility;
        var tooltip = '';

        if (stackVis === visNames.Visible) {
            tooltip = Mediasite.Player.Localization.SlideNav.ExpandStack;
        } else if (stackVis === visNames.Expanded) {
            tooltip = Mediasite.Player.Localization.SlideNav.CollapseStack;
        }

        _stack.attr('title', tooltip);
    }

    function updateStackPerspective() {
        if (!_usePerspective) {
            return;
        }

        var shouldHaveStack = (_stackState === Mediasite.Player.SlideThumb.StackVisibility.Visible ||
            _stackState === Mediasite.Player.SlideThumb.StackVisibility.Expanded);
        var hasStack = _stackFrames.length > 0;

        if (hasStack && !shouldHaveStack) {
            destroyStackFrames();
            return;
        } else if (!hasStack && shouldHaveStack) {
            makeStackFrames();
        }



        var i;
        for (i = 0; i < _stackFrames.length; i++) {
            _stackFrames[i].css('margin-left', ((i + 1) * -_perspectiveFactor * 12) + '%');
        }

    }

    function makeStackFrames() {
        _stackFrames = [];
        _stackFrames.push(makeStackFrame(1));
        _stackFrames.push(makeStackFrame(2));
        _stackFrames.push(makeStackFrame(3));
    }

    function destroyStackFrames() {
        _stackFrames = [];
        _stack.empty();
    }

    function makeStackFrame(num) {
        return $('<div class="player-slide-stack-frame" ></div>').addClass('stack-frame-' + num).appendTo(_stack);
    }

    function toSelectionClass(state) {
        return 'selection-' + state.name;
    }

    function toStackClass(state) {
        return 'stackState-' + state;
    }

    function toMouseClass(state) {
        return 'mouse-' + state;
    }

    function getAllVisualStateCssClassNames() {
        var vis = Mediasite.Player.SlideThumb.StackVisibility;

        return [
            toMouseClass(MouseVisualState.Normal),
            toMouseClass(MouseVisualState.MouseOver),
            toMouseClass(MouseVisualState.MouseOverStack),
            toMouseClass(MouseVisualState.Pressed),
            toMouseClass(MouseVisualState.StackPressed),
            toStackClass(vis.Hidden),
            toStackClass(vis.Visible),
            toStackClass(vis.Expanded),
            toSelectionClass(SelectionVisualState.Unselected),
            toSelectionClass(SelectionVisualState.Selected)
        ].join(' ');
    }

    function updateMouseVisualState(state) {
        if (_mouseVisualState == state) {
            return;
        }

        _mouseVisualState = state;
        updateVisualState();
    }

    function updateSelectionVisualState(state) {
        if (_selectionVisualState == state) {
            return;
        }

        _selectionVisualState = state;
        updateVisualState();
    }

    function updateStackVisualState(state) {
        if (_stackState == state) {
            return;
        }

        _stackState = state;
        updateVisualState();
    }

    function updateVisualState() {
        var classes = [
            toMouseClass(_mouseVisualState),
            toStackClass(_stackState),
            toSelectionClass(_selectionVisualState)
        ];

        _element.removeClass(getAllVisualStateCssClassNames());
        _element.addClass(classes.join(' '));
    }

    function captureMouse() {
        $(document).bind({
            'mousemove.slideThumbCapture': document_mousemove,
            'mouseup.slideThumbCapture': document_mouseup
        });
    }

    function releaseMouseCapture() {
        $(document).unbind('.slideThumbCapture');
    }



    function isMouseOverStack(event) {
        var outlineFudge = 2;
        return event.pageY < _imgbox.offset().top - outlineFudge;
    }


    // Events

    function OnThumbMouseDown() {
        _element.trigger('thumb-mousedown');
    }

    function OnThumbBeginDrag() {
        _element.trigger('thumb-begindrag');
    }

    function OnThumbDrag(delta) {
        _element.trigger('thumb-drag', { delta: delta });
    }

    function OnThumbEndDrag() {
        _element.trigger('thumb-enddrag');
    }

    function OnThumbClick() {
        _element.trigger('thumb-click');
    }

    function OnStackExpand() {
        _element.trigger('thumb-stackexpand', { logicalIndex: _logicalIndex });
    }

    function OnStackCollapse() {
        _element.trigger('thumb-stackcollapse', { logicalIndex: _logicalIndex });
    }


    // TODO: events: MouseEnter, MouseLeave;

};
Mediasite.Player.SlideThumb.GetNextZIndex = function () {
	var z = (Mediasite.Player.SlideThumb.LastZIndex || 0) + 1;
	Mediasite.Player.SlideThumb.LastZIndex = z;
	return 1;
};
Mediasite.Player.SlideThumb.instanceDataKey = 'slideThumb';
Mediasite.Player.SlideThumb.instanceFor = function (element) {
    return $(element).data(Mediasite.Player.SlideThumb.instanceDataKey);
};
Mediasite.Player.SlideThumb.StackVisibility = {
    Hidden: 'Hidden',
    Visible: 'Visible',
    Expanded: 'Expanded'
};

// -=====


// SlidePreviewControl
// +===========================================================================
// Arguments
//      element:  A DIV to become the slide preview control.
//      slideUrlProvider: implements GetCacheFriendlySlideUrl(n, h, a)
//                        (e.g. Mediasite.Presentation).
// 
// Events: slidePreviewInitiated
// ============================================================================
Mediasite.Player.SlidePreviewControl = function (element, slideUrlProvider) {
    var _element = $(element);
    var _urlProvider = slideUrlProvider || new FakeUrlProvider();
    var _backgroundMask, _previewDiv, _textOverlay, _img, _title, _description;
    var _previewSlide = null;
    var _previewSrc = '';
    var _previewArea = new Mediasite.Geometry.Rect(0, 0, 0, 0);
    var _isSlidePreviewEnabled = true;
    var _slideAspect = 4 / 3;


    function init() {
        _backgroundMask = $('<div />').addClass('background-mask').appendTo(_element);
        _previewDiv = $('<div />').addClass('preview-area').appendTo(_element);
        _img = $('<img />').appendTo(_previewDiv);
        $('<div />').addClass('contentFramer').appendTo(_previewDiv);

        _textOverlay = $('<div />').addClass('text-overlay').appendTo(_previewDiv);
        _title = $('<h1 />').appendTo(_textOverlay);
        _description = $('<p />').appendTo(_textOverlay);
    }


    // accessors
    // +========
    this.getElement = function () { return _element; };

    this.setSlideUrlProvider = function (value) { _urlProvider = value; };

    this.getPreviewSlide = function () { return _previewSlide; };
    this.setPreviewSlide = function (value) {
        if (value === _previewSlide) {
            return;
        }

        _previewSlide = value;
        updateSlidePreview();
    };

    this.getPreviewSrc = function () { return _previewSrc; };

    this.getPreviewArea = function () { return _previewArea; };

    // Boolean
    this.getIsSlidePreviewEnabled = function () { return _isSlidePreviewEnabled; };
    this.setIsSlidePreviewEnabled = function (value) { _isSlidePreviewEnabled = value; };

    this.getSlideAspect = function () { return _slideAspect; };
    this.setSlideAspect = function (value) { _slideAspect = value; };

    // -=====


    function updateSlidePreview() {
        if (!_previewSlide) {
            hideSlidePreview();
            return;
        }

        showSlidePreview();

        var slidePreviewUrl = _urlProvider.GetCacheFriendlySlideUrl(_previewSlide.Number, _previewDiv.height(), _slideAspect);
        if (!slidePreviewUrl || !slidePreviewUrl.resolve) {
            slidePreviewUrl = $.Deferred().resolve(slidePreviewUrl);
        }
        slidePreviewUrl.done(function(slidePreviewUrl) {
            _img.attr('src', slidePreviewUrl);
        });
        _title.text(_previewSlide.Text);

        if (_previewSlide.Description) {
            _description.html(_previewSlide.Description.replace(/\n/g, "<br />"));
            _description.show();
        } else {
            _description.hide();
        }

        var jqOffset = _previewDiv.offset();

        _previewArea = new Mediasite.Geometry.Rect(jqOffset.left, jqOffset.top, _previewDiv.width(), _previewDiv.height());
        _previewSrc = slidePreviewUrl;

        raiseEvent('slidePreviewInitiated');
    }

    function showSlidePreview() {
        _element.fadeIn(250, 'linear');
    }

    function hideSlidePreview() {
        _element.fadeOut(250, 'linear');
    }

    function raiseEvent(eventName) {
        _element.trigger(eventName);
    }



    function FakeUrlProvider() {
        this.GetCacheFriendlySlideUrl = function (n, h, a) { return ''; };
    }


    init();
};

// -=====


// SlidePreviewSubject
// +===========================================================================
// Arguments
//      thumb: Mediasite.Player.SlideThumb instance.
//      container: The DOM element for the SlideNav control.
//      lingerDuration: milliseconds of mouse pause needed to trigger mouseLinger.
//      mouseLeaveDelay: milliseconds after mouseleave before triggering delayedMouseLeave.
//
// Events (triggered on thumb.getElement()): mouseLinger, delayedMouseLeave
// ============================================================================
Mediasite.Player.SlidePreviewSubject = function (thumb, container, lingerDuration, mouseLeaveDelay) {
    var _thumb = thumb;
    var _container = $(container);
    var _lingerDuration = lingerDuration;
    var _mouseLeaveDelay = mouseLeaveDelay;
    var _lingerTimeoutId, _mouseLeaveTimeoutId;


    function init() {
        _container.bind('mousemove', container_mousemove);
        _thumb.getElement().bind({
            'mouseenter': thumb_mouseenter,
            'mouseleave': thumb_mouseleave
        });
    }



    this.getThumb = function () { return _thumb; };

    this.reportMouseEnter = function () {
        startLingerTimer();
    };

    this.die = function () {
        _container.unbind('mousemove', container_mousemove);
        _thumb.getElement().unbind('mouseenter', thumb_mouseenter);
        _thumb.getElement().unbind('mouseleave', thumb_mouseleave);

        stopLingerTimer();
        stopMouseLeaveTimer();

        _container = null;
        _thumb = null;
    };



    function lingerTimer_tick() {
        if (!_thumb) {
            return;
        }

        _thumb.getElement().trigger('mouseLinger');
    }

    function mouseLeaveTimer_tick() {
        if (!_thumb) {
            return;
        }

        _thumb.getElement().trigger('delayedMouseLeave');
    }

    function container_mousemove(e) {
        // reset linger timer
        stopLingerTimer();
        startLingerTimer();
    }

    function thumb_mouseenter(e) {
        if (!_thumb) {
            return;
        }

        stopMouseLeaveTimer();
        startLingerTimer();
    }

    function thumb_mouseleave(e) {
        stopLingerTimer();
        startMouseLeaveTimer();
    }


    function startLingerTimer() {
        _lingerTimeoutId = window.setTimeout(lingerTimer_tick, _lingerDuration);
    }

    function stopLingerTimer() {
        window.clearTimeout(_lingerTimeoutId);
    }

    function startMouseLeaveTimer() {
        _mouseLeaveTimeoutId = window.setTimeout(mouseLeaveTimer_tick, _mouseLeaveDelay);
    }

    function stopMouseLeaveTimer() {
        window.clearTimeout(_mouseLeaveTimeoutId);
    }




    init();
};
// -=====


// SlidePreviewEngine
// +===========================================================================
// Events: previewEngine-beginPreview, previewEngine-endPreview
// ============================================================================
Mediasite.Player.SlidePreviewEngine = function (containerElement) {

    var _standardLingerTime = 1.5 * 1000;
    var _quickPreviewLingerTime = 0.3 * 1000;
    var _quickPreviewWindow = 3.5 * 1000;
    var _exitPreviewDelay = 0.5 * 1000;

    var _container = $(containerElement);
    var _isEnabled = true;
    var _isPreviewing = false;
    var _initiatePreviewOnMouseLinger = false;
    var _lastPreviewEndTime = 0;
    var _subject;


    this.getIsEnabled = function () { return _isEnabled; };
    this.setIsEnabled = function (value) {
        if (value === _isEnabled) {
            return;
        }

        _isEnabled = value;

        if (_isEnabled && _subject != null) {
            _subject.reportMouseEnter();
        } else if (!_isEnabled) {
            exitPreviewMode();
        }
    };

    this.registerMouseEnter = function (thumb) {
        if (_subject) {
            _subject.die();
            _subject = null;
        }

        _initiatePreviewOnMouseLinger = true;

        _subject = new Mediasite.Player.SlidePreviewSubject(thumb, _container, getLingerDuration(), _exitPreviewDelay);
        _subject.getThumb().getElement().bind({
            'mouseLinger': subject_mouseLinger,
            'delayedMouseLeave': subject_delayedMouseLeave
        });
        _subject.reportMouseEnter();
    };



    function subject_mouseLinger(e) {
        if (_isEnabled && _initiatePreviewOnMouseLinger && _subject) {
            _isPreviewing = true;
            _initiatePreviewOnMouseLinger = false;
            onBeginPreview(_subject.getThumb());
        }
    }

    function subject_delayedMouseLeave(e) {
        if (_subject) {
            _subject.die();
            _subject = null;
        }

        exitPreviewMode();
    }

    function exitPreviewMode() {
        if (_isPreviewing) {
            _lastPreviewEndTime = new Date().getTime();
            _isPreviewing = false;
            onEndPreview();
        }
    }

    function allowQuickPreview() {
        var endOfQuickPreviewWindow = _lastPreviewEndTime + _quickPreviewWindow;
        return (_isPreviewing || endOfQuickPreviewWindow > new Date().getTime());
    }

    function getLingerDuration() {
        return allowQuickPreview() ? _quickPreviewLingerTime : _standardLingerTime;
    }




    // events
    // +=====

    function onBeginPreview(thumb) {
        _container.trigger('previewEngine-beginPreview', { thumb: thumb });
    }

    function onEndPreview() {
        _container.trigger('previewEngine-endPreview');
    }

    // -=====
};

// -=====


// SlidePreviewAnimation
// +===========================================================================
Mediasite.Player.SlidePreviewAnimation = function (containerElement) {
    var _container = $(containerElement);
    var _overlayBox, _box, _img;


    _overlayBox = $('<div />')
        .addClass('slidePreviewAnimation')
        .css({
            'position': 'absolute',
            'top': 0,
            'left': 0,
            'display': 'none',
            'z-index': 9999
        })
        .appendTo(_container);

    _box = $('<div />')
        .css({
            'position': 'absolute',
            'outline': '2px solid white',
            'overflow': 'hidden',
            'background': 'black'
        })
        .appendTo(_overlayBox);

    _img = $('<img />')
        .css({
            'display': 'block',
            'height': '100%'
        })
        .appendTo(_box);


    this.animate = function (src, thumbRect, slideRect) {

        if (!src || !src.resolve) {
            src = $.Deferred().resolve(src);
        }
        src.done(function (src) {
            _img.attr('src', src);
        });

        _overlayBox.css({
            'display': 'block',
            'opacity': 1.0
        });

        _box.css({
            'width': thumbRect.width() + 'px',
            'height': thumbRect.height() + 'px',
            'left': thumbRect.left() + 'px',
            'top': thumbRect.top() + 'px'
        });

        _box.animate({
            'width': slideRect.width() + 'px',
            'height': slideRect.height() + 'px',
            'left': slideRect.left() + 'px',
            'top': slideRect.top() + 'px'
        }, 400, 'linear');

        _overlayBox.animate({
            'opacity': 0.0
        }, 290, 'linear', opacityAnimationComplete);

    }


    function opacityAnimationComplete() {
        _overlayBox.css({
            'display': 'none',
            'opacity': 0.0
        });
    }
};

// -=====


// SlideItem
// +===========================================================================
Mediasite.Player.SlideItem = function (slides, index) {
    var _this = this;
    var _slides = slides;
    var _index = index;
    var _range = 1;
    var _count = 1;
    var _expandItemDelegate;
    var _collapseItemDelegate;


    this.getFirstSlide = function () { return _slides[_index]; };

    this.getIndex = function () { return _index; };

    this.getCount = function () { return _count; };
    this.setCount = function (value) { _count = value; };

    this.getRange = function () { return _range; };
    this.setRange = function (value) { _range = value; };

    this.getIsExpandable = function () { return _range > 1 && _count > 1; };

    this.getIsCollapsible = function () { return _range > 1 && _count == 1; };

    this.getIsExpanded = function () { return this.getIsCollapsible(); };
    this.setIsExpanded = function (value) {
        if (value && this.getIsExpandable()) {
            onExpandItem();
        } else if (this.getIsCollapsible()) {
            onCollapseItem();
        }
    };

    this.setExpandItemDelegate = function (value) { _expandItemDelegate = value; };
    this.setCollapseItemDelegate = function (value) { _collapseItemDelegate = value; };


    function onExpandItem() {
        if (_expandItemDelegate) {
            _expandItemDelegate(_this);
        }
    }

    function onCollapseItem() {
        if (_collapseItemDelegate) {
            _collapseItemDelegate(_this);
        }
    }

};
// -===========================================================================


// SlidePartitions
// +===========================================================================
// Events: currentpagechanged
// ============================================================================
Mediasite.Player.SlidePartitions = function (slides, itemsPerPage, stackingParams) {
    var _slides = [];
    var _stackedSlides = [];
    var _currentItems = [];
    var _itemsPerPage = 1000 * 1000 * 1000;
    var _pageIndex = 0;
    var _pages = 1;
    var _eventSender = $('<div></div>'); // To support multiple subscribers, use event binding rather than delegates.


    function init() {
        _itemsPerPage = itemsPerPage || Mediasite.Player.SlidePartitions.MaxItemsPerPage;
        _stackingParams = stackingParams || Mediasite.Player.SlidePartitions.DefaultStackingParams;

        updateSlides(slides);
        updateCurrentPage(0);
    }

    function updateSlides(slides) {
        _slides = slides;
        updateStackedSlides();
        updateCurrentPage(_pageIndex);
    }

    function updateStackedSlides(firstNewSlide) {
        if (_slides && typeof _slides.length === 'number' && _slides.length > 0) {
            if (_stackingParams && _slides.length > _stackingParams.minimumToTrigger) {
                //     [ Comment copied from Silverlight implementation. ]
                // This grouping logic is a little simplistic, but it's a direct translation 
                // of the way the current html viewer's slide stacking works... 
                // In other words, this is just a baseline implementation, and I hope to make a better
                // version of it soon.


                var currentGroup = _stackedSlides[_stackedSlides.length - 1];
                if (!currentGroup) {
                    currentGroup = new Mediasite.Player.SlideItem(_slides, 0);
                    _stackedSlides.push(currentGroup);
                }

                var startSlide = currentGroup.getIndex();
                var lastTime = currentGroup.getFirstSlide().Time;
                var thisTime, nextTime;

                var ix;
                for (ix = startSlide + 1; ix < _slides.length; ix++) {
                    thisTime = _slides[ix].Time;
                    nextTime = (ix + 1 === _slides.length) ? Infinity : _slides[ix + 1].Time; // For the last slide, the next slide's time is Infinity.

                    if (thisTime - lastTime > _stackingParams.threshold || nextTime - thisTime > _stackingParams.threshold || currentGroup.getRange() >= _stackingParams.maxItems) {
                        currentGroup = new Mediasite.Player.SlideItem(_slides, ix);
                        _stackedSlides.push(currentGroup);
                    } else {
                        // if we're about to become expandable
                        if (!currentGroup.getIsExpandable()) {
                            currentGroup.setExpandItemDelegate(expandItem);
                            currentGroup.setCollapseItemDelegate(collapseItem);
                        }

                        currentGroup.setRange(currentGroup.getRange() + 1);
                        currentGroup.setCount(currentGroup.getCount() + 1);
                    }

                    lastTime = _slides[ix].Time;
                }
            } else {
                var lastSlideItem = _stackedSlides[_stackedSlides.length - 1];
                var startSlide = lastSlideItem ? (lastSlideItem.getIndex() + 1) : 0;

                var i;
                for (i = startSlide; i < _slides.length; i++) {
                    _stackedSlides.push(new Mediasite.Player.SlideItem(_slides, i));
                }
            }
        }
    }


    // accessors
    this.updateSlides = function(slides) {
        updateSlides(slides);
    };

    this.getPageIndex = function () { return _pageIndex; };
    this.setPageIndex = function (value) {
        if (value < 0 || value >= _pages) {
            throw 'setPageIndex was passed an out of range value: ' + value;
        }

        updateCurrentPage(value);
    };

    this.getItemsPerPage = function () { return _itemsPerPage; };
    this.setItemsPerPage = function (value) {
        _itemsPerPage = value;
        updateCurrentPage(_pageIndex);
    };

    this.getPageCount = function () { return _pages; };

    this.getCurrentPageItems = function () { return _currentItems; };

    // methods
    this.bind = function () {
        _eventSender.bind.apply(_eventSender, arguments);
    };

    this.unbind = function () {
        _eventSender.unbind.apply(_eventSender, arguments);
    };


    // events
    function onCurrentPageChanged() {
        _eventSender.trigger('currentpagechanged');
    }


    // private functions
    function expandItem(slideItem) {
        if (slideItem.getIsExpandable()) {
            var index = jQuery.inArray(slideItem, _stackedSlides);

            var ix;
            for (ix = 1; ix < slideItem.getCount(); ix++) {
                var expandedItem = new Mediasite.Player.SlideItem(_slides, slideItem.getIndex() + ix);

                _stackedSlides.splice(index + ix, 0, expandedItem);
            }

            slideItem.setCount(1);

            updateCurrentPage(_pageIndex);
        }
    }

    function collapseItem(slideItem) {
        if (slideItem.getIsExpanded) {
            var index = jQuery.inArray(slideItem, _stackedSlides);

            var ix;
            for (ix = index + slideItem.getRange() - 1; ix > index; ix--) {
                _stackedSlides.splice(ix, 1);
            }

            slideItem.setCount(slideItem.getRange());

            updateCurrentPage(_pageIndex);
        }
    }

    function updateCurrentPage(pageIndex) {
        var actualItemsPerPage = Math.min(_itemsPerPage, _stackedSlides.length);

        _pages = Math.ceil(_stackedSlides.length / _itemsPerPage);
        _pageIndex = Math.min(pageIndex, _pages - 1);

        var start = actualItemsPerPage * _pageIndex;
        var end = Math.min(start + actualItemsPerPage, _stackedSlides.length);

        var itemsOnPage = [];

        var ix;
        for (ix = start; ix < end; ix++) {
            itemsOnPage.push(_stackedSlides[ix]);
        }

        _currentItems = itemsOnPage;
        onCurrentPageChanged();
    }



    init(); // Kick it off.
};
Mediasite.Player.SlidePartitions.MaxItemsPerPage = Math.pow(2, 32) - 1;
Mediasite.Player.SlidePartitions.DefaultStackingParams = { threshold: 2.0 * 1000, minimumToTrigger: 25, maxItems: 10 };
// -===========================================================================


Mediasite.DocumentMouseCapture = function () {
    this.captureMouse = function (capturer) {

    };

    this.releaseMouse = function (capturer) {

    };
}

// interface for mouse capturer
Mediasite.IMouseCapturer = function () {
    this.onMouseMove = function (e) { };
    this.onMouseDown = function (e) { }; // probably not used much.
    this.onMouseUp = function (e) { };
}
