#include "log.h"
#include "scratch.h"
#include "string.h"
#include "atomic.h"

struct log_event_callback {
    log_event_callback_func *func;
    i32 level;
    struct log_event_callback *next;
};

/* ========================== *
 * Global state
 * ========================== */

GLOBAL struct {
    struct atomic_i32 initialized;
    struct sys_mutex mutex;
    struct arena arena;
    log_event_callback_func *callbacks_head;
    struct sys_file file;
    b32 file_valid;
} G = { 0 }, DEBUG_ALIAS(G, G_log);

GLOBAL READONLY struct log_level_settings g_log_level_settings[LOG_LEVEL_COUNT] = {
    [LOG_LEVEL_CRITICAL] = {
        STR_NOCAST("CRITICAL"),
        0xFFFF00FF
    },

    [LOG_LEVEL_ERROR] = {
        STR_NOCAST("ERROR"),
        0xFFFF0000
    },

    [LOG_LEVEL_WARNING] = {
        STR_NOCAST("WARNING"),
        0xFFFFFF00
    },

    [LOG_LEVEL_INFO] = {
        STR_NOCAST("INFO"),
        0xFFFFFFFF
    },

    [LOG_LEVEL_DEBUG] = {
        STR_NOCAST("DEBUG"),
        0xFF30D5C8
    }
};

/* ========================== *
 * Startup
 * ========================== */

struct log_startup_receipt log_startup(struct string logfile_path)
{
    G.mutex = sys_mutex_alloc();
    G.arena = arena_alloc(GIGABYTE(64));
    if (logfile_path.len > 0) {
        /* Create / wipe log file */
        sys_file_close(sys_file_open_write(logfile_path));
        /* Keep log file open for appending */
        if (sys_is_file(logfile_path)) {
            G.file = sys_file_open_append(logfile_path);
            G.file_valid = true;
        }
    }
    atomic_i32_eval_exchange(&G.initialized, 1);
    return (struct log_startup_receipt) { 0 };
}

/* ========================== *
 * Callback
 * ========================== */

void log_register_callback(log_event_callback_func *func)
{
    if (!atomic_i32_eval(&G.initialized)) { return; }
    sys_mutex_lock(&G.mutex);
    {
        /* TODO */
        (UNUSED)func;
    }
    sys_mutex_unlock(&G.mutex);
}

/* ========================== *
 * Log
 * ========================== */

INTERNAL void append_to_logfile(struct string msg)
{
    __prof;
    if (!atomic_i32_eval(&G.initialized)) { return; }

    if (G.file_valid) {
        struct temp_arena scratch = scratch_begin_no_conflict();
        struct string msg_line = string_cat(scratch.arena, msg, STR("\n"));
        sys_file_write(G.file, BUFFER_FROM_STRING(msg_line));
        scratch_end(scratch);
    }
}

/* Panic log function is separate to enforce zero side effects other than
 * writing to log file. */
void _log_panic(struct string msg)
{
    if (!atomic_i32_eval(&G.initialized)) { return; }

    if (G.file_valid) {
        sys_file_write(G.file, BUFFER_FROM_STRING(STR("******** PANICKING ********\n")));
        sys_file_write(G.file, BUFFER_FROM_STRING(msg));
        sys_file_write(G.file, BUFFER_FROM_STRING(STR("\n***************************\n")));
    }
}

#if LOG_INCLUDE_SOURCE_LOCATION
void _log(i32 level, struct string file, u32 line, struct string msg)
#else
void _log(i32 level, struct string msg)
#endif
{
    __prof;
    if (!atomic_i32_eval(&G.initialized)) { return; }

    if (level < 0 || level >= LOG_LEVEL_COUNT) {
        sys_panic(STR("Invalid log level"));
    }

    struct temp_arena scratch = scratch_begin_no_conflict();

    struct sys_datetime lt = sys_local_time();
    u32 tid = sys_thread_id();

    struct log_level_settings settings = g_log_level_settings[level];
    struct string shorthand = settings.shorthand;

#if LOG_INCLUDE_SOURCE_LOCATION
    struct string msg_formatted = string_format(
        scratch.arena,
        STR("%F:%F:%F |Thread %F| [%F] <%F:%F> %F"),

        /* Time */
        FMT_UINT(lt.hour),
        FMT_UINT(lt.minute),
        FMT_UINT(lt.second),

        /* TID */
        FMT_UINT(tid),

        /* Level */
        FMT_STR(shorthand),

        /* Source location */
        FMT_STR(file),
        FMT_SINT(line),

        /* Message */
        FMT_STR(msg)
    );
#else
    struct string msg_formatted = string_format(
        scratch.arena,
        STR("%F:%F:%F |Thread %F| [%F] %F"),

        /* Time */
        FMT_UINT(lt.hour),
        FMT_UINT(lt.minute),
        FMT_UINT(lt.second),

        /* TID */
        FMT_UINT(tid),

        /* Level */
        FMT_STR(shorthand),

        /* Message */
        FMT_STR(msg)
    );
#endif

    __profmsg((char *)msg.text, msg.len, settings.color);
    append_to_logfile(msg_formatted);

    scratch_end(scratch);
}

#if LOG_INCLUDE_SOURCE_LOCATION
void _logfv(i32 level, struct string file, u32 line, struct string fmt, va_list args)
#else
void _logfv(i32 level, struct string fmt, va_list args)
#endif
{
    if (!atomic_i32_eval(&G.initialized)) { return; }
    struct temp_arena scratch = scratch_begin_no_conflict();
    struct string msg = string_formatv(scratch.arena, fmt, args);
#if LOG_INCLUDE_SOURCE_LOCATION
    _log(level, file, line, msg);
#else
    _log(level, msg);
#endif
    scratch_end(scratch);
}

#if LOG_INCLUDE_SOURCE_LOCATION
void _logf(i32 level, struct string file, u32 line, struct string fmt, ...)
#else
void _logf(i32 level, struct string fmt, ...)
#endif
{
    if (!atomic_i32_eval(&G.initialized)) { return; }
    va_list args;
    va_start(args, fmt);
#if LOG_INCLUDE_SOURCE_LOCATION
    _logfv(level, file, line, fmt, args);
#else
    _logfv(level, fmt, args);
#endif
    va_end(args);
}
