#include <time.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <stdarg.h>
#include <limits.h>
#include <stdlib.h>
#include <pthread.h>

#include "toml.h"
#include "log_private.h"

enum log_output
{
    LOG_OUTPUT_STDERR,
    LOG_OUTPUT_FILE,
    LOG_OUTPUT_BOTH,
};

struct log_config
{
    enum log_output output;
    enum log_level level;
    char log_file[PATH_MAX];
};

struct logger
{
    char config_file[PATH_MAX];
    struct log_config config;

    int log_fd;
    int log_file_opened_day;
};

static unsigned char level_str[6][6] = {"TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"};
static unsigned char weekday_str[7][4] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
static unsigned char month_str[12][4] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};

thread_local struct logger *__thread_local_logger;

/******************************************************************************
 * Private API
 ******************************************************************************/

static void local_time(struct tm *local)
{
    time_t t;
    time(&t);
    localtime_r(&t, local);
}

static int str_to_level(const char *level)
{
    if (level == NULL)
    {
        return -1;
    }
    if (strcasecmp(level, "TRACE") == 0)
    {
        return LOG_TRACE;
    }
    else if (strcasecmp(level, "DEBUG") == 0)
    {
        return LOG_DEBUG;
    }
    else if (strcasecmp(level, "INFO") == 0)
    {
        return LOG_INFO;
    }
    else if (strcasecmp(level, "WARN") == 0)
    {
        return LOG_WARN;
    }
    else if (strcasecmp(level, "ERROR") == 0)
    {
        return LOG_ERROR;
    }
    else if (strcasecmp(level, "FATAL") == 0)
    {
        return LOG_FATAL;
    }
    else
    {
        return -1;
    }
}

static int config_parse(struct log_config *config, const char *config_file)
{
    int ret = -1;
    FILE *fp = NULL;
    char errbuf[200];
    char *ptr_output = NULL;
    char *ptr_file = NULL;
    char *ptr_level = NULL;
    const char *ptr;
    toml_table_t *section = NULL;
    toml_table_t *table = NULL;

    fp = fopen(config_file, "r");
    if (fp == NULL)
    {
        fprintf(stderr, "(logger) open config file %s failed, %s\n", config_file, strerror(errno));
        goto error_out;
    }

    table = toml_parse_file(fp, errbuf, sizeof(errbuf));
    if (table == NULL)
    {
        fprintf(stderr, "(logger) parse config file %s failed, %s\n", config_file, errbuf);
        goto error_out;
    }

    section = toml_table_in(table, "log");
    if (section == NULL)
    {
        fprintf(stderr, "(logger) config file %s missing log section\n", config_file);
        goto error_out;
    }

    ptr = toml_raw_in(section, "output");
    if (ptr == NULL || toml_rtos(ptr, &ptr_output) != 0)
    {
        fprintf(stderr, "(logger) config file %s missing log.output\n", config_file);
        goto error_out;
    }
    if (strcasecmp(ptr_output, "stderr") == 0)
    {
        config->output = LOG_OUTPUT_STDERR;
    }
    else if (strcasecmp(ptr_output, "file") == 0)
    {
        config->output = LOG_OUTPUT_FILE;
    }
    else if (strcasecmp(ptr_output, "both") == 0)
    {
        config->output = LOG_OUTPUT_BOTH;
    }
    else
    {
        fprintf(stderr, "(logger) config file %s invalid log.output\n", config_file);
        goto error_out;
    }

    if (config->output == LOG_OUTPUT_FILE || config->output == LOG_OUTPUT_BOTH)
    {
        ptr = toml_raw_in(section, "file");
        if (ptr == NULL || toml_rtos(ptr, &ptr_file) != 0)
        {
            fprintf(stderr, "(logger) config file %s missing log.file\n", config_file);
            goto error_out;
        }
        strcpy(config->log_file, ptr_file);
    }

    ptr = toml_raw_in(section, "level");
    if (ptr == NULL || toml_rtos(ptr, &ptr_level) != 0)
    {
        fprintf(stderr, "(logger) config file %s missing log.level\n", config_file);
        goto error_out;
    }
    config->level = (enum log_level)str_to_level(ptr_level);
    if (config->level == -1)
    {
        fprintf(stderr, "config file %s invalid log.level\n", config_file);
        goto error_out;
    }

    ret = 0;

error_out:
    if (ptr_output)
    {
        free(ptr_output);
    }
    if (ptr_file)
    {
        free(ptr_file);
    }
    if (ptr_level)
    {
        free(ptr_level);
    }
    if (table)
    {
        toml_free(table);
    }
    if (fp)
    {
        fclose(fp);
    }

    return ret;
}

static int log_file_reopen(struct logger *logger)
{
    int new_fd;
    int old_fd;
    struct tm local;
    char buff[PATH_MAX * 2] = {0};
    local_time(&local);
    snprintf(buff, sizeof(buff), "%s.%d-%02d-%02d", logger->config.log_file, local.tm_year + 1900, local.tm_mon + 1, local.tm_mday);

    new_fd = open(buff, O_WRONLY | O_APPEND | O_CREAT, 0644);
    if (new_fd == -1)
    {
        fprintf(stderr, "(logger) open() \"%s\" failed, %s\n", buff, strerror(errno));
        return -1;
    }

    logger->log_file_opened_day = local.tm_mday;
    old_fd = logger->log_fd;
    logger->log_fd = new_fd;

    if (old_fd > 0)
    {
        close(old_fd);
    }

    return 0;
}

/******************************************************************************
 * Public API
 ******************************************************************************/

struct logger *log_new(const char *config_file)
{
    struct logger *logger = (struct logger *)calloc(1, sizeof(struct logger));
    if (logger == NULL)
    {
        fprintf(stderr, "(logger) logger calloc() failed, %s\n", strerror(errno));
        return NULL;
    }

    memcpy(&logger->config_file, config_file, strlen(config_file));
    if (config_parse(&logger->config, config_file) != 0)
    {
        goto error_out;
    }

    if (logger->config.output == LOG_OUTPUT_FILE || logger->config.output == LOG_OUTPUT_BOTH)
    {
        if (log_file_reopen(logger) != 0)
        {
            goto error_out;
        }
    }

    return logger;

error_out:
    log_free(logger);
    return NULL;
}

void log_free(struct logger *logger)
{
    if (logger)
    {
        if (logger->config.output == LOG_OUTPUT_FILE || logger->config.output == LOG_OUTPUT_BOTH)
        {
            if (logger->log_fd > 0)
            {
                close(logger->log_fd);
                logger->log_fd = -1;
            }
        }

        free(logger);
        logger = NULL;
    }
}

int log_check_level(struct logger *logger, enum log_level level)
{
    if (logger)
    {
        return level >= logger->config.level;
    }
    else
    {
        return 0;
    }
}

void log_reload_level(struct logger *logger)
{
    struct log_config config = {};
    if (config_parse(&config, logger->config_file) == 0)
    {
        logger->config.level = config.level;
        fprintf(stderr, "(logger) logger level reload to %s\n", level_str[config.level]);
    }
    else
    {
        fprintf(stderr, "(logger) logger level reload failed\n");
    }
}

void log_print(struct logger *logger, enum log_level level, const char *module, const char *fmt, ...)
{
    int nwrite;
    char buf[4096] = {0};
    char *p = buf;
    char *end = buf + sizeof(buf);
    va_list args;
    struct tm local;

    local_time(&local);
    // add time
    p += snprintf(p, end - p, "%s %s %d %02d:%02d:%02d %d ",
                  weekday_str[local.tm_wday], month_str[local.tm_mon], local.tm_mday, local.tm_hour, local.tm_min, local.tm_sec, local.tm_year + 1900);
    // add tid
    p += snprintf(p, end - p, "%lu ", pthread_self());
    // add level
    p += snprintf(p, end - p, "%s ", level_str[level]);
    // add module
    p += snprintf(p, end - p, "(%s), ", module);
    // add content
    va_start(args, fmt);
    p += vsnprintf(p, end - p, fmt, args);
    va_end(args);
    // add end of line
    p += snprintf(p, end - p, "\n");

    if (logger->config.output == LOG_OUTPUT_STDERR || logger->config.output == LOG_OUTPUT_BOTH)
    {
        fprintf(stderr, "%s", buf);
    }

    if (logger->config.output == LOG_OUTPUT_FILE || logger->config.output == LOG_OUTPUT_BOTH)
    {
        if (logger->log_file_opened_day != local.tm_mday)
        {
            log_file_reopen(logger);
        }
        do
        {
            nwrite = write(logger->log_fd, buf, p - buf);
        } while (nwrite == -1 && errno == EINTR);
    }
}