/// <reference path="3rdParty/jquery.js"/>
/// <reference path="Mediasite.Geometry.js"/>
/// <reference path="MultiViewLayout.js"/>

if (!Mediasite) {
    var Mediasite = {};
}
Mediasite.Player = Mediasite.Player || {};


//=============================================================================
// MultiViewStage Module
//=============================================================================
Mediasite.Player.MultiViewStage = (function ($, geometry, mvLayout, undefined) {

    //-----------------------------------------------------------------------------------
    // Stage
    // -----
    // A control for displaying one or more content streams in an adaptive layout.
    //
    // Events: stage-stream-area-order-changed
    //-----------------------------------------------------------------------------------
    function Stage(stageElement) {

        var that = this;
        var _element = $(stageElement);
        var _areas = {};
        var _registeredIdentifiers = [];
        var _orderedIdentifiers = [];
        var _lastNotifiedStreamOrderToken = '';
        var _marginSpacing = new StaticSpacingStrategy(25);
        var _gutterSpacing = new StaticSpacingStrategy(15);
        var _sideBias = SideBias.left;

        var _layoutSpec = newLayoutSpec();
        var _layoutEngine = new mvLayout.LayoutEngine();

        var _occupiedContentRect = new geometry.Rect(0, 0, 0, 0);


        function init() {

        }


        // Additional Members
        // - getCurrentLayout



        this.getElement = function () {
            return _element;
        };


        //---------------------------------------------------------------------------
        // Adds a stream to the content layout. 
        //
        //    identifier       : An identifying string, usually a StreamType.
        //    streamBox        : A StageStreamBox implementation.
        //    streamProperties : An object with properties necessary for layout.
        //                       - aspectRatio : Number
        //---------------------------------------------------------------------------
        this.addStreamArea = function (streamArea) {
            // TODO: Throw an exception on attempts to re-add of existing identifier.

            // Add the stream's element to the DOM.
            streamArea.appendTo(_element);

            // Merge some sensible defaults with the given stream properties.
            streamArea.aspectRatio = streamArea.aspectRatio || 16 / 9;
            //streamArea.nativeWidth = streamArea.nativeWidth || 1920;   // SOMEDAY, BABY.
            //streamArea.nativeHeight = streamArea.nativeHeight || 1080; // SOMEDAY...


            // Let's remember what we know about the stream. 
            // Using the identifier as the key will allow easy lookup later.
            _areas[streamArea.identifier] = streamArea;

            // Our new stream gets to be last in line.
            _registeredIdentifiers.push(streamArea.identifier);
            _orderedIdentifiers.push(streamArea.identifier);
            sanitizeStreamOrderArray();
            reactToStreamOrderChanges();

            // Update the layout spec to include our new stream.
            updateStreamSpecs();
        };


        this.getStreamArea = function (identifier) {
            return _areas[identifier];
        };

        this.getStreamAreas = function () {
            return _areas;
        };

        this.setStreamAspectRatio = function (identifier, aspectRatio) {
            var area = that.getStreamArea(identifier);
            if (area) {
                area.aspectRatio = aspectRatio;
                updateStreamSpecs();
            }
        };


        this.getAllStreamIdentifiers = function () {
            return _registeredIdentifiers.slice(0); // Return a copy to prevent side-effects.
        };


        //-------------------------------------------------------------------------------
        // Returns an array of stream identifier strings indicating the order in 
        // which streams are assigned to content boxes.
        //-------------------------------------------------------------------------------
        this.getStreamAreaOrder = function () {
            return _orderedIdentifiers.slice(0);
        };


        //-------------------------------------------------------------------------------
        // Sets the order in which streams are assigned to content boxes. Accepts an 
        // ordered array of stream identifier strings.
        //-------------------------------------------------------------------------------
        this.setStreamAreaOrder = function (identifiers /* Array */) {
            _orderedIdentifiers = identifiers;
            sanitizeStreamOrderArray(identifiers);
            reactToStreamOrderChanges();
            updateStreamSpecs();
        };


        this.getOuterSize = function () {
            return new geometry.Size(_element.outerWidth(), _element.outerHeight());
        };


        this.getInnerSize = function () {
            var outerSize = that.getOuterSize();
            var minDimension = Math.min(outerSize.width(), outerSize.height());
            var xMargin = _marginSpacing.valueFor(minDimension);
            var yMargin = _marginSpacing.valueFor(minDimension);
            return new geometry.Size(outerSize.width() - (2 * xMargin), outerSize.height() - (2 * yMargin));
        };


        //-------------------------------------------------------------------------------
        // Sets the spacing strategy used to calculate the margins around the layout.
        //-------------------------------------------------------------------------------
        this.setMarginSpacing = function (spacing) {
            _marginSpacing = spacing;
        };

        //-------------------------------------------------------------------------------
        // Sets the spacing strategy used to calculate the gutters between stream areas.
        //-------------------------------------------------------------------------------
        this.setGutterSpacing = function (spacing) {
            _gutterSpacing = spacing;
        };

        //-------------------------------------------------------------------------------
        // Sets whether or not the layout must of the balanced variety.
        //-------------------------------------------------------------------------------
        this.setForceBalanced = function (balanced) {
            _layoutSpec.balanced = balanced
        };

        //-------------------------------------------------------------------------------
        // Sets the side (left or right) to which higher priority streams are biased.
        //-------------------------------------------------------------------------------
        this.setSideBias = function (sideBias) {
            _sideBias = sideBias;
        };


        //-------------------------------------------------------------------------------
        // Updates the content layout.
        //-------------------------------------------------------------------------------
        this.updateLayout = function () {
            var i, layout;
            var containingRectCalculator = new ContainingRectCalculator();

            // Update the spec with the current stage size within the margins.
            _layoutSpec.boundingSize = that.getInnerSize();
            _layoutSpec.gutterBetween = getGutterValue();

            // The layout, Sir LayoutEngine, if you please.
            layout = _layoutEngine.getLayout(_layoutSpec);

            if (_sideBias === SideBias.right) {
                layout.flipHorizontal(_layoutSpec.boundingSize);
            }

            // Adjust for the stage margins.
            layout.centerWithin(that.getOuterSize());

            function rememberVisibleRectBoundaries(rect) {
                visibleTops.push(rect.top());
                visibleBottoms.push(rect.bottom());
                visibleLefts.push(rect.left());
                visibleRights.push(rect.right());
            }

            function calculateOverallVisibleRect() {
                var top = Math.min.apply(null, visibleTops);
                var bottom = Math.max.apply(null, visibleBottoms);
                var left = Math.min.apply(null, visibleLefts);
                var right = Math.max.apply(null, visibleRights);

                return new geometry.Rect(left, top, right - left, bottom - top);
            }

            // Apply the layout.
            $.each(_areas, function (identifier, stream) {
                var box = layout.findBox(stream.identifier);

                if (box) {
                    stream.setIsHidden(false);
                    stream.applyRect(box.rect);

                    containingRectCalculator.addRect(box.rect);
                } else {
                    stream.setIsHidden(true);
                    stream.applyRect(new geometry.Rect(0, 0, 1, 10)); // Non-zero height and width is required for some media plugins to load/play media (e.g. Silverlight).
                }
            });

            
            // Calculate the aggregate rectangle occupied by content.
            _occupiedContentRect = containingRectCalculator.getContainingRect();

        };


        //-------------------------------------------------------------------------------
        // Returns a Rect instnace that defines the boundaries of the area occupied by 
        // visible content.
        //-------------------------------------------------------------------------------
        this.getOccupiedContentRect = function () {
            return _occupiedContentRect;
        }


        function reactToStreamOrderChanges() {
            var currentStreamOrderToken = _orderedIdentifiers.join();
            if (currentStreamOrderToken !== _lastNotifiedStreamOrderToken) {
                // Update last token before firing event, because the event eventually result in the calling of this function.
                _lastNotifiedStreamOrderToken = currentStreamOrderToken;
                _element.trigger('stage-stream-area-order-changed');
            }
        }

        function sanitizeStreamOrderArray() {
            _orderedIdentifiers = toSanitizedStreamOrderArray(_orderedIdentifiers);
        }

        function toSanitizedStreamOrderArray(input) {
            // Remove identifiers from the input array so that.
            // - Each identifier may appear at most once.
            // - Each identifier must have previously been registered via addStreamArea.
            // 
            // A useful and intentional side-effect of this function
            // is that the output array is a different object than the 
            // input array. This prevents external modification of the 
            // input array from changing our internal state.

            var output;

            output = onlyUniqueIdentifiers(input);
            output = onlyRegisteredIdentifiers(output);

            return output;
        }

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

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

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

            return output;
        }

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

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

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

            return output;
        }

        function newLayoutSpec() {
            return new mvLayout.LayoutSpec();
        }

        function getGutterValue() {
            var outerSize = that.getOuterSize();
            var dimension = Math.min(outerSize.width(), outerSize.height());
            return _gutterSpacing.valueFor(dimension);
        }

        function updateStreamSpecs() {
            var i, stream, streamSpec;

            _layoutSpec.streamSpecs.length = 0;

            for (i = 0; i < _orderedIdentifiers.length; i++) {
                stream = _areas[_orderedIdentifiers[i]];
                streamSpec = new mvLayout.StreamSpec();
                streamSpec.identifier = stream.identifier;
                streamSpec.aspectRatio = stream.aspectRatio;
                _layoutSpec.streamSpecs.push(streamSpec);
            }
        }


        init();
    }


    //-----------------------------------------------------------------------------------
    // ContainingRectCalculator
    // -------------
    // Calculates the Rect that will *just* contain a configured set of Rects.
    //-----------------------------------------------------------------------------------
    function ContainingRectCalculator() {
        var visibleTops = [];
        var visibleBottoms = [];
        var visibleLefts = [];
        var visibleRights = [];

        this.addRect = function (rect) {
            visibleTops.push(rect.top());
            visibleBottoms.push(rect.bottom());
            visibleLefts.push(rect.left());
            visibleRights.push(rect.right());
        };

        this.getContainingRect = function () {
            var top = Math.min.apply(null, visibleTops);
            var bottom = Math.max.apply(null, visibleBottoms);
            var left = Math.min.apply(null, visibleLefts);
            var right = Math.max.apply(null, visibleRights);

            return new geometry.Rect(left, top, right - left, bottom - top);
        };
    }


    //-----------------------------------------------------------------------------------
    // StageLightbox
    // -------------
    // Isolates one stream at a time and provides buttons for stream-specific actions.
    //
    //    Event Name                  |  Event Object (e)
    //    ------------------------------------------------
    //    stage-popout-stream         |  { identifier: String }
    //    stage-play-as-video-stream  |  { identifier: String }
    //    stage-lightbox-activated    |  { streamIdentifier: String }
    //    stage-lightbox-deactivated  |  { }
    //-----------------------------------------------------------------------------------
    function StageLightbox(stage) {
        var that = this;
        var _stage = stage;
        var _isolatingOverlayElement;
        var _myStreamView = undefined;
        var _stageStreamView = undefined;
        var _fakeElement = undefined;
        var _primaryOnlyLayoutCalculator = new mvLayout.LayoutCalculator.PrimaryOnly();
        var _overlayButtonContainer, _overlayCloseButton, _overlayCycleButton, _overlayPopOutButton, _overlayMakePrimaryButton, _overlayPlayAsVideoButton;

        function init() {
            setupIsolationOverlay();
        }


        //-------------------------------------------------------------------------------
        // Returns a boolean indicating whether the lightbox is currently active.
        //-------------------------------------------------------------------------------
        this.isActivated = function () {
            return (_stageStreamView !== undefined);
        };


        //-------------------------------------------------------------------------------
        // Visually isolates the stream associated with the given identifier, decoupling 
        // it from it's normal position in the stage layout. Only one stream can be 
        // isolated at any given time.
        //-------------------------------------------------------------------------------
        this.activate = function (identifier) {
            if (_stageStreamView !== undefined) {
                releaseStreamView();
            }

            captureStreamView(identifier);

            updateIsolationButtons();
            enterIsolationTransition();
            that.updateLayout();
            _stage.updateLayout();

            fireEvent('stage-lightbox-activated', { streamIdentifier: identifier });
        };


        //-------------------------------------------------------------------------------
        // Returns any isolated stream to the normal stage layout.
        //-------------------------------------------------------------------------------
        this.deactivate = function () {
            if (_stageStreamView === undefined) {
                return;
            }

            releaseStreamView();

            exitIsolationTransition();
            _stage.updateLayout();

            fireEvent('stage-lightbox-deactivated');
        };


        //-------------------------------------------------------------------------------
        // Alternately activates and deactivates a stream.
        //-------------------------------------------------------------------------------
        this.toggleActivation = function (identifier) {
            if (_stageStreamView === undefined) {
                that.activate(identifier);
            } else if (_stageStreamView.identifier === identifier) {
                that.deactivate();
            } else {
                that.activate(identifier);
            }
        }


        //-------------------------------------------------------------------------------
        // Activate the next stream in order.
        //-------------------------------------------------------------------------------
        this.activateNextStream = function () {
            cycleActivatedStream(1);
        };


        //-------------------------------------------------------------------------------
        // Activate the previous stream in order.
        //-------------------------------------------------------------------------------
        this.activatePreviousStream = function () {
            cycleActivatedStream(-1);
        };


        //-------------------------------------------------------------------------------
        // Updates the lightbox layout.
        //-------------------------------------------------------------------------------
        this.updateLayout = function () {
            updateIsolationButtons();
            updateIsolatedLayout();
        };


        //-------------------------------------------------------------------------------
        // Returns a boolean indicating whether a stream can be popped out.
        // 
        // NOTE: Client code should provide a more meaningful implementation.
        //-------------------------------------------------------------------------------
        this.canStreamPopout = function (identifier) {
            return false;
        };


        //-------------------------------------------------------------------------------
        // Returns a boolean indicating whether a stream's current content type can be 
        // changed to video.
        // 
        // NOTE: Client code should provide a more meaningful implementation.
        //-------------------------------------------------------------------------------
        this.canStreamChangeToVideo = function (identifier) {
            return false;
        };


        //-------------------------------------------------------------------------------
        // Returns a boolean indicating whether a stream can become the primary stream.
        //-------------------------------------------------------------------------------
        this.canStreamChangeToPrimary = function (identifier) {
            return !isPrimary(identifier);
        };


        //-------------------------------------------------------------------------------
        // Returns a boolean indicating whether the user may cycle the activated stream.
        //-------------------------------------------------------------------------------
        this.canCycleStreams = function () {
            return (_stage.getStreamAreaOrder().length > 1);
        };



        function captureStreamView(identifier) {
            _stageStreamView = _stage.getStreamArea(identifier);
            _myStreamView = copyStreamView(_stageStreamView);
            _fakeElement = makeFakeElement();
            _stageStreamView._element = _fakeElement;
        }

        function releaseStreamView() {
            _stageStreamView._element = _myStreamView._element;
            _stageStreamView.setZIndex(0);
            _fakeElement.remove();
            _fakeElement = undefined;
            _myStreamView = undefined;
            _stageStreamView = undefined;
        }


        function makeFakeElement() {
            return $('<div></div>')
                .addClass('stage-stream-box')
                .css('position', 'absolute')
                .appendTo(_stage.getElement());
        }

        function isPrimary(identifier) {
            var order = _stage.getStreamAreaOrder();
            return (order.length > 0) && (identifier === order[0]);
        }

        function constructStreamOrder(primaryId) {
            var order, oldIndex, oldPrimary;
            order = _stage.getStreamAreaOrder();
            oldIndex = $.inArray(primaryId, order);
            if (oldIndex !== 0) {
                // Swap the positions of the old primary and new primary identifiers.
                order[oldIndex] = order[0];
                order[0] = primaryId;
            }

            return order;
        }

        function updateIsolationButtons() {
            var stream = _myStreamView;
            if (stream === undefined) {
                return;
            }

            if (that.canCycleStreams()) {
                _overlayCycleButton.show(0);
            } else {
                _overlayCycleButton.hide(0);
            }

            if (that.canStreamChangeToPrimary(stream.identifier)) {
                _overlayMakePrimaryButton.show(0);
            } else {
                _overlayMakePrimaryButton.hide(0);
            }

            if (that.canStreamPopout(stream.identifier)) {
                _overlayPopOutButton.show(0);
            } else {
                _overlayPopOutButton.hide(0);
            }

            if (that.canStreamChangeToVideo(stream.identifier)) {
                _overlayPlayAsVideoButton.show(0);
            } else {
                _overlayPlayAsVideoButton.hide(0);
            }
        }

        function updateIsolatedLayout() {
            if (_myStreamView === undefined) {
                return;
            }

            var rect;

            // Update the aspect ratio value, since the aspect ratio known to the stage may have changed.
            // For example, play-as-video may cause a video to load that has a native aspect ratio 
            // which differs from the ratio in the presentation data.
            _myStreamView.aspectRatio = _stageStreamView.aspectRatio;

            rect = getIsolatedRect(_myStreamView);

            _myStreamView.applyRect(rect);
            _myStreamView.setZIndex(100);

            _overlayButtonContainer.css({
                'left': rect.right() + 5,
                'top': rect.top()
            });
        }

        function enterIsolationTransition() {
            _isolatingOverlayElement.fadeIn(150);
        }

        function exitIsolationTransition() {
            _isolatingOverlayElement.fadeOut(150);
        }

        function fireEvent(eventName, props) {
            _stage.getElement().trigger($.Event(eventName, props || {}));
        }

        function setupIsolationOverlay() {
            _isolatingOverlayElement = $('<div></div>').addClass('stage-lightbox-overlay').appendTo(_stage.getElement());
            _isolatingOverlayElement.fadeOut(0).css('z-index', 50);

            _overlayButtonContainer = $('<div></div>').addClass('stage-lightbox-buttons').appendTo(_isolatingOverlayElement).css({
                'position': 'absolute'
            });
            _overlayCloseButton = makeOverlayButton('close').appendTo(_overlayButtonContainer);
            _overlayCycleButton = makeOverlayButton('cycle').appendTo(_overlayButtonContainer);
            _overlayPopOutButton = makeOverlayButton('popOut').appendTo(_overlayButtonContainer);
            _overlayMakePrimaryButton = makeOverlayButton('makePrimary').appendTo(_overlayButtonContainer);
            _overlayPlayAsVideoButton = makeOverlayButton('playAsVideo').appendTo(_overlayButtonContainer);

            new Mediasite.Player.SingleImageButton(_overlayCloseButton.get(0), {
                title: Mediasite.Player.Localization.SmartPlayer.CloseLightbox,
                label: false
            });

            new Mediasite.Player.SingleImageButton(_overlayCycleButton.get(0), {
                title: Mediasite.Player.Localization.SmartPlayer.CycleLightboxContent,
                label: false
            });

            new Mediasite.Player.SingleImageButton(_overlayPopOutButton.get(0), {
                title: Mediasite.Player.Localization.SmartPlayer.PopOutStream,
                label: false
            });

            new Mediasite.Player.SingleImageButton(_overlayMakePrimaryButton.get(0), {
                title: Mediasite.Player.Localization.SmartPlayer.ShowStreamAsPrimary,
                label: false
            });

            new Mediasite.Player.SingleImageButton(_overlayPlayAsVideoButton.get(0), {
                title: Mediasite.Player.Localization.SmartPlayer.PlayStreamAsVideo,
                label: false
            });

            _overlayCloseButton.on('click', function (e) {
                e.stopPropagation();
                that.deactivate();
            });

            _overlayCycleButton.on('click', function (e) {
                e.stopPropagation();
                that.activateNextStream();
            });

            _overlayPopOutButton.on('click', function (e) {
                e.stopPropagation();
                fireEvent('stage-popout-stream', { identifier: _myStreamView.identifier });
                that.deactivate();
            });

            _overlayMakePrimaryButton.on('click', function (e) {
                e.stopPropagation();
                setPrimaryStream(_myStreamView.identifier);
                that.deactivate();
            });

            _overlayPlayAsVideoButton.on('click', function (e) {
                e.stopPropagation();
                fireEvent('stage-play-as-video-stream', { identifier: _myStreamView.identifier });
                updateIsolationButtons();
            });

            _isolatingOverlayElement.on('click', function (e) {
                e.stopPropagation();
                that.deactivate();
            });
        }

        function makeOverlayButton(classPostfix) {
            var className = 'stage-lightbox-button-' + classPostfix;
            return $('<button></button>').addClass(className);
        }

        function setPrimaryStream(identifier) {
            var newOrder = constructStreamOrder(identifier);
            _stage.setStreamAreaOrder(newOrder);
            _stage.updateLayout();
        }

        function cycleActivatedStream(offset) {
            var nextIdentifier;

            if (!_myStreamView || typeof _myStreamView.identifier === 'undefined') {
                return;
            }

            // 1. Determine next stream.
            nextIdentifier = getOffsetStreamIdentifier(_myStreamView.identifier, offset);

            // 2. Activate stream.
            that.activate(nextIdentifier);
        }

        function getOffsetStreamIdentifier(currentIdentifier, offset) {
            var order, greatestIndex, currentStreamIndex, newStreamIndex;

            order = _stage.getStreamAreaOrder();
            greatestIndex = order.length - 1;
            currentStreamIndex = $.inArray(currentIdentifier, order);

            if (currentStreamIndex < 0 || currentStreamIndex > greatestIndex) {

                throw {
                    name: 'ArgumentError',
                    message: 'currentIdentifier must contain the identifier of a currently visible stream.'
                };

            }

            newStreamIndex = currentStreamIndex + offset;
            while (newStreamIndex < 0) {
                newStreamIndex += order.length;
            }
            newStreamIndex %= order.length;

            return order[newStreamIndex];
        }

        function getIsolatedRect(stream) {
            var layout, layoutOccupiedRect;
            var buttonWidthAllowance = 40;
            var spec = new mvLayout.LayoutSpec();
            var innerSize = _stage.getInnerSize();
            var outerSize = _stage.getOuterSize();
            var isolatedBoundingSize = new geometry.Size(innerSize.width() - buttonWidthAllowance, innerSize.height());

            spec.boundingSize = isolatedBoundingSize;
            spec.streamSpecs[0] = new mvLayout.StreamSpec();
            spec.streamSpecs[0].identifier = stream.identifier;
            spec.streamSpecs[0].aspectRatio = stream.aspectRatio;

            layout = _primaryOnlyLayoutCalculator.getLayout(spec);
            layout.centerWithin(outerSize);
            layoutOccupiedRect = layout.occupiedRect();

            var extraSpaceOnRight = (innerSize.width() - layoutOccupiedRect.width()) / 2; // The layout is centered, so half of the total extra space is on the right.
            if (extraSpaceOnRight < buttonWidthAllowance) {
                var lessLeft = buttonWidthAllowance - extraSpaceOnRight;
                layout.moveTo(layoutOccupiedRect.left() - lessLeft, layoutOccupiedRect.top());
            }

            return layout.findBox(stream.identifier).rect;
        }

        init();
    }    

    //-----------------------------------------------------------------------------------
    // StreamArea
    // -------------
    // Represents the box that contains one stream on the Stage.
    //
    //    Event Name
    //    ---------------
    //    element-resize
    //-----------------------------------------------------------------------------------
    function StreamArea(streamElement) {
        var self = this;
        this._element = $(streamElement);
    }

    StreamArea.prototype = {
        identifier: '',
        aspectRatio: 16 / 9,
        _isHidden: false,

        append: function (childElement) {
            this._element.append(childElement);
        },
        appendTo: function (containerElement) {
            this._element.appendTo(containerElement);
        },
        applyRect: function (rect) {
            this._element.css({
                left: rect.left() + 'px',
                top: rect.top() + 'px',
                width: rect.width() + 'px',
                height: rect.height() + 'px'
            });
            this._element.trigger('element-resize');
        },
        setZIndex: function (zIndex) {
            this._element.css('z-index', zIndex);
        },
        setIsHidden: function (isHidden) {
            if (isHidden === this._isHidden) {
                return; // Let's not involve jQuery, if nothing has changed.
            }

            this._isHidden = isHidden;

            if (isHidden) {
                this._element.addClass('stage-hidden-stream-box');
            } else {
                this._element.removeClass('stage-hidden-stream-box');
            }
        }
    };

    function copyStreamView(streamView) {
        var copy = new StreamArea(streamView._element);
        copy.identifier = streamView.identifier;
        copy.aspectRatio = streamView.aspectRatio;
        copy.canPopOut = streamView.canPopOut;
        copy.canBecomeVideo = streamView.canBecomeVideo;

        return copy;
    }

    //-----------------------------------------------------------------------------------
    // Values describing the handedness of a stage layout.
    //-----------------------------------------------------------------------------------
    var SideBias = {
        left: 'left',
        right: 'right'
    };

    //-----------------------------------------------------------------------------------
    // A spacing strategy that varies between a low and high value.
    // The behavior is defined by the configured low stop and high stop.
    //
    // + If the input dimension is less than the low stop dimension, the low value 
    //   is returned. 
    // + If it's greater than the high stop dimension, the high value is returned. 
    // + If the input dimension is between the two stops, the value returned is 
    //   determined by where the dimension falls between the low and high dimensions. 
    // 
    // lowStop and highStop are objects like so: { dimension: Number, value: Number };
    //-----------------------------------------------------------------------------------
    function LinearSpacingStrategy(lowStop, highStop) {
        var dimensionSpanBetweenStops = highStop.dimension - lowStop.dimension;
        var valueVariance = highStop.value - lowStop.value;

        // Returns the gutter spacing value for the given dimension.
        this.valueFor = function (dimension) {
            var aboveLowDim, fractionOfRange;

            if (dimension <= lowStop.dimension) {
                return lowStop.value;
            } else if (dimension >= highStop.dimension) {
                return highStop.value;
            } else {
                aboveLowDim = dimension - lowStop.dimension;
                fractionOfRange = aboveLowDim / dimensionSpanBetweenStops;
                return Math.round((fractionOfRange * valueVariance) + lowStop.value);
            }
        };

    }

    //-----------------------------------------------------------------------------------
    // A spacing strategy that always returns the configured value.
    //-----------------------------------------------------------------------------------
    function StaticSpacingStrategy(value) {

        // Returns the gutter spacing value for the given stage size. (HINT: It's always the same.)
        this.valueFor = function (dimension) {
            return value;
        };
    }

    // Public Module Defined
    return {
        Stage: Stage,
        StageLightbox: StageLightbox,
        SideBias: SideBias,
        StreamArea: StreamArea,
        StaticSpacingStrategy: StaticSpacingStrategy,
        LinearSpacingStrategy: LinearSpacingStrategy
    };

})(jQuery, Mediasite.Geometry, Mediasite.Player.MultiViewLayout);


//-----------------------------------------------------------------------------------
// Helps to maintain focus on a Stage Cycler Button.  When the stage is cycled, 
// maintain focus on the Cycle button when the cycled StreamArea is made to be primary.
//-----------------------------------------------------------------------------------
Mediasite.Player.StreamAreaCycleButtonFocuser = function () {
    var self = this;
    this.streamAreaButtonListeners = [];

    this.addStreamAreaCycleButtonListener = function (streamAreaButtonListener) {
        self.streamAreaButtonListeners.push(streamAreaButtonListener);
        streamAreaButtonListener.AddEventHandler("CycleButtonHasFocus", handleCycleButtonFocused);
    };

    function handleCycleButtonFocused() {
        $.each(self.streamAreaButtonListeners, function (index, item) {
            item.focusPrimaryCycleButton();
        });
    }
}

//--------------------------------------------------------------------------------------------------
// Factory object for creating StageCyclerButtonListener and StreamAreaToggleButtonListener objects.
//--------------------------------------------------------------------------------------------------
Mediasite.Player.StreamAreaButtonListenerFactory = {

    CreateStageCyclerButtonListener: function (streamArea, stage, stageCycler) {
        var stageCyclerButtonListener = $.extend(
            new Mediasite.Player.StreamAreaButtonListenerBase(),
            new Mediasite.Player.StageCyclerButtonListener());

        stageCyclerButtonListener.init(streamArea, stage, stageCycler);

        return stageCyclerButtonListener;
    },

    CreateStreamAreaToggleButtonListener: function (streamArea, stage, stageCycler) {
        var streamAreaToggleButtonListener = $.extend(
            new Mediasite.Player.StreamAreaButtonListenerBase(),
            new Mediasite.Player.StreamAreaToggleButtonListener());

        streamAreaToggleButtonListener.init(streamArea, stage, stageCycler);

        return streamAreaToggleButtonListener;
    }
}

//----------------------------------------------------------------------------------------------------
// Extends Mediasite.Player.StreamAreaButtonListenerBase.
// Represents the Toggle button that appears on a StreamArea within the multiview / smart player
// Listens for event from stage, streamArea and user and responds by showing/hiding the Toggle button.
//----------------------------------------------------------------------------------------------------
Mediasite.Player.StreamAreaToggleButtonListener = function () { }
Mediasite.Player.StreamAreaToggleButtonListener.prototype = {
    buttonClicked: false,

    createStreamAreaButton: function () {
        this.buttonElement = $('<button></button>')
            .addClass('toggle-stage-lightbox-button');

        this.streamArea.append(this.buttonElement);
        new Mediasite.Player.SingleImageButton(this.buttonElement.get(0), { 'title': Mediasite.Player.Localization.SmartPlayer.ToggleStageLightbox });
    },

    bindStageEvents: function () {
        var self = this;

        this.stage.getElement().on({

            'stage-lightbox-activated': function (e) {
                self.hide();
                self.disable();
            },
            'stage-lightbox-deactivated': function (e) {
                self.enable();
                self.focusToggleButton();
            }
        });

        this.stage.getElement().on('touchstart touchmove', function (e) {
            this.lastTouchInteraction = new Date();
        });

        this.stage.getElement().on('MSPointerDown MSPointerMove pointerdown pointermove', function (e) {
            var oEvent = e.originalEvent;
            var isMouse = oEvent && (oEvent.pointerType === oEvent.MSPOINTER_TYPE_MOUSE || oEvent.pointerType === "mouse");
            if (!isMouse) {
                // Though, technically, this could also be a pen,
                // I'm not sure it's wrong to give pens the touch treatment.
                self.lastTouchInteraction = new Date();
            }
        });
    },

    bindStreamAreaEvents: function () {
        var self = this;
        var oldClientX = 0;
        var oldClientY = 0;

        // Reveal the Toggle button if the mouse moves above the StreamArea.  Note that this does not occur on StageCyclerButtonListener. 
        this.streamArea._element.on('mousemove', function (e) {
            if (e.clientX == oldClientX && e.clientY == oldClientY) {
                return;
            }

            oldClientX = e.clientX;
            oldClientY = e.clientY;

            if (self.isEnabled) {
                setTimeout(function () {
                    var now = new Date();
                    var elapsed = now - self.lastTouchInteraction;
                    if (elapsed > self.touchRejectionSeconds * 1000) {
                        self.showFor(self.showDetails);
                    }
                }, 0);
            }
        });
    },

    focusToggleButton: function () {

        // Was this the button that was clicked that activated the lightbox?  If so, we want to re-focus this button 
        // as the lightbox is deactivated and the stage is set back to normal.

        if (this.buttonClicked) {
            var closeButtonHasFocus = false;
            var closeButton = $("button.stage-lightbox-button-close");

            this.buttonClicked = false;

            if (closeButton.length > 0)
                if ($(document.activeElement).is(closeButton[0]))
                    closeButtonHasFocus = true;

            if (closeButtonHasFocus) {
                this.show();
                this.buttonElement.focus();
            }
        }
    },

    bindAdditionalButtonEvents: function () {
        var self = this;

        this.buttonElement.on('click', function (e) {

            // This click event will bubble up and be handled by toggleStageLightbox in SmartPlayer/PlayerView.js,
            // as if the user clicked on the StreamArea itself.
            // See this.GetStreamViewHandler in SmartPlayer/PlayerView.js for how the click handler is set.

            // Set buttonClicked = true so we know which button activated the lightbox so we can reset its focus when the
            // lightbox is deactivated.

            self.buttonClicked = true;
        });

        // Reveal stage cycler button if tab is pressed - that way the user can tab-nav to the cycler button
        $(document).bind("keydown", function (event) {
            if (event.keyCode == 9)
                self.showFor(self.showDetails);
        });
    }
}

//--------------------------------------------------------------------------------------------------
// Extends Mediasite.Player.StreamAreaButtonListenerBase.
// Represents the Cycle button that appears on a StreamArea within the multiview / smart player
// Listens for event from stage and user and responds by showing/hiding the Cycle button.
//--------------------------------------------------------------------------------------------------
Mediasite.Player.StageCyclerButtonListener = function () {}
Mediasite.Player.StageCyclerButtonListener.prototype = {
    lastTouchInteraction:  new Date(),
    createStreamAreaButton: function () {
        this.buttonElement = $('<button></button>')
            .addClass('stage-cycle-primary-button');

        this.streamArea.append(this.buttonElement);
        new Mediasite.Player.SingleImageButton(this.buttonElement.get(0), { 'title': Mediasite.Player.Localization.SmartPlayer.CyclePrimaryContent });
    },

    bindStageEvents: function () {
        var self = this;
        var oldClientX = 0;
        var oldClientY = 0;

        this.stage.getElement().on({
            'stage-stream-area-order-changed': function (e) {
                self.makePrimaryCycleButtonVisible();
            },
            'stage-lightbox-activated': function (e) {
                self.hide();
                self.disable();
            },
            'stage-lightbox-deactivated': function (e) {
                if (self.isCycleButtonPrimary())
                    self.enable();
            },

            // Reveal Cycle button if user mouses over the stage.  Note that this happens only with the Cycle button currently.
            'mousemove': function (e) {
                if (e.clientX == oldClientX && e.clientY == oldClientY) {
                    return;
                }

                oldClientX = e.clientX;
                oldClientY = e.clientY;

                setTimeout(function () {
                    var now = new Date();
                    var elapsed = now - self.lastTouchInteraction;
                    if (elapsed > self.touchRejectionSeconds * 1000) {
                        self.showFor(self.showDetails);
                    }
                }, 0);
            }
        });

        this.stage.getElement().on('touchstart touchmove', function (e) {
            this.lastTouchInteraction = new Date();
        });

        this.stage.getElement().on('MSPointerDown MSPointerMove pointerdown pointermove', function (e) {
            var oEvent = e.originalEvent;
            var isMouse = oEvent && (oEvent.pointerType === oEvent.MSPOINTER_TYPE_MOUSE || oEvent.pointerType === "mouse");
            if (!isMouse) {
                // Though, technically, this could also be a pen,
                // I'm not sure it's wrong to give pens the touch treatment.
                self.lastTouchInteraction = new Date();
            }
        });
    },

    bindAdditionalButtonEvents: function () {
        var self = this;

        this.buttonElement.on('click', function (e) {
            self.stageCycler.forward();
            e.stopPropagation();
        });

        // reveal stage cycler button if tab is pressed - that way the user can tab-nav to the cycler button
        $(document).bind("keydown", function (event) {
            if (event.keyCode == 9)
                self.showFor(self.showDetails);
        });
    },

    additionalInit: function() { 
        this.makePrimaryCycleButtonVisible();
    },

    // The order of StreamAreas changed.  If the StreamArea is not primary, then disable and hide its Cycle button.  
    // We only show one Cycle button.  Else, enable the button.  Also, if a Cycle button was focused, focus the now 
    // visible Cycle button in order to maintain a focused Cycle button.  Because the StageCyclerButtonListener objects 
    // do not know about one another, this last step is done with the help of Mediasite.Player.StreamAreaCycleButtonFocuser 
    // which listens to the "CycleButtonHasFocus" event.

    makePrimaryCycleButtonVisible: function () {
        var order = this.stage.getStreamAreaOrder();
        var cycleButtonHasFocus = false;

        if (order.length > 0) {
            var primaryStreamArea = this.stage.getStreamArea(order[0]);

            if (($(document.activeElement).is(this.buttonElement)))
                cycleButtonHasFocus = true;

            if (this.streamArea !== primaryStreamArea) {
                this.disable();
                this.hide();
            }
            else {
                this.enable();
                if (this.shouldFocusButton) {
                    this.show();
                    this.buttonElement.focus();
                    this.shouldFocusButton = false;
                }
            }

            if (cycleButtonHasFocus) {
                this._events.fire("CycleButtonHasFocus");
            }
        }
    },

    // Called from Mediasite.Player.StreamAreaCycleButtonFocuser.  Either show and focus the button,
    // or set shouldFocusButton so that the button will get shown and focuses once it is processed
    // during makePrimaryCycleButtonVisible()

    focusPrimaryCycleButton: function () {
        if (this.isCycleButtonPrimary())
            if (this.isEnabled) {
                this.show();
                this.buttonElement.focus();
            }
            else
                this.shouldFocusButton = true;
    },

    isCycleButtonPrimary: function () {
        var order = this.stage.getStreamAreaOrder();

        if (order.length > 0) {
            var primaryStreamArea = this.stage.getStreamArea(order[0]);

            return (this.streamArea === primaryStreamArea);
        }
        return false;
    }
}

//------------------------------------------------------------------------------------------------------
// Acts as a base class for StageCyclerButtonListener and StreamAreaToggleButtonListener via $.extend().
//------------------------------------------------------------------------------------------------------
Mediasite.Player.StreamAreaButtonListenerBase = function () {
    var self = this;
    this._events = new Mediasite.Player.EventBundle();

    this.showDetails = {
        ShowForSeconds: 2.0,
        FadeInSeconds: 0.05,
        FadeOutSeconds: 0.4
    };

    this.touchRejectionSeconds = 2.0;
    this.isShowing = false;
    this.isMouseOverTogggleButton = false;
    this.lastTouchInteraction = new Date(0);
    this.isEnabled = true;
    this.buttonElement;
    this.shouldFocusButton = false;
    this.hideTimeout = null;
}

Mediasite.Player.StreamAreaButtonListenerBase.prototype = {
    init: function (streamArea, stage, stageCycler) {
        this.streamArea = streamArea;
        this.stage = stage;
        this.stageCycler = stageCycler;

        this.createStreamAreaButton();
        this.bindButtonEvents();
        this.bindStreamAreaEvents();
        this.bindStageEvents();

        this.additionalInit();        
        this.hide();
    },

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

    additionalInit: function() { },

    createStreamAreaButton: function () { },

    bindButtonEvents: function () {
        var self = this;

        this.buttonElement.hover(
            function handleIn(e) {
                self.isMouseOverTogggleButton = true;
            },
            function handleOut(e) {
                self.isMouseOverTogggleButton = false;
            }).on("blur", function () {
                setTimeout(function () {

                    // Focus has moved off of the button so hide it.  
                    // This button may be in the middle of showing itself from the tab keypress.  
                    // Do not hide the button if this is the case.

                    if (!self.isShowing && !($(document.activeElement).is(self.buttonElement)))
                        self.considerHidingToggleButton(self.showDetails);
                }, 1);
            });

        this.bindAdditionalButtonEvents();
    },

    bindAdditionalButtonEvents: function () { },

    bindStreamAreaEvents: function () {},

    bindStageEvents: function () {},

    show: function () {
        if (!this.isEnabled)
            return;

        window.clearTimeout(this.hideTimeout);
        this.buttonElement.show();
    },

    fadeIn: function (seconds, callback) {
        if (!this.isEnabled)
            return;

        this.buttonElement.fadeIn(seconds, callback);
    },

    hide: function () {
        window.clearTimeout(this.hideTimeout);
        this.buttonElement.hide();
    },

    fadeOut: function (seconds) {
        this.buttonElement.fadeOut(seconds);
    },

    showFor: function (showDetails, callback) {
        if (!this.isEnabled)
            return;

        var self = this;

        self.isShowing = true;

        window.clearTimeout(this.hideTimeout);

        this.fadeIn((this.showDetails.FadeInSeconds * 1000), callback);

        this.hideTimeout = window.setTimeout(function () {
            self.considerHidingToggleButton(self.showDetails);
            self.isShowing = false;
        }, this.showDetails.ShowForSeconds * 1000);
    },

    enable: function () {
        this.isEnabled = true;
    },

    disable: function () {
        this.isEnabled = false;
    },

    // hide button if we are done showing and we are not focused on the button
    considerHidingToggleButton: function (showDetails) {
        if (!this.isMouseOverTogggleButton && !($(document.activeElement).is(this.buttonElement))) {
            window.clearTimeout(this.hideTimeout);
            this.fadeOut(showDetails.FadeOutSeconds * 1000);
        }
    }
}
