/// <reference path="../ScriptReference/jquery-1.7.1-vsdoc.js"/>
/// <reference path="Mediasite.Core.js"/>
/// <reference path="Mediasite.Player.Core.js"/>
/// <reference path="Mediasite.Player.API.js"/>
/// <reference path="Mediasite.Player.ApiOrigin.js"/>

Mediasite.namespace("Player");
Mediasite.Player.APIBroker = function MediasitePlayerAPIBroker(options) {
    /// <param name="allowedBrokerSites" type="String|Array[String]">'*' or array of sites (site: "hostname" or "hostname:port")</param>
    /// <param name="messagePrefix" type="String[optional]">String to append to broker messages (does not include message delimiter ':')</param>
    /// <param name="events" type="Object[optional]">Event handler mapping</param>

    // #region Private properties
    var _allowedBrokerSites,
        _brokerOrigin,
        _brokerWindowInterface,
        _messagePrefix,
        _activated,
        _activationData,
        _listeningToWindowInterface;

    var self = this;
    // #endregion
    // #region Constants
    var _messageDelimiter = ":";
    var _universalOrigin = "*";
    var _nonStandardOrigin = "null"; // If the origin in question is not a scheme/host/port tuple, then return the literal string "null" and abort these steps.  http://www.w3.org/TR/html5/origin-0.html#unicode-serialization-of-an-origin
    // #endregion

    // #region Initialization
    (function initialize() {
        _allowedBrokerSites = $.isArray(options.allowedBrokerSites) && options.allowedBrokerSites.length && options.allowedBrokerSites
                || _universalOrigin;
        _messagePrefix = options.messagePrefix || "Mediasite";
        _activated = false;
        _activationData = options.activationData || [];

        if (typeof options.events === "object") {
            addHandler(options.events);
        }

    })();
    // #endregion

    // #region Public methods
    this.ListenForMessages = function (windowInterface) {
        if (_listeningToWindowInterface) return;

        _listeningToWindowInterface = windowInterface;
        _listeningToWindowInterface.addHandler('message', receiveMessageEvent);
    };

    this.SendMessage = function (data) {
        return postMessage(JSON.stringify(data));
    };

    this.SendEvent = function (event, dataArray) {
        return postEventMessage(event, dataArray);
    };

    this.CurrySendEvent = function (event, throttleTo) {
        /// <summary>Create an event handler which can pass all arguments to the external APIBroker with a given event name</summary>
        /// <param name="event" type="String">Event name</param>
        /// <param name="throttleTo" type="Number[milliseconds][optional]">Prevent another event firing within `throttleTo` milliseconds after firing initially</param>

        var data;
        var lastFired = 0;
        var timeout;
        return function eventHandler() {
            data = Array.prototype.slice.call(arguments, 0);

            if (throttleTo) {
                // TODO: make this throttler less janky

                var now = new Date();
                if (now - throttleTo < lastFired) {
                    clearTimeout(timeout);
                    var wait = now - lastFired + throttleTo;
                    timeout = setTimeout(function () {
                        timeout = null;
                        lastFired = now;
                        self.SendEvent(event, data);
                    }, wait);

                    return;
                }

                lastFired = now;
            }

            self.SendEvent(event, data);
        }
    };

    this.ReceiveCommand = receiveCommand;

    this.IsActivated = function () {
        /// <summary>Is a remote API broker listening to this local API broker?</summary>
        return _activated;
    }

    this.AddHandler = addHandler;
    this.RemoveHandler = removeHandler;
    // #endregion

    // #region Event handling
    var _eventBundle;

    function addHandler(event, handler) {
        if (!_eventBundle) _eventBundle = new Mediasite.Player.EventBundle();

        return _eventBundle.addHandler(event, handler);
    };

    function removeHandler(event, handler) {
        if (!_eventBundle) return;

        return _eventBundle.removeHandler(event, handler);
    };

    // #endregion

    // #region Message-passing
    function postEventMessage(event, dataArray) {
        /// <param name="event" type="string">Name of event</param>
        /// <param name="dataArray" type="Array|null">Data to send with event</param>

        var messageData = {
            event: event,
            data: []
        };

        if ($.isArray(dataArray)) {
            messageData.data = dataArray;
        } else if (dataArray) {
            messageData.data = [dataArray];
        }

        var message = _messagePrefix + _messageDelimiter + JSON.stringify(messageData);

        postMessage(message);
    }

    function postMessage(message) {
        var targetWindow = _brokerWindowInterface || new Mediasite.Player.WindowInterface(window.parent);
        var targetOrigin = _brokerOrigin || _universalOrigin;
        if (targetWindow && targetWindow.postMessage && targetOrigin) {
            targetWindow.postMessage(message, targetOrigin);
        }

        return true;
    };


    function receiveMessageEvent(event) {
        var data = event.data,
            sourceWindow = event.source,
            sourceOrigin = event.origin;

        var matchedOrigin = self.isAllowedBrokerOrigin(sourceOrigin, _allowedBrokerSites, _brokerOrigin);
        if (!matchedOrigin) return;

        var messageData = self.parseMessageData(data, _messagePrefix, _messageDelimiter);
        if (!(messageData && messageData.command)) return;
        // Expect messageData = { command: string, params: [ ... ] } (params is optional)

        var command = messageData.command,
            params = messageData.params;
        self.ReceiveCommand(command, params, sourceWindow, matchedOrigin);

    }

    function receiveCommand(command, params, sourceWindow, sourceOrigin) {
        var isBrokerMethod = callBrokerMethod(command, params, sourceWindow, sourceOrigin);
        if (isBrokerMethod !== false) return;

        _eventBundle.fire('command', command, params);
    }

    self.parseMessageData = function parseMessageData(messageText, messagePrefix, messageDelimiter) {
        // Expected message: 
        //     messagePrefix + messageDelimiter + JSON.stringify(object)

        var fullPrefix = messagePrefix + messageDelimiter;

        if (messageText.indexOf(fullPrefix) !== 0) return;

        var jsonData = messageText.substring(fullPrefix.length);
        var data = JSON.parse(jsonData);

        return data;
    };


    function callBrokerMethod(command, params, sourceWindow, sourceOrigin) {
        /// <returns type="false|anything">False = command is not a broker method; anything else = command is a broker method</returns>

        switch (command) {
            case '_broker_activate':
                // Verify that remote broker is on a permitted origin
                var matchedOrigin = self.findMatchingOrigin(sourceOrigin, _allowedBrokerSites);
                if (!matchedOrigin) return;

                // Set up local API to listen to specific remote broker
                _brokerWindowInterface = new Mediasite.Player.WindowInterface(sourceWindow);
                _brokerOrigin = matchedOrigin;

                // Finish handshake with remote broker
                self.SendEvent('_broker_activated', _activationData);

                // Notify local listeners that brokers are in communication
                _activated = true;
                if (_eventBundle) _eventBundle.fire('activated', params);

                break;
            default: // Command name is not a broker method
                return false;
                break;
        }
    }

    self.isAllowedBrokerOrigin = function isAllowedBrokerOrigin(origin, allowedBrokerSites, establishedBrokerOrigin) {
        /// <returns>An origin which can be used to send messages back to the source window, or a falsy value for no match</returns>
        var matchedOrigin;

        if (establishedBrokerOrigin) {
            if (establishedBrokerOrigin == _universalOrigin) {
                matchedOrigin = establishedBrokerOrigin;
            } else {
                matchedOrigin = (establishedBrokerOrigin == origin) && origin;
            }
        } else {
            matchedOrigin = self.findMatchingOrigin(origin, allowedBrokerSites);
        }

        return matchedOrigin;
    }

    self.findMatchingOrigin = function findMatchingOrigin(needleOrigin, haystackSites) {
        /// <param name="needleOrigin" type="Origin">Origin, i.e. scheme://hostname[:port]</param>
        /// <param name="haystackSites" type="String|Array[string]">'*' for all sites; or a list of sites (hostname[:port])</param>
        // static method

        var allowedOriginMatcher = Mediasite.Player.ApiOrigin.createOriginMatcherFromList(haystackSites);
        var isAllowed = allowedOriginMatcher.match(needleOrigin);

        if (allowedOriginMatcher.match(needleOrigin)) {
            if (needleOrigin === _nonStandardOrigin || needleOrigin.indexOf('file:') == 0) { // for sending messages when window.document.location.protocol = file:
                return _universalOrigin;
            }

            return needleOrigin;
        }

    };
};

Mediasite.Player.APIBroker.initialize = function (playerApi, allowedPlayerIFrameAPISites) {
    var allowedBrokerSites = allowedPlayerIFrameAPISites || '';
    var messagePrefix = 'MediasitePlayer';
    var windowInterface = new Mediasite.Player.WindowInterface(window);

    var version = {
        version: "7.2.2",
        supports: ["6.1.5", "6.1.7", "6.1.9", "6.1.11", "7.0", "7.0.28", "7.0.29", "7.2", "7.2.2"],
        application: 'MediasitePlayer'
    };


    var apiBroker = new Mediasite.Player.APIBroker({
        allowedBrokerSites: allowedBrokerSites,
        messagePrefix: messagePrefix,
        events: {
            'activated': onBrokerActivated,
            'command': onReceiveCommand
        },
        activationData: {
            version: version
        }
    });

    apiBroker.ListenForMessages(windowInterface);
    // Advertise existence/compatibility to iframe API consumers
    apiBroker.SendMessage(version);

    function onReceiveCommand(methodName, argsArray) {
        if (typeof playerApi[methodName] !== "function") {
            var error = {
                errorCode: Mediasite.Player.Error.Code.MethodNotFound
            };
        } else {
            try {
                if (argsArray && argsArray.length > 0) {
                    argsArray = transformMethodArguments(methodName, argsArray);
                }

                playerApi[methodName].apply(playerApi, argsArray);

            } catch (err) {
                var errorDetails = err.message || err.description || err;
                var error = {
                    errorCode: Mediasite.Player.Error.Code.MethodError,
                    details: errorDetails
                };
            }
        }

        if (error) {
            error.command = methodName;
            error.params = argsArray;

            apiBroker.SendEvent("error", error);
        }
    }

    function transformMethodArguments(methodName, argsArray) {
        var data = argsArray[0];

        switch (methodName) {
            case "setLayoutOptions":
                argsArray = [data.options];
                break;
            case "seekTo":
                argsArray = [data.seconds];
                break;
            case "setVolume":
                argsArray = [data.volume];
                break;
            case "setVisibleStreamTypes":
                argsArray = [data.streamTypes];
                break;
            case "unsafeAddTimedEvent":
                argsArray = [data.time, data.type, data.payload, data.id];
                break;
            case "removeTimedEvent":
                argsArray = [data.id];
                break;
        }

        return argsArray;
    }

    var _listeningToPlayer = false;
    function onBrokerActivated(data) {
        if (!(data && data[0] && typeof data[0].version != "undefined")) {
            // Pre-6.1.5 server
            apiBroker.SendEvent("error", { errorCode: 520, details: "cannot use 6.1.3 MediasitePlayerIFrameAPI with this server" });
        }

        if (!_listeningToPlayer) {
            _listeningToPlayer = true;

            // All events except throttled events
            var curriedEvents = [
                "error",
                "durationchanged",
                "playstatechanged",
                "playbackratechanged",
                "slidechanged",
                "slideadded",
                "captionchanged",
                "chapterchanged",
                "timedeventreached",
                "livestatuschanged",
                "currenttimechanged",
                "playerstatechanged",
                "visiblestreamschanged"
            ];
            $.each(curriedEvents, function (i, eventName) {
                var handler = apiBroker.CurrySendEvent(eventName);
                playerApi.addHandler(eventName, handler);
            });

            // All throttled events
            var throttledEvents = {
                "volumechanged": 400
            };

            $.each(throttledEvents, function (eventName, throttleTo) {
                var handler = apiBroker.CurrySendEvent(eventName, throttleTo);
                playerApi.addHandler(eventName, handler);
            });

            // This event isn't a simple pass-through. Handle it separately.
            playerApi.addHandler("timedeventlistchanged", handleTimedEventListChanged);

            if (!playerApi.isReady()) {
                playerApi.addHandler('ready', onReady);
                onLoaded();
            }
        }

        if (playerApi.isReady()) {
            onReady();
        }
    }

    function onReady() {
        // Send presentation state
        updateState(true);

        // Send onReady event
        apiBroker.SendEvent("ready");
    }

    function onLoaded() {
        // Send presentation state
        updateState(false);

        // Send onReady event
        var playerState = playerApi.getPlayerState();

        if (playerState.state != Mediasite.Player.PlayerState.NotReady) {
            var event = {};
            event.state = playerState.state;
            event.isLive = playerState.isLive;
            apiBroker.SendEvent("playerstatechanged", event);
        }
    }

    function updateState(isReady) {
        // Send presentation state
        var readyDependentState = {};
        var independentState = {};

        if (isReady) {
            readyDependentState =
            {
                ready: playerApi.isReady(),
                playState: playerApi.getPlayState(),
                playerState: playerApi.getPlayerState(),
                currentTime: playerApi.getCurrentTime(),
                duration: playerApi.getDuration(),
                volume: playerApi.getVolume(),
                muted: playerApi.isMuted(),
                playbackRate: playerApi.getPlaybackRate()
            };
        }

        independentState = {
            ready: playerApi.isReady(),
            playerState: playerApi.getPlayerState(),
            liveStatus: playerApi.getLiveStatus(),
            chapters: playerApi.getChapters(),
            timedevents: playerApi.getTimedEvents(),
            slides: playerApi.getAllSlides(),
            captions: playerApi.getCaptions(),
            links: playerApi.getLinks(),
            pollsUri: playerApi.getPollsUri(),
            allStreams: playerApi.getAllStreams(),
            visibleStreamTypes: playerApi.getVisibleStreamTypes(),
            pastSessionPlayedSegments: playerApi.getPastSessionPlayedSegments()
        };

        var state = $.extend({}, readyDependentState, independentState);

        apiBroker.SendEvent("_api_state", [state]);
    }

    function handleTimedEventListChanged() {
        var event = {
            timedEvents: playerApi.getTimedEvents()
        }
        apiBroker.SendEvent("timedeventlistchanged", event);
    }
    // #endregion

    return apiBroker;
};


Mediasite.Player.APIBroker.playerNotInitialized = function(errorState) {

    var messagePrefix = 'MediasitePlayer';
    var windowInterface = new Mediasite.Player.WindowInterface(window);

    var apiBroker = new Mediasite.Player.APIBroker({
        messagePrefix: messagePrefix
    });

    apiBroker.ListenForMessages(windowInterface);
    apiBroker.SendMessage(errorState);
};
