Show:
/**
 * @module Player
 */


/**
 * I am the VideolinksController. I am responsible for managing all the {{#crossLink "Videolink"}}videolinks{{/crossLink}}
 * in the current {{#crossLink "HypervideoModel"}}HypervideoModel{{/crossLink}}, and for displaying them for viewing and editing.
 *
 * @class VideolinksController
 * @static
 */



FrameTrail.defineModule('VideolinksController', function(){


    var videolinks        = FrameTrail.module('HypervideoModel').videolinks, // can be shadowed be function local vars
        ViewVideo         = FrameTrail.module('ViewVideo'),
        videolinkInFocus  = null,
        openedLink        = null;



    /**
     * I tell all videolinks in the 
     * {{#crossLink "HypervideoModel/videolinks:attribute"}}HypervideoModel/videolinks attribute{{/crossLink}}
     * to render their elements into the DOM.
     * @method initController
     */
    function initController() {

        var videolinks = FrameTrail.module('HypervideoModel').videolinks;
        

        for (var i = 0; i < videolinks.length; i++) {

            videolinks[i].renderTimelineInDOM();
            videolinks[i].renderTilesAndContentInDOM();

        }

        distributeTiles();


    };



    /**
     * I remove all tileElements and videolinkElements from the DOM and then
     * re-append them again.
     *
     * This has the purpose that the DOM elements must appear in a sorted order by their start time. So this method has to called
     * after the user has finished editing.
     *
     * @method rearrangeTilesAndContent
     */
    function rearrangeTilesAndContent() {

        var videolinks = FrameTrail.module('HypervideoModel').videolinks;
        
        ViewVideo.VideolinkTiles.empty();
        ViewVideo.VideolinkContainer.empty();

        for (var i = 0; i < videolinks.length; i++) {

            videolinks[i].renderTilesAndContentInDOM();

        }

        distributeTiles();


    };


    /**
     * I am a central method of the VideolinksController.
     * I am called from the update functions inside the HypervideoController
     * and I set the activeState of the videolinks according to the current time.
     *
     * @method updateStatesOfVideolinks
     * @param {Number} currentTime
     */
    function updateStatesOfVideolinks(currentTime) {

        var videolink;

        for (var idx in videolinks) {

            videolink = videolinks[idx];

            if (    videolink.data.start <= currentTime
                 && videolink.data.end   >= currentTime) {

                if (!videolink.activeState) {

                    videolink.setActive();

                }

            } else {

                if (videolink.activeState) {

                    videolink.setInactive();

                }

            }

        }

        if (videolinkInFocus && !videolinkInFocus.activeState) {
            videolinkInFocus.setActive();
        }


    };


    /**
     * I distribute the tileElements in the tileContainer, so that they
     * match closely to the position of their related timelineElements.
     * When they would start to overlap, I arrange them in groups.
     *
     * See also {{#crossLink "Videolink"}}Videolink{{/crossLink}}.
     *
     * @method distributeTiles
     */
    function distributeTiles() {

        var videolinks          = FrameTrail.module('HypervideoModel').videolinks,
            videoDuration       = FrameTrail.module('HypervideoModel').duration,
            sliderParent        = ViewVideo.VideolinkTiles,
            containerElement    = ViewVideo.VideolinkTileSlider,
            groupCnt            = 0,
            gap                 = 3,
            thisTileElement,
            previousElement,
            previousElementRightPos,
            startTime,
            endTime,
            middleTime,
            desiredPosition,
            finalPosition;


        containerElement.children().removeAttr('data-group-id');
        containerElement.children().css({
            position: '',
            left:     ''
        });

        function getTotalWidth(collection, addition){

            var totalWidth = 0;
            collection.each(function() {
                totalWidth += $(this).width()+addition;
            });
            return totalWidth;

        }

        function getNegativeOffsetRightCorrection(leftPosition, collectionWidth) {
            
            var offsetCorrection,
                mostRightPos = leftPosition + collectionWidth + (gap*2);

            if ( mostRightPos >= sliderParent.width() ) {
                
                offsetCorrection = mostRightPos - sliderParent.width();

                return offsetCorrection;
                
            }

            return 0;
        }

        // Cancel if total width > container width
        if ( getTotalWidth(containerElement.children(), 3) > sliderParent.width() ) {
            containerElement.width( getTotalWidth(containerElement.children(), 3) );
            return;
        } else {
            containerElement.width('');
        }
        
        // Distribute Items
        for (var i = 0; i < videolinks.length; i++) {

            thisTileElement = videolinks[i].tileElement;



            if (i > 0) {
                previousElement         = videolinks[i-1].tileElement;
                previousElementRightPos = previousElement.position().left + previousElement.width();
            }

            startTime   = videolinks[i].data.start;
            endTime     = videolinks[i].data.end;
            middleTime  = startTime + ( (endTime-startTime)/2 );
            
            desiredPosition = ( (sliderParent.width() / videoDuration) * middleTime ) - ( thisTileElement.width()/2 );

            
            thisTileElement.attr({
                'data-in':  startTime,
                'data-out': endTime
            });

            if (desiredPosition <= 0) {
                finalPosition = 0;
                thisTileElement.removeAttr('data-group-id');
                groupCnt++;

            } else if (desiredPosition < previousElementRightPos + gap) {

                finalPosition = previousElementRightPos + gap;
                
                if (previousElement.attr('data-group-id')) {

                    containerElement.children('[data-group-id="'+ previousElement.attr('data-group-id') +'"]').attr('data-group-id', groupCnt);

                } else {

                    previousElement.attr('data-group-id', groupCnt);

                }

                thisTileElement.attr('data-group-id', groupCnt);
                groupCnt++;

            } else {

                finalPosition = desiredPosition;
                thisTileElement.removeAttr('data-group-id');
                groupCnt++;

            }

            thisTileElement.css({
                position: "absolute",
                left: finalPosition + "px"
            });

        }

        // Re-Arrange Groups

        var groupCollection,
            p,
            previousGroupCollection,
            previousGroupCollectionRightPos,
            totalWidth,
            groupStartTime,
            groupEndTime,
            groupMiddleTime,
            desiredGroupPosition,
            correction,
            negativeOffsetRightCorrection,
            groupIDs;
        
        function arrangeGroups() {

            groupIDs = [];

            containerElement.children('[data-group-id]').each(function() {
                if ( groupIDs.indexOf( $(this).attr('data-group-id') ) == -1 ) {
                    groupIDs.push($(this).attr('data-group-id'));
                }
            });
            
            for (var i=0; i < groupIDs.length; i++) {
                
                var g = groupIDs[i];

                groupCollection = containerElement.children('[data-group-id="'+ g +'"]');

                if (groupCollection.length < 1) {
                    continue;
                }

                if ( groupIDs[i-1] ) {
                    p = groupIDs[i-1];
                    previousGroupCollection         = containerElement.children('[data-group-id="'+ p +'"]');
                    previousGroupCollectionRightPos = previousGroupCollection.eq(0).position().left + getTotalWidth( previousGroupCollection, 3 );
                }

                totalWidth      = getTotalWidth( groupCollection, 3 );

                groupStartTime  = parseInt(groupCollection.eq(0).attr('data-in'));
                groupEndTime    = parseInt(groupCollection.eq(groupCollection.length-1).attr('data-out'));
                groupMiddleTime = groupStartTime + ( (groupEndTime-groupStartTime)/2 );

                desiredGroupPosition = ( (sliderParent.width() / videoDuration) * groupMiddleTime ) - ( totalWidth/2 );

                correction = groupCollection.eq(0).position().left - desiredGroupPosition;

                if ( groupCollection.eq(0).position().left - correction >= 0 && desiredGroupPosition > previousGroupCollectionRightPos + gap ) {
                    
                    groupCollection.each(function() {
                        $(this).css('left', '-='+ correction +'');
                    });

                } else if ( groupCollection.eq(0).position().left - correction >= 0 && desiredGroupPosition < previousGroupCollectionRightPos + gap ) {
                    
                    var  attachCorrection = groupCollection.eq(0).position().left - previousGroupCollectionRightPos;
                    groupCollection.each(function() {
                        
                        $(this).css('left', '-='+ attachCorrection +'');

                    });

                    if ( groupCollection.eq(0).prev().length ) {
                        
                        var prevElem = groupCollection.eq(0).prev();

                        if ( prevElem.attr('data-group-id') ) {

                            previousGroupCollection.attr('data-group-id', g);

                        } else {

                            prevElem.attr('data-group-id', g);
                            
                        }
                        
                    }

                }

            }

        }

        arrangeGroups();
        


        // Deal with edge case > tiles outside container on right side
        
        var repeatIteration;

        function solveRightEdgeOverlap() {

            repeatIteration = false;

            for (var i = 0; i < videolinks.length; i++) {

                thisTileElement = videolinks[i].tileElement;

                var g = undefined;
                
                if ( thisTileElement.attr('data-group-id') ) {
                    g = thisTileElement.attr('data-group-id');
                    groupCollection = containerElement.children('[data-group-id="'+ g +'"]');
                } else {
                    groupCollection = thisTileElement;
                }

                if (groupCollection.eq(0).prev().length) {
                    
                    previousElement = groupCollection.eq(0).prev();

                    if ( previousElement.attr('data-group-id') ) {

                        previousGroupCollection         = containerElement.children('[data-group-id="'+ previousElement.attr('data-group-id') +'"]');
                        previousGroupCollectionRightPos = previousGroupCollection.eq(0).position().left + getTotalWidth( previousGroupCollection, 3 );

                    } else {

                        previousGroupCollection         = previousElement;
                        previousGroupCollectionRightPos = previousElement.position().left + previousElement.width() + gap;
                        
                    }

                    
                } else {
                    previousGroupCollectionRightPos = 0;
                }

                totalWidth = getTotalWidth( groupCollection, 3 );

                currentGroupCollectionLeft = groupCollection.eq(0).position().left;
                currentGroupCollectionRightPos = groupCollection.eq(0).position().left + totalWidth;

                negativeOffsetRightCorrection = getNegativeOffsetRightCorrection(currentGroupCollectionLeft, totalWidth);

                if ( currentGroupCollectionLeft - negativeOffsetRightCorrection >= 0  && negativeOffsetRightCorrection > 1 ) {
                    
                    if ( currentGroupCollectionLeft - negativeOffsetRightCorrection > previousGroupCollectionRightPos + gap ) {
                        
                        groupCollection.each(function() {
                            $(this).css('left', '-='+ negativeOffsetRightCorrection +'');
                        });

                    } else if ( currentGroupCollectionLeft - negativeOffsetRightCorrection < previousGroupCollectionRightPos + gap ) {

                        var attachCorrection = currentGroupCollectionLeft - previousGroupCollectionRightPos;
                        groupCollection.each(function() {
                            $(this).css('left', '-='+ attachCorrection +'');
                        });

                        if ( !g && previousElement.length && previousElement.attr('data-group-id') ) {
                            
                            thisTileElement.attr('data-group-id', previousElement.attr('data-group-id'));

                        }

                        if ( previousElement.attr('data-group-id') ) {

                            containerElement.children('[data-group-id="'+ previousElement.attr('data-group-id') +'"]').attr('data-group-id', g);
                            
                        } else {

                            previousElement.attr('data-group-id', g);

                        }                        
                        
                        
                        repeatIteration = false;                            

                    }

                }

            }

            if ( repeatIteration ) {
                solveRightEdgeOverlap();
            }

        }

        solveRightEdgeOverlap();
        

    }


    /**
     * When we are in the editMode annotations, the timeline should
     * show all timeline elements stacked, which is what I do.
     * @method stackTimelineView
     */
    function stackTimelineView() {
        
        ViewVideo.VideolinkTimeline.CollisionDetection({spacing:0, includeVerticalMargins:true});
        ViewVideo.adjustLayout();
        ViewVideo.adjustHypervideo();

    };


    /**
     * When we are in the editMode annotations, the timeline should
     * show all timeline elements stacked. After leaving this mode,
     * I have to reset the timelineElements and the timeline to their normal
     * layout.
     * @method resetTimelineView
     * @private
     */
    function resetTimelineView() {
        
        ViewVideo.VideolinkTimeline.css('height', '');
        ViewVideo.VideolinkTimeline.children('.timelineElement').css({
            top:    '',
            right:  '',
            bottom: '',
            height: ''
        });

    };



    /**
     * When the editMode 'links' was entered, the #EditingOptions area
     * should show two tabs: 
     * * a list of (draggable) thumbnails with available hypervideos
     * * a text form, where the user can manually input a link URL
     *
     * @method initEditOptions
     * @private
     */
    function initEditOptions() {

        ViewVideo.EditingOptions.empty();


        var hypervideos = FrameTrail.module('Database').hypervideos,
            thumb,

            videolinkEditingOptions = $('<div id="VideolinkEditingTabs">'
                                    +   '    <ul>'
                                    +   '        <li>'
                                    +   '            <a href="#Videolist">Choose Hypervideo</a>'
                                    +   '        </li>'
                                    +   '        <li>'
                                    +   '            <a href="#EnterVideolink">Enter Link</a>'
                                    +   '        </li>'
                                    +   '    </ul>'
                                    +   '    <div id="Videolist">'
                                    +   '    </div>'
                                    +   '    <div id="EnterVideolink">'
                                    +   '        <label for="VideolinkHref">URL or relative path</label>'
                                    +   '        <input  id="VideolinkHref" type="text" placeholder="http://myhypervideo/">'
                                    +   '        <label for="VideolinkName">Name of Link</label>'
                                    +   '        <input  id="VideolinkName" type="text" placeholder="My Video">'
                                    +   '        <button id="CreateVideolink">Create Link</button>'
                                    +   '    </div>'
                                    +   '</div>')
                                    .tabs({
                                        heightStyle: "fill"
                                    }),

            videolist = videolinkEditingOptions.find('#Videolist');


        for (var id in hypervideos) {
            thumb = FrameTrail.newObject('Hypervideo', hypervideos[id]).renderThumb();
            thumb.draggable({
                containment:    '#MainContainer',
                helper:         'clone',
                appendTo:       'body',
                distance:       10,
                zIndex:         1000,

                start: function( event, ui ) {
                    ui.helper.css({
                        top: $(event.currentTarget).offset().top + "px",
                        left: $(event.currentTarget).offset().left + "px",
                        width: $(event.currentTarget).width() + "px",
                        height: $(event.currentTarget).height() + "px"
                    });
                    $(event.currentTarget).addClass('dragPlaceholder');
                },
                
                stop: function( event, ui ) {
                    $(event.target).removeClass('dragPlaceholder');
                }

            });

            videolist.append(thumb);
        }
        

        videolinkEditingOptions.find('#CreateVideolink').click(function(){

            var startTime       = FrameTrail.module('HypervideoController').currentTime,
                videoDuration   = FrameTrail.module('HypervideoModel').duration,
                endTime         = (startTime + 4 > videoDuration) 
                                    ? videoDuration
                                    : startTime + 4,
                

                newVideolink = FrameTrail.module('HypervideoModel').newVideolink({
                    "start":        startTime,
                    "end":          endTime,
                    "name":         videolinkEditingOptions.find('#VideolinkName').val(),
                    "href":         videolinkEditingOptions.find('#VideolinkHref').val()
                });

            
            newVideolink.renderTimelineInDOM();
            rearrangeTilesAndContent();

            newVideolink.startEditing();
            updateStatesOfVideolinks(FrameTrail.module('HypervideoController').currentTime);
            setVideolinkInFocus(newVideolink);

            stackTimelineView();

        });

        
        ViewVideo.EditingOptions.append(videolinkEditingOptions);

        


    };


    /**
     * When the editMode 'link' has been entered, the 
     * videolink timeline should be droppable for new items
     * (from the tab of available hypervideos, see {{#crossLink "VideolinksController/initEditOptions:method"}}VideolinksController/initEditOptions{{/crossLink}}).
     * A drop event should trigger the process of creating a new videolink.
     * My parameter is true or false to activate or deactivate this behavior.
     * @method makeTimelineDroppable
     * @param {Boolean} active
     */
    function makeTimelineDroppable(active) {

        if (active) {

            ViewVideo.VideolinkTimeline.droppable({
                accept:         '.hypervideoThumb',
                activeClass:    'droppableActive',
                hoverClass:     'droppableHover',
                tolerance:      'touch',

                over: function( event, ui ) {
                    ViewVideo.PlayerProgress.find('.ui-slider-handle').addClass('highlight');
                },

                out: function( event, ui ) {
                    ViewVideo.PlayerProgress.find('.ui-slider-handle').removeClass('highlight');
                },

                drop: function( event, ui ) {

                    var projectID       = FrameTrail.module('RouteNavigation').projectID,
                        hypervideoID    = ui.helper.attr('data-hypervideoID'),
                        hypervideoName  = ui.helper.attr('data-name'),
                        videoDuration   = FrameTrail.module('HypervideoModel').duration,
                        startTime       = FrameTrail.module('HypervideoController').currentTime,
                        endTime         = (startTime + 4 > videoDuration) 
                                            ? videoDuration
                                            : startTime + 4,

                        
                        newVideolink = FrameTrail.module('HypervideoModel').newVideolink({
                            "start":  startTime,
                            "end":    endTime,
                            "name":   hypervideoName,
                            "href":   '?project=' + projectID + '&hypervideo=' + hypervideoID
                        });

                    
                    newVideolink.renderTimelineInDOM();
                    rearrangeTilesAndContent();

                    newVideolink.startEditing();
                    updateStatesOfVideolinks(FrameTrail.module('HypervideoController').currentTime);
                    setVideolinkInFocus(newVideolink);

                    stackTimelineView();
                    
                    ViewVideo.PlayerProgress.find('.ui-slider-handle').removeClass('highlight');


                }

            });


        } else {


            ViewVideo.VideolinkTimeline.droppable('destroy');

        }

    }



    /**
     * When a videolink is set into focus, I have to tell 
     * the old videolink in the var videolinkInFocus, that it
     * is no longer in focus. Then I store the videolink (or null)
     * from my parameter in the var videolinkInFocus, and inform it 
     * about it.
     * @method setVideolinkInFocus
     * @param {Videolink} videolink
     * @private
     */
    function setVideolinkInFocus(videolink) {


        if (videolinkInFocus) {
            
            videolinkInFocus.permanentFocusState = false;
            videolinkInFocus.removedFromFocus();

        }

        videolinkInFocus = videolink;
        
        if (videolinkInFocus) {
            videolinkInFocus.gotInFocus();
        }

        updateStatesOfVideolinks(FrameTrail.module('HypervideoController').currentTime);

        return videolink;


    };

    


    /**
     * I listens to the global state 'editMode'.
     *
     * If we enter the editMode "links" I prepare all videolinks for editing, prepare the timeline
     * and the "editOptions" interface.
     *
     * When leaving I reset them.
     *
     * @method toggleEditMode
     * @param {String} editMode
     * @param {String} oldEditMode
     */
    function toggleEditMode(editMode, oldEditMode) {

        var videolinks = FrameTrail.module('HypervideoModel').videolinks;


        if(editMode === 'links' && oldEditMode !== 'links') {

            for (var idx in videolinks) {

                videolinks[idx].startEditing();

            }

            stackTimelineView();
            initEditOptions();
            makeTimelineDroppable(true);
            


        } else if (oldEditMode === 'links' && editMode !== 'links') {

            for (var idx in videolinks) {

                videolinks[idx].stopEditing();

            }

            setVideolinkInFocus(null);
            resetTimelineView();
            rearrangeTilesAndContent();
            makeTimelineDroppable(false);       

        }

    }


    /**
     * I react to changes in the global state "viewSize" (which is triggerd by a resize event of the window).
     * @method changeViewSize
     */
    function changeViewSize() {

        distributeTiles();

    }


    /**
     * I react to changes in the global state viewSizeChanged.
     * The state changes after a window resize event 
     * and is meant to be used for performance-heavy operations.
     *
     * @method onViewSizeChanged
     * @private
     */
    function onViewSizeChanged() {

        

    }


    /**
     * I react to changes in the global state "sidebarOpen".
     * @method toggleSidebarOpen
     */
    function toggleSidebarOpen() {

        var maxSlideDuration = 280,
            interval;

        interval = window.setInterval(distributeTiles, 40);
        
        window.setTimeout(function(){

            window.clearInterval(interval);

        }, maxSlideDuration)


    }


    /**
     * I open the videolinkElement of a videolink in the videolinkContainer.
     *
     * If my parameter is null, I close the videolinkContainer.
     *
     * @method setOpenedLink
     * @param {Videolink} videolink
     */
    function setOpenedLink(videolink) {

        openedLink = videolink

        for (var idx in videolinks) {

            videolinks[idx].videolinkElement.removeClass('open');
            videolinks[idx].tileElement.removeClass('open');

            videolinks[idx].videolinkElement.children('iframe').attr('src', '');

        }

        if (videolink) {

            // randomVersion allows to use the same iFrame src several times
            var randomVersion  = Math.round(Math.random() * (100 - 1) + 1),
                fragmentSplit  = videolink.data.href.split('#'),
                randomizedLink = fragmentSplit[0] + '&v=' + randomVersion + '#' + fragmentSplit[1];
            
            videolink.videolinkElement.children('iframe').attr('src', randomizedLink);

            videolink.videolinkElement.addClass('open');
            videolink.tileElement.addClass('open');
            ViewVideo.shownDetails = 'videolinks';

        } else {

            ViewVideo.shownDetails = null;

        }

    }

    /**
     * I am the starting point for the process of deleting 
     * a videolink. I call other necessary methods for it.
     * @method deleteVideolink
     * @param {Videolink} videolink
     */
    function deleteVideolink(videolink) {

        setVideolinkInFocus(null);
        videolink.removeFromDOM();
        distributeTiles();
        FrameTrail.module('HypervideoModel').removeVideolink(videolink);
        stackTimelineView();

    }

        
    /**
     * I react to changes in the global state "viewMode".
     *
     * @method toggleViewMode
     * @param {String} viewMode
     * @param {String} oldViewMode
     */
    function toggleViewMode(viewMode, oldViewMode){

        if (viewMode === 'video' && oldViewMode !== 'video') {
            distributeTiles();
        }

    }


    return {

        onChange: {
            editMode:        toggleEditMode,
            viewSize:        changeViewSize,
            viewSizeChanged: onViewSizeChanged,
            sidebarOpen:     toggleSidebarOpen,
            viewMode:        toggleViewMode
        },

        initController:             initController,
        updateStatesOfVideolinks:   updateStatesOfVideolinks,
        stackTimelineView:          stackTimelineView,
        deleteVideolink:            deleteVideolink,

        /**
         * I hold the currently opened videolink (or null, when there is no opened link).
         * I use the {{#crossLink "VideolinksController/setOpenedLink:method"}}VideolinksController/setOpenedLink(){{/crossLink}}.
         * @attribute openedLink
         * @type Videolink
         */
        get openedLink()            { return openedLink               },
        set openedLink(videolink)   { return setOpenedLink(videolink) },

        /**
         * I hold the videolink which is "in focus" (or null, when there is no link in focus).
         * I use the {{#crossLink "VideolinksController/setVideolinkInFocus:method"}}VideolinksController/setVideolinkInFocus(){{/crossLink}}.
         * @attribute videolinkInFocus
         * @type Videolink
         */
        set videolinkInFocus(videolink) { return setVideolinkInFocus(videolink) },
        get videolinkInFocus()          { return videolinkInFocus               }


    };

});