/// <reference path="../ScriptReference/jquery-1.7.1-vsdoc.js"/>
/// <reference path="Mediasite.Core.js"/>
/// <reference path="Mediasite.Player.Core.js"/>
/// <reference path="Mediasite.Player.Localization.js"/>
/// <reference path="3rdParty/json2.js"/>
/*global Mediasite*/

Mediasite.namespace("Player");

Mediasite.Player.Model = function (dataProvider) {
    var $ = jQuery;
    var self = this;

    var presentation;
    var slideStream;
    var chapterTimingSearcher;
    var timedEventRepository;
    var timelineThingRepository;
    var timelineThingMonitor;
    var timedEventMarkerOptionsLookup;
    var captionTimeSearcher;
    var pauseTimingSearcher;
    var pauseEvents = [];
    var currentChapterTime = 0;
    var currentTimedEventTime = 0;
    var currentCaptionText;
    var currentMediaState;
    var rawMediaState;
    var currentMediaPosition = 0;
    var currentMediaPositionWasFromMediaPlayer = false;
    var currentlyPaused = false;
    var emailMessageTemplate;
    var inviteMessgeTemplate;
    var userEmailAddress;

    var pollForLiveStartTimer = null;
    var pollLiveInterval = 5000;
    var pollLiveWaitInterval = 15000;
    var pollLiveEventInterval = 10000;
    var pollLiveEventReadAheadWindow = 60 * 1000;
    var pingServerTimer = null;
    var pingServerInterval = 30000;
    var inactivityTimer = null;
    var inactivityInterval = 1200000;
    var inactivityMonitorInterval = 1000;
    var lastInactiveTime = null;
    var delayedCheckForTimelineThingsDelay = 1000; // milliseconds
    var delayedCheckForTimelineThingsTimeoutHandle;
    var _liveStartDelay = 0;
    var _liveJumpBackDetectionEnabled = false;
    var _isCheckingLiveStatusOnJumpBack = false;
    var _positionJumpBackThreshold = 1 * 1000; // milliseconds
    var _lastPositionUpdate = 0;
    var currentPlayerState = Mediasite.Player.PlayerState.NotReady;
    var _visibleStreamTypes = [];

    var defaultMarkerOptions = { color: 'white', text: ''};

    var liveEndTime = -1;
    var lastLiveEventTime = -1;
    var clockSkew = 0;

    var hasPerformedEndPresentation = false;
    var livePlayStalledDetector;

    var logger = Mediasite.Logging.LoggerFactory("PlayerModel");

    var _events = new Mediasite.Player.EventBundle();
    var _eventNames = Mediasite.Player.ModelEvent;

    this.AddEventHandler = function (eventName, handler) {
        _events.addHandler(eventName, handler);
    };

    function checkCurrentChapter(currentPosition) {
        var currentItem = self.GetChapterAt(currentPosition);

        if (typeof (currentItem) === "undefined" || currentItem.Number < 1) {
            if (currentPosition === 0) {
                return;
            }
            currentItem = new Mediasite.Chapter(0, 0, "");
        }
        if (currentChapterTime !== currentItem.Time) {
            currentChapterTime = currentItem.Time;
            _events.fire(_eventNames.ChapterChange, currentItem);
        }
    }


    function checkForTimelineThings() {
        var isPlaying, mediaTimeInSeconds, playbackRate, epocTimeInSeconds;

        delayedCheckForTimelineThings(); // Schedule a failsafe check.

        if (!currentMediaPositionWasFromMediaPlayer) {
            return;
        }

        isPlaying = (currentPlayerState.State === Mediasite.Player.PlayerState.Playing);
        mediaTimeInSeconds = (currentMediaPosition || 0) / 1000;

        timelineThingMonitor.updatePlayheadInfo(mediaTimeInSeconds, isPlaying);
    }

    function delayedCheckForTimelineThings() {
        clearTimeout(delayedCheckForTimelineThingsTimeoutHandle);
        delayedCheckForTimelineThingsTimeoutHandle = setTimeout(function () {
            checkForTimelineThings();
        }, delayedCheckForTimelineThingsDelay);
    }

    function timelineThingReachedDelegate(timedEvent) {
        logger.Info('TimedEventReached - time:' + timedEvent.time + ', type: ' + timedEvent.type);
        _events.fire(_eventNames.TimedEventReached, timedEvent);
    }

    function checkCurrentCaption(currentPosition) {
        if (presentation.DisableCaptionDisplay) {
            return;
        }

        var captions = self.GetCaptionsAt(currentPosition);
        var combinedCaptions = '';
        var i;
        for (i = 0; i < captions.length; i += 1) {
            if (captions[i] && typeof captions[i].Text === 'string') {
                combinedCaptions += captions[i].Text;
            }
        }

        if (combinedCaptions !== currentCaptionText) {
            currentCaptionText = combinedCaptions;
            _events.fire(_eventNames.CaptionChange, captions);
        }
    }


    function checkPauseState(currentPosition) {
        var pauseEvent = pauseTimingSearcher.GetCurrentItem(currentPosition);
        if (typeof (pauseEvent) !== "undefined") {
            if (pauseEvent.Paused !== currentlyPaused) {
                currentlyPaused = pauseEvent.Paused;
                if (currentlyPaused) {
                    presentation.PlayStatus = Mediasite.Player.PresentationStatus.LivePaused;
                }
                else {
                    presentation.PlayStatus = Mediasite.Player.PresentationStatus.Live;
                }
                _events.fire(_eventNames.LiveStatusChanged, presentation.PlayStatus);
            }
        }
    }

    // callback is passed a boolean indicating success (true).
    function getLiveEvents(callback) {
        var i;
        if (presentation.PlayStatus === Mediasite.Player.PresentationStatus.Live || presentation.PlayStatus === Mediasite.Player.PresentationStatus.LivePaused) {

            dataProvider.GetLiveEvents(JSON.stringify({ playbackTicket: presentation.PlaybackTicketId, sinceTimeInMs: lastLiveEventTime, currentTimeInMs: Math.floor(currentMediaPosition + pollLiveEventReadAheadWindow) }), successCallback, errorCallback);

            return true;
        }

        function successCallback(data) {
            try {

                var timedEvents = [];
                for (i = 0; i < data.d.length; i++) {
                    lastLiveEventTime = Math.max(lastLiveEventTime, data.d[i].Time);
                    switch (data.d[i].EventType) {
                        case Mediasite.Player.TimelineEvent.PresentationEnd:
                            logger.Debug('Received PresentationEnd');
                            liveEndTime = data.d[i].Time;
                            if (currentMediaState == Mediasite.Player.MediaState.Playing) {
                                var millisecondsTilEnd = Math.max(0, liveEndTime - currentMediaPosition);
                                logger.Debug('PresentationEnd in ' + millisecondsTilEnd / 1000 + ' seconds.');
                                window.setTimeout(PerformEndPresentation, millisecondsTilEnd);
                            } else {
                                PerformEndPresentation();
                            }
                            break;
                        case Mediasite.Player.TimelineEvent.Slide:
                            if (hasSlides()) {
                                var slide = new Mediasite.Slide(data.d[i].Sequence, data.d[i].Time, "", "");
                                slide.SetUrlProvider(slideStream.SlideUrlProvider);
                                slide.StreamType = slideStream.StreamType;
                                slideStream.LiveSlideQueue.push(slide);
                            }
                            break;
                        case Mediasite.Player.TimelineEvent.PauseStart:
                            pauseEvents.push({ Time: data.d[i].Time, Paused: true });
                            break;
                        case Mediasite.Player.TimelineEvent.PauseEnd:
                            pauseEvents.push({ Time: data.d[i].Time, Paused: false });
                            break;
                        case Mediasite.Player.TimelineEvent.TimedEvent:
                            timedEvents.push(data.d[i]);
                            break;
                    }
                }

                for (i = 0; i < timedEvents.length; i += 1) {
                    timelineThingRepository.addThing(Mediasite.Player.TimedEventUtility.toProperTimedEvent(timedEvents[i]));
                }

            } catch (e) { }

            if (callback) {
                callback(true);
            }
        }

        function errorCallback(xhr) {
            var skipCallback = false;
            try {
                var response = JSON.parse(xhr.responseText);
                if (response && response.FaultType === Mediasite.Player.PlayerServiceFaultType.InvalidTicket) {
                    _events.fire(_eventNames.InactivityExceeded);

                    // The callback is not called when the specific error is inactivity.
                    // NOTE: The original behavior is retained, but this smells bad. 
                    //       This funtion seems to "know" that the callback is used to 
                    //       reschedule the polling timeout. Not calling the callback 
                    //       function stops polling, but that decision should probably 
                    //       be made elsewhere.
                    skipCallback = true;
                }
            } catch (e) { }

            if (callback && !skipCallback) {
                callback(false);
            }

        }

        return false;
    }

    function pingServerTimerCallback() {
        var callback = function () { pingServerTimer = setTimeout(pingServerTimerCallback, pollLiveEventInterval); };
        getLiveEvents(callback);
    }

    function pollForLiveStartTimerCallback() {
        var successCallback = function (data) { pollForLiveStartCallbackSuccess(data); };
        var errorCallback = function () { pollForLiveStartTimer = setTimeout(pollForLiveStartTimerCallback, pollLiveWaitInterval); };
        dataProvider.GetLiveStatus(JSON.stringify({ playbackTicket: presentation.PlaybackTicketId, presentationId: presentation.PresentationId }), successCallback, errorCallback)
    }

    function pollForLiveStartCallbackSuccess(data) {
        presentation.PlayStatus = data.d.PlayStatus;
        switch (presentation.PlayStatus) {
            case Mediasite.Player.PresentationStatus.ScheduledForLive:
                _events.fire(_eventNames.LiveStatusChanged, presentation.PlayStatus);
                pollForLiveStartTimer = setTimeout(pollForLiveStartTimerCallback, pollLiveWaitInterval);
                break;
            case Mediasite.Player.PresentationStatus.OpenForLive:
                _events.fire(_eventNames.LiveStatusChanged, presentation.PlayStatus);
                pollForLiveStartTimer = setTimeout(pollForLiveStartTimerCallback, pollLiveInterval);
                break;
            case Mediasite.Player.PresentationStatus.Live:
            case Mediasite.Player.PresentationStatus.LivePaused:
                var liveStartTimeInMs = data.d.LiveStartUnixTimeInMs + clockSkew;
                var transitionToLiveDelay = calculateLiveStartDelay(liveStartTimeInMs, _liveStartDelay);

                logger.Debug('Delaying the transition from Waiting to Live by ' + transitionToLiveDelay + 'ms');
                window.setTimeout(function () {
                    var liveStartTime = new Date(data.d.LiveStartUnixTimeInMs + clockSkew);

                    function doTransitionToLive() {
                        
                        // Many browsers (e.g. Google Chrome) prevent the start of video playback 
                        // in background tabs. This can cause bad behavior. Having a player in a 
                        // background tab when a presentation is *waiting to begin" seems like a 
                        // common enough case. To remedy, let's wait until the page is visible. 
                        // See BUG46047.
                        if (document && document.visibilityState && document.visibilityState !== 'visible') {
                            logger.Info('Wait until window is visible to begin live playback.');
                            $(document).one('visibilitychange', function (e) {
                                logger.Info('Window is now visible. Begin live playback.');
                                doTransitionToLive();
                            });
                            return;
                        }

                        _events.fire(_eventNames.LiveStatusChanged, presentation.PlayStatus);
                        var liveStartTime = new Date(data.d.LiveStartUnixTimeInMs + clockSkew);

                        _events.fire(_eventNames.SetLiveStartTime, liveStartTime);
                        logger.Info("Live start time set", liveStartTime);

                        _events.fire(_eventNames.StartMedia, []);
                        logger.Info("Start media: ", []);

                        getLiveEvents();
                    }

                    // TODO: If any stream's selecteded source requires a requery on live, do it now.
                    if (presentation.mustRefreshVideoUrlsOnTransitionToLive) {
                        updateVideoUrls(doTransitionToLive);
                    } else {
                        doTransitionToLive();
                    }
                }, transitionToLiveDelay);
                break;
            case Mediasite.Player.PresentationStatus.LiveEnded:
            case Mediasite.Player.PresentationStatus.NotAvailable:
                _events.fire(_eventNames.LiveStatusChanged, presentation.PlayStatus);
                PerformEndPresentation();
                break;
        }
    }

    function earliestTimeToAttemptLive(liveStart, unsafeLiveStartWindow) {
        liveStart = liveStart || Number.POSITIVE_INFINITY; // Prevent missing liveStart value. UNEXPECTED.
        unsafeLiveStartWindow = unsafeLiveStartWindow || 0;

        return liveStart + unsafeLiveStartWindow;
    }

    function calculateLiveStartDelay(liveStart, unsafeLiveStartWindow) {
        var safe, now, result;

        safe = earliestTimeToAttemptLive(liveStart, unsafeLiveStartWindow);
        now = (new Date()).getTime();

        result = safe - now;
        result = Math.max(0, result); // A negative delay makes no sense.
        result = Math.min(unsafeLiveStartWindow, result); // A delay longer than the unsafe window makes no sense.

        return result;
    }

    function updateVideoUrls(callback) {
        logger.Info('Requesting updated stream info.');
        
        function successCallback(data) {
            var i, j, k;
            var freshStreams = data.d;
            var staleStreams = presentation.Streams;
            var staleStream, freshStream;
            var staleVideoUrl, freshVideoUrl;
            var propsToUpdate = ['MulticastAddress', 'MulticastDiagnosticsServer', 'MulticastPort', 'MulticastReceiverKeyStore', 'MulticastReceiverUrl'];
            var propName;

            // Iterate through the Streams.
            for (i = 0; i < freshStreams.length; i += 1) {
                freshStream = freshStreams[i];
                staleStream = findStreamInArray(staleStreams, freshStream.StreamType);

                if (!staleStream) {
                    continue;
                }

                // Iterate through the VideoUrls for the specific Stream object.
                for (j = 0; j < freshStream.VideoUrls.length; j += 1) {
                    freshVideoUrl = freshStream.VideoUrls[j];
                    staleVideoUrl = findVideoUrlInArray(staleStream.VideoUrls, freshVideoUrl.MediaType, freshVideoUrl.MimeType, freshVideoUrl.ServerType);

                    if (!staleVideoUrl) {
                        continue;
                    }

                    // Update properties on the specific VideoUrl object.
                    for (k = 0; k < propsToUpdate.length; k += 1) {
                        propName = propsToUpdate[k];
                        staleVideoUrl[propName] = freshVideoUrl[propName];
                    }
                }
            }


            callback();
        }

        function errorCallback(data) {
            // TODO: Log error, but there's nothing we can do.
            callback();
        }

        dataProvider.GetStreams(presentation.PlaybackTicketId, successCallback, errorCallback);
    }

    function findStreamInArray(array, streamType) {
        return findInArray(array, function(item) {
            return (item.StreamType === streamType);
        });
    }

    function findVideoUrlInArray(array, mediaType, mimeType, serverType) {
        return findInArray(array, function(item) {
            return (item.MediaType === mediaType &&
                item.MimeType === mimeType &&
                item.ServerType === serverType);
        });
    }

    function findInArray(array, testFunc) {
        var i;

        for (i = 0; i < array.length; i += 1) {
            if (testFunc(array[i])) {
                // Found a match!
                return array[i];
            }
        }

        // Didn't find a matching stream.
        return null;
    }

    function pollForLiveStart(interval) {
        clearTimeout(pollForLiveStartTimer);
        pollForLiveStartTimer = setTimeout(pollForLiveStartTimerCallback, interval);
    }

    function startServerPing() {
        clearTimeout(pingServerTimer);
        clearTimeout(inactivityTimer);
        pingServerTimer = setTimeout(pingServerTimerCallback, pollLiveEventInterval);
    }

    function inactivityTimerCallback() {
        var now = new Date().getTime();
        if (now - lastInactiveTime > inactivityInterval) {
            _events.fire(_eventNames.InactivityExceeded);
        }
        else {
            scheduleInactivityCheck();
        }
    }

    function stopServerPing() {
        clearTimeout(pingServerTimer);

        lastInactiveTime = new Date().getTime();

        scheduleInactivityCheck();
    }

    function scheduleInactivityCheck() {
        clearTimeout(inactivityTimer);
        inactivityTimer = setTimeout(inactivityTimerCallback, inactivityMonitorInterval);
    }

    this.Initialize = function (playerContext) {
        var i;

        if (!playerContext || !playerContext.CurrentPresentation || playerContext.CurrentPresentation.PresentationId == undefined) {
            return;
        }

        presentation = playerContext.CurrentPresentation;
        slideStream = presentation.GetStream(Mediasite.ContentStreamType.Slide);

        clockSkew = presentation.ServerClockSkew;
        document.title = presentation.Title;

        chapterTimingSearcher = new Mediasite.Player.TimingSearcher(presentation.Chapters);

        timelineThingRepository = new Mediasite.Player.TimelineThingRepository();
        for (i = 0; i < presentation.TimedEvents.length; i += 1) {
            timelineThingRepository.addThing(Mediasite.Player.TimedEventUtility.toProperTimedEvent(presentation.TimedEvents[i]));
        }

        timelineThingMonitor = new Mediasite.Player.TimelineThingMonitor();
        timelineThingMonitor.repository = timelineThingRepository;
        timelineThingMonitor.timelineThingReachedDelegate = timelineThingReachedDelegate;
        timelineThingMonitor.updatePlayheadInfo(0, false);

        timedEventMarkerOptionsLookup = new Mediasite.Player.LookupTable();
        
        var chapterTimedEvents = $.map(self.GetTimedEvents(), function (te, index) {
            if (te.type === "application/vnd.sonicfoundry.chapter.xml") {
                return te;
            } else {
                return;
            }
        });
        // Add timeline markers for Chapters if chapter navigation is allowed by player.
        if (playerContext.PlayerLayoutOptions && playerContext.PlayerLayoutOptions.AllowChapterNavigation) {
            $.each(self.GetTimedEvents(), function (index, te) {
                if (te.type === "application/vnd.sonicfoundry.chapter.xml") {
                    self.SetTimedEventMarkerOptions(te.id, { color: '#D4BF86', text: te.payload });
                }
            });
        }
        

        pauseTimingSearcher = new Mediasite.Player.TimingSearcher(pauseEvents);

        captionTimeSearcher = makeCaptionTimeSearcher(presentation.Transcript);

    };

    function makeCaptionTimeSearcher(transcript) {
        var i;
        var searcher = new Mediasite.Player.CaptionTimeSearcher();
        var searchableCaptions = transcriptToArrayOfSearchableTimeItems(transcript);

        for (i = 0; i < searchableCaptions.length; i += 1) {
            searcher.addItem(searchableCaptions[i]);
        }

        return searcher;
    }

    function transcriptToArrayOfSearchableTimeItems(transcript) {
        var searchableItems = [];
        var newSearchable;
        var tItem;

        for (i = 0; i < transcript.length; i += 1) {
            tItem = transcript[i];
            newSearchable = new Mediasite.Player.SearchableTimeItem(tItem.Time / 1000, tItem.EndTime / 1000, tItem);
            searchableItems.push(newSearchable);
        }

        return searchableItems;
    }

    this.LoadMedia = function () {
        switch (presentation.PlayStatus) {
            case Mediasite.Player.PresentationStatus.OnDemand:
                _events.fire(_eventNames.StartMedia, []);
                break;
            case Mediasite.Player.PresentationStatus.Live:
            case Mediasite.Player.PresentationStatus.LivePaused:
                if (hasSlides() && slideStream.Slides.length) {
                    lastLiveEventTime = slideStream.Slides[slideStream.Slides.length - 1].Time;
                }
                _events.fire(_eventNames.StartMedia, []);
                _events.fire(_eventNames.LiveStatusChanged, presentation.PlayStatus);
                if (presentation.LiveStartUnixTimeInMs) {
                    var liveStartTime = new Date(presentation.LiveStartUnixTimeInMs + clockSkew);
                    _events.fire(_eventNames.SetLiveStartTime, liveStartTime);
                    logger.Info("Live start time set", liveStartTime);
                }
                break;
            case Mediasite.Player.PresentationStatus.ScheduledForLive:
                _events.fire(_eventNames.LiveStatusChanged, presentation.PlayStatus);
                pollForLiveStart(pollLiveWaitInterval);
                break;
            case Mediasite.Player.PresentationStatus.OpenForLive:
                _events.fire(_eventNames.LiveStatusChanged, presentation.PlayStatus);
                pollForLiveStart(pollLiveInterval);
                break;
            case Mediasite.Player.PresentationStatus.LiveEnded:
            case Mediasite.Player.PresentationStatus.NotAvailable:
                _events.fire(_eventNames.LiveStatusChanged, presentation.PlayStatus);
                PerformEndPresentation();
                break;
        }
    };

    this.GetStreamsMetadata = function () {
        var streamTypes = $.map(presentation.Streams, function (stream) { return stream.StreamType; });
        var streams = $.map(streamTypes, presentation.GetStream.bind(presentation));
        var metadatas = $.map(streams, function (stream) {
            return {
                streamType: stream.StreamType,
                audioOnly: stream.AudioOnly,
                aspectRatio: stream.AspectRatio
            };
        });

        return metadatas;
    };

    this.GetVisibleStreamTypes = function () {
        return _visibleStreamTypes;
    };

    this.SetVisibleStreamTypes = function (streamTypes) {

        streamTypes = onlyUniqueStreamTypes(streamTypes);
        streamTypes = onlyValidStreamTypes(streamTypes);
        streamTypes = withoutAudioOnlyStreamTypes(streamTypes);

        _visibleStreamTypes = streamTypes;
        _events.fire(_eventNames.VisibleStreamsChanged, streamTypes);
    };

    this.GetLiveStatus = function () {
        return presentation.PlayStatus;
    };

    this.UpdateMediaInfo = function (mediaInfo) {
        var i, info, stream;

        // Update stream data to reflect reality. Sometimes the DB could contain incorrect data.
        if (mediaInfo && mediaInfo.length) {
            for (i = 0; i < mediaInfo.length; i += 1) {
                info = mediaInfo[i];
                stream = presentation.GetStream(info.StreamType);
                if (stream) {
                    stream.AudioOnly = info.AudioOnly;
                    stream.AspectRatio = info.AspectRatio;
                }
            }

            // It's possible that changes to the AudioOnly property of 
            // streams may result in a change to the visible streams. 
            // Reset the visible streams to allow detection of a change.
            this.SetVisibleStreamTypes(this.GetVisibleStreamTypes());
        }
    };

    this.UpdateMediaState = function (state) {
        if (state !== currentMediaState) {
            switch (state) {
                case Mediasite.Player.MediaState.Playing:
                    currentMediaState = state;
                    startServerPing();
                    getLiveEvents();
                    startDetectingLiveStalled();
                    checkForTimelineThings();
                    break;
                case Mediasite.Player.MediaState.MediaEnded:
                case Mediasite.Player.MediaState.Paused:
                case Mediasite.Player.MediaState.Stopped:
                    currentMediaState = state;
                    stopServerPing();
                    getLiveEvents();
                    break;
            }

            // currentMediaState does not always reflect the true media state, 
            // since it is only updated for some state changes. 
            // Until we determine whether that is intentional or not, let's 
            // store the current media state in a new variable (rawMediaState) 
            // so that we can provide the true state to LivePlayStalledDetector.
            rawMediaState = state;
        }
    };

    this.UpdatePlayerState = function (playerState) {
        currentPlayerState = playerState;
        _events.fire(_eventNames.PlayerStateChanged, playerState);
    };

    this.GetPlayerState = function () {
        return currentPlayerState;
    };

    this.UpdateMediaVolume = function (volume) {
        dataProvider.UserPreferences.SetValue(Mediasite.Player.Options.Volume, volume);
    };

    this.SetInactivityTimeout = function (timeout) {
        inactivityInterval = timeout;
    };

    this.SetPreference = function (pref, val) {
        dataProvider.UserPreferences.SetValue(pref, val);
    };

    this.TogglePreference = function (pref) {
        dataProvider.UserPreferences.ToggleValue(pref);
    };

    this.GetChapterAt = function (position) {
        return chapterTimingSearcher.GetCurrentItem(position);
    };

    this.GetChapters = function () {
        return presentation.Chapters;
    };

    this.GetTimedEvents = function () {
        return timelineThingRepository.getAllThings();
    };

    this.AddTimedEvent = function (timedEvent) {
        var id = timelineThingRepository.addThing(timedEvent);
        _events.fire(_eventNames.TimedEventListChanged);
        return id;
    };

    this.RemoveTimedEvent = function (id) {
        timelineThingRepository.removeThing(id);
        _events.fire(_eventNames.TimedEventListChanged);
        

        _events.fire(_eventNames.TimelineMarkerRemoved, id);
    };

    this.SetTimedEventMarkerOptions = function (timedEventId, options) {
        var oldOptions = self.GetTimedEventMarkerOptions(timedEventId);
        var inputOptions = {
            color: options.color,
            text: options.text,
        };
        newOptions = $.extend({}, defaultMarkerOptions, oldOptions, inputOptions);
        timedEventMarkerOptionsLookup.add(timedEventId, newOptions);

        var marker = makeTimelineMarkerForId(timedEventId);

        if (!marker) {
            return;
        }

        if (oldOptions) {
            // CHANGED.
            _events.fire(_eventNames.TimelineMarkerUpdated, marker);
        } else {
            // ADDED NEW.
            _events.fire(_eventNames.TimelineMarkerAdded, marker);
        }
        
    };

    this.ClearTimedEventMarkerOptions = function (timedEventId) {
        var oldOptions = this.GetTimedEventMarkerOptions(timedEventId);
        if (!oldOptions) {
            return;
        }

        timedEventMarkerOptionsLookup.remove(timedEventId);
        _events.fire(_eventNames.TimelineMarkerRemoved, timedEventId);
    };

    this.GetTimedEventMarkerOptions = function (timedEventId) {
        return timedEventMarkerOptionsLookup.getByKey(timedEventId);
    };

    this.GetTimelineMarkers = function () {
        return makeTimelineMarkers();
    };

    this.GetLinks = function () {
        return presentation.Links;
    };

    this.GetPollsUri = function () {
        var pollingLocation = presentation.PollingLocation;
        if (!pollingLocation) return '';

        if (pollingLocation.indexOf('://') > 0) {
            return pollingLocation;
        }

        var baseUri = document.location.href.match("^https?://[.\\d\\w\\-]+(:\\d+)?/");
        if (!baseUri) return;

        // assume that pollingLocation looks like /Path/To/Mediasite/Polls
        var uri = baseUri[0] + pollingLocation.substring(1);
        return uri;
    };

    this.GetCaptionsAt = function (position) {
        var results = [];
        var i;
        var searchableItems = captionTimeSearcher.search(position / 1000); // captionTimeSearcher deals with seconds, but position is in milliseconds.

        for (i = 0; i < searchableItems.length; i += 1) {
            results.push(searchableItems[i].item);
        }

        return results;
    };

    this.GetCaptions = function () {
        return presentation.Transcript;
    };

    this.GetSlides = function (streamType) {

        streamType = streamType != undefined ? streamType : Mediasite.ContentStreamType.Slide;
        var stream = presentation.GetStream(streamType);

        return stream.Slides;
    };

    this.GetAllSlides = function () {

        function compareTime(a, b) {
            return (a.time - b.time);
        }

        var allSlides = [];

        $.each(presentation.Streams, function (index, stream) {
            allSlides = allSlides.concat(stream.Slides);
        });

        allSlides.sort(compareTime);

        return allSlides;
    };

    this.GetSlideAt = function (position, streamType) {

        streamType = streamType != undefined ? streamType : Mediasite.ContentStreamType.Slide;
        var stream = presentation.GetStream(streamType);

        var slideTimingSearcher = new Mediasite.Player.TimingSearcher(stream.Slides);

        return slideTimingSearcher.GetCurrentItem(position);
    };

    this.UpdatePosition = function (position, isFromMediaPlayer) {

        // This is the natural way to end a live presentation. (And it never happens this way!)
        if (liveEndTime > 0 && liveEndTime < position) {
            PerformEndPresentation();
            return;
        }


        if (_liveJumpBackDetectionEnabled) {
            // Detect a live position decrease
            detectAndHandleLiveJumpBack(position);
        }

        if (_isCheckingLiveStatusOnJumpBack) {
            // We're currently checking with the server to see if 
            // the presentation has ended. Shhh! Don't tell anybody 
            // else about this position update.
            return;
        } else {
            // I guess we should apply this position update.
            applyPosition(position, isFromMediaPlayer);
        }
    };

    this.SetIsSeeking = function (isSeeking) {
        if (isSeeking) {
            timelineThingMonitor.beginSeek();
        } else {
            timelineThingMonitor.endSeek(currentMediaPosition / 1000);
        }
    };

    this.FireEvent = function (eventName, data) {
        _events.fire(eventName, data);
    };

    function detectAndHandleLiveJumpBack(position) {
        // Near the end of a live HLS stream, sometimes the reported 
        // position jumps to some lesser time. This can cause the 
        // player to never reach the designated PresentationEnd time. 
        // If we detect that the position has significantly decreased 
        // and we've gotton a PresentationEnd event from the service 
        // (regardless of how far in the future we expect it to 
        // happen), just end immediately. Of course, this condition 
        // can be triggered by the position returning to zero after 
        // successfully stopping, but it does no harm to call 
        // PerformEndPresentation more than once.

        var liveJumpedBack = (position < _lastPositionUpdate - _positionJumpBackThreshold) && (presentation.PlayStatus != Mediasite.Player.PresentationStatus.OnDemand);
        _lastPositionUpdate = position;

        if (liveJumpedBack) {
            logger.Debug("Live position jumped back.");
            if (haveSeenPresentationEndEvent()) {
                logger.Debug("We already have the PresentationEnd event, so end immediately.");
                PerformEndPresentation();
            } else if (!_isCheckingLiveStatusOnJumpBack) {
                logger.Debug("We don't have the PresentationEnd event, so let's check with the server...");
                _isCheckingLiveStatusOnJumpBack = true;
                getLiveEvents(function (success) {
                    _isCheckingLiveStatusOnJumpBack = false;
                    if (haveSeenPresentationEndEvent()) {
                        logger.Debug("Server says PresentationEnded.");
                        PerformEndPresentation();
                    } else {
                        logger.Debug("Server says it doesn't know anything about PresentationEnd.");
                    }
                });
            }
        }
    }

    function applyPosition(position, isFromMediaPlayer) {
        var positionChanged = position !== currentMediaPosition;

        currentMediaPosition = position;
        currentMediaPositionWasFromMediaPlayer = isFromMediaPlayer;

        // TimelineThings checking will handle it's own debouncing and *must* be called every time.
        checkForTimelineThings();

        if (positionChanged) {
            checkCurrentCaption(position);
            checkPauseState(position);
            checkCurrentChapter(position);
        }

        
    }

    this.HandleMediaEvent = function (event) {
        // Handle end events from legacy recorders.
        // 
        // The 6.x Recorder does not insert an end event inside the media stream, 
        // instead updating the presentation's play status on the server when 
        // live broadcast ends. 
        // 
        // The 5.x Recorder does not update the play status right away so there 
        // can be a significant delay between the media ending and the player 
        // getting the LiveEnded message from the server. Fortunately, live 
        // media from the 5.x Recorder includes an end event. 

        if (event.Type === "MS5" && event.Command === "HE('E')") {
            var isBroadcasting = (presentation.PlayStatus == Mediasite.Player.PresentationStatus.Live) || (presentation.PlayStatus == Mediasite.Player.PresentationStatus.LivePaused);
            if (isBroadcasting) {
                PerformEndPresentation();
            }
        }
    };

    this.HandleMediaError = function (error) {

        if (presentation.PlayStatus == Mediasite.Player.PresentationStatus.LiveEnded || presentation.PlayStatus == Mediasite.Player.PresentationStatus.NotAvailable) {
            // Looks like a live presentation ended and we were lucky enough to get 
            // a play status update before the media error. Woo-hoo!
            logger.Debug('The media element raised an exception after a transition to LiveEnded or NotAvailable.');
            PerformEndPresentation();
            return;
        }

        var isLively = (presentation.PlayStatus == Mediasite.Player.PresentationStatus.Live || presentation.PlayStatus == Mediasite.Player.PresentationStatus.LivePaused);

        if (!isLively) {
            // In non-live states (e.g. OnDemand), we should report the media error right away.
            _events.fire(_eventNames.MediaError, error);
            return;
        }


        // Live presentation media can end abruptly if the live-end countdown on the 
        // recorder is cancelled early. This can cause the the media element's 
        // buffer to run dry, resulting in a media error. This is especially true 
        // with Smooth Streaming content. We don't want to bother the user with these 
        // errors and would like to ensure that the player UI makes it into 
        // presentation-ended state.

        // Though the presentation ended event may be scheduled for some time in the 
        // future, if we've seen it come from the server, we're safe to just 
        // transition to the presentation-ended state.
        if (haveSeenPresentationEndEvent()) {
            logger.Debug('The media element raised an exception at the end of a live presentation. liveEndTime = ' + liveEndTime);
            PerformEndPresentation();
            return;
        }


        // Immediately check the server for a Presentation End event.
        checkServerForPresentationEndEvent(0, function () {

            // We didn't get an End event, but we can't give up yet. 
            // The server caches live events for some seconds, so we need 
            // to check again after an appropriate delay to be sure 
            // that the cache isn't hiding our sacred End event.
            var delay = 5.1 * 1000; // Cache expiration is set to 5 seconds.
            checkServerForPresentationEndEvent(delay, function () {
                // Huh, this looks like an actual error.
                // I guess we should report it.
                _events.fire(_eventNames.MediaError, error);
            });
        });

    };

    function checkServerForPresentationEndEvent(msToDelay, notYetEndedCallback) {
        logger.Debug('Checking the server for the Presentation Ended event. Delay = ' + msToDelay);
        window.setTimeout(function () {
            getLiveEvents(function (success) {
                if (haveSeenPresentationEndEvent()) {
                    logger.Debug('The media element raised an exception at the end of a live presentation. liveEndTime = ' + liveEndTime);
                    PerformEndPresentation();
                } else {
                    notYetEndedCallback();
                }
            });
        }, msToDelay);
    }

    function haveSeenPresentationEndEvent() {
        return (liveEndTime > 0);
    }

    this.GetUserEmailAddress = function () {
        if (typeof (emailMessageTemplate) !== "undefined") {
            _events.fire(_eventNames.UserEmailAddressLoaded, emailMessageTemplate);
        } else {
            dataProvider.GetUsersEmailAddress(null, successCallback, errorCallback);
        }

        function successCallback(data) {
            emailMessageTemplate = {
                EmailAddress: data.d.EmailAddress
            };
            _events.fire(_eventNames.UserEmailAddressLoaded, emailMessageTemplate);
        }

        function errorCallback() {

        }
    };

    this.UpdateAskQuestionEmailAddress = function (emailAddress) {
        emailMessageTemplate = emailAddress;
    };

    this.GetEmailInvite = function () {

        if (typeof (inviteMessgeTemplate) !== "undefined") {
            _events.fire(_eventNames.EmailInviteLoaded, inviteMessgeTemplate);
        }
        else {

            dataProvider.GetInvite(JSON.stringify({ playbackTicketId: presentation.PlaybackTicketId }), successCallback, errorCallback);
        }

        function successCallback(data) {
            inviteMessgeTemplate = {
                Message: data.d.MessageBody,
                Subject: data.d.Subject,
                ToAddresses: data.d.ToAddresses,
                FromAddress: data.d.FromAddress,
                MailTo: data.d.MailTo,
                UseServerForSMTP: data.d.UseServerForSMTP
            };
            _events.fire(_eventNames.EmailInviteLoaded, inviteMessgeTemplate);
        }

        function errorCallback() {
            _events.fire(_eventNames.EmailInviteLoaded, { Subject: presentation.Title, Message: window.location.href });
        }
    };

    this.SendEmail = function (email) {

        dataProvider.UserPreferences.SetValue(Mediasite.Player.Options.UserFromEmail, email.FromAddress);
        dataProvider.SendInvite(JSON.stringify({ playbackTicketId: presentation.PlaybackTicketId, invite: email }), successCallback, errorCallback);

        logger.Debug("Sending email");

        function successCallback(data) {
            _events.fire(_eventNames.SendEmailComplete, data.d.Success, data.d.Message);
            logger.Debug("Send email invite complete.", "Success? " + data.d.Success);
        }

        function errorCallback(xhr) {
            _events.fire(_eventNames.SendEmailComplete, false, Mediasite.Player.Localization.Common.AsyncCommunicationError + ' - ' + xhr.status + ' ' + xhr.statusText);
            logger.Debug("Send email invite failed", xhr.status, xhr.statusText);
        }
    };

    this.SendQuestion = function (question) {

        dataProvider.UserPreferences.SetValue(Mediasite.Player.Options.UserFromName, question.Submitter);
        dataProvider.UserPreferences.SetValue(Mediasite.Player.Options.UserFromEmail, question.SubmitterEmail);

        dataProvider.SendQuestion(JSON.stringify({ playbackTicketId: presentation.PlaybackTicketId, question: question }), successCallback, errorCallback);

        logger.Debug("Sending question");

        function successCallback(data) {
            _events.fire(_eventNames.SendQuestionComplete, data.d.Success, data.d.Message);
            logger.Debug("Send question completed.  Successful? ", data.d.Success);
        }

        function errorCallback(xhr) {
            _events.fire(_eventNames.SendQuestionComplete, false, Mediasite.Player.Localization.Common.AsyncCommunicationError + ' - ' + xhr.status + ' ' + xhr.statusText);
            logger.Debug("Send question failed", xhr.status, xhr.statusText);
        }
    };

    function hasSlides() {
        return slideStream && slideStream.HasSlides();
    }

    function isPlayerEmbedded() {
        return (window.self != window.top);
    }

    function getContainerUrl() {
        if (!isPlayerEmbedded()) return;

        try {
            var containerUrl = window.parent.document.location.href;
            if (!containerUrl) return;

            return containerUrl;
        } catch (err) { /* Allow script execution to continue, even if error console shows a security error */ }
    }

    this.EnableLiveJumpBackDetection = function () {
        _liveJumpBackDetectionEnabled = true;
    };

    this.EnableLiveStartDelay = function (delayInMs) {
        _liveStartDelay = delayInMs;
    };


    function makeTimelineMarkerForId(timedEventId) {
        var timedEvent = timelineThingRepository.getById(timedEventId);
        if (!timedEvent) {
            return;
        }

        return makeTimelineMarker(timedEvent);
    }

    function makeTimelineMarker(timedEvent) {
        var options = self.GetTimedEventMarkerOptions(timedEvent.id);

        if (typeof options !== 'object') {
            return;
        }

        return {
            id: timedEvent.id,
            time: timedEvent.time,
            text: options.text || defaultMarkerOptions.text,
            color: options.color || defaultMarkerOptions.color
        };
    }

    function makeTimelineMarkers() {
        var timedEvents = self.GetTimedEvents();
        var markers = $.map(timedEvents, function (timedEvent, index) {
            return makeTimelineMarker(timedEvent);
        });

        return markers;
    }

    function withoutAudioOnlyStreamTypes(input) {
        var i, streamType, stream;
        var output = [];

        for (i = 0; i < input.length; i += 1) {
            streamType = input[i];
            stream = presentation.GetStream(streamType);
            if (stream && !stream.AudioOnly) {
                output.push(streamType);
            }
        }

        return output;
    }

    function onlyUniqueStreamTypes(input) {
        var i, identifier;
        var output = [];

        for (i = 0; i < input.length; i++) {
            identifier = input[i];

            if ($.inArray(identifier, output) == -1) {
                output.push(identifier);
            }
        }

        return output;
    }

    function onlyValidStreamTypes(input) {
        var i, streamType;
        var output = [];

        for (i = 0; i < input.length; i += 1) {
            streamType = input[i];
            if (presentation.GetStream(streamType)) {
                output.push(streamType);
            }
        }

        return output;
    }


    function PerformEndPresentation() {
        if (!hasPerformedEndPresentation) {
            presentation.PlayStatus = Mediasite.Player.PresentationStatus.LiveEnded;
            _events.fire(_eventNames.LiveStatusChanged, Mediasite.Player.PresentationStatus.LiveEnded);
            hasPerformedEndPresentation = true;
        }
    }

    function startDetectingLiveStalled() {
        if (livePlayStalledDetector) {
            return; // It's already detecting, dude.
        }

        livePlayStalledDetector = new LivePlayStalledDetector(
            function () { return presentation.PlayStatus; }, // getPlayState
            function () { return rawMediaState; }, // getMediaState
            function () { return currentMediaPosition; }, // getCurrentPosition
            function () { return liveEndTime > 0; } // getIsPresentationEndNear
        );

        livePlayStalledDetector.LiveStreamStalledErrorCallback = function () {
            livePlayStalledDetector.Stop();
            // TODO: YELL REALLY LOUD!
        };
        livePlayStalledDetector.LiveStreamEndedCallback = function () {
            livePlayStalledDetector.Stop();
            PerformEndPresentation();
        };

        livePlayStalledDetector.Start();
    }


    function LivePlayStalledDetector(getPlayState, getMediaState, getCurrentPosition, getIsPresentationEndNear) {
        var self = this;
        var msToEndCheck = 2 * 1000;
        var msToMediaError = 15 * 1000;

        var tickInterval = 500;
        var ticksStalled = 0;
        var ticksToTriggerEndCheck = Math.ceil(msToEndCheck / tickInterval);
        var ticksToTriggerMediaError = Math.ceil(msToMediaError / tickInterval);
        var timer;

        var lastPosition = Number.MAX_VALUE;


        this.LiveStreamStalledErrorCallback = function () { logger.Debug('default LiveStreamStalledErrorCallback'); };
        this.LiveStreamEndedCallback = function () { logger.Debug('default LiveStreamEndedCallback'); };


        this.Start = function () {
            this.Stop();

            timer = window.setInterval(timer_tick, tickInterval);
        };

        this.Stop = function () {
            if (timer) {
                window.clearInterval(timer);
            }
            lastPosition = Number.MAX_VALUE;
            ticksStalled = 0;
        };


        function timer_tick() {
            var mediaState = getMediaState();
            var isPlaying = (mediaState == Mediasite.Player.MediaState.Playing) || (mediaState == Mediasite.Player.MediaState.Waiting);
            var playState = getPlayState();
            var isBroadcasting = (playState == Mediasite.Player.PresentationStatus.Live) || (playState == Mediasite.Player.PresentationStatus.LivePaused);

            //debug('playState = ' + playState + '; mediaState = ' + mediaState);

            if (!isBroadcasting || !isPlaying) {
                // Can't be stalled during live play, if we're not live or we're not playing.
                this.ticksStalled = 0;
                return;
            }

            // Update the count of our clock ticks which have occurred with the player position stalled.

            var currentPosition = getCurrentPosition();
            if (lastPosition == currentPosition) {
                ticksStalled++;
                logger.Debug('LivePlayStalledDetector.ticksStalled = ' + ticksStalled);
            } else {
                ticksStalled = 0;
            }
            lastPosition = currentPosition;


            if (ticksStalled >= ticksToTriggerEndCheck && getIsPresentationEndNear()) {
                logger.Debug('Exceeded ticksToTriggerEndCheck and the presentation end is near.');

                // It looks like the presentation end is near.
                // Let's be smart and prematurely end this thing.
                // 
                // We expect that the presentation end event will happen some seconds from now, 
                // but we don't want the user to wait with a stalled player until then. 
                // The media has ended, but in some live scenarios the player will happily 
                // stay connected with no real data coming in. 

                if (self.LiveStreamEndedCallback) {
                    self.LiveStreamEndedCallback();
                }

                self.Stop();
            } else if (ticksStalled == ticksToTriggerMediaError) {
                logger.Debug('Exceeded ticksToTriggerMediaError');

                // As far as we know, the end of the live presentation is not imminent.
                // We must interpret this as a media error.
                // 
                // The player can remain connected to the publishing point even when 
                // the recorder is no longer providing bytes. For example, unplugging 
                // the recorder from the network during a live Smooth Streaming 
                // presentation can have this effect.

                if (self.LiveStreamStalledErrorCallback) {
                    self.LiveStreamStalledErrorCallback();
                }

                self.Stop();
            }
        }
    }
};


Mediasite.Player.PresentationContentSearch = function (dataProvider, playbackTicketId) {
    var _events = new Mediasite.Player.EventBundle();
    var _eventNames = Mediasite.Player.ModelEvent;

    this.AddEventHandler = function (eventName, handler) {
        _events.addHandler(eventName, handler);
    };

    this.search = function (searchTerm) {
        searchTerm = sanitize(searchTerm);

        if (searchTerm.length == 0) {
            _events.fire(_eventNames.SearchComplete, searchTerm, []);
        } else {
            dataProvider.Search(searchTerm, successCallback, errorCallback);
        }
    };

    function successCallback(searchResults, searchTerm) {
        if ($.isArray(searchResults) && searchResults.length > 0) {
            searchResults = searchResults.sort(function sortByTime(a, b) { return a.Time - b.Time; });
            conditionalReduce(searchResults,
                            function (a, b) { return a.Time == b.Time && a.Type == b.Type; },
                            function (a, b) {
                                a.Text = [a.Text, b.Text].join(' ... ');
                                return a;
                            }
                        );
        }

        _events.fire(_eventNames.SearchComplete, searchTerm, searchResults);
    }

    function errorCallback() {
        _events.fire(_eventNames.SearchError);
    }

    function sanitize(str) {
        return $.trim(str);
    }

    var conditionalReduce = function (array, predicate, reduce) {
        if (!array || !array.length || !predicate || !reduce) return array;

        var i = 0;
        while (i + 1 < array.length) {
            var left = array[i];
            var right = array[i + 1];

            if (predicate(left, right)) {
                left = reduce(left, right);
                array.splice(i, 2, left);
            } else {
                i++;
            }
        }

        return array;
    };
};
