module Logs

using ProgressLogging: asprogress
using InteractiveUtils
using Dates: Dates
using Logging

import Term:
    Theme,
    textlen,
    escape_brackets,
    unescape_brackets,
    reshape_text,
    has_markup,
    int,
    highlight,
    term_theme,
    truncate,
    ltrim_str

import ..Consoles: console_width, console_height, change_scroll_region, move_to_line
import ..Renderables: AbstractRenderable
import ..Style: apply_style
import ..Tprint: tprintln
import ..Boxes: ROUNDED
import ..Progress:
    ProgressBar,
    ProgressJob,
    DescriptionColumn,
    ProgressColumn,
    render,
    start!,
    stop!,
    addjob!,
    removejob!,
    update!,
    getjob,
    SeparatorColumn,
    PercentageColumn

export TermLogger, install_term_logger

DEFAULT_LOGGER = global_logger()

# ---------------------------- create term logger ---------------------------- #
"""
    TermLogger

Custom logger type.
"""
struct TermLogger <: AbstractLogger
    io::IO
    theme::Theme
    pbar::ProgressBar
end
TermLogger(theme::Theme) = TermLogger(stderr, theme)

function TermLogger(io::IO, theme::Theme)
    return TermLogger(
        io,
        theme,
        ProgressBar(;
            transient = true,
            columns = [
                DescriptionColumn,
                SeparatorColumn,
                ProgressColumn,
                SeparatorColumn,
                PercentageColumn,
            ],
        ),
    )
end

# set logger beavior
Logging.min_enabled_level(logger::TermLogger) = Logging.Info

Logging.shouldlog(logger::TermLogger, level, _module, group, id) = true
Logging.catch_exceptions(logger::TermLogger) = true

# --------------------------- handle logger message -------------------------- #

"""
    print_closing_line(color::String, width::Int)

Print the final line of a log message with style and date info
"""
function print_closing_line(color::String, width::Int = 48)
    tprintln(
        "  {$color bold dim}$(ROUNDED.bottom.left)" *
        "$(ROUNDED.row.mid)"^(width) *
        "{/$color bold dim}",
    )
    _date = Dates.format(Dates.now(), "e, dd u yyyy")
    _time = Dates.format(Dates.now(), "HH:MM:SS")
    pad = width - textlen(_date * _time) - 2
    return tprintln(
        " "^pad * "{dim}$_date{/dim} {bold dim underline}$_time{/bold dim underline}";
        highlight = false,
    )
end

"""
    handle_progress(logger::TermLogger, prog)

Handle progress information passed by `ProgressLogging`

It creates/adds/removes `ProgressJob`s to the logger's
`ProgressBar` to create progress visualizations. 
"""
function handle_progress(logger::TermLogger, prog)
    pbar = logger.pbar
    pbar.running || start!(pbar)

    # if progress not started yet, ignore
    if isnothing(prog.fraction) || prog.fraction == 0
        return nothing
    end

    # check if a job exists for this progress, or add one
    job = getjob(pbar, prog.id)
    job = if isnothing(job)
        addjob!(
            pbar;
            description = length(prog.name) > 0 ? prog.name : "running...",
            N = 100,
            transient = false,
            id = prog.id,
        )
    else
        job
    end

    # if done, remove job.
    if prog.done || prog.fraction == 1.0
        job.i = 100
        stop!(job)
    else
        update!(job; i = (Int ∘ floor)(prog.fraction * 100))
    end

    # render
    if all(map(j -> j.finished, pbar.jobs))
        map(j -> removejob!(pbar, j), pbar.jobs)
        stop!(pbar)
    else
        render(pbar)
    end
end

"""
    handle_message(logger::TermLogger, lvl, msg, _mod, group, id, file, line; kwargs...)

Handle printing of log messages, with style!.

In addition to the log message and info such as file/line and time of log, 
it prints kwargs styled by their type.
"""
function Logging.handle_message(
    logger::TermLogger,
    lvl,
    msg,
    _mod,
    group,
    id,
    file,
    line;
    kwargs...,
)
    _progress = asprogress(lvl, msg, _mod, group, id, file, line; kwargs...)
    isnothing(_progress) || return handle_progress(logger, _progress)

    # get name of function where logging message was called from
    fname = ""
    for frame in stacktrace()
        if "$(frame.file)" == file && frame.line == line
            fname = ".{underline}$(frame.func){/underline}"
        end
    end

    # prepare styles
    color = if lvl == Logging.Info
        logger.theme.info
    elseif lvl == Logging.Debug
        logger.theme.debug
    elseif lvl == Logging.Warn
        logger.theme.warn
    elseif lvl == Logging.Error
        logger.theme.error
    else
        "#90CAF9"
    end

    outline_markup = "$color dim"
    vert = "{$outline_markup}" * ROUNDED.mid.left * "{/$outline_markup}"

    # style message
    msg = if msg isa AbstractString
        reshape_text(has_markup(msg) ? msg : "{#8abeff}$msg{/#8abeff}", 64)
    else
        string(msg)
    end

    # get the first line of information
    content = "{$color underline bold}@$(string(lvl)){/$color underline bold} {#edb46f }($(_mod)$fname):{/#edb46f }"

    # for multi-lines message, print each line separately.
    msg_lines::Vector{AbstractString} = split(msg, "\n")
    for n in 2:length(msg_lines)
        msg_lines[n] = "  $vert   " * " "^textlen(content) * "{#8abeff}" * msg_lines[n]
    end

    length(msg_lines) > 0 && (content *= "  " * msg_lines[1])

    tprintln(content; highlight = false)
    tprintln.(msg_lines[2:end]; highlight = false)

    # if no kwargs we can just quit
    if length(kwargs) == 0 || length(msg_lines) == 0
        print_closing_line(color)
        return nothing
    end

    # get padding width
    _types = string.(typeof.(collect(values(kwargs))))
    _types = map(t -> ltrim_str(t, 22), _types)
    for (i, _type) in enumerate(values(kwargs))
        typeof(_type) <: Function && (_types[i] = string(Function))
    end

    wpad = max(textlen.((_types))...) + 2
    ks = truncate.(string.(keys(kwargs)), 28)
    namepad = max(textlen.(ks)...)

    # print all kwargs
    tprintln("  $vert"; highlight = false)
    for (k, v, _type) in zip(ks, values(kwargs), _types)
        # get line stub
        pad = wpad - textlen(_type) - 1
        line =
            "  $vert" *
            " "^pad *
            " {$(logger.theme.type) dim}($(_type)){/$(logger.theme.type) dim}" *
            " {bold #e0e0e0}$k{/bold #e0e0e0}"

        epad = namepad - textlen(string(k))
        line *= " "^epad * " {bold red}={/bold red} "
        lpad = textlen(line) - 4

        # get value style
        if v isa Number
            _style = logger.theme.number
        elseif v isa Symbol
            _style = logger.theme.symbol
        elseif v isa AbstractString
            _style = logger.theme.string
        elseif v isa AbstractVector
            _style = logger.theme.number
            _size = length(v)
            v = escape_brackets(string(v))
            v = textlen(v) > 33 ? v[1:30] * "..." : v
            v *= "\n {white}$(_size) {/white}{dim}items{/dim}"
        elseif v isa AbstractArray || v isa AbstractMatrix
            _style = logger.theme.number
            _size = size(v)
            v = ltrim_str("$(typeof(v)) {dim}<: $(supertypes(typeof(v))[end-1]){/dim}", 30)
            v *=
                "\n {dim}shape: {default white}" *
                join(string.(_size), " × ") *
                "{/default white}{/dim}"

        elseif v isa Function
            _style = logger.theme.func
        elseif v isa AbstractRenderable
            _style = "default"
            v = "$(typeof(v)) \e[2m(size: $(v.measure))\e[0m"
        else
            _style = nothing
        end

        # print value lines
        try
            v = reshape_text(ltrim_str(string(v), 177), 60)
        catch
            v = ltrim_str(string(v), 60)
        end
        vlines = split(v, "\n")

        vlines = if !isnothing(_style)
            map(ln -> "{" * _style * "}" * ln * "{/" * _style * "}", vlines)
        else
            highlight.(vlines)
        end

        tprintln(line * vlines[1]; highlight = false)
        if length(vlines) ≥ 1
            for ln in vlines[2:end]
                tprintln("  $vert " * " "^lpad * ln; highlight = false)
            end
        end
    end
    return print_closing_line(color)
end

# ---------------------------- install term logger --------------------------- #

"""
    install_term_logger(theme::Theme=theme)

Install `TermLogger` as the global logging system.

`theme::Theme` can be passed to specify the theme to use for styling objects.
"""
function install_term_logger(theme::Theme = term_theme[])
    _logger = TermLogger(theme)
    return global_logger(_logger)
end

function uninstall_term_logger()
    _lg = global_logger(DEFAULT_LOGGER)
    logger = global_logger(DEFAULT_LOGGER)
    return logger
end

end
