var Mediasite = window.Mediasite || {};
Mediasite.Player = Mediasite.Player || {};




Mediasite.Player.CurrentViewTracking = function () {
    if (!(this instanceof Mediasite.Player.CurrentViewTracking)) return new Mediasite.Player.CurrentViewTracking();

    var self = this;

    var logger = Mediasite.Logging && Mediasite.Logging.LoggerFactory("Playback Reporting")
        || // fallback for standalone testing
        {Debug: function () { console.log(arguments); }
    };

    var detectorToleranceFactor = 0.5;
    var inactivityDelay = 2 * 1000; // milliseconds
    var heartbeatDelay = 30 * 1000; // milliseconds
    var segmentCombinationToleranceSeconds = 1;
    var noBookmarkZoneSize = 3; // seconds withing video duration in which we won't bookmark.

    var getIsLive = function () { return false; };
    var getVideoDuration = function () { return 0; };
    var serviceReportMediaViewCalledCount = 0;
    var inactivityTimeout, heartbeatTimeout;
    var _dataProvider, _playbackTicketId;
    var segmentsMemory = new Mediasite.Player.WatchedSegmentsMemory();

    var playDetector = new Mediasite.Player.HeuristicPlayDetector();
    playDetector.tolerance = detectorToleranceFactor;
    playDetector.segmentStartAction = handleSegmentStart;
    playDetector.segmentEndAction = handleSegmentEnd;


    function scheduleHeartbeat() {
        window.clearTimeout(heartbeatTimeout);
        heartbeatTimeout = window.setTimeout(onHeartbeat, heartbeatDelay);
    }

    function onHeartbeat() {
        try {

            // Cause a segment end so we can report it.
            playDetector.endSegment();

            reportSegmentsSinceLastReport();

        } catch (err) {
            logger.Debug('Error in Playback Reporting onHeartbeat');
        }

        // See you again soon!
        scheduleHeartbeat();
    }

    function reportSegmentsSinceLastReport(synchronous) {
        var segments = segmentsMemory.getAllSegments();
        segmentsMemory.reset();

        reportPlayed(segments, synchronous);
    }

    function handleSegmentStart(startPosition) {
        clearTimeout(inactivityTimeout);

        _events.fire(_eventNames.Start, startPosition);
    }

    function handleSegmentEnd(startPosition, endPosition, isImportant) {
        segmentsMemory.addSegment(startPosition, endPosition);


        _events.fire(_eventNames.End, startPosition, endPosition);

        clearTimeout(inactivityTimeout);
    }


    self.onPositionChanged = onPositionChanged;
    self.onPlaybackRateChanged = onPlaybackRateChanged;


    function onPlaybackRateChanged(newPlaybackRate) {
        playDetector.handlePlaybackRateChange(newPlaybackRate);
    }

    function onPositionChanged(position) {
        clearTimeout(inactivityTimeout);
        // ... but assume playback stopped if we don't hear more soon
        inactivityTimeout = setTimeout(function () {
            playDetector.endSegment();
        }, inactivityDelay);

        playDetector.handlePositionChange(position); // playDetector works in milliseconds.
    };


    var _events = new Mediasite.Player.EventBundle();
    var _eventNames = Mediasite.Player.CurrentViewTracking.Event;
    this.addHandler = function (eventName, handler) {
        _events.addHandler(eventName, handler);
    };

    self.trackPresentation = function (playerApi) {
        if (!playerApi) {
            logger.Debug("Reporting cannot track presentation: no player API to attach to");
            return;
        }

        var self = this;

        getIsLive = function () { return playerApi.getPlayerState().isLive; };
        getVideoDuration = function () { return Math.floor(playerApi.getDuration()); };

        playerApi.addHandler("currenttimechanged", function (e) {
            self.onPositionChanged(e.currentTime);
        });

        playerApi.addHandler("playbackratechanged", function (e) {
            self.onPlaybackRateChanged(e.currentRate);
        });


        // When the session ends, make sure to report end!
        $(window).on('beforeunload', function () {
            playDetector.endSegment();
            reportSegmentsSinceLastReport(true);
        });

        scheduleHeartbeat();
    };

    self.reportToDataProvider = function (playbackTicketId, dataProvider) {
        if (!playbackTicketId || !dataProvider) {
            logger.Debug("Reporting cannot report to data provider: no playback ticket or data provider");
            return;
        }

        _playbackTicketId = playbackTicketId;
        _dataProvider = dataProvider;
    };


    function reportPlayed(segments, synchronous) {
        var async = (synchronous !== true);
        var duration;
        var serviceSegments;
        var bookmarkPosition;

        if (!_dataProvider || !_playbackTicketId) {
            // Function was called before playbackTicketId and dataProvider were provided.
            return;
        }

        bookmarkPosition = calcBookmarkPosition(segments, getIsLive());
        serviceSegments = toServiceSegments(segments);

        if (serviceSegments.length < 1) {
            // There are no non-zero duration segments.
            return;
        }

        _dataProvider.ReportMediaView(JSON.stringify({ playbackTicket: _playbackTicketId, segments: serviceSegments, bookmarkPosition: bookmarkPosition }), async);

        serviceReportMediaViewCalledCount += 1;
        logger.Debug('ReportMediaView ' + makeReportMediaViewDescription(serviceSegments, bookmarkPosition));
        logger.Debug('ReportMediaView has been called ' + serviceReportMediaViewCalledCount + ' times.');
    }

    function toServiceSegments(inputArray) {
        // Transforms an array of TimelineSegments into an array of segment 
        // objects appropriate for data provider's ReportMediaView method. 
        // 
        // Positions in the output are expressed in integers.
        // Abutting segments are combined in the output.


        var i, seg, start, end, duration;
        var resultArray = [];

        var getLastItem = function () {
            if (resultArray.length < 1) {
                return null;
            }

            return resultArray[resultArray.length - 1];
        };

        var getLastEnd = function () {
            var lastItem = getLastItem();

            if (!lastItem) {
                return NaN;
            }

            return lastItem.StartTime + lastItem.Duration;
        };

        var extendLastItem = function (start, duration) {
            var lastItem, newEnd, newDuration;

            newEnd = start + duration;
            lastItem = getLastItem();
            newDuration = newEnd - lastItem.StartTime;
            lastItem.Duration = newDuration;
        };

        var addNewItem = function (start, duration) {
            resultArray.push({ StartTime: start, Duration: duration });
        };

        var add = function (start, duration) {
            if (duration <= 0) {
                // Zero durations can be skipped.
                return;
            }

            if (Math.abs(start - getLastEnd()) <= segmentCombinationToleranceSeconds) {
                // This segment starts at or very near the end of the last one.
                extendLastItem(start, duration);
            } else {
                // This segment is not a continuation of the last.
                addNewItem(start, duration);
            }
        };



        for (i = 0; i < inputArray.length; i += 1) {
            seg = inputArray[i];
            start = Math.max(0, Math.floor(seg.start));
            end = Math.max(0, Math.floor(seg.end));
            duration = Math.max(0, end - start);

            add(start, duration);
        }

        return resultArray;
    };

    function makeReportMediaViewDescription(serviceSegments, bookmarkPosition) {
        var i;
        var joint = ', ';
        var parts = [];


        function seviceSegmentDescription(serviceSegment) {
            var start = secondsToMinutesAndSeconds(serviceSegment.StartTime);
            var end = secondsToMinutesAndSeconds(serviceSegment.StartTime + serviceSegment.Duration);

            return '(' + start + ' - ' + end + ')';
        }

        function secondsToMinutesAndSeconds(totalSeconds) {
            var m, ss;
            var isNegative = totalSeconds < 0;

            totalSeconds = Math.floor(Math.abs(totalSeconds));
            m = Math.floor(totalSeconds / 60) + '';
            ss = (totalSeconds % 60) + '';
            while (ss.length < 2) {
                ss = '0' + ss;
            }
            if (isNegative) {
                m = '-' + m;
            }
            return m + ':' + ss;
        }


        for (i = 0; i < serviceSegments.length; i += 1) {
            parts.push(seviceSegmentDescription(serviceSegments[i]));
        }

        return 'segments: [' + parts.join(joint) + '], bookmarkPosition: ' + secondsToMinutesAndSeconds(bookmarkPosition);
    }





    function calcBookmarkPosition(segments, isLive) {
        var result = 0;

        if (!isLive && segments.length > 0) {
            result = Math.floor(segments[segments.length - 1].end);

            if (isInNoBookmarkZone(result)) {
                // No sense bookmarking a position sooo close to 
                // the video duration. Interpret this to mean we 
                // reached the end and should clear out (zero) 
                // the bookmark.
                result = 0;
            }
        }

        return result;
    }

    function isInNoBookmarkZone(position) {
        var diff = getVideoDuration() - position;
        return diff >= 0 && diff <= noBookmarkZoneSize;
    }
};


Mediasite.Player.HeuristicPlayDetector = function () {

    var self = this;
    var lastPositionChange = null;
    var firstPositionChangeOfSegment = null;

    this.tolerance = 0.5; // The amount above/below the expected position that is still considered a hit.
    this.playbackRate = 1.0;

    this.playingCalculator = Mediasite.Player.HeuristicPlayDetector.isProbablyPlaying;
    this.timeProvider = new Mediasite.Player.HeuristicPlayDetector.StandardTimeProvider();
    this.logger = createLogger();

    // True eventing is overkill. We just need a single function to call when each "event" occurs.
    this.segmentStartAction = function (startPosition) { };
    this.segmentEndAction = function (startPosition, endPosition, isImportant) { };

    this.handlePlaybackRateChange = function (rate) {
        this.playbackRate = rate;

        // TODO: Attempt minimize segment end actions caused by changes in playback rate.
    };

    this.handlePositionChange = function (position) {
        var playing;
        var clockTimeInSeconds = self.timeProvider.getCurrentTime() / 1000;
        var positionChange = { time: clockTimeInSeconds, position: position };

        if (lastPositionChange) {
            try {
                playing = self.playingCalculator(lastPositionChange.time, lastPositionChange.position, positionChange.time, positionChange.position, self.playbackRate, self.tolerance);
            } catch (err) {
                if (err.name = Mediasite.Player.HeuristicPlayDetector.ErrorNames.timeAnomaly) {
                    // Times should always be increasing.
                    // However, there are some conditions that can cause JavaScript to report a 
                    // time value that is earlier than the previous one. For example, the client 
                    // computer's time may have been updated via NTP.

                    self.logger.Debug('Times provided by timeProvider were out of order.');
                }

                // We certainly can't say that we're playing. Bite the bullet and  end the
                // current segment so we can start fresh with a new segment.
                playing = false;
            }

            if (playing) {

                if (!firstPositionChangeOfSegment) {
                    startSegment();
                }

            } else { // Seek or something. Does not look to be "just playing along".
                self.endSegment();
            }
        }

        lastPositionChange = positionChange;
    };


    this.endSegment = function (isImportantNotification) {
        isImportantNotification = isImportantNotification || false;

        if (firstPositionChangeOfSegment && lastPositionChange) {
            // Notify segment was played!
            self.segmentEndAction(firstPositionChangeOfSegment.position, lastPositionChange.position, isImportantNotification);
        }

        // Clear segment start.
        // This will result in the start of a new segment with the next qualifying position changes.
        firstPositionChangeOfSegment = null;
    };


    function startSegment() {
        // Set segment start if empty.
        firstPositionChangeOfSegment = lastPositionChange;

        // Report segment start!
        self.segmentStartAction(firstPositionChangeOfSegment.position);
    }


    function createLogger() {
        var logger = Mediasite.Logging && Mediasite.Logging.LoggerFactory("Playback Reporting")
            || // fallback for standalone testing
            {
            Debug: function () { console.log(arguments); }
        };

        return logger;
    }
};

Mediasite.Player.HeuristicPlayDetector.isProbablyPlaying = function (time1, position1, time2, position2, playbackRate, toleranceFactor) {
    var deltaTime, deltaPosition, expectedPositionChange;
    var enforceNonNegativeNumber = function (val) {
        if (typeof val !== 'number') {
            val = 0;
        }

        return Math.max(0, val);
    };

    time1 = enforceNonNegativeNumber(time1);
    position1 = enforceNonNegativeNumber(position1);
    time2 = enforceNonNegativeNumber(time2);
    position2 = enforceNonNegativeNumber(position2);
    playbackRate = enforceNonNegativeNumber(playbackRate);
    toleranceFactor = enforceNonNegativeNumber(toleranceFactor);

    if (time1 > time2) {
        // Non-increasing clock times represent a programming error so we throw!
        throw { name: 'Argument Error', message: 'time2 must be greater than time1.' };
    }

    if (position1 > position2) {
        // Non-increasing positions indicate a seek or perhaps a pause.
        return false;
    }

    deltaTime = time2 - time1;
    deltaPosition = position2 - position1;

    expectedPositionChange = deltaTime * playbackRate;

    return Math.abs(expectedPositionChange - deltaPosition) <= (expectedPositionChange * toleranceFactor);
};

Mediasite.Player.HeuristicPlayDetector.ErrorNames = {
    timeAnomaly: 'Time Anomaly'
};


Mediasite.Player.HeuristicPlayDetector.StandardTimeProvider = function () {
    var useModernMode = (window.performance && typeof window.performance.now === 'function');
    this.getCurrentTime = function () {
        if (useModernMode) {
            return window.performance.now();
        } else {
            return new Date().getTime();
        }
    };
};




Mediasite.Player.CurrentViewTracking.Event = {
    Start: "start",
    End: "end"
};



// ------------------------------------------------------
Mediasite.Player.StateReporting = function () {
    if (!(this instanceof Mediasite.Player.StateReporting)) return new Mediasite.Player.StateReporting();
    var self = this;

    var logger = Mediasite.Logging && Mediasite.Logging.LoggerFactory("Playback Reporting")
        || // fallback for standalone testing
        {
        Debug: function () { console.log(arguments); }
    };

    var _playbackTicketId;
    var _dataProvider;

    // #region ReportMediaOpened
    var _mediaPlayerType;
    var _reportedMediaOpened = false;
    self.reportMediaOpened = function () {
        if (!_dataProvider) return;
        if (_reportedMediaOpened) return;
        _reportedMediaOpened = true;

        var data = {
            playbackTicket: _playbackTicketId,
            playerType: "Javascript",
            mediaPlayerType: Mediasite.Player.MediaPlayerType.GetDisplayName(_mediaPlayerType),
            embeddedPlayer: window.self != window.top,  // future: improve to detect when embedded not in an iframe
            url: document.location.href
        };

        var container = getContainerUrl();
        if (container) {
            data.container = container;
        }


        _dataProvider.ReportMediaOpen(JSON.stringify(data));


        logger.Debug("Reporting media opened",
            "\n mediaPlayerType=" + data.mediaPlayerType,
            "\n url=" + data.url,
            "\n embedded=" + data.embeddedPlayer);
    };


    function getContainerUrl() {
        if (window.self == window.top) return;

        try {
            // FUTURE: improve to detect when embedded not in an iframe
            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 */ }
    }

    self.setMediaPlayerType = function (value) {
        _mediaPlayerType = value;
    }


    // #endregion

    self.watchPlayer = function (playerApi) {
        playerApi.addHandler({
            "playstatechanged": function (e) {
                self.reportMediaOpened();
            }
        });
    };

    self.reportToDataProvider = function (playbackTicketId, dataProvider) {
        if (!playbackTicketId || !dataProvider) {
            logger.Debug("Reporting cannot report to data provider: no playback ticket or data provider");
            return;
        }

        _playbackTicketId = playbackTicketId;
        _dataProvider = dataProvider;
    };
};


Mediasite.Player.TimelineSegment = function (start, end) {
    this.start = start;
    this.end = end;
};
Mediasite.Player.TimelineSegment.prototype = {
    clone: function () {
        return new Mediasite.Player.TimelineSegment(this.start, this.end);
    }
};

Mediasite.Player.WatchedSegmentsMemory = function () {

    var segments = [];

    this.addSegment = function (start, end) {
        segments.push(new Mediasite.Player.TimelineSegment(start, end));
    };

    this.getAllSegments = function () {
        var result = [];
        var i;
        for (i = 0; i < segments.length; i += 1) {
            result.push(segments[i].clone());
        }
        return result;
    };

    this.reset = function () {
        segments = [];
    };
};
