var MediasitePlayCover = (function (undefined) {

    var SIZE_CHECK_INTERVAL_MS = 500;

    var PRESENTATION_STATUS = {
        NotAvailable: "NotAvailable",
        ScheduledForLive: "ScheduledForLive",
        OpenForLive: "OpenForLive",
        Live: "Live",
        LivePaused: "LivePaused",
        LiveEnded: "LiveEnded",
        OnDemand: "OnDemand"
    };

    var THUMBNAIL_ELEMENT_CLASS = "mediasite-thumbnail";

    var WAITING_ELEMENT_CLASS = "mediasite-waiting-widget message-widget";
    var WAITING_NORMAL_CLASS = WAITING_ELEMENT_CLASS;
    var WAITING_LAST_HOUR_CLASS = WAITING_ELEMENT_CLASS + " last-hour";
    var WAITING_LAST_MINUTE_CLASS = WAITING_ELEMENT_CLASS + " last-minute";
    var WAITING_MAIN_TEXT_CLASS = "waiting-text";
    var WAITING_TIME_TEXT_CLASS = "waiting-time";

    var OUTER_ELEMENT_CLASS = "mediasite-playcover";
    var CUSTOMIZED_CLASS = "customized";
    var TITLE_ELEMENT_CLASS = "title-region";
    var PLAY_BUTTON_CLASS = "play-button";
    var SVG_CIRCLE_CLASS = "play-circle";
    var SVG_ARROW_CLASS = "play-arrow";

    var SHADE_OVERLAY_CLASS = "overlay-sheet shade-overlay"
    var LOADING_OVERLAY_CLASS = "overlay-sheet loading-overlay";
    var PLAYABLE_OVERLAY_CLASS = "overlay-sheet playable-overlay";
    var CUSTOM_OVERLAY_CLASS = "overlay-sheet custom-overlay";
    var LIVE_OVERLAY_CLASS = "overlay-sheet live-overlay";
    var WAITING_OVERLAY_CLASS = "overlay-sheet waiting-overlay";
    var ENDED_OVERLAY_CLASS = "overlay-sheet ended-overlay";
    var UNAVAILABLE_OVERLAY_CLASS = "overlay-sheet unavailable-overlay";

    var LIVE_INDICATOR_CLASS = "live-indicator";
    var LIVE_TEXT_CLASS = "live-text";
    var LOADING_TEXT_CLASS = "loading-text message-widget";
    var ENDED_TEXT_CLASS = "ended-text message-widget";
    var UNAVAILABLE_TEXT_CLASS = "unavailable-text message-widget";
    var CUSTOM_CONTENT_WRAPPER_CLASS = "custom-content-wrapper";
    var CUSTOM_MESSAGE_WRAPPER_CLASS = "custom-message-wrapper";
    var CUSTOM_MESSAGE_CLASS = "custom-message";

    var NO_TITLE_CLASS = "no-title";
    var NO_PLAYBUTTON_CLASS = "no-playbutton";

    var widthClassesSpec = {
        "width-tiny": { min: 0, max: 349 },
        "width-small": { min: 350, max: 499 },
        "width-medium": { min: 500, max: 749 },
        "width-big": { min: 750, max: 1099 },
        "width-bigger": { min: 1100, max: 1499 },
        "width-huge": { min: 1500, max: Infinity }
    };

    var heightClassesSpec = {
        "height-tiny": { min: 0, max: 196 },
        "height-small": { min: 197, max: 280 },
        "height-medium": { min: 281, max: 421 },
        "height-big": { min: 422, max: 618 },
        "height-bigger": { min: 619, max: 843 },
        "height-huge": { min: 844, max: Infinity }
    };


    var appendable = {
        rootElement: null,
        appendToDom: function (container) {
            if (container && container.appendChild && this.rootElement && this.rootElement.nodeType === 1) {
                container.appendChild(this.rootElement);
            }
        },
        removeFromDom: function () {
            if (this.rootElement && this.rootElement.parentElement && this.rootElement.parentElement.removeChild) {
                this.rootElement.parentElement.removeChild(this.rootElement);
            }
        }
    };

    function makeAppendable(obj) {
        mixInProps(obj, appendable);
    }

    function mixInProps(target, mixIn) {
        for (prop in mixIn) {
            if (mixIn.hasOwnProperty(prop)) {
                if (!target.hasOwnProperty(prop)) {
                    target[prop] = mixIn[prop];
                }
            }
        }
    }


    function Thumbnail() {
        var self = this;
        var element;


        function init() {
            element = document.createElement("div");
            element.className = THUMBNAIL_ELEMENT_CLASS;
        }


        makeAppendable(this);

        Object.defineProperty(this, "rootElement", {
            get: function () {
                return element;
            }
        });

        Object.defineProperty(this, "imageUrl", {
            set: function (url) {
                if (url) {
                    element.style.backgroundImage = "url(" + url + ")";
                } else {
                    element.style.backgroundImage = "none";
                }
                
            }
        });

        init();
    }


    function WaitingWidget(ui_strings) {
        ui_strings = ui_strings || {};
        var strings = {
            waitingForLive: ui_strings.waitingForLive || "Waiting for broadcast to begin.",
            timeUnknown: ui_strings.timeUnknown || "--:--",
            timeFormat_mmss: ui_strings.timeFormat_mmss || "%%mm%%:%%ss%%",
            timeFormat_hhmmss: ui_strings.timeFormat_hhmmss || "%%hh%%:%%mm%%:%%ss%%"
        }

        var self = this;
        var mainText, timeText;
        var airDate = 0;
        var updateTimeoutToken;
        var shouldShowCountdown = true;


        function init() {

            mainText = makeParagraph(strings.waitingForLive, WAITING_MAIN_TEXT_CLASS);
            timeText = makeParagraph(strings.timeUnknown, WAITING_TIME_TEXT_CLASS);

            self.rootElement = makeDiv(WAITING_ELEMENT_CLASS);
            self.rootElement.appendChild(mainText);
            self.rootElement.appendChild(timeText);

            scheduleUpdate(500);
        }


        makeAppendable(this);

        Object.defineProperty(this, "text", {
            set: function (value) {
                mainText.innerText = value;
            }
        });

        Object.defineProperty(this, "airDateUnixTime", {
            set: function (value) {
                airDate = value;
                updateTime();
            }
        });

        Object.defineProperty(this, "shouldShowCountdown", {
            set: function (value) {
                shouldShowCountdown = !!value;
                updateTime();
            }
        });

        function updateTime() {
            var seconds = getSecondsUntilAirDate();
            var text = timeTextFromSeconds(seconds);
            var show = shouldShowCountdown && (text && text.length > 0);
            
            timeText.innerText = text;
            timeText.style.display = show ? "block" : "none";
            self.rootElement.className = getOuterCssClass(seconds);
        }

        function getSecondsUntilAirDate() {
            return (airDate - getNowUnixTime()) / 1000;
        }

        function getOuterCssClass(secondsToGo) {
            if (secondsToGo <= 60) {
                return WAITING_LAST_MINUTE_CLASS;
            } else if (secondsToGo <= 60 * 60) {
                return WAITING_LAST_HOUR_CLASS;
            } else {
                return WAITING_NORMAL_CLASS;
            }
        }

        function scheduleUpdate(ms) {
            cancelScheduledUpdate();

            function scheduleNext() {
                updateTimeoutToken = setTimeout(callback, ms);
            }
            function callback() {
                updateTime();
                scheduleNext();
            };
            
            scheduleNext();
        }

        function cancelScheduledUpdate() {
            clearTimeout(updateTimeoutToken);
        }

        function getNowUnixTime() {
            return (new Date()).getTime();
        }

        function timeTextFromSeconds(seconds) {
            if (typeof seconds != "number") {
                return strings.timeUnknown;
            } else if (seconds < -60) {
                return "";
            } else if (seconds < 0) {
                return formatTimeText(0);
            } else if (seconds < 24 * 60 * 60) {
                return formatTimeText(seconds);
            } else {
                return "";
            }
        }

        function formatTimeText(seconds) {
            var mr, min, sec, format, result;

            sec = Math.floor(seconds % 60);
            min = Math.floor(seconds / 60) % 60;
            hr = Math.floor(seconds / (60 * 60));

            if (hr > 0) {
                format = strings.timeFormat_hhmmss || "";
            } else {
                format = strings.timeFormat_mmss || "";
            }

            result = format.replace("%%hh%%", hr + "");
            result = result.replace("%%mm%%", zeroPad(min + "", 2));
            result = result.replace("%%ss%%", zeroPad(sec + "", 2));

            return result;
        }

        function zeroPad(string, targetDigits) {
            while (string.length < targetDigits) {
                string = "0" + string;
            }
            return string;
        }

        init();
    }


    function PlayCoverView(ui_strings) {
        ui_strings = ui_strings || {};
        var _strings = {
            loading: ui_strings.loading || "Loading...",
            play: ui_strings.play || "Play",
            liveIndicator: ui_strings.liveIndicator || "Live",
            liveBroadcastEnded: ui_strings.liveBroadcastEnded || "Live broadcast has ended.",
            unavailable: ui_strings.unavailable || "The presentation is currently unavailable.",
            tryAgain: ui_strings.tryAgain || "Please try again later."
        }

        var self = this;
        var titleRegion, titleParagraph;
        var loadingOverlay, loadingMessage;
        var liveOverlay, liveIndicator, liveMessage, liveGraphic;
        var waitingOverlay, waitingWidget, waitingText;
        var endedOverlay, endedText;
        var unavailableOverlay, unavailableText;
        var playableOverlay, playButton, playButtonGraphic;
        var shadeOverlay;
        var customOverlay, customWrapper, customMessageWrapper, customMessage, customPlayButton, customPlayButtonGraphic;
        var thumbnail;
        var clickAction = function () { };
        var isCustomized = false;
        var shouldShowTitle = true;
        var shouldShowPlayButton = true;
        var modeCssClass = "mode-loading";
        var widthCssClass = "width-medium";
        var heightCssClass = "height-medium";
        var widthAtLastCheck = -1;
        var heightAtLastCheck = -1;
        var sizeCheckIntervalToken;


        function init() {
            
            loadingMessage = makeParagraph(_strings.loading, LOADING_TEXT_CLASS);
            loadingOverlay = makeDiv(LOADING_OVERLAY_CLASS);
            loadingOverlay.appendChild(loadingMessage);

            playButtonGraphic = makePlayButtonGraphic();
            playButton = makeButton(playButtonGraphic, PLAY_BUTTON_CLASS);
            playButton.setAttribute("title", _strings.play);
            playableOverlay = makeDiv(PLAYABLE_OVERLAY_CLASS);
            playableOverlay.appendChild(playButton);
            
            shadeOverlay = makeDiv(SHADE_OVERLAY_CLASS);
            
            customPlayButtonGraphic = makePlayButtonGraphic();
            customPlayButton = makeButton(customPlayButtonGraphic, PLAY_BUTTON_CLASS);
            customPlayButton.setAttribute("title", _strings.play);
            customMessage = makeDiv(CUSTOM_MESSAGE_CLASS);
            customMessageWrapper = makeDiv(CUSTOM_MESSAGE_WRAPPER_CLASS);
            customMessageWrapper.appendChild(customMessage);
            customWrapper = makeDiv(CUSTOM_CONTENT_WRAPPER_CLASS);
            customWrapper.appendChild(customPlayButton); // Button added before message for tab ordering.
            customWrapper.appendChild(customMessageWrapper);
            customOverlay = makeDiv(CUSTOM_OVERLAY_CLASS);
            customOverlay.appendChild(customWrapper);

            liveGraphic = makeLiveDot();
            liveMessage = makeParagraph(_strings.liveIndicator, LIVE_TEXT_CLASS);
            liveIndicator = makeDiv(LIVE_INDICATOR_CLASS);
            liveIndicator.appendChild(liveGraphic);
            liveIndicator.appendChild(liveMessage);
            liveOverlay = makeDiv(LIVE_OVERLAY_CLASS);
            liveOverlay.appendChild(liveIndicator);
            
            waitingOverlay = makeDiv(WAITING_OVERLAY_CLASS);
            waitingWidget = new WaitingWidget(ui_strings);
            waitingWidget.appendToDom(waitingOverlay);

            endedText = makeParagraph(_strings.liveBroadcastEnded, ENDED_TEXT_CLASS);
            endedOverlay = makeDiv(ENDED_OVERLAY_CLASS);
            endedOverlay.appendChild(endedText);

            unavailableText = makeParagraph("", UNAVAILABLE_TEXT_CLASS);
            unavailableText.appendChild(makeSpan(_strings.unavailable));
            unavailableText.appendChild(makeSpan(_strings.tryAgain));
            unavailableOverlay = makeDiv(UNAVAILABLE_OVERLAY_CLASS);
            unavailableOverlay.appendChild(unavailableText);

            titleParagraph = makeParagraph("");
            titleRegion = makeTitleRegion(titleParagraph, true);

            self.rootElement = makeDiv(OUTER_ELEMENT_CLASS);
            self.rootElement.appendChild(loadingOverlay);
            self.rootElement.appendChild(playableOverlay);
            self.rootElement.appendChild(shadeOverlay);
            self.rootElement.appendChild(customOverlay);
            self.rootElement.appendChild(liveOverlay);
            self.rootElement.appendChild(waitingOverlay);
            self.rootElement.appendChild(endedOverlay);
            self.rootElement.appendChild(unavailableOverlay);
            self.rootElement.appendChild(titleRegion);
            self.rootElement.addEventListener("click", handleClick.bind(self));

            thumbnail = new Thumbnail();
            thumbnail.appendToDom(self.rootElement);

            setTimeout(checkOuterSize, 0);
            beginWatchingForResize();
        }

        makeAppendable(this);

        Object.defineProperty(this, "titleText", {
            set: function (value) {
                titleParagraph.innerText = value;
            }
        });

        Object.defineProperty(this, "shouldShowTitle", {
            set: function (value) {
                shouldShowTitle = !!value;
                updateOuterCssClasses();
            }
        });

        Object.defineProperty(this, "shouldShowPlayButton", {
            set: function (value) {
                shouldShowPlayButton = !!value;
                updateOuterCssClasses();
            }
        });

        Object.defineProperty(this, "shouldShowLiveCountdown", {
            set: function (value) {
                waitingWidget.shouldShowCountdown = !!value;
            }
        });

        Object.defineProperty(this, "thumbnailUrl", {
            set: function (url) {
                thumbnail.imageUrl = url;
            }
        });

        Object.defineProperty(this, "backgroundImageUrl", {
            set: function (url) {
                self.rootElement.style.backgroundImage = "url(" + url + ")";
            }
        });

        Object.defineProperty(this, "clickAction", {
            value: function () { },
            writable: true
        });

        Object.defineProperty(this, "airDateUnixTime", {
            set: function (value) {
                waitingWidget.airDateUnixTime = value;
            }
        });

        Object.defineProperty(this, "customMessageHtml", {
            set: function (value) {
                // TODO: Sanitize HTML.
                var haveHtml = (typeof value === "string" && value.length > 0);
                customMessage.innerHTML = haveHtml ? value : "";
                isCustomized = haveHtml;
                updateOuterCssClasses();
            }
        });


        this.goLoading = function () {
            setModeCssClass("mode-loading");
        };

        this.goOnDemand = function () {
            setModeCssClass("mode-ondemand");
        };

        this.goLive = function () {
            setModeCssClass("mode-live");
        };

        this.goLiveEnded = function () {
            setModeCssClass("mode-ended");
        };

        this.goWaiting = function () {
            setModeCssClass("mode-waiting");
        };

        this.goUnavailable = function () {
            setModeCssClass("mode-unavailable");
        };


        this.destroy = function () {
            clearInterval(sizeCheckIntervalToken);
            self.removeFromDom();
        };


        function updateOuterCssClasses() {
            var classes = [];

            classes.push(OUTER_ELEMENT_CLASS);
            classes.push(modeCssClass);
            classes.push(widthCssClass);
            classes.push(heightCssClass);

            if (isCustomized) {
                classes.push(CUSTOMIZED_CLASS);
            }

            if (!shouldShowTitle) {
                classes.push(NO_TITLE_CLASS);
            }

            if (!shouldShowPlayButton) {
                classes.push(NO_PLAYBUTTON_CLASS);
            }

            self.rootElement.className = classes.join(" ");

        }

        function setModeCssClass(newModeCssClass) {
            modeCssClass = newModeCssClass;
            updateOuterCssClasses();
        }
        

        function beginWatchingForResize() {
            sizeCheckIntervalToken = setInterval(checkOuterSize, SIZE_CHECK_INTERVAL_MS);
        }

        function checkOuterSize() {

            var w = self.rootElement.clientWidth;
            var h = self.rootElement.clientHeight;
            var widthClass = widthCssClass;
            var heightClass = heightCssClass;

            if (w !== widthAtLastCheck) {
                widthAtLastCheck = w;
                widthClass = getWidthSizeClass(w);
            }

            if (h !== heightAtLastCheck) {
                heightAtLastCheck = h;
                heightClass = getHeightSizeClass(h);
            }

            if (widthClass !== widthCssClass || heightClass !== heightCssClass) {
                widthCssClass = widthClass;
                heightCssClass = heightClass;
                updateOuterCssClasses();
            }
        }


        init();
    }
    

    function PlayCover(ui_strings) {
        var self = this;
        var presInfo;
        var view;
        var isReady = false;
        var propStorage = {};
        var lastSetViewValues = {};
        var resolvedThumbnailUrl;
        var liveStartDelay = 15 * 1000;
        var airDateStatusCheckTimeout;


        function init() {
            view = new PlayCoverView(ui_strings);
            view.clickAction = doClickAction;
            view.goLoading();
        }
        

        makeAppendable(this);

        defProp(this, "rootElement", {
            get: function () { return view.rootElement; }
        });


        prop(self, "title", onPropertySet);
        prop(self, "airDateUnixTime", onSetAirDate);
        prop(self, "presentationStatus", onPropertySet);
        prop(self, "customMessageHtml", onPropertySet);

        defThumbnailProp(self, "thumbnailUrl");
        defThumbnailProp(self, "defaultThumbnailUrl");

        defProp(self, "options", { value: {}, writable: false });

        prop(self.options, "backgroundImageUrl", onPropertySet);
        prop(self.options, "shouldShowTitle", onPropertySet, true);
        prop(self.options, "shouldShowPlayButton", onPropertySet, true);
        prop(self.options, "shouldShowLiveCountdown", onPropertySet, true);
        prop(self.options, "shouldDelayLiveUntilAirDate", onPropertySet, false);
        

        this.clickAction = function () { };
        
        this.ready = function (readyCallback) {
            isReady = true;

            updateView(function () {
                setTimeout(readyCallback, 0);
            });
        };

        this.destroy = function () {
            view.destroy();
        };

        function chainResolveThumbnails(thumbPropName, nextThumbPropName, callback) {
            callback = callback || function () { };

            var thumb = thumbPropName && propStorage[thumbPropName] || newThumbnailInfo();

            if (!thumb.url) {
                // This thumb has no URL (e.g. null or empty string).
                if (nextThumbPropName) {
                    chainResolveThumbnails(nextThumbPropName, null, callback);
                } else {
                    callback(null);
                }

                return;
            }

            if (thumb.didResolve) {
                // We've already resolved this thumb, so save us from extra work.
                callback(thumb.resolvedUrl);
                return;
            }

            resolveThumbnailUrl(thumb, function (resolvedThumb) {
                var nextThumb = nextThumbPropName && propStorage[nextThumbPropName];

                resolvedThumb = resolvedThumb || {};
                propStorage[thumbPropName] = resolvedThumb;

                if (resolvedThumb.didResolve) {
                    callback(resolvedThumb.resolvedUrl);
                } else if (nextThumb) {
                    chainResolveThumbnails(nextThumbPropName, null, callback);
                } else {
                    callback(null);
                }
            });
        }

        function resolveThumbnailUrl(inputThumb, callback) {
            if (inputThumb.triedResolve) {
                callback(inputThumb);
                return;
            }

            var outputThumb = newThumbnailInfo(inputThumb.url);
            imageUrlToDataUrl(inputThumb.url, function (result) {
                outputThumb.triedResolve = true;
                if (result.loadingError) {
                    outputThumb.didResolve = false;
                    outputThumb.resolvedUrl = "";
                } else {
                    outputThumb.didResolve = true;
                    outputThumb.resolvedUrl = result.dataUrl || url;
                }
                callback(outputThumb);
            });
        }

        function doClickAction() {
            if (isReady && typeof self.clickAction === "function") {
                self.clickAction();
            }
        }

        function onPropertySet(name, value) {
            updateView();
        }

        function onSetAirDate(name, value) {
            clearTimeout(airDateStatusCheckTimeout);

            var now = (new Date()).getTime();
            var msUntilAirDate = self.airDateUnixTime - now;
            var delay = Math.max(0, msUntilAirDate);

            setTimeout(updateView, delay);
        }

        function updateViewMode() {
            if (isReady) {
                setCoverModeForPlayState(view, getEffectivePresentationState());
            }
        }

        function updateView(completedCallback) {
            completedCallback = completedCallback || function () { };

            setViewPropertyIfDifferent("backgroundImageUrl", self.options.backgroundImageUrl);

            if (isReady) {
                getThumbnailUrlForView(function (url) {
                    setViewPropertyIfDifferent("thumbnailUrl", url);

                    setViewPropertyIfDifferent("shouldShowTitle", self.options.shouldShowTitle);
                    setViewPropertyIfDifferent("shouldShowPlayButton", self.options.shouldShowPlayButton);
                    setViewPropertyIfDifferent("shouldShowLiveCountdown", self.options.shouldShowLiveCountdown);
                    setViewPropertyIfDifferent("backgroundImageUrl", self.options.backgroundImageUrl);
                    setViewPropertyIfDifferent("titleText", self.title);
                    setViewPropertyIfDifferent("airDateUnixTime", self.airDateUnixTime);
                    setViewPropertyIfDifferent("customMessageHtml", self.customMessageHtml);

                    updateViewMode();

                    completedCallback(true);
                });
            } else {
                completedCallback(false);
            }
        };

        function getThumbnailUrlForView(callback) {
            if (isUnavailable(getEffectivePresentationState())) {
                chainResolveThumbnails("defaultThumbnailUrl", null, callback);
            } else {
                chainResolveThumbnails("thumbnailUrl", "defaultThumbnailUrl", callback);
            }
        }

        function setViewPropertyIfDifferent(propName, value) {
            var wasSetBefore = lastSetViewValues.hasOwnProperty(propName);
            var lastSetValue = lastSetViewValues[propName];

            if (!wasSetBefore || lastSetValue !== value) {
                lastSetViewValues[propName] = value;
                view[propName] = value;
            }
        }

        function getEffectivePresentationState() {
            if (self.options.shouldDelayLiveUntilAirDate && isCurrentlyLive(self.presentationStatus) && !haveReachedAirDate(self.airDateUnixTime)) {
                return PRESENTATION_STATUS.ScheduledForLive;
            } else {
                return self.presentationStatus;
            }
        }

        function haveReachedAirDate(airDateUnixTime) {
            var closeEnoughThreshold = 500; // milliseconds
            var now = (new Date()).getTime();
            airDateUnixTime = airDateUnixTime || 0;

            return now >= (airDateUnixTime - closeEnoughThreshold);
        }

        function setCoverModeForPlayState(view, playState) {
            if (isCurrentlyLive(playState)) {
                view.goLive();
            } else if (isWaitingToBeginLive(playState)) {
                view.goWaiting();
            } else if (isLiveEnded(playState)) {
                view.goLiveEnded();
            } else if (isOnDemand(playState)) {
                view.goOnDemand();
            } else {
                view.goUnavailable();
            }
        }

        function isOnDemand(playState) {
            return playState === PRESENTATION_STATUS.OnDemand;
        }

        function isCurrentlyLive(playState) {
            return playState === PRESENTATION_STATUS.Live || playState === PRESENTATION_STATUS.LivePaused;
        }

        function isWaitingToBeginLive(playState) {
            return playState === PRESENTATION_STATUS.ScheduledForLive || playState === PRESENTATION_STATUS.OpenForLive;
        }

        function isLiveEnded(playState) {
            return playState === PRESENTATION_STATUS.LiveEnded;
        }

        function isPlayableState(playState) {
            return playState === PRESENTATION_STATUS.OnDemand || isCurrentlyLive(playState);
        }

        function isUnavailable(playState) {
            return playState === PRESENTATION_STATUS.NotAvailable;
        }

        function prop(obj, name, onSet, initialValue) {
            propStorage[name] = initialValue;
            defProp(obj, name, {
                get: function () { return propStorage[name]; },
                set: function (val) { propStorage[name] = val; onSet(name, val); }
            });
        }

        function newThumbnailInfo(url) {
            return {
                url: url || null,
                triedResolve: false,
                didResolve: false,
                resolvedUrl: null
            };
        }

        function setThumbnailUrlByName(name, url) {
            var info = propStorage[name];
            if (info && info.url === url) {
                return; // Nothing changed.
            }

            propStorage[name] = newThumbnailInfo(url);
        }

        function getThumbnailUrlByName(name) {
            var info = propStorage[name];
            return info && info.url;
        }

        function defThumbnailProp(obj, name) {
            propStorage[name] = newThumbnailInfo();

            defProp(obj, name, {
                set: function (url) {
                    setThumbnailUrlByName(name, url);
                    updateView();
                },
                get: function () {
                    return getThumbnailUrlByName(name);
                }
            });
        }

        init();
    }


    function PresentationStatusWatcher(playCover, getPresentationStatus) {
        var INTERVAL_MS = 1 * 1000;
        var NEAR_AIR_DATE_MS = 5 * 60 * 1000;

        var BACKUP_CALL_ADDITIONAL_DELAY = 10 * 1000;
        var LIVE_CALL_DELAY = 15 * 1000;
        var WAITING_CALL_DELAY = 10 * 1000;
        var NEARLIVE_CALL_DELAY = 5 * 1000;
        var OTHER_CALL_DELAY = 30 * 1000;

        var self = this;
        var intervalToken;
        var nextCallTime = 0;


        function init() {
            startStatusCheckPolling();
            maybeStopStatusCheckPolling();
        }
        

        this.destroy = function () {
            stopStatusCheckPolling();
        };


        function intervalTick() {
            if (now() >= nextCallTime) {
                callForStatus();
            }
        }

        function calcCallDelay() {
            var status = (playCover && playCover.presentationStatus) || "";
            var airDateUnixTime = (playCover && playCover.airDateUnixTime) || 0;
            var isNearAirDate = Math.abs(now() - airDateUnixTime) < 5 * 60 * 1000;

            if (status == PRESENTATION_STATUS.OpenForLive || (status == PRESENTATION_STATUS.ScheduledForLive && isNearAirDate)) {
                return NEARLIVE_CALL_DELAY;
            } else if (status == PRESENTATION_STATUS.ScheduledForLive) {
                return WAITING_CALL_DELAY;
            } else if (status == PRESENTATION_STATUS.Live || status == PRESENTATION_STATUS.LivePaused) {
                return LIVE_CALL_DELAY;
            } else {
                return OTHER_CALL_DELAY;
            }
        }

        function callForStatus() {
            nextCallTime = now() + calcCallDelay() + BACKUP_CALL_ADDITIONAL_DELAY;
            getPresentationStatus(receiveStatus);
        }

        function receiveStatus(data) {
            playCover.presentationStatus = data.status;

            nextCallTime = now() + calcCallDelay();

            maybeStopStatusCheckPolling();
        }

        function startStatusCheckPolling() {
            intervalToken = setInterval(intervalTick, INTERVAL_MS);
        }

        function stopStatusCheckPolling() {
            clearInterval(intervalToken);
        }

        function maybeStopStatusCheckPolling() {
            var currentStatus = playCover && playCover.presentationStatus;
            if (currentStatus === PRESENTATION_STATUS.OnDemand || currentStatus === PRESENTATION_STATUS.NotAvailable) {
                stopStatusCheckPolling();
            }
        }
        
        function now() {
            return (new Date()).getTime();
        }


        init();
    }


    var defProp = Object.defineProperty;

    function handleClick(e) {
        if (e && e.target && e.target.tagName === 'A') {
            // Clicking on a link should not count as clicking on the Play Cover.
            return;
        }

        if (typeof this.clickAction === "function") {
            this.clickAction();
        }
    }

    function performClickAction(self) {
        if (typeof self.clickAction === "function") {
            self.clickAction();
        }
    }

    function makePlayButtonGraphic() {
        var svg, circle, arrow;
        var ns = "http://www.w3.org/2000/svg";

        svg = document.createElementNS(ns, "svg");
        svg.setAttribute("viewBox", "0 0 100 100");

        circle = document.createElementNS(ns, "circle");
        circle.setAttribute("class", SVG_CIRCLE_CLASS);
        circle.setAttribute("cx", "50");
        circle.setAttribute("cy", "50");
        circle.setAttribute("r", "50");
        circle.setAttribute("fill", "#000");
        circle.setAttribute("opacity", "0.5");
        svg.appendChild(circle);

        arrow = document.createElementNS(ns, "path");
        arrow.setAttribute("class", SVG_ARROW_CLASS);
        arrow.setAttribute("d", "M 30,20 85,50 30,80");
        arrow.setAttribute("fill", "#fff");
        arrow.setAttribute("opacity", "0.9");
        svg.appendChild(arrow);

        return svg;
    }

    function makeLiveDot() {
        var svg, circle;
        var ns = "http://www.w3.org/2000/svg";

        svg = document.createElementNS(ns, "svg");
        svg.setAttribute("viewBox", "0 0 100 100");

        circle = document.createElementNS(ns, "circle");
        circle.setAttribute("cx", "50");
        circle.setAttribute("cy", "50");
        circle.setAttribute("r", "50");
        circle.setAttribute("fill", "#ee0000");
        circle.setAttribute("opacity", "1.0");
        svg.appendChild(circle);

        return svg;
    }

    function makeButton(graphic, cssClass) {
        var button = document.createElement("button");
        button.className = cssClass;
        button.appendChild(graphic);
        return button;
    }

    function makeTitleRegion(titleParagraph, hidden) {
        var region = document.createElement("div");
        region.className = TITLE_ELEMENT_CLASS;
        region.appendChild(titleParagraph);
        region.style.diplay = hidden ? "block" : "none";
        return region;
    }

    function makeDiv(cssClassName) {
        return makeTag("div", "", cssClassName);
    }

    function makeParagraph(text, cssClass) {
        return makeTag("p", text, cssClass);
    };

    function makeSpan(text, cssClass) {
        return makeTag("span", text, cssClass);
    };

    function makeTag(tagName, text, cssClass) {
        var t = document.createElement(tagName);
        t.className = cssClass;
        t.innerText = text;
        return t;
    }

    function imageUrlToDataUrl(imageUrl, callback) {
        var img = document.createElement('img');
        var result = {
            loadingError: false,
            dataUrl: ""
        };

        img.addEventListener('load', function (e) {
            var resultUrl;
            try {
                result.dataUrl = imgToDataUrl(img);
            } catch (err) {
                result.dataUrl = "";
            }
            callback(result);
        });
        img.addEventListener('error', function (e) {
            result.loadingError = true;
            callback(result);
        });
        img.src = imageUrl;
    }

    function imgToDataUrl(img) {
        var canvas = document.createElement('canvas');
        var context = canvas.getContext('2d');

        canvas.width = img.width;
        canvas.height = img.height;

        context.drawImage(img, 0, 0);
        return canvas.toDataURL();
    }

    function findMatchingSizeClassNames(size, sizeClassesSpec) {
        var name, range;
        var matches = [];

        size = size || 0;
        sizeClassesSpec = sizeClassesSpec || {};

        for (name in sizeClassesSpec) {
            if (sizeClassesSpec.hasOwnProperty(name)) {
                range = sizeClassesSpec[name];
                if (size >= range.min && size <= range.max) {
                    matches.push(name);
                }
            }
        }

        return matches;
    }

    function getSizeClass(size, sizeClassesSpec) {
        var classNames = findMatchingSizeClassNames(size, sizeClassesSpec);
        return classNames.join(" ");
    }

    function getWidthSizeClass(w) {
        return getSizeClass(w, widthClassesSpec);
    }

    function getHeightSizeClass(h) {
        return getSizeClass(h, heightClassesSpec);
    }


    function createPlayCover(ui_strings) {
        return new PlayCover(ui_strings);
    }

    function createPresentationStatusWatcher(playCover, getPresentationStatus) {
        return new PresentationStatusWatcher(playCover, getPresentationStatus);
    }


    return {
        PRESENTATION_STATUS: PRESENTATION_STATUS,

        create: createPlayCover,
        createStatusWatcher: createPresentationStatusWatcher,

        PlayCover: PlayCover,
        PresentationStatusWatcher: PresentationStatusWatcher,

        PlayCoverView: PlayCoverView,
        Thumbnail: Thumbnail,
        WaitingWidget: WaitingWidget,

        imageUrlToDataUrl: imageUrlToDataUrl
    };
})();