#include "logger.h"

#include "logger_module.h"
#include "module_system.h"

#include <stdio.h>
#include <stdarg.h>
#include <time.h>

#define BZ_GET_LOGGER_MODULE() ((BzLoggerModule *) bzModuleRetrieve(BZ_MODULE_LOGGER))

bool bzLoggerInit() {
    if (bzModuleIsAllocated(BZ_MODULE_LOGGER))
        return false;

    BzLoggerModule *logger = bzModuleAllocate(BZ_MODULE_LOGGER, sizeof(*logger));
    if (logger == NULL)
        return false;

    logger->level = BZ_LOG_INFO;
    logger->silent = false;
    setvbuf(stdout, logger->buffer, _IOFBF, sizeof(logger->buffer));


    return true;
}

void bzLoggerDeinit() {
    BzLoggerModule *logger = BZ_GET_LOGGER_MODULE();
    bzModuleFree(BZ_MODULE_LOGGER);
}

void bzLoggerEnableDefaultLog(bool enable) {
    BZ_GET_LOGGER_MODULE()->silent = !enable;
}

void bzLoggerSetLevel(BzLoggerLevel level) {
    BZ_GET_LOGGER_MODULE()->level = level;
}

BzLoggerLevel bzLoggerGetLevel() {
    return BZ_GET_LOGGER_MODULE()->level;
}


static const char *LEVEL_STRINGS[] = {
        "TRACE",
        "DEBUG",
        "INFO",
        "WARNING",
        "ERROR",
        "FATAL"
};
static const char *LEVEL_COLORS[] = {
        "\x1b[90m", // TRACE  -> Bright black
        "\x1b[96m", // DEBUG  -> Bright cyan
        "\x1b[32m", // INFO   -> Green
        "\x1b[33m", // WARN   -> Yellow
        "\x1b[31m", // ERROR  -> Red
        "\x1b[91m", // FATAL  -> Bright red
};

static void bzLoggerStdOutCallback(BzLoggerEvent *ev) {
    fprintf(stdout, "%s %s%-5s\x1b[0m ",ev->time,
            LEVEL_COLORS[ev->level], LEVEL_STRINGS[ev->level]);
    if (ev->file) {
        fprintf(stdout, "\x1b[90m(%s:%d)\x1b[0m ",
                ev->file, ev->line);
    }
    vfprintf(stdout, ev->fmt, ev->va);
    fprintf(stdout, "\n");
    fflush(stdout);
}

static void bzLoggerFileCallback(BzLoggerEvent *ev) {
    FILE *fp = ev->userData;
    fprintf(fp, "%s %-5s  ", ev->time, LEVEL_STRINGS[ev->level]);
    if (ev->file) {
        fprintf(fp, "(%s:%d): ", ev->file, ev->line);
    }
    vfprintf(fp, ev->fmt, ev->va);
    fflush(fp);
}

void bzLoggerLog(BzLoggerLevel level, const char *file, int line, const char *fmt, ...) {
    va_list args;
    va_start(args, fmt);
    bzLoggerLogV(level, file, line, fmt, args);
    va_end(args);
}
void bzLoggerLogV(BzLoggerLevel level, const char *file, int line, const char *fmt, va_list va) {
    BzLoggerEvent event = {NULL,  NULL, level, file, line, fmt};
    const int loggerLevel = bzLoggerGetLevel();

    time_t t = time(NULL);
    struct tm *lt = localtime(&t);
    char timeBuf[16];
    size_t lastIdx = strftime(timeBuf, sizeof(timeBuf), "%H:%M:%S", lt);
    // TODO: Assert
    timeBuf[lastIdx] = '\0';
    event.time = timeBuf;

    if (!BZ_GET_LOGGER_MODULE()->silent && level >= loggerLevel) {
        event.userData = stderr;
        va_copy(event.va, va);
        bzLoggerStdOutCallback(&event);
        va_end(event.va);
    }

    for (int i = 0; i < BZ_LOGGER_MAX_CALLBACKS; ++i) {
        BzLoggerCallback cb = BZ_GET_LOGGER_MODULE()->callbacks[i];
        if (!cb.callback) break;

        if (level >= cb.level) {
            event.userData = cb.userData;

            va_copy(event.va, va);
            cb.callback(&event);
            va_end(event.va);
        }
    }
}
void bzLoggerOnlyLog(BzLoggerLevel level, const char *fmt, ...) {
    va_list args;
    va_start(args, fmt);
    bzLoggerOnlyLogV(level, fmt, args);
    va_end(args);
}
void bzLoggerOnlyLogV(BzLoggerLevel level, const char *fmt, va_list va) {
    bzLoggerLogV(level, NULL, -1, fmt, va);
}


bool bzLoggerAddFP(void *fp, BzLoggerLevel level) {
    return bzLoggerAddCB(&(BzLoggerCallback) {
            .callback = bzLoggerFileCallback,
            .level = level,
            .userData = fp
    });
}

bool bzLoggerAddCB(const BzLoggerCallback *callback) {
    for (int i = 0; i < BZ_LOGGER_MAX_CALLBACKS; ++i) {
        BzLoggerCallback *cbSlot = &BZ_GET_LOGGER_MODULE()->callbacks[i];

        if (cbSlot->callback == NULL) {
            *cbSlot = *callback;
            return true;
        }
    }

    return false;
}