/*global Mediasite :true*/
if (typeof Mediasite === "undefined" || !Mediasite) {
    var Mediasite = {};
}

Mediasite.namespace = function () {
    var a = arguments, o = null, i, j, d;
    for (i = 0; i < a.length; i = i + 1) {
        d = (a[i].toString()).split(".");
        o = Mediasite;

        for (j = (d[0] === "Mediasite") ? 1 : 0; j < d.length; j = j + 1) {
            o[d[j]] = o[d[j]] || {};
            o = o[d[j]];
        }
    }
    return o;
};

Mediasite.Slide = function (number, time, text, description) {
    this.Number = number;
    this.Time = time;
    this.Text = text;
    this.Description = description;

    var _urlProvider;
    this.SetUrlProvider = function (urlProvider) {
        _urlProvider = urlProvider;
    }
    this.GetSlideUrl = function () {
        if (!_urlProvider) return;
        return _urlProvider.GetSlideUrl(this.Number);
    };
};

Mediasite.Slide.getCacheFriendlyImageSize = function (desiredHeight, aspect) {
    var cacheFriendlySlideHeights = [64, 128, 256, 512, 768];
    var i;
    for (i = 0; i < cacheFriendlySlideHeights.length; i++) {
        var height = cacheFriendlySlideHeights[i];
        if (desiredHeight <= height) {
            return new Mediasite.Geometry.Size(Math.round(height * aspect), height);
        }
    }

    return Mediasite.Geometry.Size.Zero();
}

Mediasite.Chapter = function (number, time, text) {
    this.Number = number;
    this.Time = time;
    this.Text = text;
};

Mediasite.Link = function (url, description) {
    this.Url = url;
    this.Description = description;
};

Mediasite.Presenter = function (name, imageUrl, email, bioUrl) {
    this.Name = name;
    this.ImageUrl = imageUrl;
    this.Email = email;
    this.BioUrl = bioUrl;
};

Mediasite.TimingEventType = {
    Caption: 1,
    SlideText: 2
};

Mediasite.ContentStreamType =
{
    Video1: 0,
    Slide: 2,
    Presentation: 3,
    Video2: 4,
    Video3: 5
};

Mediasite.ContentType =
{
    None: 0,
    Video: 1,
    Slide: 2
};


Mediasite.Presentation = function (manifest) {
    var self = this;
    this.PlaybackTicketId = manifest.PlaybackTicketId;
    this.PresentationId = manifest.PresentationId;
    this.Title = manifest.Title;
    this.Description = manifest.Description;
    this.ThumbnailUrl = manifest.ThumbnailUrl
    this.Date = manifest.AirDate;
    this.Time = manifest.AirTime;
    this.Duration = manifest.Duration;
    this.UnixTime = manifest.UnixTime;
    this.Presenters = manifest.Presenters;
    this.Links = manifest.SupportingLinks;
    this.Chapters = manifest.Chapters;
    this.TimedEvents = manifest.TimedEvents;
    this.HasCaptions = manifest.Transcript && manifest.Transcript.length > 0;
    this.DisableCaptionDisplay = manifest.DisableCaptionDisplay;
    this.IsStandAlone = manifest.IsStandAlone;
    this.HasSearchableText = manifest.HasSearchableText;
    this.ServerVersion = manifest.Version;
    this.ForumEnabled = manifest.ForumEnabled;
    this.SharingEnabled = manifest.SharingEnabled;
    this.PollingEnabled = manifest.PollingEnabled;
    this.PollingLocation = manifest.PollingLocation;
    this.PlayStatus = manifest.PlayStatus;
    this.HasSlideContent = manifest.HasSlideContent;
    this.Transcript = manifest.Transcript;
    this.PreferSmoothStreaming = manifest.PreferSmoothStreaming;
    this.ServerClockSkew = manifest.ServerClockSkew;
    this.LiveStartUnixTimeInMs = manifest.LiveStartUnixTimeInMs;
    this.PlayerExtensions = manifest.PlayerExtensions;
    this.MediaState = 0;

    this.GetStream = function (contentStreamType) {
        var selectedStream = undefined;

        $.each(this.Streams, function (index, stream) {
            if (contentStreamType == stream.StreamType) {
                selectedStream = stream;
                return false;
            }

            return true;
        });

        return selectedStream;
    };

    this.GetPlayerExtension = function (extensionType) {
        var selectedExtension = null;
        $.each(self.PlayerExtensions, function (index, extension) {

            if (extension.ExtensionType == extensionType) {
                selectedExtension = extension;
                return false;
            }

            return true;
        });

        return selectedExtension;
    };

    this.IsLivePresentation = function () {
        return self.PlayStatus === Mediasite.Player.PresentationStatus.ScheduledForLive ||
            self.PlayStatus === Mediasite.Player.PresentationStatus.OpenForLive ||
            self.PlayStatus === Mediasite.Player.PresentationStatus.Live ||
            self.PlayStatus === Mediasite.Player.PresentationStatus.LivePaused ||
            self.PlayStatus === Mediasite.Player.PresentationStatus.LiveEnded;
    };

    //Build Streams
    this.Streams = [];

    if (!manifest.Streams) {

        if (self.Streams.length == 0) {
            self.Streams.push(new Mediasite.Player.Stream({ StreamType: Mediasite.ContentStreamType.Video1 }));
            self.Streams.push(new Mediasite.Player.Stream({ StreamType: Mediasite.ContentStreamType.Video2 }));
            self.Streams.push(new Mediasite.Player.Stream({ StreamType: Mediasite.ContentStreamType.Video3 }));
        }
        return;
    }

    $.each(manifest.Streams, function (index, stream) {
        var playerStream = $.extend({}, new Mediasite.Player.Stream(stream));
        self.Streams.push(playerStream);
    });

};

Mediasite.namespace("BrowserDetect");

/* Environment */
Mediasite.BrowserDetect.Embedded = function () {
    return top !== self;
};

Mediasite.BrowserDetect.Local = function () {
    return !!/file/i.exec(location.protocol);
}

Mediasite.BrowserDetect.GetBaseUri = function() {
	var baseHref = window.location.href;
	if (baseHref.indexOf('?') != -1) {
		baseHref = baseHref.substr(0, baseHref.indexOf('?'));
	}
	if (baseHref.indexOf('#') != -1) {
		baseHref = baseHref.substr(0, baseHref.indexOf('#'));
	}
	if (baseHref.indexOf('.html') !== -1) {
		var lastIndexOfSlash = 0;
		while (true) {
			var nextIndex = baseHref.indexOf('/', lastIndexOfSlash);
			if (nextIndex != -1) {
				lastIndexOfSlash = nextIndex + 1;
			} else {
				break;
			}
		}
		baseHref = baseHref.substr(0, lastIndexOfSlash);
	}
	
	return baseHref;
};

/* Browser */

Mediasite.BrowserDetect.IOS = function () {
    return !!/iphone|ipod|ipad/i.exec(navigator.userAgent);
};

Mediasite.BrowserDetect.IOS6 = function () {
    return Mediasite.BrowserDetect.IOS() && !!/OS 6_/.exec(navigator.userAgent);
};

Mediasite.BrowserDetect.iosVersion = function () {
    var results = /(?:iPhone|iPod|iPad).*OS (\d+)_/.exec(navigator.userAgent);
    if (results && results.length > 1) {
        return Number(results[1]);
    } else {
        return 0;
    }
};

Mediasite.BrowserDetect.iosVersionDetailed = function () {
    var version;
    var results = /(?:iPhone|iPod|iPad).*OS (\d+)(?:_(\d+)){0,1}(?:_(\d+)){0,1}/.exec(navigator.userAgent);
    
    if (!results) {
        return { major: 0, minor: 0, revision: 0 };
    }

    version = {
        major: results[1] || 0,
        minor: results[2] || 0,
        revision: results[3] || 0,
        build: results[4] || 0
    };

    return version;
};

Mediasite.BrowserDetect.IOS10AndGreater = function () {
    return Mediasite.BrowserDetect.IOS() && parseInt(Mediasite.BrowserDetect.iosVersion()) >= 10;
};

Mediasite.BrowserDetect.Android = function () {
    return !!/android|crmo/i.exec(navigator.userAgent);
};

Mediasite.BrowserDetect.Firefox = function () {
    var matches = navigator.userAgent.match(/Firefox\/(\d*)/);
    if (matches) {
        return matches[1];
    }

    return false;
};

Mediasite.BrowserDetect.InternetExplorer = function () {
    var matches = navigator.userAgent.match(/(?:\b(MS)?IE\s+|\bTrident\/7\.0;.*\s+rv:)(\d+)/);
    if (matches) {
        return matches[2];
    }

    return false;
};

Mediasite.BrowserDetect.InternetExplorerARM = function () {
    var ua = navigator.userAgent.toLowerCase();

    var matches = ua.indexOf("msie") != -1 && ua.indexOf("arm") != -1
    return matches;
};

Mediasite.BrowserDetect.Edge = function () {
    if (/Edge\/\d\d/.exec(navigator.userAgent)) {
        return true;
    }

    return false;
};

Mediasite.BrowserDetect.Chrome = function () {
    if (Mediasite.BrowserDetect.Edge()) {
        return false; // Edge browser immitates the Chrome UA string. But don't be fooled.
    }

    if (/chrome/i.exec(navigator.userAgent)) {
        return true;
    }

    return false;
};

Mediasite.BrowserDetect.ChromeAndroid = function () {
    if (/Android/i.exec(navigator.userAgent) && /Chrome/i.exec(navigator.userAgent)) {
        return true;
    }
        
    return false;
};

Mediasite.BrowserDetect.MacOS10DotWhat = function () {
    var matches = navigator.userAgent.match(/mac os x (\d+)_(\d+)/i);
    if (matches && matches.length == 3) {
        return Number(matches[2]);
    } else {
        return -1;
    }
};

Mediasite.BrowserDetect.MacOSX = function () {
    return Mediasite.BrowserDetect.MacOS10DotWhat() > -1;
};

Mediasite.BrowserDetect.Safari = function () {
    // Chrome mentions Safari in its UA string.
    return /safari/i.exec(navigator.userAgent) && !Mediasite.BrowserDetect.Chrome();
};

Mediasite.BrowserDetect.MacSafari = function () {
    return Mediasite.BrowserDetect.Safari() && Mediasite.BrowserDetect.MacOSX();
};

Mediasite.BrowserDetect.Blackberry = function () {
    if (/blackberry|rim/i.exec(navigator.userAgent)) {
        return true;
    }

    return false;
};

Mediasite.BrowserDetect.Mobile = function () {
    return !!(Mediasite.BrowserDetect.IOS() || Mediasite.BrowserDetect.Android() || Mediasite.BrowserDetect.Blackberry());
};

Mediasite.BrowserDetect.MultiVideoCapableMobile = function () {
    var iosTenThreeOrGreater = (Mediasite.BrowserDetect.IOS() && Mediasite.BrowserDetect.iosVersionDetailed().minor >= 3);
    return !!(iosTenThreeOrGreater || Mediasite.BrowserDetect.Android());
};

Mediasite.BrowserDetect.IEMetro = function () {
    var ieVersion = Mediasite.BrowserDetect.InternetExplorer();
    if (ieVersion > 9) {   

        //This can return a false positive if desktop IE10 disables ActiveX Controls

        //Check ActiveX Support
        var supported = null;

        try {
            supported = !!new ActiveXObject("htmlfile");
        } catch (e) {
            supported = false;
        }

        return !supported;
    }

    return false;
};

/* --- end Mediasite.BrowserDetect --- */


Mediasite.GetQueryParam = function (name, format) {
    /// <summary>Get a query parameter from the current URI</summary>
    /// <param name="name" type="string">Query parameter key</param>
    /// <param name="format" optional="true" type="Mediasite.GetQueryParam.AsX">Format/type for parsing parameter value</param>

    var regexS = "[\\?&]" + name + "=([^&#]*)";
    var regex = new RegExp(regexS, "i");
    var results = regex.exec(window.location.href);
    if (results === null) {
        return "";
    }

    var value = decodeURIComponent(results[1].replace(/\+/g, " "));

    switch (format) {
        case Mediasite.GetQueryParam.AsBool:
            value = value.toLowerCase();
            value = value === 'true' ? true : value === 'false' ? false : null;
            break;
        case Mediasite.GetQueryParam.AsInt:
            value = parseInt(value, 10);
            break;
    }

    return value;
};

// Query parameter formats
Mediasite.GetQueryParam.AsBool = 'bool';
Mediasite.GetQueryParam.AsInt = 'int';

Mediasite.Cookie = function (name) {
    var valueDelimiter = "&";
    var values = {};
    var expires = null;
    var path = null;

    function readCookieData() {
        var i, parts;
        if (document.cookie) {
            var begin = document.cookie.indexOf(name + "=");
            if (begin !== -1) {
                begin += name.length + 1;
                var end = document.cookie.indexOf(";", begin);
                if (end === -1) {
                    end = document.cookie.length;
                }

                var rawCookie = document.cookie.substring(begin, end);
                var entries = rawCookie.split(valueDelimiter);

                for (i = 0; i < entries.length; i++) {
                    parts = entries[i].split("=");

                    if (parts.length === 2) {
                        values[parts[0]] = decodeURIComponent(parts[1]);
                    }
                }
            }
        }
    }

    readCookieData();

    function writeCookieData() {
        var key, now, newCookie;
        if (!expires) {
            now = new Date();
            expires = new Date(now.getFullYear() + 1, now.getMonth(), now.getDate());
        }

        var cookieData = "";
        for (key in values) {
            if (values.hasOwnProperty(key)) {
                cookieData += key + "=" + encodeURIComponent(values[key]) + valueDelimiter;
            }
        }
        cookieData = cookieData.substring(0, cookieData.length - 1);

        newCookie = name + "=" + cookieData +
            ((path) ? "; path=" + path : "") +
		    ((expires) ? "; expires=" + expires.toGMTString() : "");

        document.cookie = newCookie;
    }

    this.GetAll = function() {
        return values;
    };

    this.GetName = function () {
        return name;
    };

    this.GetExpires = function () {
        return expires;
    };

    this.SetExpires = function (date) {
        expires = date;
    };

    this.GetPath = function () {
        return path;
    };

    this.SetPath = function (p) {
        path = p;
    };

    this.SetValue = function (name, value) {
        values[name] = value;
        writeCookieData();
    };

    this.GetValue = function (name, def) {
        if (!values[name]) {
            return def;
        }
        return values[name];
    };

    this.GetNumberValue = function (name, def) {
        if (!values[name]) {
            return def;
        }
        return Number(values[name]);
    };

    this.GetBoolValue = function (name, def) {
        if (!values[name]) {
            return def;
        }
        if (values[name].toLowerCase() === "true") {
            return true;
        }
        else {
            return false;
        }
    };
};

Mediasite.Urls = (function (window, $, undefined) {
    var module = {};

    module.EscapedUnicodeRegex = /\%u([0-9A-F]{4})/g;
    module.UnicodeHtmlEntityReplacement = "&#x$1;"

    module.EscapeForMailto = function (val) {
        /// Escape the string, then fix escaped Unicode references for mailto 

        var useHtmlEntities = false;

        var msieVersion = Mediasite.BrowserDetect.InternetExplorer();
        if (msieVersion && msieVersion <= 6) {
            useHtmlEntities = true;
        }


        var escaped = escape(val);

        if (useHtmlEntities) {
            // http://blogs.msdn.com/b/ie/archive/2007/02/12/international-mailto-uris-in-ie7.aspx
            var escapedAndFixed = escaped.replace(module.EscapedUnicodeRegex, module.UnicodeHtmlEntityReplacement);
        } else {
            var escapedAndFixed = escaped.replace(module.EscapedUnicodeRegex, function (match) {
                return encodeURI(unescape(match));
            });
        }

        return escapedAndFixed;
    }

    module.ContainsUnicode = function (url) {
        var decodedUri = escape(decodeURI(url));
        if (module.EscapedUnicodeRegex.test(decodedUri)) {
            return true;
        }

        return false;
    }

    return module;
})(this, jQuery);

Mediasite.Logging = (function (window, $, undefined) {
    var module = {};


    // #region Logging

    // #region Enums/Constants
    module.LogLevel = {
        Unknown: 0,

        // If names or numbers are changed, also update Min/MaxLevel and Names
        Debug: 1,
        Info: 2,
        Warning: 3,
        Error: 4,
        Critical: 5,


        MinLevel: 1,
        MaxLevel: 5,

        Names: {
            1: "Debug",
            2: "Info",
            3: "Warning",
            4: "Error",
            5: "Critical"
        },

        GetDisplayName: function (logLevel) {
            return module.LogLevel.Names[logLevel] || "Unknown";
        }
    };

    var defaultLogName = "Default";
    // #endregion


    // #region Global debugger options
    module.ConsoleOutputEnabled = false; /// Enable logging to host console
    module.AddOutput = function (handler) {
        /// <summary>Add an output handler for all loggers</summary>
        /// <param name="handler" type="function(LogItem)"></param>

        outputCallbacks.add(handler);
    }
    module.RemoveOutput = function (handler) {
        /// <summary>Remove a previously-added output handler for log items</summary>
        /// <param name="handler" type="function"></param>
        outputCallbacks.remove(handler);
    }
    // #endregion


    // #region Logger 
    module.LoggerFactory = function (name, options) {
        return new module.Logger(name, options);
    }

    module.Logger = function Logger(name, options) {
        /// <summary>Create a new Logger instance</summary>
        /// <param name="name" type="string">
        this._id = registerLogger(name, options, this);
    };

    module.Logger.Defaults = {
        filterLevel: module.LogLevel.MinLevel,    /// Messages must be at least this level to be logged
        console: true,     /// Print messages to host console when module.ConsoleOutputEnabled = true
        showLogName: false,  /// Prepend log name to message text
        showLogLevel: false, /// Prepend log level to message text
        showDate: false,     /// Prepend date to message text
        separator: " "
    };

    module.Logger.prototype = {
        Log: function (logLevel, message) {
            /// <summary>Add a message to the debug log</summary>
            /// <param name="logLevel" type="LogLevel">Log level</param>
            /// <param name="message" type="string" parameterArray="true">Text to log (variadic)</param>
            var success = false;

            var logger = getLogger(this._id);
            var msgOptions = {
                logLevel: logLevel
            };

            // Extract message items from arguments
            var msgItems;
            if (message) {
                if (message.length && typeof message !== 'string') {
                    // message is an array or array-like
                    msgItems = [].slice.call(message);
                } else {
                    // Get message and arguments following it
                    msgItems = [].slice.call(arguments, 1);
                }
            } else {
                msgItems = ["[no message]"];
            }

            // Create log item
            var logItem = prepareLogItem(logger.name, logger.options, msgOptions, msgItems);

            // Append to internal logs
            if (!logs[logger.name]) {   // Sanity check that a log collection exists for this logger
                logs[logger.name] = [];
            }
            logs[logger.name].push(logItem);


            if (logger.options.filterLevel <= logLevel) {
                // Output log item

                // Send log item to browser console
                if (module.ConsoleOutputEnabled && logger.options.console && window.console && window.console.log) {
                    window.console.log(logItem.FormattedMessage);
                }

                // Send message to logger's output functions
                logger.output.fire(logItem);

                // Send log item to global output functions
                outputCallbacks.fire(logItem);

                success = true;
            }

            return success;
        },
        Debug: function () { this.Log.call(this, module.LogLevel.Debug, arguments); },
        Info: function () { this.Log.call(this, module.LogLevel.Info, arguments); },
        Warning: function () { this.Log.call(this, module.LogLevel.Warning, arguments); },
        Error: function () { this.Log.call(this, module.LogLevel.Error, arguments); },
        Critical: function () { this.Log.call(this, module.LogLevel.Critical, arguments); },

        Event: function (evt, evtThis, msg, level) {
            /// <summary>Log an event</summary>
            /// <param name="evt" type="jQuery.Event">The event in question</param>
            /// <param name="evtThis" domElement="true">The event handler's `this`</param>
            /// <param name="msg" type="string">Extra text to append to the first line of the log</param>
            /// <param name="level" type="LogLevel">Log level, default Debug</param>
            if (arguments.length < 4) {
                level = module.Level.Debug;
            }

            var targetElm = evt.target
                ? evt.target.nodeName
                    ? evt.target.nodeName.toString().toLowerCase() + '#' + evt.target.id
                    : evt.target.toString()
                : null;
            var relatedTargetElm = evt.relatedTarget
                ? evt.relatedTarget.nodeName
                    ? evt.relatedTarget.nodeName.toString().toLowerCase() + '#' + evt.relatedTarget.id
                    : evt.relatedTarget.toString()
                : null;
            var thisElm = evtThis
                ? evtThis.nodeName
                    ? evtThis.nodeName.toString().toLowerCase() + '#' + evtThis.id
                    : evtThis.toString()
                : null;

            this.Log(level, evt.type, '(event)', (msg ? msg : ''),
                        (targetElm ? '\n   target: ' + targetElm : ''),
                        (relatedTargetElm ? '\n   related: ' + relatedTargetElm : ''),
                        (thisElm ? '\n   this:    ' + thisElm : ''));
        },

        Name: function () {
            /// <summary>Get the log name associated with this logger</summary>
            var logger = getLogger(this._id);
            return logger.name;
        },

        Option: function (key, value) {
            /// <summary>Get/set an option for this logger</summary>
            /// <param name="key" type="string">Name of logger option (see Logger.Defaults)</param>
            /// <param name="value" type="string" optional="true">New value for option</param>
            /// <returns>New value, if successful; otherwise, null</returns>

            var logger = getLogger(this._id);
            if (logger) {
                if (arguments.length === 1) {
                    // Get value
                    value = logger.options[key];
                } else if (module.Logger.Defaults.hasOwnProperty(key)) {
                    // Set value
                    logger.options[key] = value;
                } else {
                    value = null;
                }
            }

            return value;
        },

        AddOutput: function (handler) {
            var logger = getLogger(this._id);
            logger.output.add(handler);
        },

        RemoveOutput: function (handler) {
            var logger = getLogger(this._id);
            logger.output.remove(handler);
        },

        Head: function (maxSize) {
            var logger = getLogger(this._id);
            return module.Head(maxSize, logger.options.filterLevel, logger.name);
        },

        Tail: function (maxSize) {
            var logger = getLogger(this._id);
            return module.Tail(maxSize, logger.options.filterLevel, logger.name);
        }

    };

    module.LogItem = function LogItem(message, timestamp, logName, logLevel, options) {
        /// <param name="message" type="string"></param>
        /// <param name="timestamp" type="Date"></param>
        /// <param name="logName" type="string"></param>
        /// <param name="options" type="object" optional="true"></param>

        this.Message = message;
        this.Timestamp = timestamp;
        this.LogLevel = logLevel;
        this.LogName = logName;

        // #region Format message
        this.FormattedTimestamp = formatTimestamp(timestamp);

        if (options) {
            var formattedMessage = [];
            if (options.showLogName) {
                // Prepend log name
                var logNameItem = ['[', logName, ']'].join("");
                formattedMessage.push(logNameItem);
            }

            if (options.showLogLevel) {
                // Prepend log name
                var logLevelItem = ['{', module.LogLevels.GetDisplayName(logLevel), '}'].join("");
                formattedMessage.push(logLevelItem);
            }

            if (options.showDate) {
                // Prepend formatted timestamp
                var timestampItem = ['(', this.FormattedTimestamp, ')'].join("");
                formattedMessage.push(timestampItem);
            }
        }

        formattedMessage.push(message);
        formattedMessage = formattedMessage.join(options.separator);
        this.FormattedMessage = formattedMessage;
        // #endregion
    }

    module.LogItem.prototype = {
        Message: null,      // string
        FormattedMessage: null,          // string
        Timestamp: null,    // Date
        FormattedTimestamp: null,       // string
        LogLevel: null,     // LogLevel
        LogName: null       // string
    };

    // #endregion

    // #region Quick access to default logger
    module.Log = function (/* ... message */) {
        /// <summary>Quickly log a message to the default logger</summary>
        defaultLogger.Debug.apply(defaultLogger, Array.prototype.slice.call(arguments, 0));
    };

    module.LogEvent = function (evt, evtThis, msg) {
        defaultLogger.Event(evt, evtThis, msg, module.LogLevel.Debug);
    }

    // #endregion

    // #region Pulling from logs
    module.LogNames = function () {
        /// <summary>Fetch the names of all the log files currently in memory</summary>
        /// <returns type="elementString">List of names</returns>
        var logNames = [];
        for (var logName in logs) {
            logNames.push(logName);
        }

        return logNames;
    }

    module.DefaultLogName = function () {
        return defaultLogName;
    }

    module.Head = function (maxSize, filterLevel, logName) {
        return fetchLogItems(logName, filterLevel, maxSize, true, 0);
    };

    module.Tail = function (maxSize, filterLevel, logName) {
        return fetchLogItems(logName, filterLevel, maxSize, false, 0);
    };

    function fetchLogItems(logName, filterLevel, maxSize, goForward, offset) {
        /// <summary>Fetch log items from a log</summary>
        /// <param name="logName" type="string">Name of log (default: default log)</param>
        /// <param name="filterLevel" type="LogLevel">Minimum log level to fetch (default: LogLevel.MinLevel)</param>
        /// <param name="maxSize" type="number" integer="true">Maximum number of log items to return.  0 for all matching items. (default: some small number).</param>
        /// <param name="goForward" type="bool">Step through source logs going forward? (default: true)</param>
        /// <param name="offset" type="number">How many matching items to omit (default: 0)</param>

        var logItems;
        if (logName) {
            if (logs.hasOwnProperty(logName)) {
                logItems = logs[logName];
            } else {
                logItems = [];
            }
        } else {
            logItems = [];
            for (var logName in logs) {
                if (!logs.hasOwnProperty(logName)) continue;

                var currLogItems = logs[logName];
                logItems = logItems.concat(currLogItems);
                logItems.sort(function (a, b) {
                    return a.Timestamp - b.Timestamp;
                });
            }
        }

        if (!logItems) {
            return logItems;
        }

        goForward = goForward !== false;

        filterLevel = module.LogLevel.MinLevel <= filterLevel && filterLevel <= module.LogLevel.MaxLevel
            ? filterLevel
            : module.LogLevel.MinLevel; // Default filter level

        offset = offset ? parseInt(offset) : 0;

        // Set up variables

        maxSize = typeof maxSize === "number"
            ? (maxSize > 0 ? maxSize : logItems.length)
            : 10;   // Default max size

        var startLine = goForward ? 0 : logItems.length - 1;

        var maxLi = goForward ? logItems.length - 1 : startLine;

        var step = goForward ? 1 : -1;

        // Fetch at most `maxSize` items from `logItems` into `filterItems`, where each item's LogLevel is at least `filterLevel`; skipping the first `offset` amount
        var filteredItems = [];
        for (var li = startLine;
            (0 <= li && li <= maxLi) && (filteredItems.length < maxSize);
            li += step) {

            var logItem = logItems[li];
            if (logItem.LogLevel >= filterLevel) {
                if (offset) {
                    // Skip this item
                    offset--;
                } else {
                    if (goForward) {
                        filteredItems.push(logItem);
                    } else {
                        filteredItems.unshift(logItem);
                    }
                }
            }
        }

        return filteredItems;

    }
    // #endregion

    // #endregion Logging

    // #region Private objects
    var loggers = [];   // [ id: loggerMetadata, id: loggerMetadata, ... ]
    var logs = {};  // { logName: [LogItem, LogItem, ...], logName: ...]
    var outputCallbacks = $.Callbacks("unique");
    var defaultLogger = new module.Logger(defaultLogName); // Create default logger, id = 0
    // #endregion

    // #region Private methods
    function getLogger(loggerID) {
        var logger;
        if (arguments.length > 0 && (0 < loggerID && loggerID < loggers.length)) {
            logger = loggers[loggerID];
        } else {
            // Default logger
            logger = loggers[0];
        }
        return logger;
    }


    function registerLogger(name, options, loggerApi) {
        // Sanitize name
        name = typeof name === "string" && name.length > 0 ? name : defaultLogName;

        var options = $.extend({}, module.Logger.Defaults, options);


        var logger = {
            name: name,
            options: options,
            output: $.Callbacks('unique')
        };

        loggers.push(logger);
        return loggers.length - 1;
    }

    function pad(subject, length, padding, toRight) {
        /// <summary>Pad a string to a minimum length</summary>
        /// <param name="subject">What to pad</param>
        /// <param name="length" type="Number" integer="true">Minimum length to pad to</param>
        /// <param name="padding" type="string">What to pad with</param>
        /// <param name="toRight" type="bool">Pad on the right side? (default: false, pad on left)</param>

        // Sanitize parameters
        subject = subject !== null && typeof subject !== "undefined"
            ? subject + ""
            : "";
        length = length || 0;
        padding = padding != null && typeof padding !== "undefined"
            ? padding
            : " ";
        toRight = !!toRight;

        // Pad string
        while (subject.length < length) {
            subject = toRight
                ? subject + padding
                : padding + subject;
        }

        return subject;
    }

    function prepareLogItem(logName, loggerOptions, msgOptions, msgItems) {
        /// <summary>Prepare a log message for display and storage</summary>
        /// <param name="loggerOptions" type="object">Map of logger options</param>
        /// <param name="msgOptions" type="object">Map of message options</param>
        /// <param name="msgItems" type="array" elementType="string">Items to be logged as a single message</param>

        var options = $.extend({}, module.Logger.Defaults, loggerOptions, msgOptions)

        // Determine timestamp
        var timestamp = null;
        if (options.timestamp && (options.timestamp instanceof Date || typeof options.timestamp === "string" || typeof options.timestamp === "number")) {
            try {
                timestamp = new Date(options.date);
            } catch (ex) { }
        }

        if (timestamp == null) {
            timestamp = new Date();
        }

        var message = [];

        // Append all message items to message
        message = message.concat(msgItems);
        for (var i = 0; i < message.length; i++) {
            if (typeof message[i] == "object") {
                message[i] = JSON.stringify(message[i]).replace('","' , '",\n"');
            }
        }
        message = message.join(options.separator);

        var logItem = new module.LogItem(message, timestamp, logName, options.logLevel, options);
        return logItem;
    }

    function formatTimestamp(timestamp) {
        /// <summary>Format a Date object in the form (HH:MM:SS.MMM)</summary>
        /// <param name="timestamp" type="Date"></param>
        /// <returns type="string">(HH:MM:SS.MMM)</returns>

        var formattedTimestamp = [
                pad(timestamp.getHours(), 2, 0),
                ":", pad(timestamp.getMinutes(), 2, 0),
                ":", pad(timestamp.getSeconds(), 2, 0),
                ".", pad(timestamp.getMilliseconds(), 3, 0),
                ].join("");
        return formattedTimestamp;
    }

    // #endregion

    return module;
})(window, jQuery);

Mediasite.namespace("Debug");

Mediasite.Debug.Repo = (function () {
    var module = {};
    var dataRepository = {};

    module.Put = function (key, value) {
        /// <summary>Store a value by name in a globally-accessible repository.</summary>
        /// <param name="key" type="number|string">Identifier for value</param>
        /// <param name="value">Value to store</param>
        /// <returns>`value`</returns>

        if (typeof key === "number" || typeof key === "string") {
            dataRepository[key] = value;
        }

        return value;
    };

    module.Push = function (key, value) {
        /// <summary>Append a value onto a named array in a globally-accessible repository. If the array does not exist, it will be created. If that name is keyed to a non-array value, it will be converted into an array.</summary>
        /// <param name="key" type="number|string">Identifier for array</param>
        /// <param name="value">Value to append to array</param>
        /// <returns>[`existing value`, `existing value`, ..., `value`]</returns>

        var array = module.Fetch(key);
        if (!$.isArray(array)) {
            array = array !== undefined
                    ? [array]
                    : [];
        }

        array.push(value);

        return module.Put(key, array);
    };

    module.Fetch = function (key) {
        /// <summary>Fetch a value by name from a globally-accessible repository</summary>
        /// <param name="name" type="number|string">Identifier for value</param>
        /// <returns>If `name` has been stored previously, the last stored value; otherwise, null</returns>

        var value;
        if (key !== null && typeof key !== "undefined" && dataRepository.hasOwnProperty(key)) {
            value = dataRepository[key];
        }

        return value;
    };

    module.Keys = function () {
        /// <summary>Fetch the keys of all the objects currently stored in the repository</summary>
        /// <returns type="elementString">List of names</returns>
        var keys = [];
        for (var key in dataRepository) {
            keys.push(key);
        }

        return keys;

    };

    return module;
})();

String.prototype.replaceholders = function () {
    /// <summary>Replace placeholders in a format string.
    /// <para>this (the string) should be in the format "foo %%0%% %%1%%".</para>
    /// <para>Example: "%%0%% foo %%1%% %%0%%".replaceholders("baz", "bar") returns "baz foo bar baz".</para>
    /// </summary>

    var replaced = this + ""; // Clone original string 
    if (this && this.length > 0) {
        for (var i = 0, len = arguments.length; i < len; i++) {
            var search = new RegExp('%%' + i + '%%', 'g');
            var replace = arguments[i];
            replaced = replaced.replace(search, replace);
        }
    }

    return replaced;
};

String.prototype.format = function () {
    var formatted = this;
    for (var i = 0; i < arguments.length; i++) {
        var regexp = new RegExp('\\{' + i + '\\}', 'gi');
        formatted = formatted.replace(regexp, arguments[i]);
    }
    return formatted;
};

String.prototype.unescapeJavascriptSerialized = function () {
    /**
    * Unescape characters which have been escaped for JavaScript serialization
    */

    var string = this;

    // replace characters with escaped unicode points with the literal characters
    // eg apostrophe: \\u0027 --> &#39;
    string = string.replace(/\\u([a-f\d]{4})/igm, function (match, unicodePointHex) {
        return String.fromCharCode(parseInt(unicodePointHex, 16));
    });

    string = string.replace(/\\'/gm, '\'');
    string = string.replace(/\\"/gm, '"');
    string = string.replace(/\\n/gm, '\n');
    string = string.replace(/\\t/gm, '\t');

    string = string.replace(/\\\\/gm, "\\");

    return string;
};

String.prototype.formatAsHtml = function() {
    var string = this;

    // Sanitize 'not-html'
    string = string.replace(/</gm, '&lt;');
    string = string.replace(/>/gm, '&gt;');

    // replace line breaks with <br />
    string = string.replace(/[\r\n|\n|\r]/gm, '<br />');

    string = string.replace(/\t/gm, '&nbsp;');

    return string;
};


if (!('lastIndexOf' in String.prototype)) {
    // Shim for IE8-
    String.prototype.lastIndexOf = function (needle, i /* optional */) {
        var haystack = this;

        if (i === undefined) {
            i = haystack.length - 1;
        } else {
            i = Math.max(0, Math.min(haystack.length - 1, i));
        }

        for (; i >= 0; i--) {
            var index = haystack.indexOf(needle, i);
            if (index !== -1) {
                return index;
            }
        }

        return -1;
    };
}

// Production steps of ECMA-262, Edition 5, 15.4.4.14
// Reference: http://es5.github.io/#x15.4.4.14
if (!Array.prototype.indexOf) {
    Array.prototype.indexOf = function (searchElement, fromIndex) {

        var k;

        // 1. Let O be the result of calling ToObject passing
        //    the this value as the argument.
        if (this == null) {
            throw new TypeError('"this" is null or not defined');
        }

        var O = Object(this);

        // 2. Let lenValue be the result of calling the Get
        //    internal method of O with the argument "length".
        // 3. Let len be ToUint32(lenValue).
        var len = O.length >>> 0;

        // 4. If len is 0, return -1.
        if (len === 0) {
            return -1;
        }

        // 5. If argument fromIndex was passed let n be
        //    ToInteger(fromIndex); else let n be 0.
        var n = +fromIndex || 0;

        if (Math.abs(n) === Infinity) {
            n = 0;
        }

        // 6. If n >= len, return -1.
        if (n >= len) {
            return -1;
        }

        // 7. If n >= 0, then Let k be n.
        // 8. Else, n<0, Let k be len - abs(n).
        //    If k is less than 0, then let k be 0.
        k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);

        // 9. Repeat, while k < len
        while (k < len) {
            // a. Let Pk be ToString(k).
            //   This is implicit for LHS operands of the in operator
            // b. Let kPresent be the result of calling the
            //    HasProperty internal method of O with argument Pk.
            //   This step can be combined with c
            // c. If kPresent is true, then
            //    i.  Let elementK be the result of calling the Get
            //        internal method of O with the argument ToString(k).
            //   ii.  Let same be the result of applying the
            //        Strict Equality Comparison Algorithm to
            //        searchElement and elementK.
            //  iii.  If same is true, return k.
            if (k in O && O[k] === searchElement) {
                return k;
            }
            k++;
        }
        return -1;
    };
}


// Modifies an already created Error instance. 
// This preserves the stack property.
Mediasite.ArgumentError = function ArgumentError(err) {
    err.name = 'ArgumentError';
    return err;
};