/// <reference path="jquery-1.5.2-vsdoc.js" />
/// <reference path="jquery-1.5.2.js" />
/// <reference path="Mediasite.Core.js" />
/// <reference path="Mediasite.Geometry.js" />
/// <reference path="Mediasite.Css.js" />
/// <reference path="SlideInspector.js" />

if (!Mediasite) {
    var Mediasite = {};
}
Mediasite.Player = Mediasite.Player || {};


// SlideViewer
// +==================================================================
// Events: navigate, tap
// ===================================================================
Mediasite.Player.SlideViewer = function (element, slideUrlProvider) {

    var _this = this;
    var _element;
    var _slideUrlProvider;
    var _panelCssClass = 'panel';
    var _panels = []; // instances of SlideViewerPanel
    var _panelCount = 3; // should be an odd integer.
    var _panelsPerSide = Math.floor(_panelCount / 2);
    var _currentPanelIndex = 0;
    var _panelOffset = 0;
    var _dragDistance = 0;
    var _returnForceInterval = null;
    var _isGrabbed = false;
    var _slides = [];
    var _currentSlideIndex = -1;
    var _minSlideIndex = 0;
    var _maxSlideIndex = 0;
    var _slideAspectRatio = 4 / 3;
    var _startSlideInfo = null;
    var _endSlideInfo = null;
    var _emptySlideInfo = null;
    var _pendingNavigateTo = null;
    var _hud;
    var _canHideZoomHud = false;
    var _canShowZoomHud = true;
    var _isZoomHudVisible = true;
    var _zoomHudVisibilityAnimationDuration = 0;
    var _isNavigating = false;

    function init() {
        _element = $(element);
        _element.css('-ms-touch-action', 'none');

        _slideUrlProvider = slideUrlProvider || new DummySlideUrlProvider();

        _emptySlideInfo = makeSpecialSlideInfo(new Mediasite.Slide(0, 0, 'Empty Slide', ''), null, null);

        var hudElement = $('<div class="zoomhud" />').appendTo(_element);
        _hud = new Mediasite.Player.ZoomHud(hudElement);
        _hud.bind({
            'zoominclicked': hud_zoominclicked,
            'zoomoutclicked': hud_zoomoutclicked
        });

        var i;
        for (i = 0; i < _panelCount; i++) {
            _panels[i] = makePanel();
        }

        makePanels();
        layoutPanels();

        startReturnForce();

        _element.bind('containerresize', element_resize);
    }


    this.getPanelCount = function () { return _panelCount; };
    this.setPanelCount = function (value) {
        // The number of panels should always be odd.
        // If an even number is passed in here, we add one.
        _panelsPerSide = Math.floor(value / 2);
        _panelCount = (_panelsPerSide * 2) + 1;

        makePanels();
        assignSlidesToPanels();
        layoutPanels();
    };

    function makePanels() {
        $('.panel', _element).remove();
        _panels = [];

        var i;
        for (i = 0; i < _panelCount; i++) {
            _panels[i] = makePanel();
        }
    }

    this.getSlideUrlProvider = function () { return _slideUrlProvider; };
    this.setSlideUrlProvider = function (value) {
        _slideUrlProvider = value;
        assignSlidesToPanels();
    };

    this.getSlides = function () {
        return _slides;
    };
    this.setSlides = function (value) {
        _slides = value;
        updateSlideIndexBounds();
        assignSlidesToPanels();
        layoutPanels();
    };

    this.getCurrentSlideIndex = function () {
        return Math.min(Math.max(_currentSlideIndex, _minSlideIndex), _maxSlideIndex);
    };
    this.setCurrentSlideIndex = function (value) {
        _currentSlideIndex = Math.min(_maxSlideIndex, Math.max(_minSlideIndex, value));
        assignSlidesToPanels();
        layoutPanels();
    };

    this.getSlideAspectRatio = function () {
        return _slideAspectRatio;
    };
    this.setSlideAspectRatio = function (value) {
        _slideAspectRatio = value;
    };

    this.getHasStartSlide = function () { !!_startSlideInfo; };

    this.setStartSlide = function (slide, imageUrl, linkUrl) {
        _startSlideInfo = makeSpecialSlideInfo(slide, imageUrl, linkUrl);

        updateSlideIndexBounds();
        assignSlidesToPanels();
        layoutPanels();
    };

    this.getHasEndSlide = function () { !!_endSlideInfo };

    this.setEndSlide = function (slide, imageUrl, linkUrl) {
        _endSlideInfo = makeSpecialSlideInfo(slide, imageUrl, linkUrl);

        updateSlideIndexBounds();
        assignSlidesToPanels();
        layoutPanels();
    };

    this.getCanHideZoomHud = function () { return _canHideZoomHud; };
    this.setCanHideZoomHud = function (value) {
        _canHideZoomHud = value;
        updateZoomZoomHudVisibility();
    };

    this.getCanShowZoomHud = function () { return _canShowZoomHud; };
    this.setCanShowZoomHud = function (value) {
        _canShowZoomHud = value;
        updateZoomZoomHudVisibility();
    };

    this.getZoomHudVisibilityAnimationDuration = function () { return _zoomHudVisibilityAnimationDuration; };
    this.setZoomHudVisibilityAnimationDuration = function (value) {
        _zoomHudVisibilityAnimationDuration = value;
    };


    var _raiseTapOnPanelRelease = false;


    // event handlers
    // +=============
    function panel_grab(e, data) {
        // Disable the force that moves the panels back to their resting position 
        // while the user is holding a panel. 
        // This ensures that a panel in motion will stop when the user grabs it.
        _isGrabbed = true;
        _raiseTapOnPanelRelease = true;
    }

    function panel_drag(e, data) {
        _raiseTapOnPanelRelease = false;

        var atTheEnd =
            (isRightmostSlide() && data.offset < 0) ||
            (isLeftmostSlide() && data.offset > 0);

        _dragDistance = atTheEnd ? Math.floor(data.offset / 2) : data.offset;
        layoutPanels();
    }

    function panel_release(e, data) {
        if (_raiseTapOnPanelRelease) {
            onTap();
        }

        if (isInspectorMode()) {
            return;
        }

        var shift = 0;
        var halfFrameWidth = _element.width() / 2;
        if (getTotalOffset() > halfFrameWidth) {
            // Dropped Right 
            shift = -1;
        } else if (getTotalOffset() < -halfFrameWidth) {
            // Dropped Left 
            shift = 1;
        } else if (data.swipeDirection === Mediasite.SwipeDirection.Right) {
            // Swiped Right 
            shift = -1;
        } else if (data.swipeDirection === Mediasite.SwipeDirection.Left) {
            // Swiped Left 
            shift = 1;
        }

        if ((isLeftmostSlide() && shift < 0) || (isRightmostSlide() && shift > 0)) {
            shift = 0;
        }

        setPanelOffset(_panelOffset + _dragDistance);
        _dragDistance = 0;
        shiftCurrentBy(shift);

        _isGrabbed = false;
    }

    function element_resize(e, data) {
        if (e.preventPropagation) { e.preventPropagation(); }

        // The containerresize event should only be handled when triggered from above.
        // If this handler is called from an event propagating up, 
        // then a handler farther down the DOM tree should prevent propagation.

        layoutPanels();

        forEachPanel(function (panel, index) {
            panel.resize();
        });
    }

    function panel_zoomfactorchanged(e, data) {
        if (this === _panels[_currentPanelIndex].getElement().get(0)) {
            updateZoomHud();
        }
    }

    function panel_capabilitieschanged(e, data) {
        if (this === _panels[_currentPanelIndex].getElement().get(0)) {
            updateZoomHud();
        }
    }

    function hud_zoominclicked(e, data) {
        var currentPanel = _panels[_currentPanelIndex];
        if (currentPanel) {
            currentPanel.zoomIn();
        }
    }

    function hud_zoomoutclicked(e, data) {
        var currentPanel = _panels[_currentPanelIndex];
        if (currentPanel) {
            currentPanel.zoomOut();
        }
    }

    // -=====


    function onNavigate(time) {
        _element.trigger('navigate', { time: time });
    }

    function onTap() {
        _element.trigger('tap');
    }


    function isInspectorMode() {
        return _panelCount === 1;
    }

    function updateSlideIndexBounds() {
        _minSlideIndex = (_startSlideInfo) ? -1 : 0;
        _maxSlideIndex = (_endSlideInfo) ? _slides.length : _slides.length - 1;
    }

    function isRightmostSlide() {
        return _this.getCurrentSlideIndex() === _maxSlideIndex;
    }

    function isLeftmostSlide() {
        return _this.getCurrentSlideIndex() === _minSlideIndex;
    }

    function assignSlidesToPanels() {
        forEachPanel(function (panel, i) {
            var logicalIndex = toLogicalIndex(i);
            var slideIndex = slideIndexForPanel(logicalIndex);

            if (isValidSlideIndex(slideIndex)) {
                var slideInfo = getSlideInfo(slideIndex);

                if (!slideInfo) { alert(slideIndex); }

                panel.setSlideImage(slideInfo.image);
                panel.setSlideToolTip(slideInfo.tooltip);
                panel.setSlideAspectRatio(_slideAspectRatio);
            }
        });
    }

    function existsSlideForLogicalIndex(logicalIndex) {
        return isValidSlideIndex(slideIndexForPanel(logicalIndex));
    }

    function slideIndexForPanel(logicalIndex) {
        return _this.getCurrentSlideIndex() + logicalIndex;
    }

    function isValidSlideIndex(slideIndex) {
        return (slideIndex >= _minSlideIndex && slideIndex <= _maxSlideIndex);
    }

    function getSlideInfo(slideIndex) {
        var result = null;

        if (slideIndex === -1) {
            result = _startSlideInfo;
        } else if (slideIndex === _slides.length) {
            result = _endSlideInfo;
        } else {

            var slide = _slides[slideIndex];

            if (slide) {

                var tooltip = (slide.Text) ? slide.Text : Mediasite.Player.Localization.Common.Slide + " " + slide.Number;
                tooltip += (slide.Description) ? ' - ' + slide.Description : '';

                result = {
                    image: new Mediasite.Player.SlideImage(slide.Number, _slideUrlProvider),
                    tooltip: tooltip,
                    time: slide.Time,
                    link: null
                };
            }
        }

        return result || _emptySlideInfo;
    }

    function makeSpecialSlideInfo(slide, imageUrl, linkUrl) {
        if (!slide) {
            return null;
        }

        return {
            image: {
                getSlideNumber: function () { return slide.Number; },
                getFullUrl: function () { return imageUrl; },
                getCacheFriendlyUrl: function (height, aspect) { return imageUrl; }
            },
            tooltip: slide.Text,
            time: slide.Time,
            link: linkUrl
        };
    }

    function shiftCurrentBy(indexDelta) {
        _currentSlideIndex = _this.getCurrentSlideIndex() + indexDelta;
        setCurrentPanelIndex(_currentPanelIndex + indexDelta);
        setPanelOffset(_panelOffset + (indexDelta * _element.width()));
        assignSlidesToPanels();
        layoutPanels();
    }

    function setCurrentPanelIndex(value) {
        var newPanelIndex = (value % _panelCount);
        while (newPanelIndex < 0) {
            newPanelIndex += _panelCount;
        }

        if (newPanelIndex !== _currentPanelIndex) {
            _currentPanelIndex = newPanelIndex;

            _pendingNavigateTo = _this.getCurrentSlideIndex();

            updateZoomHud();
        }
    }

    function updateZoomHud() {
        var currentPanel = _panels[_currentPanelIndex];

        if (_hud.getZoomFactor() !== currentPanel.getZoomFactor()) {
            _hud.setZoomFactor(currentPanel.getZoomFactor());
        }

        if (_hud.getZoomInEnabled() != currentPanel.getCanZoomIn()) {
            _hud.setZoomInEnabled(currentPanel.getCanZoomIn());
        }

        if (_hud.getZoomOutEnabled() != currentPanel.getCanZoomOut()) {
            _hud.setZoomOutEnabled(currentPanel.getCanZoomOut());
        }

        if (currentPanel.getCanZoomOut()) {
            if (!_hud.getIsExpanded()) {
                _hud.expand(200);
            }
        } else {
            if (_hud.getIsExpanded()) {
                _hud.collapse(200);
            }
        }

        updateZoomZoomHudVisibility();
    }

    function updateZoomZoomHudVisibility() {
        if (!_canShowZoomHud) {
            hideZoomHud();
            return;
        }

        if (!_canHideZoomHud) {
            if (!_isZoomHudVisible) {
                showZoomHud();
            }
            return;
        }

        if (!getCanZoomOut() && _isZoomHudVisible) {
            hideZoomHud();
        } else if (getCanZoomOut() && !_isZoomHudVisible) {
            showZoomHud();
        }
    }

    function showZoomHud() {
        _isZoomHudVisible = true;
        _hud.getElement().stop(true, true).fadeIn(_zoomHudVisibilityAnimationDuration);
    }

    function hideZoomHud() {
        _isZoomHudVisible = false;
        _hud.getElement().stop(true, true).fadeOut(_zoomHudVisibilityAnimationDuration);
    }

    function getCanZoomOut() {
        var current = _panels[_currentPanelIndex];
        if (current) {
            return current.getCanZoomOut();
        } else {
            return false;
        }
    }

    function startReturnForce() {
        _returnForceInterval = window.setInterval(returnForceInterval_tick, 20);
    }

    function returnForceInterval_tick() {
        if (_isGrabbed) {
            return; // Disabled!
        }

        if (_panelOffset === 0) {
            return; // Panels are already at rest.
        }

        // force increases with distance from resting position.
        var minimumDelta = 1;
        var maximumDelta = _element.width() / 5;
        var absoluteOffset = Math.abs(_panelOffset);
        var d = absoluteOffset / 4;
        var delta = Math.min(Math.max(d, minimumDelta), maximumDelta);
        delta = (delta > absoluteOffset) ? absoluteOffset : delta;

        if (_panelOffset < 0) {
            delta = -delta;
        }

        setPanelOffset(_panelOffset - delta);
        layoutPanels();
    }

    function forEachPanel(action) {
        var i;
        for (i = 0; i < _panelCount; i++) {
            action(_panels[i], i);
        }
    }

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

    function layoutPanels() {
        forEachPanel(function (panel, i) {
            var logicalIndex = toLogicalIndex(i);
            var left, top;

            if (existsSlideForLogicalIndex(logicalIndex)) {
                var position = (logicalIndex * _element.width() * 1.05) + getTotalOffset();
                left = position + 'px';
                top = '0';
            } else {
                left = '200%';
                top = '0';
            }

            if (areTransformsAvailable()) {
                panel.getElement().css(Mediasite.Css.getCssPropertyName('transform'), 'translate3d(' + left + ', ' + top + ', 0)');
            } else {
                panel.getElement().css({ 'left': left, 'top': top });
            }
        });

        // If the user has stopped manipulating the slides and the slides 
        // have come to rest, trigger the navigate event.
        if (!_isGrabbed && !_isNavigating && Math.abs(getTotalOffset()) === 0) {
            if (_pendingNavigateTo === _this.getCurrentSlideIndex()) {
                _isNavigating = true;
                onNavigate(getSlideInfo(_pendingNavigateTo).time);
                _isNavigating = false;
            }
            _pendingNavigateTo = null;
        }
    }



    function getTotalOffset() {
        if (isInspectorMode()) {
            return 0;
        }

        return _panelOffset + _dragDistance;
    }

    function setPanelOffset(value) {
        // Besides initialization, this should be the only place that 
        // the value of _panelOffset is set.
        _panelOffset = value;


        if (_panelOffset != 0) {
            return;
        }

        // Zoom all the way out of the panels adjacent to the current panel, 
        // if they are not currently dragged into view.
        forEachPanel(function (panel, i) {
            var logicalIndex = toLogicalIndex(i);
            if ((logicalIndex === -1 && getTotalOffset() <= 0) ||
                (logicalIndex === 1 && getTotalOffset() >= 0)) {

                panel.zoomAllOut();
            }
        });
    }

    function makePanel() {
        var div = $('<div></div>').addClass(_panelCssClass).css({
            'width': '100%',
            'height': '100%',
            'position': 'absolute'
        });
        _element.append(div);

        var panel = new Mediasite.Player.SlideViewerPanel(div);

        panel.getElement().bind({
            'panel-grab': panel_grab,
            'panel-drag': panel_drag,
            'panel-release': panel_release,
            'panel-zoomfactorchanged': panel_zoomfactorchanged,
            'panel-capabilitieschanged': panel_capabilitieschanged
        });

        return panel;
    }

    // Convert typical zero based array indices to a system in which 
    // the current panel is index zero, panels to the left have negative 
    // indices, and panels to the right have positive indices.
    function toLogicalIndex(index) {
        var logical = index - _currentPanelIndex;
        if (logical > _panelsPerSide) {
            logical -= _panelCount;
        } else if (logical < -_panelsPerSide) {
            logical += _panelCount;
        }
        return logical;
    }


    function DummySlideUrlProvider() {

        this.GetSlideUrl = function (number) { return null; };
        this.GetCacheFriendlySlideUrl = function (number, height, aspect) { return null; };

    }


    init();
};

// -=====


// SlideViewerPanel
// +===========================================================================
// Events: panel-grab, panel-drag, panel-release, 
//         panel-zoomfactorchanged, panel-capabilitieschanged
// ============================================================================
Mediasite.Player.SlideViewerPanel = function (element) {
    var _element, _slideElement; // primary elements
    var _slide, _viewport, _zoomEngine;

    var _dragging = false;
    var _dragOffset = 0;
    var _pendingSwipe = null;
    var _pointerTimeout = null;


    function init() {
        // Primary Elements
        _element = $(element).css(getSelectFocusDragCss()).css('overflow', 'hidden').attr("tabindex", "-1");
        if (_element.css('position') == 'static') {
            _element.css('position', 'relative');
        }
        _slideElement = $('<div class="slideviewer-zoomable-slide" />').appendTo(_element);

        // Logical Actors
        _slide = new Mediasite.Player.ZoomableSlide(_slideElement);
        _viewport = new Mediasite.Player.ZoomViewport(_element);
        _zoomEngine = new Mediasite.Player.ZoomEngine();

        // Make the element swipeable before binding to the touch events,
        // so that we'll receive the swipe events prior to the touchend event. 
        // This is important because we want to know about the swipe before 
        // triggering the release event.
        new Mediasite.Swipeable(element);

        _viewport.bind({
            'mousedragged': viewport_mousedragged,
            'mousewheelup': viewport_mousewheelup,
            'mousewheeldown': viewport_mousewheeldown,
            'sizechanged': viewport_sizechanged,
            'pinched': viewport_pinched
        });

        _slide.bind('nativesizechanged', slide_nativesizechanged);

        _zoomEngine.bind({
            'contentrectchanged': zoomEngine_contentrectchanged,
            'capabilitieschanged': zoomEngine_capabilitieschanged,
            'zoomfactorchanged': zoomEngine_zoomfactorchanged,
            'overpan': zoomEngine_overpan
        });

        if (window.navigator.msPointerEnabled) {
            var myEl = _element.get(0);
            myEl.addEventListener('MSPointerDown', element_touchstart);
            myEl.addEventListener('MSPointerMove', element_touchmove);
            myEl.addEventListener('MSPointerUp', element_touchend);
            myEl.addEventListener('MSPointerCancel', element_touchcancel);
            myEl.addEventListener('MSPointerOut', element_pointerout);
            myEl.addEventListener('MSPointerOver', element_pointerover);
        } else {
            _element.bind({
                'touchstart': element_touchstart,
                'touchmove': element_touchmove,
                'touchend': element_touchend,
                'touchcancel': element_touchcancel
            });
        }

        _element.bind('swipe', element_swipe);

        // Coax the ZoomEngine to perform an initial content rect calculation.
        _zoomEngine.setViewportSize(_viewport.getSize());
    }


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

    this.getTotalDragOffset = function () { return _dragOffset; };

    this.getSlideImage = function () { return _slide.getSlideImage(); };
    this.setSlideImage = function (value) {
        _slide.setSlideImage(value);
    };

    this.getSlideToolTip = function () { return _slide.getToolTip(); };
    this.setSlideToolTip = function (value) { _slide.setToolTip(value); };

    this.getSlideAspectRatio = function () { return _slide.getAspectRatio(); };
    this.setSlideAspectRatio = function (value) { _slide.setAspectRatio(value); };

    // Delagate to some ZoomEngine properties.
    this.getZoomFactor = function () { return _zoomEngine.getZoomFactor(); };
    this.getCanZoomIn = function () { return _zoomEngine.getCanZoomIn(); };
    this.getCanZoomOut = function () { return _zoomEngine.getCanZoomOut(); };


    // Delegate to some ZoomEngine methods.
    this.zoomAllOut = function () {
        if (_zoomEngine.getCanZoomOut()) {
            _zoomEngine.zoomAllOut();
        }
    };
    this.zoomIn = function () { _zoomEngine.zoomIn(); };
    this.zoomOut = function () { _zoomEngine.zoomOut(); };



    this.resize = function () {
        _zoomEngine.setViewportSize(_viewport.getSize());
    };


    // = Event Handlers =
    function viewport_mousedragged(e, data) {

        var xSum = _dragOffset + data.delta.x;
        var crossesZero = (_dragOffset < 0 && xSum >= 0) || (_dragOffset > 0 && xSum <= 0);

        if (crossesZero) {
            var overZero = xSum;
            setDragOffset(0);
            _zoomEngine.pan(overZero, data.delta.y);
        } else if (_dragOffset !== 0) {
            setDragOffset(xSum);
            _zoomEngine.pan(0, data.delta.y);
        } else { // _dragOffset === 0
            _zoomEngine.pan(data.delta.x, data.delta.y);
        }


    }
    function viewport_mousewheelup(e, data) {
        _zoomEngine.zoomIn(data.position);
    }
    function viewport_mousewheeldown(e, data) {
        _zoomEngine.zoomOut(data.position);
    }
    function viewport_sizechanged() {
        _zoomEngine.setViewportSize(_viewport.getSize());
    }
    function viewport_pinched(e, data) {
        if ((data.scaleFactor === 1) || // Huh?
            (data.scaleFactor > 1 && !_zoomEngine.getCanZoomIn()) || // We're already zoomed all the way in.
            (data.scaleFactor < 1 && !_zoomEngine.getCanZoomOut())) { // We're already zoomed all the way out.

            return;
        }

        // If the panel is at rest, we zoom in on the center of the pinch, just like the SlideInspector control.
        // However, if the panel is dragged either left or right, the user can see the edge of the panel and 
        // will expect the zoom to keep the edge of the slide image at the panel edge. To accomplish this, 
        // zoom in as if the pinch happened at the very edge of the panel and adjust the drag offset so that 
        // the actual pinch location remains under the user's fingers.

        if (Math.abs(_dragOffset) < 10) {
            setDragOffset(0);
            _zoomEngine.zoom(data.position, data.scaleFactor);
        } else {
            var relativeX = data.position.x() - _element.offset().left; // pinch position relative to _element.
            var growthFactor = data.scaleFactor - 1.0;
            var edgeX = (_dragOffset < 0) ? _element.width() : 0;
            var touchDistanceFromEdge = relativeX - edgeX;
            var slideGrowth = touchDistanceFromEdge * growthFactor;
            var newOffset = _dragOffset - slideGrowth;
            newOffset = (_dragOffset < 0) ? Math.min(newOffset, 0) : Math.max(newOffset, 0);
            setDragOffset(newOffset);

            var zoomPosition = new Mediasite.Geometry.Point(edgeX, data.position.y());
            _zoomEngine.zoom(zoomPosition, data.scaleFactor);
        }
    }
    function slide_nativesizechanged() {
        _zoomEngine.setNativeSize(_slide.getNativeSize());
    }
    function zoomEngine_contentrectchanged(e, data) {
        _slide.applyRect(data.rect, data.mayAnimate);
    }
    function zoomEngine_capabilitieschanged() {
        _element.trigger('panel-capabilitieschanged');

        _element.css('cursor', (_zoomEngine.getCanZoomOut()) ? 'pointer' : 'default');
    }
    function zoomEngine_zoomfactorchanged() {
        _element.trigger('panel-zoomfactorchanged');
    }
    function zoomEngine_overpan(e, data) {
        if (!isDragging()) {
            return;
        }

        if (data.x === 0) {
            return;
        }

        setDragOffset(_dragOffset + Math.round(data.x))
    }




    function element_touchstart(event) {
        if (!isSingleTouch(event)) {
            return;
        }

        beginDragSession();

        _element.trigger('panel-grab');
    }

    function element_touchmove(event) {
        // The move action now happens in zoomEngine_overpan.
    }

    function element_touchend(event) {
        if (isDragging()) {
            var releaseEventArgs = { offset: _dragOffset, swipeDirection: Mediasite.SwipeDirection.None };
            if (_pendingSwipe && _pendingSwipe.session === getCurrentDragSession()) {
                releaseEventArgs.swipeDirection = _pendingSwipe.direction;
            }

            endDragSession();

            setDragOffset(0); // Doing this after ending the drag session will not trigger a 'panel-drag' event.
            _element.trigger('panel-release', releaseEventArgs);
        }
    }

    function element_touchcancel(event) {
        element_touchend(event);
    }

    function element_swipe(e, data) {
        if (_dragOffset == 0) {
            // A true swipe should result in a non-zero drag offset.
            // If the offset is zero, we're probably zoomed in on a 
            // slide and the user just performed a fast pan. We only 
            // want to register a swipe if an image is panned all the 
            // way to the panel's edge.
            return;
        }

        if (!data || !data.direction) {
            return;
        }

        if (data.direction !== Mediasite.SwipeDirection.Left && data.direction !== Mediasite.SwipeDirection.Right) {
            // We only care about swipes to the right or left.
            return;
        }

        _pendingSwipe = { session: getCurrentDragSession(), direction: data.direction };
    }

    function element_pointerout(event) {
        _pointerTimeout = setTimeout(element_touchend, 50);
    }

    function element_pointerover(event) {
        clearTimeout(_pointerTimeout);
    }


    // Drag Session Management
    // +======================
    var _dragCounter = 0; // increments for each drag session.
    var _currentDragSession = null;

    function beginDragSession() {
        _currentDragSession = ++_dragCounter;
    }

    function endDragSession() {
        _currentDragSession = null;
    }

    function isDragging() {
        return typeof _currentDragSession === 'number';
    }

    function getCurrentDragSession() { return _currentDragSession; }
    // -=====


    function getSelectFocusDragCss() {
        var prop = Mediasite.Css.getCssPropertyName;

        var result = {};
        result[prop('user-select')] = 'none';
        //result[prop('user-focus')] = 'ignore';
        result[prop('user-drag')] = 'none';

        return result;
    }

    function setDragOffset(value) {
        // Besides initialization, this should be the only place that 
        // the value of _dragOffset is set.
        _dragOffset = value;

        if (isDragging()) {
            _element.trigger('panel-drag', { offset: _dragOffset });
        }
    }

    function isSingleTouch(event) {
        if (window.navigator.msPointerEnabled) {
            return event.pointerType !== event.MSPOINTER_TYPE_MOUSE && event.pointerType !== "mouse" && event.isPrimary;
        } else {
            var e = event.originalEvent;
            return (e && e.touches && e.touches.length === 1);
        }
    }


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


// Swipeable
// +==================================================================
// Events: swipe, swipeleft, swiperight, swipeup, swipedown
// ===================================================================
Mediasite.Swipeable = function (element) {

    var _element;
    var _tracker = null;

    function init() {
        _element = $(element);

        if (window.navigator.msPointerEnabled) {
            var myEl = _element.get(0);
            myEl.addEventListener('MSPointerDown', element_touchstart);
            myEl.addEventListener('MSPointerMove', element_touchmove);
            myEl.addEventListener('MSPointerUp', element_touchend);
            myEl.addEventListener('MSPointerCancel', element_touchcancel);
        } else {
            _element.bind({
                'touchstart': element_touchstart,
                'touchmove': element_touchmove,
                'touchend': element_touchend,
                'touchcancel': element_touchcancel
            });
        }
    }


    function element_touchstart(event) {
        if (!isSingleTouch(event)) {
            return;
        }

        _tracker = new Mediasite.SwipeTracker();

        var point = getSingleTouchPagePosition(event);
        _tracker.reportPosition(point);
    }

    function element_touchmove(event) {
        event.preventDefault();
        if (!isSingleTouch(event)) {
            return;
        }

        if (_tracker) {
            var point = getSingleTouchPagePosition(event);
            _tracker.reportPosition(point);
        }
    }

    function element_touchend(event) {
        if (!_tracker) {
            return;
        }

        if (isSingleTouch(event)) {
            var point = getSingleTouchPagePosition(event);
            _tracker.reportPosition(point);
        }

        var direction = _tracker.getDetectedSwipe();
        if (direction != Mediasite.SwipeDirection.None) {
            onSwipe(direction);
        }

        _tracker = null;
    }

    function element_touchcancel(event) {
        _tracker = null;
    }


    function onSwipe(direction) {
        switch (direction) {
            case Mediasite.SwipeDirection.Up:
                _element.trigger('swipeup');
                break;
            case Mediasite.SwipeDirection.Down:
                _element.trigger('swipedown');
                break;
            case Mediasite.SwipeDirection.Left:
                _element.trigger('swipeleft');
                break;
            case Mediasite.SwipeDirection.Right:
                _element.trigger('swiperight');
                break;
            default:
                return; // None or bogus.
        }

        _element.trigger('swipe', { direction: direction });
    }

    function isSingleTouch(event) {
        if (window.navigator.msPointerEnabled) {
            return event.pointerType !== event.MSPOINTER_TYPE_MOUSE && event.pointerType !== "mouse" && event.isPrimary;
        } else {
            var e = event.originalEvent;
            return (e && e.touches && e.touches.length === 1);
        }
    }

    function getSingleTouchPagePosition(e) {
        if (window.navigator.msPointerEnabled) {
            return new Mediasite.Geometry.Point(e.pageX, e.pageY);
        } else {
            var ee = e.originalEvent;
            return new Mediasite.Geometry.Point(ee.touches[0].pageX, ee.touches[0].pageY);
        }
    }

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


Mediasite.SwipeTracker = function (nowProvider) {
    var _nowProvider = nowProvider || new Mediasite.nowProvider();
    var _samplePeriod = 500; // milliseconds.
    var _minimumDistance = 50; // pixels.
    var _minimumRate = 500 / 1000; // pixels per millisecond.
    var _detectedSwipe = Mediasite.SwipeDirection.None;
    var _spots = [];


    this.getDetectedSwipe = function () { return _detectedSwipe; };


    this.reportPosition = function (position) {
        _spots.push(new Spot(position, _nowProvider.now()));
        trimExpiredSpots();
        calculate();
    };


    function trimExpiredSpots() {
        // Don't trim down to just one, regardless of age.
        while (_spots.length > 2) {
            if (getTimeDelta() > _samplePeriod) {
                _spots.shift(); // The oldest spot is outside the sample period, so push it off a cliff.
            } else {
                break; // The oldest spot is within the sample period, so stop the bloodshed.
            }
        }
    }

    function getPositionDelta() {
        if (_spots.length < 1) {
            return new Mediasite.Geometry.Point(0, 0);
        }

        var o = getOldestSpot();
        var n = getNewestSpot();
        return new Mediasite.Geometry.Point(n.position.x() - o.position.x(), n.position.y() - o.position.y());
    }

    function getTimeDelta() {
        if (_spots.length < 1) {
            return 0;
        }

        return getNewestSpot().time.getTime() - getOldestSpot().time.getTime();
    }

    function getNewestSpot() { return _spots[_spots.length - 1]; }

    function getOldestSpot() { return _spots[0]; }

    function calculate() {
        var timeDelta = getTimeDelta();
        if (timeDelta <= 0) {
            _detectedSwipe = Mediasite.SwipeDirection.None;
            return; // No change in time, no swipey.
        }

        var positionDelta = getPositionDelta();
        var maxDistanceAlongAnAxis = Math.max(Math.abs(positionDelta.x()), Math.abs(positionDelta.y()));
        if (maxDistanceAlongAnAxis < _minimumDistance) {
            _detectedSwipe = Mediasite.SwipeDirection.None;
            return; // You call that little finger jitter a swipe? No swipey!
        }

        var actualRate = maxDistanceAlongAnAxis / timeDelta;
        if (actualRate < _minimumRate) {
            _detectedSwipe = Mediasite.SwipeDirection.None;
            return; // Awwww, so close, but no swipey.
        }

        // Looks like we have a swipe!
        _detectedSwipe = deltaToSwipeDirection(positionDelta);
    }

    function deltaToSwipeDirection(delta) {
        if (Math.abs(delta.x()) >= Math.abs(delta.y())) {
            return (delta.x() > 0) ? Mediasite.SwipeDirection.Right : Mediasite.SwipeDirection.Left;
        } else {
            return (delta.y() > 0) ? Mediasite.SwipeDirection.Down : Mediasite.SwipeDirection.Up;
        }
    }


    // inner class Spot
    // +===============
    function Spot(position, time) {
        this.position = position;
        this.time = time;
    }
    // -=====

};


Mediasite.SwipeDirection = {
    None: 'None',
    Left: 'Left',
    Right: 'Right',
    Up: 'Up',
    Down: 'Down'
};


Mediasite.nowProvider = function () {
    this.now = function () {
        return new Date();
    }
}

