#include "config.h"
#include "fsutils.h"
#include "runtime.h"
#include "threading.h"

#include "aio.h"
#include "buffer_manager.h"
#include "file_tab.h"
#include "mem_arena.h"
#include "string_storage.h"

#include <ini.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>

#define RT_MAX_CVARS          1024
#define RT_MAX_EVENT_HANDLERS 1024

typedef struct rt_cvar_change_event_list {
    rt_cvar_change_event_handler_fn *Handler;
    void *userdata;
    struct rt_cvar_change_event_list *next;
} rt_cvar_change_event_list;

typedef struct {
    rt_cvar *cvar;
    rt_cvar_change_event_list *change_event_handlers;
} rt_cvar_container;

static rt_cvar_container _vars[RT_MAX_CVARS];
static unsigned int _next_cvar = 0;
static rt_cvar_change_event_list _change_event_handlers[RT_MAX_EVENT_HANDLERS];
static unsigned int _next_event_handler = 0;
static rt_mutex *_mutex                 = NULL;

RT_DLLEXPORT void rtRegisterCVAR(rt_cvar *cvar) {
    if (!_mutex)
        _mutex = rtCreateMutex();
    rtLockMutex(_mutex);
    if (_next_cvar < RT_MAX_CVARS) {
        rtLog("CVAR", "Registered cvar %s", cvar->name);
        _vars[_next_cvar].cvar                  = cvar;
        _vars[_next_cvar].change_event_handlers = NULL;
        ++_next_cvar;
    } else {
        rtReportError("CVAR", "Ran out of space for CVars");
    }
    rtUnlockMutex(_mutex);
}

RT_DLLEXPORT rt_cvar *rtGetCVAR(const char *name) {
    if (!_mutex)
        _mutex = rtCreateMutex();
    rt_cvar *var = NULL;
    rtLockMutex(_mutex);
    for (unsigned int i = 0; i < _next_cvar; ++i) {
        if (strcmp(name, _vars[i].cvar->name) == 0) {
            var = _vars[i].cvar;
            break;
        }
    }
    rtUnlockMutex(_mutex);
    return var;
}

RT_DLLEXPORT void rtRegisterCVARChangeEventHandler(rt_cvar *cvar,
                                                   rt_cvar_change_event_handler_fn *event_handler,
                                                   void *userdata) {
    RT_VERIFY(_mutex);
    rtLockMutex(_mutex);
    if (_next_event_handler == RT_MAX_EVENT_HANDLERS) {
        rtReportError("CVAR", "Ran out of space for CVar change event handlers");
        rtUnlockMutex(_mutex);
        return;
    }

    for (unsigned int i = 0; i < _next_cvar; ++i) {
        if (_vars[i].cvar == cvar) {
            rt_cvar_change_event_list *entry = &_change_event_handlers[_next_event_handler++];
            entry->Handler                   = event_handler;
            entry->next                      = _vars[i].change_event_handlers;
            entry->userdata                  = userdata;
            _vars[i].change_event_handlers   = entry;
        }
    }
    rtUnlockMutex(_mutex);
}

RT_DLLEXPORT void rtNotifyCVARChange(const rt_cvar *cvar) {
#ifndef RT_DONT_LOG_CVAR_CHANGES
    switch (cvar->type) {
    case RT_CVAR_TYPE_INT:
        rtLog("CVAR", "Changed %s to %d.", cvar->name, cvar->i);
        break;
    case RT_CVAR_TYPE_FLOAT:
        rtLog("CVAR", "Changed %s to %f.", cvar->name, (double)cvar->f);
        break;
    case RT_CVAR_TYPE_STRING:
        rtLog("CVAR", "Changed %s to '%s'.", cvar->name, cvar->s);
        break;
    case RT_CVAR_TYPE_SIZE:
        rtLog("CVAR", "Changed %s to %zu.", cvar->name, cvar->sz);
        break;
    default:
        rtLog("CVAR", "Changed %s, but the cvar has an invalid type.", cvar->name);
        break;
    }
#endif

    RT_VERIFY(_mutex);
    rtLockMutex(_mutex);
    for (unsigned int i = 0; i < _next_cvar; ++i) {
        if (_vars[i].cvar == cvar) {
            rt_cvar_change_event_list *entry = _vars[i].change_event_handlers;
            rtUnlockMutex(_mutex);

            while (entry) {
                /* We don't want to hold the mutex while the event handler runs,
                 * because that would prevent any calls to CVar functions. */
                entry->Handler(_vars[i].cvar, entry->userdata);

                rtLockMutex(_mutex);
                entry = entry->next;
                rtUnlockMutex(_mutex);
            }
            return;
        }
    }
    /* Not found... */
    rtUnlockMutex(_mutex);
}

static int Handler(void *user, const char *section, const char *name, const char *value) {
    /* We currently ignore sections. They could be used to categorize variables by system? */
    RT_UNUSED(section);
    const char *file_path = user;

    rt_cvar *cvar = rtGetCVAR(name);
    if (!cvar) {
        /* Not a critical error. */
        rtLog("CVAR", "Unknown CVar %s in config file %s.", name, file_path);
        return 1;
    }

    int num_read = 0;
    switch (cvar->type) {
    case RT_CVAR_TYPE_INT:
        num_read = sscanf(value, "%d", &cvar->i);
        break;
    case RT_CVAR_TYPE_FLOAT:
        num_read = sscanf(value, "%f", &cvar->f);
        break;
    case RT_CVAR_TYPE_STRING: {
        num_read   = 1;
        char *copy = rtStoreString(value);
        if (!copy) {
            rtReportError("CVAR",
                          "Failed to store string value of cvar %s in config file %s.",
                          name,
                          file_path);
            return 0;
        }
        cvar->s = copy;
        break;
    }
    case RT_CVAR_TYPE_SIZE:
        num_read = sscanf(value, "%zu", &cvar->sz);
        break;
    default:
        rtReportError("CVAR", "CVar %s has an invalid type.", cvar->name);
        return 0;
    }

    if (num_read == 1) {
        rtNotifyCVARChange(cvar);
    } else {
        rtLog("CVAR", "Failed to read value of CVar %s in config file %s.", cvar->name, file_path);
    }

    return 1;
}

/* Config files are ini files */
static rt_result ProcessConfigFile(const void *buffer, size_t fsz, const char *file_path) {
    rt_result result = RT_SUCCESS;
    if (ini_parse_string(buffer, Handler, (void *)file_path) < 0) {
        rtLog("CVAR", "Failed to parse config file '%s'.", file_path);
        return RT_UNKNOWN_ERROR;
    }
    return result;
}

RT_DLLEXPORT rt_result rtProcessConfigFiles(unsigned int count, const rt_file_id *fids) {
    if (count > RT_LOAD_BATCH_MAX_SIZE) {
        rtReportError("CVAR",
                      "Got %u config files, but the maximum number of config files is %u.",
                      count,
                      RT_LOAD_BATCH_MAX_SIZE);
        return RT_INVALID_VALUE;
    }

    typedef struct {
        void *buffer;
        size_t fsz;
        const char *path;
    } config_file_buf;

    rt_temp_arena temp = rtGetTemporaryArena(NULL, 0);
    if (!temp.arena)
        return RT_OUT_OF_MEMORY;
    config_file_buf *configs = RT_ARENA_PUSH_ARRAY_ZERO(temp.arena, config_file_buf, count);
    if (!configs) {
        rtReturnTemporaryArena(temp);
        return RT_OUT_OF_MEMORY;
    }
    rt_aio_handle *aios = RT_ARENA_PUSH_ARRAY_ZERO(temp.arena, rt_aio_handle, count);
    if (!configs) {
        rtReturnTemporaryArena(temp);
        return RT_OUT_OF_MEMORY;
    }
    rt_result res = RT_SUCCESS;

    rt_load_batch load_batch;
    load_batch.num_loads = count;
    for (unsigned int i = 0; i < count; ++i) {
        const char *path = rtGetFilePath(fids[i]);
        if (!path) {
            rtReportError("CVAR", "Invalid config file path!");
            res = RT_INVALID_VALUE;
            goto out;
        }
        size_t fsz               = rtGetFileSize(path);
        load_batch.loads[i].dest = rtAllocBuffer(fsz);
        if (!load_batch.loads[i].dest) {
            rtReportError("CVAR", "Failed to allocate memory for config file.");
            res = RT_OUT_OF_MEMORY;
            goto out;
        }
        load_batch.loads[i].num_bytes = fsz;
        load_batch.loads[i].offset    = 0;
        load_batch.loads[i].file      = fids[i];

        configs[i].buffer = load_batch.loads[i].dest;
        configs[i].fsz    = fsz;
        configs[i].path   = path;
    }

    if ((res = rtSubmitLoadBatch(&load_batch, aios)) != RT_SUCCESS) {
        goto out;
    }

    for (unsigned int i = 0; i < count; ++i) {
        rt_aio_state state = rtWaitForAIOCompletion(aios[i]);
        if (state == RT_AIO_STATE_FINISHED) {
            res = ProcessConfigFile(configs[i].buffer, configs[i].fsz, configs[i].path);
            if (res != RT_SUCCESS)
                goto out;
        } else {
            RT_ASSERT(state == RT_AIO_STATE_FAILED, "Unexpected aio state.");
            res = RT_UNKNOWN_ERROR;
            goto out;
        }
    }

out:
    for (unsigned int i = 0; i < count; ++i) {
        if (configs[i].buffer)
            rtReleaseBuffer(configs[i].buffer, configs[i].fsz);
        if (aios[i] != RT_AIO_INVALID_HANDLE)
            rtReleaseAIO(aios[i]);
    }
    rtReturnTemporaryArena(temp);
    return res;
}

/* This gets called before _any_ engine systems are available (especially AIO, ftab and
 * Buffermanager) */
void ProcessEarlyEngineConfigs(void) {
    const char *engine_cfgs[] = {"cfg/engine_early.cfg"};
    void *buf                 = NULL;
    size_t buf_sz             = 0;

    for (unsigned int i = 0; i < RT_ARRAY_COUNT(engine_cfgs); ++i) {
        if (!rtDoesFileExist(engine_cfgs[i])) {
            rtLog("CVAR", "Could not find early engine config file %s", engine_cfgs[i]);
            continue;
        }
        size_t fsz = rtGetFileSize(engine_cfgs[i]);
        if ((fsz + 1) > buf_sz) {
            buf    = realloc(buf, fsz + 1);
            buf_sz = fsz + 1;
        }
        if (!rtSyncReadWholeFile(engine_cfgs[i], buf, buf_sz)) {
            rtLog("CVAR", "Failed to read early engine config file %s", engine_cfgs[i]);
            continue;
        }
        ((char *)buf)[fsz] = '\0';
        if (ProcessConfigFile(buf, fsz, engine_cfgs[i]) != RT_SUCCESS) {
            rtLog("CVAR", "Failed to process early engine config file %s", engine_cfgs[i]);
        }
    }
    free(buf);
}