'use strict';

/**
 * Extends Map and adds a few convenient functions.
 */
class EnhancedMap extends Map {
    /**
     * If key has a mapping already returns the currently associated value. If
     * there is no mapping, calls the computer which must return a value V.
     * The value is then stored for given key and returned.
     * @param key Key
     * @param computer Computer which is called only if key has no mapping yet.
     * @return Existing value if the key already existed, or the newly computed value.
     */
    computeIfAbsent(key, computer) {
        if (this.has(key)) {
            return this.get(key);
        }
        const newValue = computer(key);
        this.set(key, newValue);
        return newValue;
    }
    /**
     * If the key exists already calls given computer, if the key does not exist
     * this method does nothing.
     *
     * The computer is called with current key and current value associated. The
     * computer can return a (new) value V or undefined. When undefined is returned
     * the key is removed from this map, when a V is returned the key is updated
     * with the new value V.
     * @param key Key
     * @param computer Computer which is called only if the key has a mapping already
     * @return Undefined if the key has no mapping, otherwise the value returned from computer
     */
    computeIfPresent(key, computer) {
        const currentValue = this.get(key);
        if (currentValue === undefined) {
            return undefined;
        }
        const newValue = computer(key, currentValue);
        if (newValue !== undefined) {
            this.set(key, newValue);
        }
        else {
            this.delete(key);
        }
        return newValue;
    }
    /**
     * Computes a value for given key, the computer can return a value V (in which case the map
     * will set the value for given key), if it returns undefined the mapping for key K will be
     * removed.
     * @param key Key to compute
     * @param computer Computer which is called, note that the currentValue argument contains the existing
     *                 value or is undefined when no mapping exists for the key.
     * @return The newly computed value
     */
    compute(key, computer) {
        const currentValue = this.get(key);
        const newValue = computer(key, currentValue);
        if (newValue) {
            this.set(key, newValue);
        }
        else {
            this.delete(key);
        }
        return newValue;
    }
}

/**
 * Internal log level (note: do NOT use LogLevel, or we get circular loading issues!)
 */
var InternalLogLevel;
(function (InternalLogLevel) {
    InternalLogLevel[InternalLogLevel["Trace"] = 0] = "Trace";
    InternalLogLevel[InternalLogLevel["Debug"] = 1] = "Debug";
    InternalLogLevel[InternalLogLevel["Info"] = 2] = "Info";
    InternalLogLevel[InternalLogLevel["Warn"] = 3] = "Warn";
    InternalLogLevel[InternalLogLevel["Error"] = 4] = "Error";
})(InternalLogLevel || (InternalLogLevel = {}));
/**
 * Internal logger, this is NOT for end users. Instead this is used to enable logging for typescript-logging itself in case of problems.
 *
 * @param name Name of logger
 */
function getInternalLogger(name) {
    return provider.getLogger(name);
}
/**
 * Can be used to change the *internal* logging of the library.
 * Has no effect on end user logging.
 *
 * As such should normally not be used by end users.
 */
const INTERNAL_LOGGING_SETTINGS = {
    /**
     * Changes the log level for the internal logging (for all new and existing loggers)
     * @param level New log level
     */
    setInternalLogLevel: (level) => provider.changeLogLevel(level),
    /**
     * Changes where messages are written to for all new and existing loggers),
     * by default they are written to the console.
     * @param fnOutput Function to write messages to
     */
    setOutput: (fnOutput) => provider.changeOutput(fnOutput),
    /**
     * Resets the log level and output back to defaults (level to error and writing to console)
     * for all new and existing loggers.
     */
    reset: () => provider.reset(),
};
class InternalLoggerImpl {
    constructor(name, level, fnOutput) {
        this._name = name;
        this._level = level;
        this._fnOutput = fnOutput;
    }
    trace(msg) {
        this.log(InternalLogLevel.Trace, msg);
    }
    debug(msg) {
        this.log(InternalLogLevel.Debug, msg);
    }
    error(msg, error) {
        this.log(InternalLogLevel.Error, msg, error);
    }
    info(msg) {
        this.log(InternalLogLevel.Info, msg);
    }
    warn(msg, error) {
        this.log(InternalLogLevel.Warn, msg, error);
    }
    setLevel(level) {
        this._level = level;
    }
    setOutput(fnOutput) {
        this._fnOutput = fnOutput;
    }
    log(level, msg, error) {
        if (this._level > level) {
            return;
        }
        // tslint:disable-next-line:no-console
        this._fnOutput(`${InternalLogLevel[this._level].toString()} <INTERNAL LOGGER> ${this._name} ${msg()}${error ? "\n" + error.stack : ""}`);
    }
}
class InternalProviderImpl {
    constructor() {
        this._loggers = new EnhancedMap();
        this._logLevel = InternalLogLevel.Error;
        this._fnOutput = InternalProviderImpl.logConsole;
    }
    getLogger(name) {
        return this._loggers.computeIfAbsent(name, key => new InternalLoggerImpl(key, this._logLevel, this._fnOutput));
    }
    changeLogLevel(level) {
        this._logLevel = level;
        this._loggers.forEach(logger => logger.setLevel(level));
    }
    changeOutput(_fnOutput) {
        this._fnOutput = _fnOutput;
        this._loggers.forEach(logger => logger.setOutput(this._fnOutput));
    }
    reset() {
        this.changeLogLevel(InternalLogLevel.Error);
        this._fnOutput = InternalProviderImpl.logConsole;
        this._loggers.forEach(logger => logger.setOutput(this._fnOutput));
    }
    static logConsole(msg) {
        // tslint:disable-next-line:no-console
        if (console && console.log) {
            // tslint:disable-next-line:no-console
            console.log(msg);
        }
    }
}
const provider = new InternalProviderImpl();

var InternalLogger = /*#__PURE__*/Object.freeze({
  __proto__: null,
  INTERNAL_LOGGING_SETTINGS: INTERNAL_LOGGING_SETTINGS,
  get InternalLogLevel () { return InternalLogLevel; },
  getInternalLogger: getInternalLogger
});

/**
 * Log level for a logger.
 */
var LogLevel;
(function (LogLevel) {
    // Do not change values/order. Logging a message relies on this.
    LogLevel[LogLevel["Trace"] = 0] = "Trace";
    LogLevel[LogLevel["Debug"] = 1] = "Debug";
    LogLevel[LogLevel["Info"] = 2] = "Info";
    LogLevel[LogLevel["Warn"] = 3] = "Warn";
    LogLevel[LogLevel["Error"] = 4] = "Error";
    LogLevel[LogLevel["Fatal"] = 5] = "Fatal";
    LogLevel[LogLevel["Off"] = 6] = "Off";
})(LogLevel || (LogLevel = {}));
/* tslint:disable:no-namespace */
(function (LogLevel) {
    /**
     * Convert given value to LogLevel, if not matching returns undefined.
     * @param val Value to convert
     */
    function toLogLevel(val) {
        switch (val.toLowerCase()) {
            case "trace":
                return LogLevel.Trace;
            case "debug":
                return LogLevel.Debug;
            case "info":
                return LogLevel.Info;
            case "warn":
                return LogLevel.Warn;
            case "error":
                return LogLevel.Error;
            case "fatal":
                return LogLevel.Fatal;
            case "off":
                return LogLevel.Off;
            default:
                return undefined;
        }
    }
    LogLevel.toLogLevel = toLogLevel;
})(LogLevel || (LogLevel = {}));
/* tslint:disable:enable-namespace */

/**
 * Standard logger implementation that provides the basis for all loggers.
 */
class CoreLoggerImpl {
    constructor(runtime) {
        this._runtime = runtime;
    }
    get id() {
        return this._runtime.id;
    }
    get logLevel() {
        return this._runtime.level;
    }
    get runtimeSettings() {
        /* Return it as new literal, we don't want people to play with our internal state */
        return Object.assign({}, this._runtime);
    }
    set runtimeSettings(runtime) {
        this._runtime = runtime;
    }
    trace(message, ...args) {
        this.logMessage(LogLevel.Trace, message, args);
    }
    debug(message, ...args) {
        this.logMessage(LogLevel.Debug, message, args);
    }
    info(message, ...args) {
        this.logMessage(LogLevel.Info, message, args);
    }
    warn(message, ...args) {
        this.logMessage(LogLevel.Warn, message, args);
    }
    error(message, ...args) {
        this.logMessage(LogLevel.Error, message, args);
    }
    fatal(message, ...args) {
        this.logMessage(LogLevel.Fatal, message, args);
    }
    logMessage(level, logMessageType, args) {
        if (this._runtime.level > level) {
            return;
        }
        const nowMillis = Date.now();
        const message = typeof logMessageType === "string" ? logMessageType : logMessageType();
        const errorAndArgs = CoreLoggerImpl.getErrorAndArgs(args);
        /*
         * Deal with raw message here.
         */
        switch (this._runtime.channel.type) {
            case "RawLogChannel":
                this._runtime.channel.write({
                    message,
                    exception: errorAndArgs.error,
                    args: errorAndArgs.args,
                    timeInMillis: nowMillis,
                    level,
                    logNames: this._runtime.name,
                }, this._runtime.argumentFormatter);
                return;
            case "LogChannel":
                this._runtime.channel.write(this.createLogMessage(message, level, errorAndArgs, nowMillis));
                break;
        }
    }
    formatArgValue(value) {
        try {
            return this._runtime.argumentFormatter(value);
        }
        catch (e) {
            // We don't really care what failed, except that the convert function failed.
            return `>>ARG CONVERT FAILED: '${value !== undefined ? value.toString() : "undefined"}'<<`;
        }
    }
    createLogMessage(message, level, errorAndArgs, nowMillis) {
        let errorResult;
        const error = errorAndArgs.error;
        const args = errorAndArgs.args;
        if (error) {
            errorResult = `${error.name}: ${error.message}`;
            if (error.stack) {
                errorResult += `@\n${error.stack}`;
            }
        }
        /*
         * We need to add the date, and log names (in front of the now formatted message).
         * Finally we also need to format any additional arguments and append after the message.
         */
        const dateFormatted = this._runtime.dateFormatter(nowMillis);
        let levelAsStr = LogLevel[level].toUpperCase();
        if (levelAsStr.length < 5) {
            levelAsStr += " ";
        }
        const names = typeof this._runtime.name === "string" ? this._runtime.name : this._runtime.name.join(", ");
        const argsFormatted = typeof args !== "undefined" && args.length > 0 ? (" [" + (args.map(arg => this.formatArgValue(arg))).join(", ") + "]") : "";
        const completedMessage = dateFormatted + " " + levelAsStr + " [" + names + "] " + message + argsFormatted;
        return {
            message: completedMessage,
            error: errorResult,
        };
    }
    static getErrorAndArgs(args) {
        /*
          The args are optional, but the first entry may be an Error or a function to an Error, or finally be a function to extra arguments.
          The last is only true, if the length of args === 1, otherwise we expect args starting at pos 1 and further to be just that - args.
         */
        if (args.length === 0) {
            return {};
        }
        let error;
        let actualArgs;
        const value0 = args[0];
        /* If the first argument is an Error, we can stop straight away, the rest are additional arguments then if any */
        if (value0 instanceof Error) {
            error = value0;
            actualArgs = args.length > 1 ? args.slice(1) : undefined;
            return { error, args: actualArgs };
        }
        /* If the first argument is a function, it means either it will return the Error, or if the array length === 1 a function, returning the arguments */
        if (typeof value0 === "function") {
            const errorOrArgs = value0();
            if (errorOrArgs instanceof Error) {
                error = errorOrArgs;
                actualArgs = args.length > 1 ? args.slice(1) : undefined;
                return { error, args: actualArgs };
            }
            if (args.length === 1) {
                /* The first argument was a function, we assume it returned the extra argument(s) */
                if (Array.isArray(errorOrArgs)) {
                    return { args: errorOrArgs.length > 0 ? errorOrArgs : undefined };
                }
                else {
                    /* No idea what was returned we just assume a single value */
                    return { args: errorOrArgs };
                }
            }
            else {
                /*
                  This is a weird situation but there's no way to avoid it, the first argument was a function but did not return an Error and the args are > 1,
                  so just add the args returned, as well as any remaining.
                */
                if (Array.isArray(errorOrArgs)) {
                    return { args: [...errorOrArgs, ...args.slice(1)] };
                }
                return { args: [errorOrArgs, ...args.slice(1)] };
            }
        }
        /* All args are ordinary arguments, or at least the first arg was not an Error or a Function, so we add all as args */
        return { args };
    }
}

/**
 * Pad given value with given fillChar from the beginning (default is an empty space)
 * @param value Value to pad
 * @param length The length the string must be
 * @param fillChar The padding char (1 char length allowed only)
 * @return Padded string or the same string if it is already of given length (or larger).
 */
function padStart(value, length, fillChar = " ") {
    return padInternal(value, length, "start", fillChar);
}
/**
 * Pad given value with given fillChar from the end (default is an empty space)
 * @param value Value to pad
 * @param length The length the string must be
 * @param fillChar The padding char (1 char length allowed only)
 * @return Padded string or the same string if it is already of given length (or larger).
 */
function padEnd(value, length, fillChar = " ") {
    return padInternal(value, length, "end", fillChar);
}
/**
 * Returns the max length of a string value in given array
 * @param arr Array to check
 * @return Max length, 0 if array is empty
 */
function maxLengthStringValueInArray(arr) {
    return arr
        .map(v => v.length)
        .reduce((previous, current) => {
        if (current > previous) {
            return current;
        }
        return previous;
    }, 0);
}
function padInternal(value, length, padType, fillChar = " ") {
    if (length <= value.length) {
        return value;
    }
    if (fillChar.length > 1) {
        throw new Error(`Fill char must be one char exactly, it is: ${fillChar.length}`);
    }
    const charsNeeded = length - value.length;
    let padding = "";
    for (let i = 0; i < charsNeeded; i++) {
        padding += fillChar;
    }
    if (padType === "start") {
        return padding + value;
    }
    return value + padding;
}

/**
 * Default argument formatter function, used by the library, see {@link ArgumentFormatterType}.
 * Can be used by an end user as well if needed.
 * @param arg The argument to format
 * @returns argument stringified to string (JSON.stringify), if arg is undefined returns "undefined" (without quotes).
 */
function formatArgument(arg) {
    if (arg === undefined) {
        return "undefined";
    }
    return JSON.stringify(arg);
}
/**
 * Default date formatter function, used by the library, see {@link DateFormatterType}.
 * Can be used by an end user as well if needed.
 * @param millisSinceEpoch Milliseconds since epoch
 * @returns The date in format: yyyy-MM-dd HH:mm:ss,SSS (example: 2021-02-26 09:06:28,123)
 */
function formatDate(millisSinceEpoch) {
    const date = new Date(millisSinceEpoch);
    const year = date.getFullYear();
    const month = padStart((date.getMonth() + 1).toString(), 2, "0");
    const day = padStart(date.getDate().toString(), 2, "0");
    const hours = padStart(date.getHours().toString(), 2, "0");
    const minutes = padStart(date.getMinutes().toString(), 2, "0");
    const seconds = padStart(date.getSeconds().toString(), 2, "0");
    const millis = padStart(date.getMilliseconds().toString(), 3, "0");
    return `${year}-${month}-${day} ${hours}:${minutes}:${seconds},${millis}`;
}

/* tslint:disable:no-console */
/**
 * Default standard LogChannel which logs to console.
 */
class ConsoleLogChannel {
    constructor() {
        this.type = "LogChannel";
    }
    write(msg) {
        if (console && console.log) {
            console.log(msg.message + (msg.error ? `\n${msg.error}` : ""));
        }
    }
}

/* tslint:disable:no-namespace */
/**
 * Provides access to various default channels provided by typescript logging.
 */
var DefaultChannels;
(function (DefaultChannels) {
    /**
     * Create a new standard LogChannel that logs to the console.
     */
    function createConsoleChannel() {
        return new ConsoleLogChannel();
    }
    DefaultChannels.createConsoleChannel = createConsoleChannel;
})(DefaultChannels || (DefaultChannels = {}));

/**
 * Implementation for {@link LogProvider}
 */
class LogProviderImpl {
    constructor(name, settings) {
        this._log = getInternalLogger("core.impl.LogProviderImpl");
        this._name = name;
        this._settings = settings;
        this._loggers = new EnhancedMap();
        this._idToKeyMap = new EnhancedMap();
        this._globalRuntimeSettings = { level: settings.level, channel: settings.channel };
        this._nextLoggerId = 1;
        this._log.trace(() => `Created LogProviderImpl with settings: ${JSON.stringify(this._settings)}`);
    }
    get runtimeSettings() {
        return Object.assign(Object.assign({}, this._settings), { level: this._globalRuntimeSettings.level, channel: this._globalRuntimeSettings.channel });
    }
    getLogger(name) {
        return this.getOrCreateLogger(name);
    }
    updateLoggerRuntime(log, settings) {
        this._log.debug(() => `Updating logger ${log.id} runtime settings using: '${JSON.stringify(settings)}'`);
        const key = this._idToKeyMap.get(log.id);
        if (key === undefined) {
            this._log.warn(() => `Cannot update logger with id: ${log.id}, it was not found.`);
            return false;
        }
        this._loggers.computeIfPresent(key, (currentKey, currentValue) => {
            currentValue.runtimeSettings = LogProviderImpl.mergeRuntimeSettingsIntoLogRuntime(currentValue.runtimeSettings, settings);
            return currentValue;
        });
        return true;
    }
    updateRuntimeSettings(settings) {
        this._log.debug(() => `Updating global runtime settings and updating existing loggers runtime settings using: '${JSON.stringify(settings)}'`);
        this._globalRuntimeSettings = {
            /*
             * Undefined check is necessary, as level is a number (and LogLevel.Trace = 0), a ternary check otherwise results in the annoying "truthy/falsy"
             * behavior of javascript where 0 is seen as false.
             */
            level: settings.level !== undefined ? settings.level : this._globalRuntimeSettings.level,
            channel: settings.channel !== undefined ? settings.channel : this._globalRuntimeSettings.channel,
        };
        this._loggers.forEach(logger => logger.runtimeSettings = LogProviderImpl.mergeRuntimeSettingsIntoLogRuntime(logger.runtimeSettings, settings));
    }
    /**
     * Removes all state and loggers, it reverts back to as it was after initial construction.
     */
    clear() {
        this._loggers.clear();
        this._idToKeyMap.clear();
        this._globalRuntimeSettings = Object.assign({}, this._settings);
        this._nextLoggerId = 1;
    }
    getOrCreateLogger(name) {
        const key = LogProviderImpl.createKey(name);
        const logger = this._loggers.computeIfAbsent(key, () => {
            const runtime = {
                level: this._globalRuntimeSettings.level,
                channel: this._globalRuntimeSettings.channel,
                id: this.nextLoggerId(),
                name,
                argumentFormatter: this._settings.argumentFormatter,
                dateFormatter: this._settings.dateFormatter,
            };
            return new CoreLoggerImpl(runtime);
        });
        this._idToKeyMap.computeIfAbsent(logger.id, () => key);
        return logger;
    }
    nextLoggerId() {
        const result = this._name + "_" + this._nextLoggerId;
        this._nextLoggerId++;
        return result;
    }
    static mergeRuntimeSettingsIntoLogRuntime(currentSettings, settings) {
        return Object.assign(Object.assign({}, currentSettings), { 
            /*
             * Undefined check is necessary, as level is a number (and LogLevel.Trace = 0), a ternary check otherwise results in the annoying "truthy/falsy"
             * behavior of javascript where 0 is seen as false.
             */
            level: settings.level !== undefined ? settings.level : currentSettings.level, channel: settings.channel !== undefined ? settings.channel : currentSettings.channel });
    }
    static createKey(name) {
        if (typeof name === "string") {
            return name;
        }
        return name.join(",");
    }
}

/**
 * Create a new LogProvider, this is for flavor usage only. End users should not
 * use this and instead use whatever the flavor offers to build some config and
 * get loggers from there.
 */
function createLogProvider(name, settings) {
    return new LogProviderImpl(name, settings);
}

var index = /*#__PURE__*/Object.freeze({
  __proto__: null,
  EnhancedMap: EnhancedMap,
  maxLengthStringValueInArray: maxLengthStringValueInArray,
  padEnd: padEnd,
  padStart: padStart
});

/**
 * Implementation for Category.
 */
class CategoryImpl {
    constructor(logger, name, parent, fnGetOrCreateChildCategory) {
        this._children = [];
        this._logger = logger;
        this._name = name;
        this._parent = parent;
        this._fnGetOrCreateChildCategory = fnGetOrCreateChildCategory;
    }
    get name() {
        return this._name;
    }
    get parent() {
        return this._parent;
    }
    get path() {
        const result = [];
        let tmpCat = this;
        while (tmpCat !== undefined) {
            result.push(tmpCat.name);
            tmpCat = tmpCat.parent;
        }
        return result.reverse();
    }
    get children() {
        return [...this._children];
    }
    addChild(childCategory) {
        /* The parent of the child can only be set in the constructor, but verify it's our instance */
        if (childCategory.parent !== this) {
            throw new Error(`Cannot add child '${childCategory.name}', expected parent '${this._name} but got ${childCategory.parent ? childCategory.parent.name : "undefined"}'`);
        }
        this._children.push(childCategory);
    }
    getChildCategory(name) {
        const existing = this._children.find(c => c.name === name);
        if (existing !== undefined) {
            return existing;
        }
        return this._fnGetOrCreateChildCategory(name, this);
    }
    get id() {
        return this._logger.id;
    }
    get logLevel() {
        return this._logger.logLevel;
    }
    get runtimeSettings() {
        return this._logger.runtimeSettings;
    }
    get logger() {
        return this._logger;
    }
    trace(message, ...args) {
        this._logger.trace(message, ...args);
    }
    debug(message, ...args) {
        this._logger.debug(message, ...args);
    }
    info(message, ...args) {
        this._logger.info(message, ...args);
    }
    warn(message, ...args) {
        this._logger.warn(message, ...args);
    }
    error(message, ...args) {
        this._logger.error(message, ...args);
    }
    fatal(message, ...args) {
        this._logger.fatal(message, ...args);
    }
}

/**
 * The separator we use in a path for a category, this cannot be used in a name for a category.
 */
const CATEGORY_PATH_SEPARATOR = "#";
/**
 * Implementation for CategoryProvider.
 */
class CategoryProviderImpl {
    constructor(name, config) {
        this._name = name;
        this._initialConfig = config;
        this._categoryStorage = new CategoryStorage(name, config);
    }
    get name() {
        return this._name;
    }
    get config() {
        return this._initialConfig;
    }
    get runtimeConfig() {
        return this._categoryStorage.getCurrentRuntimeConfig();
    }
    getCategory(name, parent) {
        return this._categoryStorage.getOrCreateCategory(name, parent);
    }
    updateRuntimeSettingsCategory(category, settings) {
        this._categoryStorage.updateRuntimeSettingsCategory(category, settings);
    }
    updateRuntimeSettings(settings) {
        this._categoryStorage.updateRuntimeSettings(settings);
    }
    /** Returns all currently registered categories */
    getRegisteredCategories() {
        return this._categoryStorage.getRegisteredCategories();
    }
    /** Get a category by path, note that paths must be separated by #, e.g. root#child1#child1_1 */
    getCategoryByPath(path) {
        return this._categoryStorage.getCategoryByPath(path);
    }
}
class CategoryStorage {
    constructor(categoryProviderName, config) {
        this._categoriesById = new Map();
        this._categoriesByPath = new Map();
        this._categoryProviderName = categoryProviderName;
        this._allowSameCategoryName = config.allowSameCategoryName;
        this._logProvider = createLogProvider(categoryProviderName, Object.assign({}, config));
        this.getOrCreateCategory = this.getOrCreateCategory.bind(this);
    }
    getOrCreateCategory(name, parent) {
        if (name.indexOf(CATEGORY_PATH_SEPARATOR) !== -1) {
            throw new Error(`Cannot create category '${name}', name cannot contain a '${CATEGORY_PATH_SEPARATOR}'.`);
        }
        const parentCategory = parent !== undefined ? this.getParentCategory(parent) : undefined;
        if (parentCategory === undefined) {
            return this.getOrCreateRootCategory(name);
        }
        return this.getOrCreateChildCategory(name, parentCategory);
    }
    updateRuntimeSettingsCategory(category, settings) {
        /* Verify that the category originates from our provider */
        if (!category.id.startsWith(this._categoryProviderName)) {
            throw new Error(`Category '${category.name}' with LogId '${category.id}' is from a different CategoryProvider, cannot mix categories between providers.`);
        }
        const categoryImpl = this._categoriesById.get(category.id);
        if (categoryImpl === undefined) {
            return;
        }
        this._logProvider.updateLoggerRuntime(categoryImpl.logger, { level: settings.level });
        /* Recurse children if needed */
        if (settings.disableRecursion !== undefined && settings.disableRecursion) {
            return;
        }
        categoryImpl.children.forEach(childCategory => this.updateRuntimeSettingsCategory(childCategory, settings));
    }
    updateRuntimeSettings(settings) {
        this._logProvider.updateRuntimeSettings(settings);
    }
    getCurrentRuntimeConfig() {
        return Object.assign(Object.assign({}, this._logProvider.runtimeSettings), { allowSameCategoryName: this._allowSameCategoryName });
    }
    /** Returns all currently registered categories */
    getRegisteredCategories() {
        return [...this._categoriesById.values()];
    }
    /** Get a category by path, note that paths must be separated by #, e.g. root#child1#child1_1 */
    getCategoryByPath(path) {
        return this._categoriesByPath.get(path);
    }
    getParentCategory(parent) {
        let parentCategory;
        if (CategoryStorage.isLogId(parent)) {
            parentCategory = this._categoriesById.get(parent);
            if (parentCategory === undefined) {
                throw new Error(`Parent category was not found by LogId '${parent}'. Are you sure you passed in the correct argument?`);
            }
        }
        else {
            parentCategory = this._categoriesById.get(parent.id);
            if (parentCategory === undefined) {
                throw new Error(`Parent category '${parent.name}' was not found by it's LogId '${parent.id}'. Are you sure you passed in the correct parent category?`);
            }
        }
        return parentCategory;
    }
    getOrCreateRootCategory(name) {
        /* It's a root category, does it exist already? */
        const existingCategory = this._categoriesByPath.get(name);
        if (existingCategory) {
            if (!this._allowSameCategoryName) {
                throw new Error(`Category '${name} already exists, config flag allowSameCategoryName=false - therefore matching version 1 behavior (hence this Error)`);
            }
            return existingCategory;
        }
        /* Creating a root category */
        const logger = this._logProvider.getLogger(name);
        const category = new CategoryImpl(logger, name, undefined, this.getOrCreateCategory);
        this._categoriesById.set(category.id, category);
        this._categoriesByPath.set(name, category);
        return category;
    }
    getOrCreateChildCategory(name, parentCategory) {
        /* Verify that the parent category originates from our provider */
        if (!parentCategory.id.startsWith(this._categoryProviderName)) {
            throw new Error(`Parent category '${parentCategory.name}' with LogId '${parentCategory.id}' is from a different CategoryProvider, cannot mix categories between providers.`);
        }
        /* Create full path, parent path and child next */
        const path = [...parentCategory.path, name].join(CATEGORY_PATH_SEPARATOR);
        const existingChildCategory = this._categoriesByPath.get(path);
        if (existingChildCategory) {
            if (!this._allowSameCategoryName) {
                throw new Error(`Child category '${name} already exists for parent category ${parentCategory.name}, config flag allowSameCategoryName=false - therefore matching version 1 behavior (hence this Error)`);
            }
            return existingChildCategory;
        }
        /* No child yet, create it */
        const logger = this._logProvider.getLogger(path);
        /* Apply the settings from the parent category, a child always gets what its parent has */
        this._logProvider.updateLoggerRuntime(logger, {
            level: parentCategory.logger.runtimeSettings.level,
            channel: parentCategory.logger.runtimeSettings.channel,
        });
        const childCategory = new CategoryImpl(logger, name, parentCategory, this.getOrCreateCategory);
        this._categoriesById.set(childCategory.id, childCategory);
        this._categoriesByPath.set(childCategory.path.join(CATEGORY_PATH_SEPARATOR), childCategory);
        parentCategory.addChild(childCategory);
        return childCategory;
    }
    static isLogId(parent) {
        return typeof parent === "string";
    }
}

/**
 * Implementation of the CategoryControlProvider.
 */
class CategoryControlProviderImpl {
    constructor(provider, messageChannel) {
        this._provider = provider;
        this._messageChannel = messageChannel;
        this._originalLogLevels = CategoryControlProviderImpl.loadCurrentGroupLogLevels(provider);
    }
    get name() {
        return this._provider.name;
    }
    /**
     * Shows current settings.
     */
    showSettings() {
        /*
          We create this kind of output:
    
          Available categories (CategoryProvider 'test'):
            [0, root                level=Error]
            [1, - child1            level=Warn ]
            [2, - my awesome child  level=Error]
            [3,   - another child   level=Error]
            [4, anotherRoot         level=Error]
            [5, - child x           level=Error]
         */
        let result = `Available categories (CategoryProvider '${this._provider.name}'):\n`;
        const categories = this.createCategoryInfoHierarchy();
        const maxWidthIndex = categories.size.toString().length;
        /* Note depth means on how deeply nested a child is, each depth is multiplied by 1 spaces (length) */
        const maxWidthIdentifier = index.maxLengthStringValueInArray([...categories.values()].map(value => value.category.name + " ".repeat(value.depth)));
        const providerLines = [...categories.values()]
            .map((category, idx) => CategoryControlProviderImpl.createSettingLineCategory(category, idx, maxWidthIndex, maxWidthIdentifier));
        result += providerLines.join("\n") + (providerLines.length > 0 ? "\n" : "");
        this._messageChannel(result);
    }
    help() {
        const msg = `You can use the following commands (CategoryProvider ${this._provider.name}):\n` +
            "  showSettings()\n" +
            "    Shows the current configuration settings.\n" +
            "  update(level: CategoryControlProviderLogLevel, categoryId?: number | string, noRecurse?: boolean)\n" +
            "    Change the log level for a category (by default recursively).\n" +
            "      @param level      The log level to set - must be one of 'trace', 'debug', 'info', 'warn', 'error' or 'fatal'\n" +
            "      @param categoryId The category id or path of a category (e.g. root#child1) to update. Use showSettings() for id and/or name.\n" +
            "                        When omitted, it applies the level to all categories recursively.\n" +
            "  reset()\n" +
            "    Resets the log levels of the config groups back to when this control provider was created.\n" +
            "  save()\n" +
            "    Saves the current log levels for all categories of this provider. Use restore() to load last saved state.\n" +
            "  restore()\n" +
            "    Restore stored saved state, if any. Log levels will be set according to the saved state.\n" +
            "  help()\n" +
            "    Shows this help.\n";
        this._messageChannel(msg);
    }
    reset() {
        const currentCategories = new Map(this._provider.getRegisteredCategories().map(cat => [cat.id, cat]));
        /*
         * For all stored categories, update them if we can still find them and remove them from "currentCategories".
         */
        this._originalLogLevels.forEach((value, key) => {
            const category = currentCategories.get(key);
            if (category !== undefined) {
                this._provider.updateRuntimeSettingsCategory(category, { level: value, disableRecursion: true });
            }
            currentCategories.delete(key);
        });
        /*
         * For any remaining categories (these are new compared to when originals were loaded), set their parent levels.
         *
         * This is just a best effort, we had no previous log levels available for them after all.
         */
        currentCategories.forEach(category => {
            if (category.parent !== undefined) {
                this._provider.updateRuntimeSettingsCategory(category, { level: category.parent.logLevel, disableRecursion: true });
            }
        });
        /* Update the levels so we're up-to-date again */
        this._originalLogLevels = CategoryControlProviderImpl.loadCurrentGroupLogLevels(this._provider);
        this._messageChannel("Successfully reset log levels back to original state (from when this CategoryControlProvider was created).");
    }
    save() {
        if (!localStorage) {
            this._messageChannel("Cannot save state, localStorage is not available.");
            return;
        }
        const saveDataForAllRootCategories = this._provider.getRegisteredCategories()
            .filter(cat => cat.parent === undefined)
            .map(rootCategory => CategoryControlProviderImpl.createCategorySaveData(rootCategory));
        const saveData = {
            name: this._provider.name,
            rootCategories: saveDataForAllRootCategories,
        };
        localStorage.setItem(this.createKey(), JSON.stringify(saveData));
        this._messageChannel(`Successfully saved state for CategoryControlProvider '${this._provider.name}'.`);
    }
    restore(logRestoreFailures) {
        const finalLogRestoreFailures = logRestoreFailures !== undefined ? logRestoreFailures : true;
        if (!localStorage) {
            if (finalLogRestoreFailures) {
                this._messageChannel(`Will not attempt to restore state for CategoryControlProvider '${this._provider.name}', localStorage is not available.`);
            }
            return;
        }
        const key = this.createKey();
        const value = localStorage.getItem(key);
        if (value === null) {
            if (finalLogRestoreFailures) {
                this._messageChannel(`Cannot restore state for CategoryControlProvider '${this._provider.name}', no data available.`);
            }
            return;
        }
        try {
            const savedData = JSON.parse(value);
            if (this._provider.name !== savedData.name) {
                if (finalLogRestoreFailures) {
                    this._messageChannel(`Cannot restore state for CategoryControlProvider '${this._provider.name}', data is not for provider - found name '${savedData.name}'.`);
                }
                return;
            }
            this.restoreBySaveData(savedData, finalLogRestoreFailures);
            this._messageChannel(`Successfully restored state for CategoryControlProvider '${this._provider.name}'`);
            this._originalLogLevels = CategoryControlProviderImpl.loadCurrentGroupLogLevels(this._provider);
        }
        catch (e) {
            localStorage.removeItem(key);
            this._messageChannel(`Cannot restore state for CategoryControlProvider '${this._provider.name}', data is not valid. Invalid data removed from localStorage.`);
        }
    }
    update(level, categoryId, noRecurse) {
        if (typeof categoryId === "undefined") {
            this.updateAll(level);
        }
        else if (typeof categoryId === "number") {
            this.updateByIndex(level, categoryId, noRecurse !== undefined ? noRecurse : false);
        }
        else {
            this.updateByPath(level, categoryId, noRecurse !== undefined ? noRecurse : false);
        }
    }
    updateAll(level) {
        const logLevel = LogLevel.toLogLevel(level);
        this._provider.getRegisteredCategories()
            .filter(cat => cat.parent === undefined)
            .forEach(cat => this._provider.updateRuntimeSettingsCategory(cat, { level: logLevel }));
        this._messageChannel(`Updated all categories to use log level '${level.toLowerCase()}'`);
    }
    updateByPath(level, path, noRecurse) {
        const category = this._provider.getCategoryByPath(path);
        if (category === undefined) {
            this._messageChannel(`Failed to find a provider by path '${path}', please make sure to separate the parts by a ${CATEGORY_PATH_SEPARATOR}.`);
            return;
        }
        this._provider.updateRuntimeSettingsCategory(category, { level: LogLevel.toLogLevel(level), disableRecursion: noRecurse });
        this._messageChannel(`Successfully updated category '${category.name}' with path '${path}' to log level '${level.toLowerCase()}'${noRecurse ? "" : " and recursively applied to children (if any)"}.`);
    }
    updateByIndex(level, index, noRecurse) {
        if (index < 0) {
            this._messageChannel(`Cannot update category by index '${index}', it is negative.`);
            return;
        }
        const categories = this.createCategoryInfoHierarchy();
        if (index >= categories.size) {
            this._messageChannel(`Cannot update category by index '${index}', it is outside of the range of available categories, use showSettings() to see the indices.`);
            return;
        }
        const category = [...categories.values()][index].category;
        this._provider.updateRuntimeSettingsCategory(category, { level: LogLevel.toLogLevel(level), disableRecursion: noRecurse });
        this._messageChannel(`Successfully updated category '${category.name}' by index '${index}' to log level '${level.toLowerCase()}'${noRecurse ? "" : " and recursively applied to children (if any)"}.`);
    }
    restoreBySaveData(saveData, logCannotRestore) {
        const restoreCategory = (categorySaveData, currentPath) => {
            const newPath = currentPath.length > 0 ? (currentPath + CATEGORY_PATH_SEPARATOR + categorySaveData.name) : categorySaveData.name;
            const category = this._provider.getCategoryByPath(newPath);
            if (category !== undefined) {
                const newLevel = LogLevel.toLogLevel(categorySaveData.level);
                if (newLevel !== undefined) {
                    this._provider.updateRuntimeSettingsCategory(category, { level: newLevel, disableRecursion: true });
                }
                else if (logCannotRestore) {
                    this._messageChannel(`CategoryControlProvider '${this._provider.name}' - cannot restore log level for category path '${newPath}', log level is invalid.`);
                }
                for (const childSaveData of categorySaveData.children) {
                    restoreCategory(childSaveData, newPath);
                }
            }
            else if (logCannotRestore) {
                this._messageChannel(`CategoryControlProvider '${this._provider.name}' - failed to find a Category by path '${newPath}', will not restore category (and children)`);
            }
        };
        for (const rootSaveData of saveData.rootCategories) {
            restoreCategory(rootSaveData, "");
        }
    }
    createKey() {
        return `CategoryProvider-${this._provider.name}`;
    }
    createCategoryInfoHierarchy() {
        const result = new Map();
        const rootCategories = this._provider.getRegisteredCategories().filter(cat => cat.parent === undefined);
        rootCategories.forEach(category => CategoryControlProviderImpl.addCategoryInfoHierarchy(category, 0, result));
        return result;
    }
    static createCategorySaveData(category) {
        return {
            name: category.name,
            level: LogLevel[category.logLevel],
            children: category.children.map(child => this.createCategorySaveData(child)),
        };
    }
    static loadCurrentGroupLogLevels(provider) {
        return new Map(provider.getRegisteredCategories().map(category => [category.id, category.logLevel]));
    }
    static createSettingLineCategory(categoryInfo, index$1, maxWidthIndex, maxWidthIdentifier) {
        const prefix = " ".repeat(categoryInfo.depth);
        const catName = prefix + categoryInfo.category.name;
        return `  [${index.padStart(index$1.toString(), maxWidthIndex)}, ${index.padEnd(catName, maxWidthIdentifier)} (level=${index.padEnd(categoryInfo.logLevel, 5)})]`;
    }
    static addCategoryInfoHierarchy(category, currentDepth, result) {
        result.set(category.id, {
            category,
            logLevel: LogLevel[category.logLevel],
            depth: currentDepth,
        });
        category.children.forEach(child => this.addCategoryInfoHierarchy(child, currentDepth + 1, result));
    }
}

function categoryConfigDebug(config) {
    return `CategoryConfig=level: ${LogLevel[config.level].toString()}, allowSameCategoryName=${config.allowSameCategoryName}`;
}

/**
 * Provider for the category flavor, each provider is a unique instance that can be used to
 * get categories/loggers from.
 */
class CategoryProviderService {
    constructor() {
        this._log = InternalLogger.getInternalLogger("category.impl.CategoryProviderService");
        this._providers = new index.EnhancedMap();
    }
    createLogProvider(name, config) {
        const result = this._providers.compute(name, (key, currentValue) => {
            if (currentValue) {
                throw new Error(`CategoryProvider with name '${name}' already exists, cannot create another.`);
            }
            const finalConfig = mergeWithDefaults(config);
            this._log.debug(() => `Creating new CategoryProvider with name '${name}', using config settings '${categoryConfigDebug(finalConfig)}'.`);
            return new CategoryProviderImpl(name, finalConfig);
        });
        if (result) {
            return result;
        }
        throw new Error("No CategoryProvider? This is a bug.");
    }
    getCategoryControl(fnValue) {
        const fnMessageChannel = fnValue ? fnValue : (value) => {
            // tslint:disable-next-line:no-console
            if (console && console.log) {
                // tslint:disable-next-line:no-console
                console.log(value);
            }
            else {
                throw new Error("Cannot use console (it is not present), please specify a custom function to write to.");
            }
        };
        return {
            help: () => fnMessageChannel(CategoryProviderService.help()),
            showSettings: () => fnMessageChannel(this.showSettings()),
            getProvider: (id) => this.getCategoryControlProviderByIdOrName(id, fnMessageChannel),
        };
    }
    clear() {
        this._providers.clear();
    }
    /* Functions for CategoryControl follow */
    showSettings() {
        let result = "Available CategoryProviders:\n";
        const maxWidthIndex = this._providers.size.toString().length;
        const maxWidthName = index.maxLengthStringValueInArray([...this._providers.keys()]);
        const lines = [...this._providers.entries()].map((entry, index$1) => {
            const name = entry[0];
            /* [idx, name] */
            return `  [${index.padStart(index$1.toString(), maxWidthIndex)}, ${index.padEnd(name, maxWidthName)}]`;
        });
        result += lines.join("\n") + (lines.length > 0 ? "\n" : "");
        return result;
    }
    getCategoryControlProviderByIdOrName(id, messageChannel) {
        if (typeof id === "string") {
            const provider = this._providers.get(id);
            if (provider === undefined) {
                throw new Error(`Provider with name '${id}' does not exist.`);
            }
            return new CategoryControlProviderImpl(provider, messageChannel);
        }
        const providers = [...this._providers.values()];
        if (id < 0 || id >= providers.length) {
            throw new Error(`Provider with index '${id}' does not exist (outside of range).`);
        }
        return new CategoryControlProviderImpl(providers[id], messageChannel);
    }
    static help() {
        return "You can use the following commands:\n" +
            "  showSettings()\n" +
            "    Shows the current configuration settings.\n" +
            "  getProvider: (id: number | string): CategoryControlProvider\n" +
            "    Get access to a CategoryControlProvider to change log levels.\n" +
            "      @param id The id (use showSettings to see) or name of the provider\n" +
            "  help()\n" +
            "    Shows this help.\n";
    }
}
/**
 * Singleton instance to the service, for internal usage only. Must NOT be exported to end user.
 */
const CATEGORY_PROVIDER_SERVICE = new CategoryProviderService();
const CATEGORY_LOG_CONTROL = fnValue => CATEGORY_PROVIDER_SERVICE.getCategoryControl(fnValue);
function mergeWithDefaults(config) {
    const defaultConfig = {
        channel: DefaultChannels.createConsoleChannel(),
        allowSameCategoryName: true,
        level: LogLevel.Error,
        dateFormatter: formatDate,
        argumentFormatter: formatArgument,
    };
    if (!config) {
        return defaultConfig;
    }
    return {
        channel: config.channel ? config.channel : defaultConfig.channel,
        allowSameCategoryName: config.allowSameCategoryName !== undefined ? config.allowSameCategoryName : defaultConfig.allowSameCategoryName,
        level: config.level !== undefined ? config.level : defaultConfig.level,
        dateFormatter: config.dateFormatter ? config.dateFormatter : defaultConfig.dateFormatter,
        argumentFormatter: config.argumentFormatter ? config.argumentFormatter : defaultConfig.argumentFormatter,
    };
}

// tslint:disable-next-line:no-namespace
exports.CategoryProvider = void 0;
(function (CategoryProvider) {
    /**
     * Creates a new  provider with given name and configuration. If a provider
     * with such name already exists, an Error will be thrown.
     * @param name Name for provider, must be unique
     * @param config The config for the provider, if not specified uses defaults (logging to console, with LogLevel=Error, ...).
     */
    function createProvider(name, config) {
        return CATEGORY_PROVIDER_SERVICE.createLogProvider(name, config);
    }
    CategoryProvider.createProvider = createProvider;
    /**
     * Resets and clears *all* created CategoryProviders, every category/logger that was retrieved previously
     * will be invalid afterwards.
     *
     * This call essentially reverts the CategoryProvider back to it's initial state. This should normally not be used
     * unless absolutely necessary.
     */
    function clear() {
        CATEGORY_PROVIDER_SERVICE.clear();
    }
    CategoryProvider.clear = clear;
})(exports.CategoryProvider || (exports.CategoryProvider = {}));

exports.CATEGORY_LOG_CONTROL = CATEGORY_LOG_CONTROL;
//# sourceMappingURL=typescript-logging-category.js.map
