/**
 * @module Player
 */
/**
 * I am the HypervideoController.
 *
 * I am the central controller module of the application. I control the interactions between the UI elements of the hypervideo and its data model.
 *
 * My two most important jobs are:
 *   * I init the video element, all its UI controls, and my sub-controllers (for annotations, overlays, videolinks)
 *   * I control the playback und the update handlers to show time-based contents
 *
 * @class HypervideoController
 * @static
 */
FrameTrail.defineModule('HypervideoController', function(){
    
    var HypervideoModel        = FrameTrail.module('HypervideoModel'),
        ViewVideo                = FrameTrail.module('ViewVideo'),
        AnnotationsController  = FrameTrail.initModule('AnnotationsController'),
        OverlaysController     = FrameTrail.initModule('OverlaysController'),
        VideolinksController   = FrameTrail.initModule('VideolinksController'),
        SubtitlesController    = FrameTrail.initModule('SubtitlesController'),
        InteractionController  = FrameTrail.initModule('InteractionController'),
        isPlaying              = false,
        currentTime            = 0,
        muted                    = false,
        nullVideoStartDate     = 0,
        
        highPriorityInterval   = 40,
        lowPriorityInterval    = 180,
        nullVideoInterval      = 40,
        
        highPriorityIntervalID = null,
        lowPriorityIntervalID  = null,
        nullVideoIntervalID    = null,
        highPriorityUpdater    = null,
        lowPriorityUpdater     = null,
        videoElement           = ViewVideo.Video;
    /**
     * I initialize the Controller.
     *
     * I check, wether there are playable video source files, and append the right <source> nodes to the video element.
     * Otherwise I prepare the "Null Player", meaning a simulated playback machine, which serves as a timer for update functions.
     *
     * After the video has sufficiently loaded (or the "Null Player" is ready), I initalize the UI control (play button and progress bar).
     * 
     * @method initController
     * @param {Function} callback
     * @param {Function} failCallback
     * @param {Boolean} update
     */
    function initController(callback, failCallback, update) {
        var RouteNavigation = FrameTrail.module('RouteNavigation'),
            projectID         = RouteNavigation.projectID,
            hypervideoID    = RouteNavigation.hypervideoID,
            _video            = $(videoElement);
        updateDescriptions();
        _video.width(1920).height(1080);
        if (HypervideoModel.hasHTML5Video) {
            highPriorityUpdater = highPriorityUpdater_HTML5;
            lowPriorityUpdater  = lowPriorityUpdater_HTML5;
            FrameTrail.changeState('videoWorking', true);
            _video.append('<source src="../_data/projects/' + projectID + '/resources/' + HypervideoModel.sourceFiles.mp4  +'" type="video/mp4"></source>');
            _video.append('<source src="../_data/projects/' + projectID + '/resources/' + HypervideoModel.sourceFiles.webm +'" type="video/webm"></source>');
            _video.on('play',  _play);
            _video.on('pause', _pause);
            _video.on('seeking', function() {
                FrameTrail.changeState('videoWorking', true);
            });
            _video.on('waiting', function() {
                FrameTrail.changeState('videoWorking', true);
            });
            _video.on('canplaythrough', function() {
                FrameTrail.changeState('videoWorking', false);
            });
            _video.on('seeked', function() {
                FrameTrail.changeState('videoWorking', false);
            });
            _video.attr('preload', 'auto');
            videoElement.load();
            initVideo(
                function(){
                    HypervideoModel.duration = videoElement.duration;
                    if (update) {
                        AnnotationsController.updateController();
                    } else {
                        AnnotationsController.initController();
                    }
                    
                    OverlaysController.initController();
                    VideolinksController.initController();
                    SubtitlesController.initController();
                    initPlayButton();
                    initProgressBar();
                    InteractionController.initController();
                    if (RouteNavigation.hashTime) {
                        setCurrentTime(RouteNavigation.hashTime);
                    }
                    callback.call();
                },
                failCallback
            );
        } else {
            highPriorityUpdater = highPriorityUpdater_NullVideo;
            lowPriorityUpdater  = lowPriorityUpdater_NullVideo;
            if (update) {
                AnnotationsController.updateController();
            } else {
                AnnotationsController.initController();
            }
            
            OverlaysController.initController();
            VideolinksController.initController();
            SubtitlesController.initController();
            initPlayButton();
            initProgressBar();
            InteractionController.initController();
            if (RouteNavigation.hashTime) {
                setCurrentTime(RouteNavigation.hashTime);
            }
            callback.call();
        }
        FrameTrail.module('RouteNavigation').onHashTimeChange = function() {
            setCurrentTime(RouteNavigation.hashTime);
        };
        
    };
    /**
     * I delay the execution of callback until enough data from the video source file has loaded.
     *
     * readyState == 0 means, that metadata is loaded. This is needed to know the __duration__ of the video.
     *
     * @method initVideo
     * @param {Function} callback
     * @param {Function} failCallback
     * @private 
     */
    function initVideo(callback, failCallback) {
        var waitingInterval = 500,     // milliseconds
            counter            = 50;     // 25 seconds waiting time
        function checkReadyState() {
            if (videoElement.readyState > 0){
                callback();
            } else {
                if (--counter) {
                    window.setTimeout(checkReadyState, waitingInterval);
                } else {
                    failCallback(
                            'VideoPlayer: Received no data within the time limit of '
                        +     Math.round(waitingInterval * 50 / 1000) 
                        +    ' seconds.'
                    );
                    
                }
                
            }
        }
        checkReadyState();
    };
    /**
     * I init the UI of the play button and connect it with the play/pause functions.
     *
     * @method initPlayButton
     * @private 
     */
    function initPlayButton(){
        ViewVideo.PlayButton.click(function(){
            if (isPlaying) {
                pause();
            } else {
                play();
            }
        })        
    };
    /**
     * I initialize the UI elements of the progress bar.
     *
     * This depends on the duration of the video already being known.
     *
     * I make the DOM element a jQuery UI Slider, and attach its event listeners.
     *
     * @method initProgressBar
     * @private 
     */
    function initProgressBar() {
        ViewVideo.duration = formatTime(HypervideoModel.duration);
        ViewVideo.PlayerProgress.slider({
            value: 0,
            step: 0.01,
            orientation: "horizontal",
            range: "min",
            max: HypervideoModel.duration,
            animate: false,                    
            create: function(evt, ui) {
                        var circle      = $('<div class="ui-slider-handle-circle"></div>'),
                            innerCircle = $('<div class="ui-slider-handle-circle-inner"></div>'),
                            _evtTarget  = $(evt.target);
                        
                        innerCircle.appendTo(circle);
                        _evtTarget.children('.ui-slider-handle').append(circle);
                        ViewVideo.adjustLayout();
                        ViewVideo.adjustHypervideo();
                    },
            slide:  function(evt, ui) {
                        setCurrentTime(ui.value);
                        
                    },
            start:     function(evt, ui) {
                        
                    },
            stop:     function(evt, ui) {
                        
                    }
        });
    };
    /**
     * I update the descriptions of the hypervideo and of the current project, which is shown in the UI in the {{#crossLink "Sidebar"}}Sidebar{{/crossLink}}
     * @method updateDescriptions
     */
    function updateDescriptions() {
        var created = new Date(HypervideoModel.created).toLocaleString('de-DE', {
                    year: 'numeric',
                    month: '2-digit',
                    day: '2-digit',
                    hour: '2-digit',
                    minute: '2-digit'
                }).replace(/\./g, '.'),
            changed = new Date(HypervideoModel.lastchanged).toLocaleString('de-DE', {
                    year: 'numeric',
                    month: '2-digit',
                    day: '2-digit',
                    hour: '2-digit',
                    minute: '2-digit'
                }).replace(/\./g, '.');
        FrameTrail.module('Sidebar').ProjectDescription = FrameTrail.module('Database').project.description;
        FrameTrail.module('Sidebar').VideoDescription   = (    '<div>' + HypervideoModel.description + '</div>'
                                                      +    '<div class="descriptionLabel">Author</div>'
                                                      +    '<div>' + HypervideoModel.creator + '</div>'
                                                      + '<div class="descriptionDates">'
                                                      +    '    <div class="descriptionLabel">Created</div>'
                                                      +    '    <div>' + created + '</div>'
                                                      +    '    <div class="descriptionLabel">Last changed</div>'
                                                      +    '    <div>' + changed + '</div>'
                                                      + '</div>'
                                                    );
    };
    /**
     * I am the high priority update function, when there is a HTML5 video element present.
     *
     * I am called from the browser runtime environment via its window.setInterval mechanism. The interval is defined in the 
     * {{#crossLink "HypervideoController/_play:method"}}_play method{{/crossLink}}, and the interval length is set to 40 milliseconds.
     *
     * I fetch the currentTime attribute from the video element and store it in {{#crossLink "HypervideoController/currentTime:attribute"}}.
     *
     * I update the slider position of the progress bar.
     *
     * @method highPriorityUpdater_HTML5
     * @private 
     */
    function highPriorityUpdater_HTML5() {
        currentTime = videoElement.currentTime;
        ViewVideo.PlayerProgress.slider('value', currentTime);
    };
    /**
     * I am the low priority update function, when there is a HTML5 video element present.
     *
     * I am called from the browser runtime environment via its window.setInterval mechanism. The interval is defined in the
     * {{#crossLink "HypervideoController/_play:method"}}_play method{{/crossLink}}, and the interval length is set to 180 milliseconds.
     *
     * I perform the following tasks:
     *
     * * Display the currentTime in the UI (numeric display in progress bar)
     * * Call all sub-controllers ({{#crossLink "OverlaysController"}}OverlaysController{{/crossLink}}, {{#crossLink "VideosController"}}VideosController{{/crossLink}}, {{#crossLink "AnnotationsController"}}AnnotationsController{{/crossLink}}), to update the state for which they are responsible for.
     *
     * @method lowPriorityUpdater_HTML5
     * @private 
     */
    function lowPriorityUpdater_HTML5() {
        ViewVideo.currentTime = formatTime(currentTime);
        OverlaysController.updateStatesOfOverlays(currentTime);
        VideolinksController.updateStatesOfVideolinks(currentTime);
        AnnotationsController.updateStatesOfAnnotations(currentTime);
        SubtitlesController.updateStatesOfSubtitles(currentTime);
        
    };
    /**
     * I am the high priority update function, when there is no HTML5 video element ("Null Player").
     *
     * I am called from the browser runtime environment via its window.setInterval mechanism. The interval is defined in the 
     * {{#crossLink "HypervideoController/_play:method"}}_play method{{/crossLink}}, and the interval length is set to 40 milliseconds.
     *
     * I update the slider position of the progress bar.
     *
     * @method highPriorityUpdater_NullVideo
     * @private 
     */
    function highPriorityUpdater_NullVideo() {
        ViewVideo.PlayerProgress.slider('value', currentTime);
    };
    /**
     * I am the low priority update function, when there is no HTML5 video element ("Null Player").
     *
     * I am called from the browser runtime environment via its window.setInterval mechanism. The interval is defined in the
     * {{#crossLink "HypervideoController/_play:method"}}_play method{{/crossLink}}, and the interval length is set to 180 milliseconds.
     *
     * I perform the following tasks:
     *
     * * Display the currentTime in the UI (numeric display in progress bar)
     * * Call all sub-controllers ({{#crossLink "OverlaysController"}}OverlaysController{{/crossLink}}, {{#crossLink "VideosController"}}VideosController{{/crossLink}}, {{#crossLink "AnnotationsController"}}AnnotationsController{{/crossLink}}), to update the state for which they are responsible for.
     *
     * @method lowPriorityUpdater_NullVideo
     * @private 
     */
    function lowPriorityUpdater_NullVideo() {
        ViewVideo.currentTime = formatTime(currentTime);
        OverlaysController.updateStatesOfOverlays(currentTime);
        VideolinksController.updateStatesOfVideolinks(currentTime);
        AnnotationsController.updateStatesOfAnnotations(currentTime);
        SubtitlesController.updateStatesOfSubtitles(currentTime);
        
    };
    /**
     * I am the update function of the "Null Player", which sets the {{#crossLink "HypervideoController/currentTime:attribute"}}.
     *
     * When the currentTime reaches the duration of the null video, I stop playback.
     *
     * @method nullVideoUpdater
     * @private 
     */
    function nullVideoUpdater() {
        currentTime = (Date.now() - nullVideoStartDate) / 1000;
        
        if (currentTime >= HypervideoModel.duration) {
            currentTime = HypervideoModel.duration;
            pause();
        }
    };
    /**
     * I am the function, which starts the playback of the hypervideo.
     *
     * When there is a HTML5 video present, i simply call its .play() method,
     * which in turn triggers the "play" event of the <video> element;
     * The {{#crossLink "HypervideoController/_play:method"}}_play(){{/crossLink}} method is set as event handler for this event.
     *
     * When there is no HTML5 video ("Null player"), then I do two things:
     * * I check if the currentTime reached the end of the "null video", and reset it to 0 if necessary.
     * * I store the computer's current system clock time in the module var nullVideoStartDate (from this number the {{#crossLink "HypervideoController/nullVideoUpdater:method"}}nullVideoUpdater(){{/crossLink}}) can calculate the new currentTime.
     *
     * @method play
     */
    function play() {
        if (HypervideoModel.hasHTML5Video) {
            videoElement.play();
        } else {
            if (!isPlaying){
                if (currentTime === HypervideoModel.duration) {
                    currentTime = 0;
                }
                nullVideoStartDate = Date.now() - (currentTime * 1000)
                _play();
            }
            
        }
    };
    /**
     * I pause the playback of the hypervideo.
     *
     * When there is a HTML5 video present, i call its .pause() method,
     * which in turn triggers the "pause" event of the <video> element;
     * The {{#crossLink "HypervideoController/_pause:method"}}_pause(){{/crossLink}} method is set as event handler for the pause event.
     *
     * When there is no HTML5 video ("null player") I directly call the {{#crossLink "HypervideoController/_pause:method"}}_pause(){{/crossLink}} method.
     *
     * @method pause
     */
    function pause() {
        if (HypervideoModel.hasHTML5Video) {
            videoElement.pause();
        } else {
            _pause();
        }
    };
    /**
     * After playback has started, we need to do several things: 
     * * Register interval functions (highPriorityUpdater and highPriorityInterval; if necessary: nullVideoUpdater)
     * * Change play button into a pause button
     * * Tell the {{#crossLink "OverlaysController/syncMedia:method"}}OverlaysController to synchronize media{{/crossLink}}.
     *
     * @method _play
     * @private
     */
    function _play() {
        highPriorityIntervalID = window.setInterval(highPriorityUpdater, highPriorityInterval);
        lowPriorityIntervalID  = window.setInterval(lowPriorityUpdater,  lowPriorityInterval);
        if (!HypervideoModel.hasHTML5Video) {
            nullVideoIntervalID = window.setInterval(nullVideoUpdater,  nullVideoInterval);
        }
        ViewVideo.PlayButton.addClass('playing');
        isPlaying = true;
        OverlaysController.syncMedia();
    };
    /**
     * After playback has paused, we need to do several things: 
     * * Clear the interval functions (highPriorityUpdater and highPriorityInterval; if necessary: nullVideoUpdater)
     * * Change pause button back into play button
     * * Tell the {{#crossLink "OverlaysController/syncMedia:method"}}OverlaysController to synchronize media{{/crossLink}}
     *
     * @method _pause
     * @private
     */
    function _pause() {
        window.clearInterval(highPriorityIntervalID);
        window.clearInterval(lowPriorityIntervalID);
        if (!HypervideoModel.hasHTML5Video) {
            window.clearInterval(nullVideoIntervalID);
        }
        ViewVideo.PlayButton.removeClass('playing');
        isPlaying = false;
        OverlaysController.syncMedia();
    };
    
    /**
     * The HypervideoController stores the {{#crossLink "HypervideoController/currentTime:attribute"}}currentTime{{/crossLink}}.
     * When this property is being set, several things have to happen:
     * * The currentTime of the <video> element has to be updated...
     * * or – when there is no video source file – the nullVideoStartDate has to be updated
     * * The update functions have to be called (highPriorityUpdater and lowPriorityUpdater)
     * * The OverlaysController has to {{#crossLink "OverlaysController/syncMedia:method"}}synchronize media{{/crossLink}}
     *
     * @method setCurrentTime
     * @param {Number} aNumber
     * @return Number
     * @private
     */
    function setCurrentTime(aNumber) {
        
        
        if (HypervideoModel.hasHTML5Video) {
            videoElement.currentTime = currentTime = aNumber;
            
        } else {
            currentTime = aNumber;
            nullVideoStartDate = Date.now() - (currentTime * 1000)
        }
        highPriorityUpdater();
        lowPriorityUpdater();
        OverlaysController.syncMedia();
        
        return aNumber;
    };
    /**
     * The HypervideoController stores the {{#crossLink "HypervideoController/muted:attribute"}}muted{{/crossLink}}.
     * When this property is being set, the muted attribute of the <video> element has to be updated
     * (only when there is a video source file)
     *
     * @method setMuted
     * @param {Boolean} muted
     * @return muted
     * @private
     */
    function setMuted(muted) {
        
        if (HypervideoModel.hasHTML5Video) {
            videoElement.muted = muted;
            
        }
        OverlaysController.muteMedia(muted);
        
    };
    
    /**
     * I take a number, which represents a time in seconds,
     * and return a formatted string like HH:MM:SS
     *
     * @method formatTime
     * @param {Number} aNumber
     * @return String
     */
    function formatTime(aNumber) {
        var hours, minutes, seconds, hourValue;
        seconds     = Math.ceil(aNumber);
        hours         = Math.floor(seconds / (60 * 60));
        hours         = (hours >= 10) ? hours : '0' + hours;
        minutes     = Math.floor(seconds % (60*60) / 60);
        minutes     = (minutes >= 10) ? minutes : '0' + minutes;
        seconds     = Math.ceil(seconds % (60*60) % 60);
        seconds     = (seconds >= 10) ? seconds : '0' + seconds;
        if (hours >= 1) {
            hourValue = hours + ':';
        } else {
            hourValue = '';
        }
        return hourValue + minutes + ':' + seconds;
    };
    /**
     * Cancel all currently running intervals
     *
     * @method clearIntervals
     * @private
     */
    function clearIntervals() {
        
        window.clearInterval(highPriorityIntervalID);
        window.clearInterval(lowPriorityIntervalID);
        if (!HypervideoModel.hasHTML5Video) {
            window.clearInterval(nullVideoIntervalID);
        }
    };
    return {
        initController: initController,
        play: play,
        pause: pause,
        updateDescriptions: updateDescriptions,
        clearIntervals:     clearIntervals,
        /**
         * This read-only attribute tells if the hypervideo is playing or not.
         * It is set by {{#crossLink "HypervideoController/_play:method"}}_play(){{/crossLink}} and {{#crossLink "HypervideoController/_pause:method"}}_pause(){{/crossLink}}
         *
         * @attribute isPlaying
         * @readOnly
         */
        get isPlaying()          { return isPlaying               },
        
        /**
         * This attributes stores the currentTime of the hypervideo.
         *
         * When this attribute is being read, it returns the value, which was automatically updated by {{#crossLink "HypervideoController/highPriorityUpdater_HTML5:method"}}highPriorityUpdater_HTML5(){{/crossLink}} or respectively {{#crossLink "HypervideoController/nullVideoUpdater:method"}}nullVideoUpdater(){{/crossLink}}.
         *
         * When the attribute is being set, like this:
         *
         *     FrameTrail.module('HypervideoController').currentTime = 3
         *
         * then the {{#crossLink "HypervideoController/setCurrentTime:method"}}setCurrentTime(){{/crossLink}} is called.
         *
         * @attribute currentTime
         */
        get currentTime()        { return currentTime             },
        set currentTime(aNumber) { return setCurrentTime(aNumber) },
        /**
         * These attributes store the muted state of the hypervideo.
         *
         * The muted state is set like this:
         *
         *     FrameTrail.module('HypervideoController').muted = true
         *
         * then the {{#crossLink "HypervideoController/setMuted:method"}}setMuted(){{/crossLink}} is called.
         *
         * @attribute muted
         */
        get muted()              { return muted                    },
        set muted(aBoolean)      { return setMuted(aBoolean)        }
    }
});