log
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © Geoff Hammond

//@version=5

// @description Logging library for easily displaying debug, info, warn, error and critical messages
library("log")


// █   █▀█ █▀▀   █   █ █▄▄ █▀█ ▄▀█ █▀█ █▄█
// █▄▄ █▄█ █▄█   █▄▄ █ █▄█ █▀▄ █▀█ █▀▄  █

// @TODO: Realtime filtering. Console pagination. Styling.


// ██╗  ██╗ ███████╗ ██╗      ██████╗  ███████╗ ██████╗   ██████╗
// ██║  ██║ ██╔════╝ ██║      ██╔══██╗ ██╔════╝ ██╔══██╗ ██╔════╝
// ███████║ █████╗   ██║      ██████╔╝ █████╗   ██████╔╝ ╚█████╗
// ██╔══██║ ██╔══╝   ██║      ██╔═══╝  ██╔══╝   ██╔══██╗  ╚═══██╗
// ██║  ██║ ███████╗ ███████╗ ██║      ███████╗ ██║  ██║ ██████╔╝
// ╚═╝  ╚═╝ ╚══════╝ ╚══════╝ ╚═╝      ╚══════╝ ╚═╝  ╚═╝ ╚═════╝

// COPY THESE HELPERS INTO YOUR SCRIPT

// var LOG_TRACE   = 1
// var LOG_DEBUG   = 2
// var LOG_INFO    = 3
// var LOG_WARNING = 4
// var LOG_ERROR   = 5
// var LOG_FATAL   = 6
// __LOG = log.init()
// trace(msg, float val = na) => log.trace(__LOG, str.tostring(msg) + (not na(val) ? str.format(': "{0}"', val) : ''))
// debug(msg, float val = na) => log.debug(__LOG, str.tostring(msg) + (not na(val) ? str.format(': "{0}"', val) : ''))
// info(msg, float val = na)  => log.info(__LOG, str.tostring(msg) + (not na(val) ? str.format(': "{0}"', val) : ''))
// warn(msg, float val = na)  => log.warn(__LOG, str.tostring(msg) + (not na(val) ? str.format(': "{0}"', val) : ''))
// error(msg, float val = na) => log.error(__LOG, str.tostring(msg) + (not na(val) ? str.format(': "{0}"', val) : ''))
// fatal(msg, float val = na) => log.fatal(__LOG, str.tostring(msg) + (not na(val) ? str.format(': "{0}"', val) : ''))
// log(int level, msg, color line_color = #c4c4c4) => log.log(__LOG, level, str.tostring(msg), line_color)
// debug2(a, b)               => log.debug(__LOG, str.format('{0} {1}', a, b))
// debug3(a, b, c)            => log.debug(__LOG, str.format('{0} {1} {2}', a, b, c))
// debug4(a, b, c, d)         => log.debug(__LOG, str.format('{0} {1} {2} {3}', a, b, c, d))
// debug5(a, b, c, d, e)      => log.debug(__LOG, str.format('{0} {1} {2} {3} {4}', a, b, c, d, e))
// log_pass(level)            => log.severity(__LOG) < na(level) ? LOG_ERROR : level
// log_print(int level = 0, bool clear = false, bool force = false, int rows = 40, string text_size = na, string position = na, bool line_numbers = true) =>
//    log.print(__LOG, level, clear, force, rows, text_size, position, line_numbers)


// SOME USEFUL TIPS

// By default the log console persists between bars (for history) and bars and ticks (for realtime).
// Sometimes it is useful to clear the log after each candle or tick (assuming we are using the above helpers):
//
// log_print(clear = true) // starts afresh on every bar and tick (excludes historical bars but good realtime tick analysis)
// log_print(clear = barstate.isnew) // clears the log at the start of each bar (again, excludes historical but good realtime candle analysis)
//
// It is also useful to be able to selectively understand the state of data at specific points or times within a script:
//
// if log.once()
//     debug('useful variable', my_var) // this log only gets written once, upon first execution of this statement
//
// if log.only(5)
//     debug3(a, b, c) // these variables are only logged the first five times this statement is executed
//
// log_print(clear = false) // clear must be false and you should not write other logs on every bar, or the above will be lost
//
// Final tip. If you want to view ONLY log entries of a particular level, then negate the constant:
//
// log_print(level = -LOG_DEBUG)


// ██╗ ███╗   ███╗ ██████╗   █████╗  ██████╗  ████████╗  ██████╗
// ██║ ████╗ ████║ ██╔══██╗ ██╔══██╗ ██╔══██╗ ╚══██╔══╝ ██╔════╝
// ██║ ██╔████╔██║ ██████╔╝ ██║  ██║ ██████╔╝    ██║    ╚█████╗
// ██║ ██║╚██╔╝██║ ██╔═══╝  ██║  ██║ ██╔══██╗    ██║     ╚═══██╗
// ██║ ██║ ╚═╝ ██║ ██║      ╚█████╔╝ ██║  ██║    ██║    ██████╔╝
// ╚═╝ ╚═╝     ╚═╝ ╚═╝       ╚════╝  ╚═╝  ╚═╝    ╚═╝    ╚═════╝

import GeoffHammond/assert/4


//  ██████╗  ██╗       █████╗  ██████╗   █████╗  ██╗       ██████╗
// ██╔════╝  ██║      ██╔══██╗ ██╔══██╗ ██╔══██╗ ██║      ██╔════╝
// ██║  ██╗  ██║      ██║  ██║ ██████╦╝ ███████║ ██║      ╚█████╗
// ██║  ╚██╗ ██║      ██║  ██║ ██╔══██╗ ██╔══██║ ██║       ╚═══██╗
// ╚██████╔╝ ███████╗ ╚█████╔╝ ██████╦╝ ██║  ██║ ███████╗ ██████╔╝
//  ╚═════╝  ╚══════╝  ╚════╝  ╚═════╝  ╚═╝  ╚═╝ ╚══════╝ ╚═════╝

var LOG_TRACE = 1
var LOG_DEBUG = 2
var LOG_INFO  = 3
var LOG_WARN  = 4
var LOG_ERROR = 5
var LOG_FATAL = 6

var FORMAT_TRACE = '[trace]: {0}'
var FORMAT_DEBUG = '[debug]: {0}'
var FORMAT_INFO  = '[info]: {0}'
var FORMAT_WARN  = '[warn]: {0}'
var FORMAT_ERROR = '[error]: {0}'
var FORMAT_FATAL = '[fatal]: {0}'
var FORMAT_LOG   = '[log]: {0}'

var FORMAT_LEVEL_SPLIT = ':'
var FORMAT_LEVEL       = '{0}:{1}:{2}:{3}' // level:r:g:b
var FORMAT_LINE        = '#{0} {1}'       // line num:msg


// ███████╗ ██╗   ██╗ ███╗  ██╗  █████╗  ████████╗ ██╗  █████╗  ███╗  ██╗  ██████╗
// ██╔════╝ ██║   ██║ ████╗ ██║ ██╔══██╗ ╚══██╔══╝ ██║ ██╔══██╗ ████╗ ██║ ██╔════╝
// █████╗   ██║   ██║ ██╔██╗██║ ██║  ╚═╝    ██║    ██║ ██║  ██║ ██╔██╗██║ ╚█████╗
// ██╔══╝   ██║   ██║ ██║╚████║ ██║  ██╗    ██║    ██║ ██║  ██║ ██║╚████║  ╚═══██╗
// ██║      ╚██████╔╝ ██║ ╚███║ ╚█████╔╝    ██║    ██║ ╚█████╔╝ ██║ ╚███║ ██████╔╝
// ╚═╝       ╚═════╝  ╚═╝  ╚══╝  ╚════╝     ╚═╝    ╚═╝  ╚════╝  ╚═╝  ╚══╝ ╚═════╝

array_get(arr, offset) =>
    if array.size(arr) >= offset + 1
        array.get(arr, offset)
    else
        na

tuple_push(tuples, a, b) => array.push(tuples, a), array.push(tuples, b)

tuple_set(tuples, offset, a, b) => array.set(tuples, offset * 2, a), array.set(tuples, (offset * 2) + 1, b)

tuple_get(tuples, offset) => [array_get(tuples, offset * 2), array_get(tuples, (offset * 2) + 1)]

tuple_size(tuples) => math.floor(array.size(tuples) / 2)

tuple_remove(tuples, offset) => array.remove(tuples, offset * 2), array.remove(tuples, offset * 2)

color_darken(_color, amount, int new_t = na) =>
    float decimal = 1.0 - (amount / 100.0)
    float r = math.round(color.r(_color) * decimal)
    float g = math.round(color.g(_color) * decimal)
    float b = math.round(color.b(_color) * decimal)
    float t = new_t ? new_t : color.t(_color)
    color.rgb(r, g, b, t)


// ██╗       █████╗   ██████╗
// ██║      ██╔══██╗ ██╔════╝
// ██║      ██║  ██║ ██║  ██╗
// ██║      ██║  ██║ ██║  ╚██╗
// ███████╗ ╚█████╔╝ ╚██████╔╝
// ╚══════╝  ╚════╝   ╚═════╝

write(string[] msgs, string msg, int level, color line_color) =>
    lvl = str.format(FORMAT_LEVEL, level, color.r(line_color), color.g(line_color), color.b(line_color))
    tuple_push(msgs, str.tostring(msg), lvl)

msg_level(string lvl) => math.round(str.tonumber(array.get(str.split(lvl, FORMAT_LEVEL_SPLIT), 0)))

filter(string[] unfiltered, int filter_level, bool clear, int limit, bool line_numbers = true) =>
    varip console = array.new_string()
    varip total = 0

    if clear
        array.clear(console)
        total := 0

    unfiltered_size = tuple_size(unfiltered)
    if unfiltered_size
        filtered = array.new_string()

        count = 0
        for i = unfiltered_size - 1 to 0
            [msg, lvl] = tuple_get(unfiltered, i)
            level = msg_level(lvl)
            if (filter_level >= 0 and level >= filter_level) or (filter_level < 0 and level == -filter_level)
                count += 1
                tuple_push(filtered, msg, lvl)
            if count == limit
                break

        filtered_size = tuple_size(filtered)
        if filtered_size
            for i = filtered_size - 1 to 0
                total += 1
                [msg, lvl] = tuple_get(filtered, i)
                tuple_push(console, line_numbers ? str.format(FORMAT_LINE, total, msg) : msg, lvl)
                if tuple_size(console) > limit
                    tuple_remove(console, 0)

        array.clear(unfiltered)

    console

msg_color(string lvl) =>
    parts = str.split(lvl, FORMAT_LEVEL_SPLIT)
    if array.size(parts) == 4
        r = str.tonumber(array.get(parts, 1))
        g = str.tonumber(array.get(parts, 2))
        b = str.tonumber(array.get(parts, 3))
        color.rgb(r, g, b)
    else
        runtime.error(str.format('[Log:msg_color] Unable to process color for level: {0}', lvl))
        color.red


// ██╗      ██╗ ██████╗  ██████╗   █████╗  ██████╗  ██╗   ██╗
// ██║      ██║ ██╔══██╗ ██╔══██╗ ██╔══██╗ ██╔══██╗ ╚██╗ ██╔╝
// ██║      ██║ ██████╦╝ ██████╔╝ ███████║ ██████╔╝  ╚████╔╝
// ██║      ██║ ██╔══██╗ ██╔══██╗ ██╔══██║ ██╔══██╗   ╚██╔╝
// ███████╗ ██║ ██████╦╝ ██║  ██║ ██║  ██║ ██║  ██║    ██║
// ╚══════╝ ╚═╝ ╚═════╝  ╚═╝  ╚═╝ ╚═╝  ╚═╝ ╚═╝  ╚═╝    ╚═╝

// @function Restrict execution to only happen once. Usage: if log.once()\n    happens_once()
// @returns bool, true on first execution within scope, false subsequently
export once() =>
    varip pending = true
    if pending
        pending := false
        true
    else
        false

// @function Restrict execution to happen a set number of times. Usage: if log.only(5)\n    happens_five_times()
// @param repeat int, the number of times to return true
// @returns bool, true for the set number of times within scope, false subsequently
export only(int repeat = 1) =>
    var done = 0
    if done < repeat
        done += 1
        true
    else
        false

// @function Initialises the log array
// @returns string[], tuple based array to contain all pending log entries (__LOG)
export init() => varip msgs = array.new_string()

// @function Clears the log array
// @param msgs string[], the current collection of unfiltered and unprocessed logs (__LOG)
export clear(string[] msgs) => array.clear(msgs)

// @function Writes a trace message to the log console
// @param msgs string[], the current collection of unfiltered and unprocessed logs (__LOG)
// @param msg string, the trace message to write to the log
export trace(string[] msgs, string msg) => write(msgs, str.format(FORMAT_TRACE, msg), LOG_TRACE, #00d5ca)

// @function Writes a debug message to the log console
// @param msgs string[], the current collection of unfiltered and unprocessed logs (__LOG)
// @param msg string, the debug message to write to the log
export debug(string[] msgs, string msg) => write(msgs, str.format(FORMAT_DEBUG, msg), LOG_DEBUG, #b300ff)

// @function Writes an info message to the log console
// @param msgs string[], the current collection of unfiltered and unprocessed logs (__LOG)
// @param msg string, the info message to write to the log
export info(string[] msgs, string msg) => write(msgs, str.format(FORMAT_INFO, msg), LOG_INFO, #e6e6e6)

// @function Writes a warning message to the log console
// @param msgs string[], the current collection of unfiltered and unprocessed logs (__LOG)
// @param msg string, the warn message to write to the log
export warn(string[] msgs, string msg) => write(msgs, str.format(FORMAT_WARN, msg), LOG_WARN, #d47400)

// @function Writes an error message to the log console
// @param msgs string[], the current collection of unfiltered and unprocessed logs (__LOG)
// @param msg string, the error message to write to the log
export error(string[] msgs, string msg) => write(msgs, str.format(FORMAT_ERROR, msg), LOG_ERROR, #d42000)

// @function Writes a critical message to the log console
// @param msgs string[], the current collection of unfiltered and unprocessed logs (__LOG)
// @param msg string, the fatal message to write to the log
export fatal(string[] msgs, string msg) => write(msgs, str.format(FORMAT_FATAL, msg), LOG_FATAL, #d40000)

// @function Write a log message to the log console with a custom level
// @param msgs string[], the current collection of unfiltered and unprocessed logs (__LOG)
// @param level ing, the logging level to assign to the message
// @param msg string, the log message to write to the log
export log(string[] msgs, int level, string msg, color line_color = #c4c4c4) => write(msgs, str.format(FORMAT_LOG, msg), level, line_color)

// @function Checks the unprocessed log messages and returns the highest present level
// @param msgs string[], the current collection of unfiltered and unprocessed logs (__LOG)
// @returns int, the highest level found within the unfiltered logs
export severity(string[] msgs) =>
    severity = 0
    size = tuple_size(msgs)

    if size
        for i = 0 to size - 1
            [msg, lvl] = tuple_get(msgs, i)
            level = msg_level(lvl)

            if level > severity
                severity := level
    severity

// @function Prints all log messages to the screen
// @param msgs string[], the current collection of unfiltered and unprocessed logs (__LOG)
// @param level int, the minimum required log level of each message to be displayed
// @param clear bool, clear the printed log console after each render (useful with realtime when set to barstate.isconfirmed)
// @param rows int, the number of rows to display in the log console
// @param text_size string, the text size of the log console (global size vars)
// @param position string, the position of the log console (global position vars)
// @param line_numbers bool, disable the automatic prepending of line numbers
export print(string[] msgs, int level = 0, bool clear = false, bool force = false, int rows = 40, string text_size = na, string position = na, bool line_numbers = true) =>
    console = filter(msgs, level, clear, rows, line_numbers)

    if barstate.islast or force
        size = tuple_size(console)
        if size
            var tbl = table.new(na(position) ? position.bottom_left : position, 1, rows, #000000, #FFFFFF, 1)
            table.clear(tbl, 0, 0, 0, rows - 1)

            for i = 0 to size - 1
                [msg, lvl] = tuple_get(console, i)
                color = msg_color(lvl)
                bgcolor = color_darken(color, 96)
                table.cell(tbl, 0, i, msg, text_color=color, text_halign=text.align_left, text_size=na(text_size) ? size.normal : text_size, bgcolor=bgcolor)


// ██╗   ██╗ ███╗  ██╗ ██╗ ████████╗    ████████╗ ███████╗  ██████╗ ████████╗  ██████╗
// ██║   ██║ ████╗ ██║ ██║ ╚══██╔══╝    ╚══██╔══╝ ██╔════╝ ██╔════╝ ╚══██╔══╝ ██╔════╝
// ██║   ██║ ██╔██╗██║ ██║    ██║          ██║    █████╗   ╚█████╗     ██║    ╚█████╗
// ██║   ██║ ██║╚████║ ██║    ██║          ██║    ██╔══╝    ╚═══██╗    ██║     ╚═══██╗
// ╚██████╔╝ ██║ ╚███║ ██║    ██║          ██║    ███████╗ ██████╔╝    ██║    ██████╔╝
//  ╚═════╝  ╚═╝  ╚══╝ ╚═╝    ╚═╝          ╚═╝    ╚══════╝ ╚═════╝     ╚═╝    ╚═════╝

// @function Log module unit tests, for inclusion in parent script test suite. Usage: log.unittest_log(__ASSERTS)
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
export unittest_log(string[] case) =>
    assert.new_case(case, 'Log:once')
    assert.is_true(once(), case)
    assert.is_true(once(), case)
    for i = 1 to 3
        trigger = once()
        if i == 1
            assert.is_true(trigger, case)
        else
            assert.is_false(trigger, case)

    assert.new_case(case, 'Log:only')
    assert.is_true(only(), case)
    assert.is_false(only(0), case) // but why?
    assert.is_true(only(1), case)
    assert.is_true(only(2), case)

    for i = 1 to 3
        trigger = only(2)
        if i <= 2
            assert.is_true(trigger, case)
        else
            assert.is_false(trigger, case)

    __LOG = init()
    assert.new_case(case, 'Log:init')
    assert.str_array_equal(__LOG, array.new_string(), case)

    assert.new_case(case, 'Log:write')
    write(__LOG, 'msg', 1, color.white)
    assert.str_array_equal(__LOG, array.from('msg', '1:255:255:255'), case)
    write(__LOG, 'msg2', 2, #000000)
    assert.str_array_equal(__LOG, array.from('msg', '1:255:255:255', 'msg2', '2:0:0:0'), case)

    assert.new_case(case, 'Log:clear')
    clear(__LOG)
    assert.str_array_equal(__LOG, array.new_string(), case)

    assert.new_case(case, 'Log:trace')
    trace(__LOG, 'msg')
    assert.str_array_equal(__LOG, array.from(str.format(FORMAT_TRACE, 'msg'), str.format(FORMAT_LEVEL, LOG_TRACE, 0, 213, 202)), case)
    clear(__LOG)

    assert.new_case(case, 'Log:debug')
    debug(__LOG, 'msg')
    assert.str_array_equal(__LOG, array.from(str.format(FORMAT_DEBUG, 'msg'), str.format(FORMAT_LEVEL, LOG_DEBUG, 179, 0, 255)), case)
    clear(__LOG)

    assert.new_case(case, 'Log:info')
    info(__LOG, 'msg')
    assert.str_array_equal(__LOG, array.from(str.format(FORMAT_INFO, 'msg'), str.format(FORMAT_LEVEL, LOG_INFO, 230, 230, 230)), case)
    clear(__LOG)

    assert.new_case(case, 'Log:warn')
    warn(__LOG, 'msg')
    assert.str_array_equal(__LOG, array.from(str.format(FORMAT_WARN, 'msg'), str.format(FORMAT_LEVEL, LOG_WARN, 212, 116, 0)), case)
    clear(__LOG)

    assert.new_case(case, 'Log:error')
    error(__LOG, 'msg')
    assert.str_array_equal(__LOG, array.from(str.format(FORMAT_ERROR, 'msg'), str.format(FORMAT_LEVEL, LOG_ERROR, 212, 32, 0)), case)
    clear(__LOG)

    assert.new_case(case, 'Log:fatal')
    fatal(__LOG, 'msg')
    assert.str_array_equal(__LOG, array.from(str.format(FORMAT_FATAL, 'msg'), str.format(FORMAT_LEVEL, LOG_FATAL, 212, 0, 0)), case)
    clear(__LOG)

    assert.new_case(case, 'Log:log')
    log(__LOG, 9, 'msg')
    assert.str_array_equal(__LOG, array.from(str.format(FORMAT_LOG, 'msg'), str.format(FORMAT_LEVEL, 9, 196, 196, 196)), case)
    clear(__LOG)
    log(__LOG, 9, 'msg', #000000)
    assert.str_array_equal(__LOG, array.from(str.format(FORMAT_LOG, 'msg'), str.format(FORMAT_LEVEL, 9, 0, 0, 0)), case)
    clear(__LOG)

    assert.new_case(case, 'Log:severity')
    trace(__LOG, 'msg'), assert.equal(severity(__LOG), 1, case)
    debug(__LOG, 'msg'), assert.equal(severity(__LOG), 2, case)
    info(__LOG, 'msg'), assert.equal(severity(__LOG), 3, case)
    warn(__LOG, 'msg'), assert.equal(severity(__LOG), 4, case)
    error(__LOG, 'msg'), assert.equal(severity(__LOG), 5, case)
    fatal(__LOG, 'msg'), assert.equal(severity(__LOG), 6, case)
    log(__LOG, 9, 'msg'), assert.equal(severity(__LOG), 9, case)

    assert.new_case(case, 'Log:msg_level')
    assert.equal(msg_level('1'), 1, case)
    assert.equal(msg_level(str.format('{0}', 1)), 1, case)
    assert.equal(msg_level(str.format('{0}{1}{2}', 1, FORMAT_LEVEL_SPLIT, 2)), 1, case)
    assert.equal(msg_level(str.format('{0}{1}{2}', 1, FORMAT_LEVEL_SPLIT, 2)), 1, case)
    assert.equal(msg_level(str.format(FORMAT_LEVEL, 1, 255, 255, 255)), 1, case)
    assert.equal(msg_level(str.format(FORMAT_LEVEL, 1, 0, 0, 0)), 1, case)
    assert.equal(msg_level(str.format(FORMAT_LEVEL, 1, 'in', 'theory', 'anything')), 1, case)

    assert.new_case(case, 'Log:msg_color')
    assert.color_equal(msg_color('0:255:255:255'), #FFFFFF, case)
    assert.color_equal(msg_color('0:0:0:0'), #000000, case)
    assert.color_equal(msg_color('0:0:213:202'), #00d5ca, case)
    assert.color_equal(msg_color('0:179:0:255'), #b300ff, case)
    assert.color_equal(msg_color('0:230:230:230'), #e6e6e6, case)
    assert.color_equal(msg_color('0:212:116:0'), #d47400, case)
    assert.color_equal(msg_color('0:212:32:0'), #d42000, case)
    assert.color_equal(msg_color('0:212:0:0'), #d40000, case)
    assert.color_equal(msg_color('0:196:196:196'), #c4c4c4, case)
    clear(__LOG)

    assert.new_case(case, 'Log:filter')
    write(__LOG, 'one', 1, #000000)
    write(__LOG, 'two', 2, #000000)
    write(__LOG, 'three', 3, #000000)
    lvl1 = str.format(FORMAT_LEVEL, 1, 0, 0, 0)
    lvl2 = str.format(FORMAT_LEVEL, 2, 0, 0, 0)
    lvl3 = str.format(FORMAT_LEVEL, 3, 0, 0, 0)
    assert.str_array_equal(filter(array.copy(__LOG), 1, false, 5, true), array.from(str.format(FORMAT_LINE, 1, 'one'), lvl1, str.format(FORMAT_LINE, 2, 'two'), lvl2, str.format(FORMAT_LINE, 3, 'three'), lvl3), case)
    assert.str_array_equal(filter(array.copy(__LOG), 2, false, 5, true), array.from(str.format(FORMAT_LINE, 1, 'two'), lvl2, str.format(FORMAT_LINE, 2, 'three'), lvl3), case)
    assert.str_array_equal(filter(array.copy(__LOG), 3, false, 5, true), array.from(str.format(FORMAT_LINE, 1, 'three'), lvl3), case)
    assert.str_array_equal(filter(array.copy(__LOG), -1, false, 5, true), array.from(str.format(FORMAT_LINE, 1, 'one'), lvl1), case)
    assert.str_array_equal(filter(array.copy(__LOG), -2, false, 5, true), array.from(str.format(FORMAT_LINE, 1, 'two'), lvl2), case)
    assert.str_array_equal(filter(array.copy(__LOG), -3, false, 5, true), array.from(str.format(FORMAT_LINE, 1, 'three'), lvl3), case)
    assert.str_array_equal(filter(array.copy(__LOG), 1, false, 5, false), array.from('one', lvl1, 'two', lvl2, 'three', lvl3), case)

    write(__LOG, 'two', 2, #000000)
    for i = 0 to 2
        write(__LOG, i % 2 == 0 ? 'one' : 'two', i % 2 == 0 ? 1 : 2, #000000)
        console = filter(__LOG, 1, false, 2)
        if i % 2 == 0
            assert.str_array_equal(console, array.from(str.format(FORMAT_LINE, 1 + i, 'two'), lvl2, str.format(FORMAT_LINE, 2 + i, 'one'), lvl1), case, str.format('loop1 #{0}', i + 1))
        else
            assert.str_array_equal(console, array.from(str.format(FORMAT_LINE, 1 + i, 'one'), lvl1, str.format(FORMAT_LINE, 2 + i, 'two'), lvl2), case, str.format('loop1 #{0}', i + 1))

    for i = 0 to 2
        write(__LOG, i % 2 == 0 ? 'one' : 'two', i % 2 == 0 ? 1 : 2, #000000)
        console = filter(__LOG, 1, true, 2)
        assert.str_array_equal(console, array.from(str.format(FORMAT_LINE, 1, i % 2 == 0 ? 'one': 'two'), i % 2 == 0 ? lvl1 : lvl2), case, str.format('loop2 #{0}', i + 1))

    for i = 0 to 2
        write(__LOG, 'one', 1, #000000)
        write(__LOG, 'three', 3, #000000)
        write(__LOG, i % 2 == 0 ? 'one' : 'two', i % 2 == 0 ? 1 : 2, #000000)
        console = filter(__LOG, 2, false, 2)
        if i == 0
            assert.str_array_equal(console, array.from(str.format(FORMAT_LINE, 1, 'three'), lvl3), case, str.format('loop3 #{0}', i + 1))
        else if i == 1
            assert.str_array_equal(console, array.from(str.format(FORMAT_LINE, 2, 'three'), lvl3, str.format(FORMAT_LINE, 3, 'two'), lvl2), case, str.format('loop3 #{0}', i + 1))
        else if i == 2
            assert.str_array_equal(console, array.from(str.format(FORMAT_LINE, 3, 'two'), lvl2, str.format(FORMAT_LINE, 4, 'three'), lvl3), case, str.format('loop3 #{0}', i + 1))

// @function Run the log module unit tests as a stand alone. Usage: log.unittest()
// @param verbose bool, optionally disable the full report to only display failures
export unittest(bool verbose = true) =>
    if assert.once()
        __ASSERTS = assert.init()
        unittest_log(__ASSERTS)
        assert.report(__ASSERTS, verbose)


// ██████╗  ██╗   ██╗ ███╗  ██╗
// ██╔══██╗ ██║   ██║ ████╗ ██║
// ██████╔╝ ██║   ██║ ██╔██╗██║
// ██╔══██╗ ██║   ██║ ██║╚████║
// ██║  ██║ ╚██████╔╝ ██║ ╚███║
// ╚═╝  ╚═╝  ╚═════╝  ╚═╝  ╚══╝

unittest()

__LOG = init()

if once()
    trace(__LOG, 'Hi')
    debug(__LOG, 'Sigh')
    info(__LOG, 'Ooh')
    warn(__LOG, 'Gah')
    error(__LOG, 'Yikes')
    fatal(__LOG, '#$@*')
    log(__LOG, 8, 'Umm')
    log(__LOG, 9, 'Mmmmm (custom colors)', #ed8cb4)

print(__LOG)

//          __
//        /. _\
//   .--.|/_/__
//  ''.--o/___  \
//   /.'o|_o  '.|
//  |/    |_|  '
//  '     |_|       Ooh, a fatal bug warning!
//         |_|     /
//         |_[~] o
//         |_|   U>
//         |_|   H
