/**
 * 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
});

/**
 * LogChannel that pushes log messages to a buffer.
 */
class ArrayLogChannel {
    constructor() {
        this._buffer = [];
        this.type = "LogChannel";
    }
    write(msg) {
        this._buffer.push(msg);
    }
    get logMessages() {
        return this._buffer;
    }
    get messages() {
        return this._buffer.map(msg => msg.message);
    }
}
/**
 * RawLogChannel that pushes raw log messages to a buffer.
 */
class ArrayRawLogChannel {
    constructor() {
        this._buffer = [];
        this.type = "RawLogChannel";
    }
    write(msg, _) {
        this._buffer.push(msg);
    }
    get messages() {
        return this._buffer.map(m => m.message);
    }
    get errors() {
        return this._buffer.map(m => m.exception);
    }
    get size() {
        return this._buffer.length;
    }
    get rawMessages() {
        return this._buffer;
    }
    clear() {
        this._buffer = [];
    }
}
/**
 * Test class to help test the log control.
 */
class TestControlMessage {
    constructor() {
        this._messages = [];
        this.write = this.write.bind(this);
    }
    get messages() {
        return this._messages;
    }
    write(msg) {
        this._messages.push(msg);
    }
    clear() {
        this._messages = [];
    }
}

var TestClasses = /*#__PURE__*/Object.freeze({
  __proto__: null,
  ArrayLogChannel: ArrayLogChannel,
  ArrayRawLogChannel: ArrayRawLogChannel,
  TestControlMessage: TestControlMessage
});

export { InternalLogger as $internal, TestClasses as $test, DefaultChannels, LogLevel, createLogProvider, formatArgument, formatDate, index as util };
//# sourceMappingURL=typescript-logging.esm.js.map
