/// <reference path="../ScriptReference/jquery-1.5.2.js"/>
/// <reference path="Mediasite.Core.js"/>
/// <reference path="Mediasite.Player.Core.js"/>
/// <reference path="Mediasite.Player.Localization.js"/>
/// <reference path="json2.js"/>
/*global Mediasite*/

Mediasite.namespace("Player");

Mediasite.Player.Model = function (serviceRoot) {
    var $ = jQuery;

    var presentation;
    var cookie = new Mediasite.Cookie("MediasitePlayerOptions");
    var slideTimingSearcher;
    var captionTimingSearcher;
    var pauseTimingSearcher;
    var pauseEvents = [];
    var liveSlideQueue = [];
    var currentSlideNumber = 0;
    var currentCaptionText;
    var currentMediaState;
    var rawMediaState;
    var currentMediaPosition = 0;
    var currentlyPaused = false;
    var emailMessageTemplate;

    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 _liveStartDelay = 0;
    var _liveJumpBackDetectionEnabled = false;
    var _isCheckingLiveStatusOnJumpBack = false;
    var _positionJumpBackThreshold = 1 * 1000; // milliseconds
    var _lastPositionUpdate = 0;

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

    var hasPerformedEndPresentation = false;
    var livePlayStalledDetector;

    var scriptCommandRegEx = /^HE\('([A-Z])'(\)|,(\d+)\))$/;

    var serviceContentType = "application/json; charset=utf-8";

    if (typeof (serviceRoot) === "undefined" || !serviceRoot) {
        serviceRoot = Mediasite.GlobalOptions.ServiceEndpoint;
    }


    var notifier = new Mediasite.Player.ModelEventNotifier();
    this.AddListener = function (list) {
        notifier.AddListener(list);
    };

    function getServiceLocation(methodName) {
        return serviceRoot + '/' + methodName;
    }

    function checkCurrentSlide(currentPosition) {
        if (liveSlideQueue.length > 0) {
            while (liveSlideQueue[0] && liveSlideQueue[0].Time <= currentPosition) {
                var nextSlide = liveSlideQueue.shift();
                if (presentation.Slides.length === 0 || nextSlide.Number > presentation.Slides[presentation.Slides.length - 1].Number) {
                    presentation.Slides.push(nextSlide);
                    notifier.OnAddNewSlide(nextSlide);
                }
            }
        }

        var slide = slideTimingSearcher.GetCurrentItem(currentPosition);

        if (typeof (slide) === "undefined" || slide.Number < 1) {
            if (currentPosition === 0) {
                return;
            }
            slide = new Mediasite.Slide(0, 0, "", "");
        }
        if (slide.Number !== currentSlideNumber) {
            currentSlideNumber = slide.Number;
            notifier.OnSlideChange(slide);
        }
    }

    function checkCurrentCaption(currentPosition) {
        var caption = captionTimingSearcher.GetCurrentItem(currentPosition);

        if (typeof (caption) !== "undefined" && caption.Text !== currentCaptionText) {
            currentCaptionText = caption.Text;
            notifier.OnCaptionChange(caption.Text);
        }
    }


    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;
                }
                notifier.OnLiveStatusChanged(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) {
            $.ajax({
                type: "POST",
                dataType: "json",
                contentType: serviceContentType,
                url: getServiceLocation("GetLiveEvents"),
                data: JSON.stringify({ playbackTicket: presentation.PlaybackTicketId, sinceTimeInMs: lastLiveEventTime, currentTimeInMs: Math.floor(currentMediaPosition + pollLiveEventReadAheadWindow) }),
                success: function (data) {
                    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:
                                Mediasite.Debug.Log('Received PresentationEnd');
                                liveEndTime = data.d[i].Time;
                                if (currentMediaState == Mediasite.Player.MediaState.Playing) {
                                    var millisecondsTilEnd = Math.max(0, liveEndTime - currentMediaPosition);
                                    Mediasite.Debug.Log('PresentationEnd in ' + millisecondsTilEnd / 1000 + ' seconds.');
                                    window.setTimeout(PerformEndPresentation, millisecondsTilEnd);
                                } else {
                                    PerformEndPresentation();
                                }
                                break;
                            case Mediasite.Player.TimelineEvent.Slide:
                                var slide = new Mediasite.Slide(data.d[i].Sequence, data.d[i].Time, "", "");
                                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;
                        }
                    }

                    if (callback) {
                        callback(true);
                    }
                },
                error: function (xhr) {
                    var response = JSON.parse(xhr.responseText);
                    if (response && response.FaultType === Mediasite.Player.PlayerServiceFaultType.InvalidTicket) {
                        notifier.OnInactivityExceeded();
                    } else if (callback) {
                        callback(false);
                    }
                }
            });
            return true;
        }
    }

    function pingServerTimerCallback() {
        var callback = function () { pingServerTimer = setTimeout(pingServerTimerCallback, pingServerInterval); };
        if (!getLiveEvents(callback)) {
            $.ajax({
                type: "POST",
                dataType: "json",
                contentType: serviceContentType,
                url: getServiceLocation("ReportState"),
                data: JSON.stringify({ playbackTicket: presentation.PlaybackTicketId, playerState: Mediasite.Player.MediaState.GetDisplayName(currentMediaState) }),
                success: callback,
                error: callback
            });
        }
    }

    function pollForLiveStartTimerCallback() {
        $.ajax({
            type: "POST",
            dataType: "json",
            contentType: serviceContentType,
            url: getServiceLocation("GetLiveStatus"),
            data: JSON.stringify({ playbackTicket: presentation.PlaybackTicketId, presentationId: presentation.PresentationId }),
            success: function (data) { pollForLiveStartCallbackSuccess(data); },
            error: function () { pollForLiveStartTimer = setTimeout(pollForLiveStartTimerCallback, pollLiveWaitInterval); }
        });
    }

    function pollForLiveStartCallbackSuccess(data) {
        presentation.PlayStatus = data.d.PlayStatus;
        switch (presentation.PlayStatus) {
            case Mediasite.Player.PresentationStatus.ScheduledForLive:
                pollForLiveStartTimer = setTimeout(pollForLiveStartTimerCallback, pollLiveWaitInterval);
                break;
            case Mediasite.Player.PresentationStatus.OpenForLive:
                pollForLiveStartTimer = setTimeout(pollForLiveStartTimerCallback, pollLiveInterval);
                break;
            case Mediasite.Player.PresentationStatus.Live:
            case Mediasite.Player.PresentationStatus.LivePaused:
                Mediasite.Debug.Log('Delaying the transition from Waiting to Live by ' + _liveStartDelay + 'ms');
                window.setTimeout(function () {
                    notifier.OnLiveStatusChanged(presentation.PlayStatus);
                    notifier.OnSlideChange(new Mediasite.Slide(0, 0, "", ""));
                    notifier.OnSetLiveStartTime(new Date(data.d.LiveStartUnixTimeInMs + clockSkew));
                    notifier.OnStartMedia(presentation.MediaUrls);
                    getLiveEvents();
                }, _liveStartDelay);
                break;
            case Mediasite.Player.PresentationStatus.LiveEnded:
            case Mediasite.Player.PresentationStatus.NotAvailable:
                PerformEndPresentation();
                break;
        }
    }

    function pollForLiveStart(interval) {
        if (presentation.IsStandAlone) {
            return;
        }

        clearTimeout(pollForLiveStartTimer);
        pollForLiveStartTimer = setTimeout(pollForLiveStartTimerCallback, interval);
    }

    function startServerPing() {
        if (presentation.IsStandAlone) {
            return;
        }

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

    function inactivityTimerCallback() {
        var now = new Date().getTime();
        if (now - lastInactiveTime > inactivityInterval) {
            notifier.OnInactivityExceeded();
        }
        else {
            inactivityTimer = setTimeout(inactivityTimerCallback, inactivityMonitorInterval);
        }
    }

    function stopServerPing() {
        if (presentation.IsStandAlone) {
            return;
        }

        clearTimeout(pingServerTimer);

        lastInactiveTime = new Date().getTime();
        inactivityTimer = setTimeout(inactivityTimerCallback, inactivityMonitorInterval);
    }

    this.LoadManifest = function () {

        if (Mediasite.GlobalOptions.IsLayoutPreview === true) {
            return;
        }

        if (typeof (Mediasite.PlaybackManifest) === "undefined") {
            notifier.OnLoadManifestFailure();
        }
        else {
            presentation = new Mediasite.Presentation(Mediasite.PlaybackManifest);
            clockSkew = Mediasite.PlaybackManifest.ServerClockSkew;
            document.title = presentation.Title;

            slideTimingSearcher = new Mediasite.Player.TimingSearcher(presentation.Slides);
            pauseTimingSearcher = new Mediasite.Player.TimingSearcher(pauseEvents);

            // TODO: Do the following when we have a mechanism to fire a batch of slide events at once.
            //       Though the following code solves the problem of having future slides show up in the 
            //       UI when joining a live presentation in progress, it can result in many, many slide 
            //       events firing back-to-back when the media begins to play and we get a current position.
            // 
            //if (presentation.PlayStatus == Mediasite.Player.PresentationStatus.Live ||
            //    presentation.PlayStatus == Mediasite.Player.PresentationStatus.LivePaused) {

            //    // Move all of the slides out of the presentation and into the live slide queue.
            //    if (presentation.Slides.length > 0) {
            //        lastLiveEventTime = presentation.Slides[presentation.Slides.length - 1].Time;
            //        var index;
            //        for (index = 0; index < presentation.Slides.length; index++) {
            //            liveSlideQueue.push(presentation.Slides[index]);
            //        }
            //        presentation.Slides = [];
            //    }
            //}


            notifier.OnPresentationLoaded(presentation);

            notifier.OnOptionsLoaded({
                UserFromName: cookie.GetValue(Mediasite.Player.Options.UserFromName, ""),
                UserFromEmail: cookie.GetValue(Mediasite.Player.Options.UserFromEmail, ""),
                Volume: cookie.GetNumberValue(Mediasite.Player.Options.Volume, 50),
                SlideDisplay: cookie.GetValue(Mediasite.Player.Options.SlideDisplay, Mediasite.Player.SlideDisplayPref.LargeThumbs),
                ShowSlideDescriptions: cookie.GetValue(Mediasite.Player.Options.ShowSlideDescriptions, false)
            });

            if (Mediasite.PlaybackManifest.Transcript.length > 0) {
                captionTimingSearcher = new Mediasite.Player.TimingSearcher(Mediasite.PlaybackManifest.Transcript);
            }
            else {
                captionTimingSearcher = new Mediasite.Player.TimingSearcher([]);
            }

            switch (presentation.PlayStatus) {
                case Mediasite.Player.PresentationStatus.OnDemand:
                    notifier.OnStartMedia(presentation.MediaUrls);
                    notifier.OnSlideChange(new Mediasite.Slide(0, 0, "", ""));
                    break;
                case Mediasite.Player.PresentationStatus.Live:
                case Mediasite.Player.PresentationStatus.LivePaused:
                    if (presentation.Slides.length > 0) {
                        lastLiveEventTime = presentation.Slides[presentation.Slides.length - 1].Time;
                    }
                    notifier.OnStartMedia(presentation.MediaUrls);
                    notifier.OnLiveStatusChanged(presentation.PlayStatus);
                    notifier.OnSlideChange(new Mediasite.Slide(0, 0, "", ""));
                    if (Mediasite.PlaybackManifest.LiveStartUnixTimeInMs) {
                        notifier.OnSetLiveStartTime(new Date(Mediasite.PlaybackManifest.LiveStartUnixTimeInMs + clockSkew));
                    }
                    break;
                case Mediasite.Player.PresentationStatus.ScheduledForLive:
                    pollForLiveStart(pollLiveWaitInterval);
                    break;
                case Mediasite.Player.PresentationStatus.OpenForLive:
                    pollForLiveStart(pollLiveInterval);
                    break;
                case Mediasite.Player.PresentationStatus.LiveEnded:
                case Mediasite.Player.PresentationStatus.NotAvailable:
                    PerformEndPresentation()
                    break;
            }
        }
    };

    this.UpdateMediaState = function (state) {
        if (state !== currentMediaState) {
            switch (state) {
                case Mediasite.Player.MediaState.Playing:
                    currentMediaState = state;
                    startServerPing();
                    getLiveEvents();
                    startDetectingLiveStalled();
                    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.UpdateMediaVolume = function (volume) {
        cookie.SetValue(Mediasite.Player.Options.Volume, volume);
    };

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

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

    this.TogglePreference = function (pref) {
        var prefVal = cookie.GetBoolValue(pref, false);
        cookie.SetValue(pref, !prefVal);
    };

    this.SetCurrentSlide = function (slideNumber) {
        if (slideNumber > 0 && slideNumber <= presentation.Slides.length && presentation.PlayStatus === Mediasite.Player.PresentationStatus.OnDemand) {
            currentSlideNumber = slideNumber;
            notifier.OnSlideChange(presentation.Slides[currentSlideNumber - 1]);
            notifier.OnPositionChange(presentation.Slides[slideNumber - 1].Time);
        }
    };

    this.SetCurrentChapter = function (chapterNumber) {
        if (chapterNumber > 0 && chapterNumber <= presentation.Chapters.length && presentation.PlayStatus === Mediasite.Player.PresentationStatus.OnDemand) {
            notifier.OnPositionChange(presentation.Chapters[chapterNumber - 1].Time);
        }
    };

    this.NextSlide = function () {
        if (currentSlideNumber < presentation.Slides.length && presentation.PlayStatus === Mediasite.Player.PresentationStatus.OnDemand) {
            currentSlideNumber++;
            notifier.OnSlideChange(presentation.Slides[currentSlideNumber - 1]);
            notifier.OnPositionChange(presentation.Slides[currentSlideNumber - 1].Time);
        }
    };

    this.PreviousSlide = function () {
        if (currentSlideNumber > 1 && presentation.PlayStatus === Mediasite.Player.PresentationStatus.OnDemand) {
            currentSlideNumber--;
            notifier.OnSlideChange(presentation.Slides[currentSlideNumber - 1]);
            notifier.OnPositionChange(presentation.Slides[currentSlideNumber - 1].Time);
        }
    };

    this.FirstSlide = function () {
        if (currentSlideNumber > 1 && presentation.PlayStatus === Mediasite.Player.PresentationStatus.OnDemand) {
            currentSlideNumber = 1;
            notifier.OnSlideChange(presentation.Slides[currentSlideNumber - 1]);
            notifier.OnPositionChange(presentation.Slides[currentSlideNumber - 1].Time);
        }
    };

    this.LastSlide = function () {
        if (currentSlideNumber < presentation.Slides.length && presentation.PlayStatus === Mediasite.Player.PresentationStatus.OnDemand) {
            currentSlideNumber = presentation.Slides.length;
            notifier.OnSlideChange(presentation.Slides[currentSlideNumber - 1]);
            notifier.OnPositionChange(presentation.Slides[currentSlideNumber - 1].Time);
        }
    };

    this.UpdatePosition = function (position) {

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

    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) {
            Mediasite.Debug.Log("Live position jumped back.");
            if (haveSeenPresentationEndEvent()) {
                Mediasite.Debug.Log("We already have the PresentationEnd event, so end immediately.");
                PerformEndPresentation();
            } else if (!_isCheckingLiveStatusOnJumpBack) {
                Mediasite.Debug.Log("We don't have the PresentationEnd event, so let's check with the server...");
                _isCheckingLiveStatusOnJumpBack = true;
                getLiveEvents(function (success) {
                    _isCheckingLiveStatusOnJumpBack = false;
                    if (haveSeenPresentationEndEvent()) {
                        Mediasite.Debug.Log("Server says PresentationEnded. I've always liked that server.");
                        PerformEndPresentation();
                    } else {
                        Mediasite.Debug.Log("Server says it doesn't know anything about PresentationEnd. I've never trusted that server.");
                    }
                });
            }
        }
    }

    function applyPosition(position) {
        if (position == currentMediaPosition) {
            return;
        }

        currentMediaPosition = position;
        checkCurrentSlide(position);
        checkCurrentCaption(position);
        checkPauseState(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();
            }
        }


        //        if (event.Type !== "MS5" || Mediasite.GlobalOptions.UseLiveEventPolling) {
        //            return;
        //        }

        //        var match = event.Command.match(scriptCommandRegEx);
        //        if (match != null) {
        //            switch (match[1]) {
        //                case Mediasite.Player.MediaEventType.ShowSlide:
        //                    if (typeof match[3] !== 'undefined') {
        //                        var slideNumber = parseInt(match[3], 10);
        //                        //var slide = new Mediasite.Slide(slideNumber, currentMediaPosition, "", "");
        //                        //liveSlideQueue.push(slide);
        //                        if (slideNumber > currentSlideNumber) {
        //                            for (var i = presentation.Slides.length; i < slideNumber; i++) {
        //                                var slide = new Mediasite.Slide(i + 1, currentMediaPosition, "", "");
        //                                presentation.Slides[i] = slide;
        //                                currentSlideNumber = slideNumber;
        //                                notifier.OnAddNewSlide(slide);
        //                            }
        //                        }
        //                    }
        //                    break;
        //                case Mediasite.Player.MediaEventType.EndPresentation:
        //                    presentation.PlayStatus = Mediasite.Player.PresentationStatus.LiveEnded;
        //                    notifier.OnLiveStatusChanged(presentation.PlayStatus);
        //                    break;
        //                case Mediasite.Player.MediaEventType.Pause:
        //                    presentation.PlayStatus = Mediasite.Player.PresentationStatus.LivePaused;
        //                    notifier.OnLiveStatusChanged(presentation.PlayStatus);
        //                    break;
        //                case Mediasite.Player.MediaEventType.Resume:
        //                    presentation.PlayStatus = Mediasite.Player.PresentationStatus.Live;
        //                    notifier.OnLiveStatusChanged(presentation.PlayStatus);
        //                    break;
        //            }
        //        }
    };

    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!
            log('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.
            notifier.OnMediaError(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()) {
            log('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.
                notifier.OnMediaError(error);
            });
        });

    };

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


    function log(message) {
        Mediasite.Debug.Log(message);
    }


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

    this.GetEmailInvite = function () {

        if (presentation.IsStandAlone || !presentation.SharingEnabled) {
            return;
        }
        //maybe separate the 2 or conditions and call SendEmail();
        if (typeof (emailMessageTemplate) !== "undefined") {
            notifier.OnEmailInviteLoaded(emailMessageTemplate);
        }
        else {
            $.ajax({
                type: "POST",
                dataType: "json",
                contentType: serviceContentType,
                url: getServiceLocation("GetInvite"),
                data: JSON.stringify({ playbackTicketId: presentation.PlaybackTicketId }),
                success: function (data) {
                    emailMessageTemplate = { Message: data.d.MessageBody,
                        Subject: data.d.Subject,
                        ToAddresses: data.d.ToAddresses,
                        FromAddress: data.d.FromAddress,
                        MailTo: data.d.MailTo,
                        UseServerForSMTP: data.d.UseServerForSMTP
                    };
                    notifier.OnEmailInviteLoaded(emailMessageTemplate);
                },
                error: function () { notifier.OnEmailInviteLoaded({ Subject: presentation.Title, Message: window.location.href }); }
            });
        }
    };

    this.SendEmail = function (email) {
        if (presentation.IsStandAlone) {
            return;
        }
        cookie.SetValue(Mediasite.Player.Options.UserFromEmail, email.FromAddress);

        $.ajax({
            type: "POST",
            dataType: "json",
            contentType: serviceContentType,
            url: getServiceLocation("SendInvite"),
            data: JSON.stringify({ playbackTicketId: presentation.PlaybackTicketId, invite: email }),
            success: function (data) { notifier.OnSendEmailComplete(data.d.Success, data.d.Message); },
            error: function (xhr) { notifier.OnSendEmailComplete(false, Mediasite.Player.Localization.Common.AsyncCommunicationError + ' - ' + xhr.status + ' ' + xhr.statusText); }
        });
    };

    this.SendQuestion = function (question) {
        if (presentation.IsStandAlone) {
            return;
        }

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

        $.ajax({
            type: "POST",
            dataType: "json",
            contentType: serviceContentType,
            url: getServiceLocation("SendQuestion"),
            data: JSON.stringify({ playbackTicketId: presentation.PlaybackTicketId, question: question }),
            success: function (data) { notifier.OnSendQuestionComplete(data.d.Success, data.d.Message); },
            error: function (xhr) { notifier.OnSendQuestionComplete(false, Mediasite.Player.Localization.Common.AsyncCommunicationError + ' - ' + xhr.status + ' ' + xhr.statusText); }
        });
    };

    this._searchTranscript = function _searchTranscript(searchTerm) {
        var transcript = presentation.Transcript;

        if (jQuery.isArray(transcript) && transcript.length > 0) {
            var regex = new RegExp('(' + searchTerm + ')', 'gi');
            var results = jQuery.grep(transcript, function (item, i) {
                return (item && item.Text.match(regex));
            });

            return results;
        } else {
            return [];
        }
    };
    this.SearchPresentationTextOnServer = function (searchTerm) {
        var ThisObj = this;
        // Cancel previous search
        if (this._searchPresentationTextOnServerAjax) {
            this._searchPresentationTextOnServerAjax.abort();
        }

        searchTerm = jQuery.trim(searchTerm);   // Sanitize search term

        // If search term is empty, return empty set
        if (searchTerm.length == 0) {
            notifier.OnSearchPresentationTextOnServerComplete(searchTerm, []);
            return;
        }

        var searchResults = this._searchTranscript(searchTerm); // Search captions

        if (!presentation.SearchPresentationTextOnServer || presentation.IsStandAlone) {
            // If OCR search is unavailable or this is standalone, return local search results
            notifier.OnSearchPresentationTextOnServerComplete(searchTerm, searchResults);
        } else {
            // Fetch OCR text search results from server
            this._searchPresentationTextOnServerAjax = $.ajax({
                type: "POST",
                dataType: "json",
                contentType: serviceContentType,
                url: getServiceLocation("SearchSlideText"),
                data: JSON.stringify({ playbackTicketId: presentation.PlaybackTicketId, searchTerm: searchTerm }),
                success: function (data) {
                    // Collate results and send to callback

                    // If OCR text results from server, merge them into results
                    if ($.isArray(data.d.SearchResults) && data.d.SearchResults.length > 0) {
                        $.merge(searchResults, data.d.SearchResults);
                        searchResults = searchResults.sort(function sortByTime(a, b) { return a.Time - b.Time; });
                    }

                    ThisObj._searchPresentationTextOnServerAjax = null;
                    notifier.OnSearchPresentationTextOnServerComplete(searchTerm, searchResults);
                },
                failure: function () {
                    ThisObj._searchPresentationTextOnServerAjax = null;
                    // If OCR text could not load, return local search results
                    notifier.OnSearchPresentationTextOnServerComplete(searchTerm, searchResults);
                }
            });
        }

    }

    this.ReportMediaOpened = function (mediaPlayerType) {
        if (presentation.IsStandAlone) {
            return;
        }

        $.ajax({
            type: "POST",
            dataType: "json",
            contentType: serviceContentType,
            url: getServiceLocation("ReportMediaOpen"),
            data: JSON.stringify({ playbackTicket: presentation.PlaybackTicketId, playerType: "Javascript", mediaPlayerType: Mediasite.Player.MediaPlayerType.GetDisplayName(mediaPlayerType), embeddedPlayer: (window.self !== window.top) })
        });
    };

    this.ReportMediaView = function (mediaView, synchronous) {
        if (presentation.IsStandAlone) {
            return;
        }

        var runAsync = true;
        if (synchronous === true) {
            runAsync = false;
        }

        $.ajax({
            type: "POST",
            dataType: "json",
            contentType: serviceContentType,
            url: getServiceLocation("ReportMediaView"),
            async: runAsync,
            data: JSON.stringify({ playbackTicket: presentation.PlaybackTicketId, startTime: mediaView.StartTime, duration: mediaView.Duration })
        });

        Mediasite.Debug.Log("Reporting media viewed"
            + "\n    Start time: " + mediaView.StartTime
            + (mediaView.Duration > 0
                ? "\n    Duration: " + mediaView.Duration
                : "\n    (start of segment)"));
    };

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

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



    function PerformEndPresentation() {
        if (!hasPerformedEndPresentation) {
            presentation.PlayStatus = Mediasite.Player.PresentationStatus.LiveEnded;
            notifier.OnLiveStatusChanged(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 () { debug('default LiveStreamStalledErrorCallback'); };
        this.LiveStreamEndedCallback = function () { 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++;
                debug('LivePlayStalledDetector.ticksStalled = ' + ticksStalled);
            } else {
                ticksStalled = 0;
            }
            lastPosition = currentPosition;


            if (ticksStalled >= ticksToTriggerEndCheck && getIsPresentationEndNear()) {
                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) {
                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();
            }
        }

        function debug(message) {
            //console.log(message);
        }
    }
};
