#include "core_runtime_private.h"
#include "coduo_x87emu.h" /* defines EMULATE_X87; x87 shim when it is 1 */

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

enum {
    COM_NAME_VALUE_DECIMAL_BASE = 10,
    COM_NAME_VALUE_DIGIT_ZERO = '0',
    COM_NAME_VALUE_DIGIT_NINE = '9',
    COM_NAME_VALUE_DECIMAL_POINT = '.',
    COM_NAME_VALUE_HEADER_EXTRA_NUL = 1,
    COM_OPEN_LOG_FILENAME_COPY_SIZE = 1024
};

#define COM_DEBUG_ZONE_ALLOC_MAGIC UINT32_C(0x12345678)
#define COM_DEBUG_HUNK_ALLOC_MAGIC UINT32_C(0x87654321)

typedef struct com_debug_alloc_header_s {
    uint32_t magic;
#if UINTPTR_MAX > UINT32_MAX
    /* NATIVE64_ADAPTATION: stock advances four bytes on i386.  A four-byte
     * prefix misaligns the pointer-bearing records returned by these allocators
     * on 64-bit hosts, so retain one pointer-width prefix there. */
    uint32_t nativePointerAlignmentPadding;
#endif
    uint8_t payload[];
} com_debug_alloc_header_t;

coduo_engine_name_value_t *com_nameValueList;
coduo_com_open_log_filename_t com_openLogFilename;
FILE *com_openLogFile;

float Com_ParseNameValueFloat(const char *text)
{
    float value = 0.0f;
    int32_t divisor = 0;

    while (*text != '\0') {
        if (*text < COM_NAME_VALUE_DIGIT_ZERO ||
            *text > COM_NAME_VALUE_DIGIT_NINE) {
            if (divisor == 0 && *text == COM_NAME_VALUE_DECIMAL_POINT) {
                divisor = COM_NAME_VALUE_DECIMAL_BASE;
                ++text;
                continue;
            }

            return 0.0f;
        }

        int32_t digit = *text - COM_NAME_VALUE_DIGIT_ZERO;
        if (divisor != 0) {
            /* value + fild(digit)/fild(divisor), one float store -> shim. */
#if EMULATE_X87
            value = x87f_store_f32(x87f_add(
                x87f_load_f32(value),
                x87f_div(x87f_load_i32(digit), x87f_load_i32(divisor))));
#else
            value = (float)((long double)value +
                            ((long double)digit / (long double)divisor));
#endif
            /* ORIGINAL_BINARY_BUG: after enough fractional digits this signed
             * decimal divisor wraps and later digits use unrelated or negative
             * denominators.  -fwrapv preserves the stock ADD/shift graph; see
             * original-bugs/engine-name-value-fractional-divisor-wrap.md. */
            divisor *= COM_NAME_VALUE_DECIMAL_BASE;
        } else {
            /* value * 10.0(double) + fild(digit), one float store -> shim. */
#if EMULATE_X87
            value = x87f_store_f32(x87f_add(
                x87f_mul(x87f_load_f32(value),
                         x87f_load_f64((double)COM_NAME_VALUE_DECIMAL_BASE)),
                x87f_load_i32(digit)));
#else
            value = (float)(((long double)value *
                              (double)COM_NAME_VALUE_DECIMAL_BASE) +
                             (long double)digit);
#endif
        }

        ++text;
    }

    return value;
}

void *Com_ZoneDebugAlloc(size_t size)
{
    com_debug_alloc_header_t *header;

    header = Z_Malloc(sizeof(*header) + size);
    if (header == NULL) {
        return NULL;
    }

    header->magic = COM_DEBUG_ZONE_ALLOC_MAGIC;
    return header->payload;
}

void *Com_ZoneDebugAllocClear(size_t size)
{
    void *ptr = Com_ZoneDebugAlloc(size);

    memset(ptr, 0, size);
    return ptr;
}

void *Com_HunkDebugAlloc(size_t size)
{
    com_debug_alloc_header_t *header;

    header = Hunk_Alloc(sizeof(*header) + size);
    if (header == NULL) {
        return NULL;
    }

    header->magic = COM_DEBUG_HUNK_ALLOC_MAGIC;
    return header->payload;
}

void *Com_HunkDebugAllocClear(size_t size)
{
    void *ptr = Com_HunkDebugAlloc(size);

    memset(ptr, 0, size);
    return ptr;
}

void Com_DebugFree(void *ptr)
{
    com_debug_alloc_header_t *header;

    header = ((com_debug_alloc_header_t *)ptr) - 1;
    if (header->magic == COM_DEBUG_ZONE_ALLOC_MAGIC) {
        Z_Free(header);
    }
}

coduo_engine_name_value_t *Com_AllocNameValue(const char *name)
{
    size_t nameLength = strlen(name) + COM_NAME_VALUE_HEADER_EXTRA_NUL;
    coduo_engine_name_value_t *entry =
        Com_ZoneDebugAlloc(sizeof(*entry) + nameLength);

    memset(entry, 0, sizeof(*entry));
    entry->name = entry->inlineName;
    strcpy(entry->name, name);
    entry->next = com_nameValueList;
    com_nameValueList = entry;

    return entry;
}

void Com_FreeNameValue(coduo_engine_name_value_t *entry)
{
    if (entry->stringValue != NULL) {
        Com_DebugFree(entry->stringValue);
    }

    Com_DebugFree(entry);
}

void Com_ClearNameValues(void)
{
    coduo_engine_name_value_t *entry = com_nameValueList;

    while (entry != NULL) {
        com_nameValueList = com_nameValueList->next;
        Com_FreeNameValue(entry);
        entry = com_nameValueList;
    }

    com_nameValueList = NULL;
}

coduo_engine_name_value_t *Com_FindNameValue(const char *name)
{
    for (coduo_engine_name_value_t *entry = com_nameValueList; entry != NULL;
         entry = entry->next) {
        if (Q_stricmp(entry->name, name) == 0) {
            return entry;
        }
    }

    return NULL;
}

const char *Com_GetNameValueString(const char *name)
{
    coduo_engine_name_value_t *entry = Com_FindNameValue(name);

    if (entry != NULL) {
        return entry->stringValue;
    }

    return "";
}

float Com_GetNameValueFloat(const char *name)
{
    coduo_engine_name_value_t *entry = Com_FindNameValue(name);

    if (entry != NULL) {
        return entry->floatValue;
    }

    return 0.0f;
}

coduo_engine_name_value_t *Com_GetOrCreateNameValue(const char *name,
                                                    const char *value)
{
    coduo_engine_name_value_t *entry = Com_FindNameValue(name);

    if (entry != NULL) {
        return entry;
    }

    entry = Com_AllocNameValue(name);
    entry->stringValue =
        Com_ZoneDebugAlloc(strlen(value) + 1);
    strcpy(entry->stringValue, value);
    entry->floatValue = Com_ParseNameValueFloat(entry->stringValue);
    entry->modified = qtrue;

    return entry;
}

const char *Com_GetOrCreateNameValueString(const char *name,
                                           const char *value)
{
    coduo_engine_name_value_t *entry =
        Com_GetOrCreateNameValue(name, value);

    return entry->stringValue;
}

float Com_GetOrCreateNameValueFloat(const char *name, const char *value)
{
    coduo_engine_name_value_t *entry =
        Com_GetOrCreateNameValue(name, value);

    return entry->floatValue;
}

void Com_SetNameValueString(const char *name, const char *value)
{
    coduo_engine_name_value_t *entry = Com_FindNameValue(name);

    if (entry != NULL) {
        Com_DebugFree(entry->stringValue);
    } else {
        entry = Com_AllocNameValue(name);
    }

    entry->stringValue =
        Com_ZoneDebugAlloc(strlen(value) + 1);
    strcpy(entry->stringValue, value);
    entry->floatValue = Com_ParseNameValueFloat(entry->stringValue);
    entry->modified = qtrue;
}

qboolean Com_GetNameValueModified(const char *name)
{
    coduo_engine_name_value_t *entry = Com_FindNameValue(name);

    if (entry != NULL) {
        return entry->modified;
    }

    return qfalse;
}

void Com_ClearNameValueModified(const char *name)
{
    coduo_engine_name_value_t *entry = Com_FindNameValue(name);

    if (entry != NULL) {
        entry->modified = qfalse;
    }
}

void Com_OpenLog(const char *filename)
{
    if (filename == NULL || filename[0] == '\0') {
        Com_Printf("openlog <filename>\n");
        return;
    }

    if (com_openLogFile != NULL) {
        Com_Printf("^1Error: log file %s is already opened\n",
                   com_openLogFilename);
        return;
    }

    com_openLogFile = fopen(filename, "wb");
    if (com_openLogFile == NULL) {
        Com_Printf("^1Error: can't open the log file %s\n", filename);
        return;
    }

    /* ORIGINAL_BINARY_BUG: stock copies exactly the complete 1024-byte object
     * without forcing a terminator.  Later %s diagnostics can read beyond it;
     * see original-bugs/engine-openlog-filename-unterminated.md. */
    strncpy(com_openLogFilename, filename, COM_OPEN_LOG_FILENAME_COPY_SIZE);
    Com_Printf("Opened log %s\n", com_openLogFilename);
}

void Com_OpenLogIfEnabled(const char *filename)
{
    if (Com_GetOrCreateNameValueFloat("log", "0") != 0.0f) {
        Com_OpenLog(filename);
    }
}

void Com_CloseLog(void)
{
    if (com_openLogFile == NULL) {
        return;
    }

    if (fclose(com_openLogFile) != 0) {
        Com_Printf("^1Error: can't close log file %s\n", com_openLogFilename);
        return;
    }

    com_openLogFile = NULL;
    Com_Printf("Closed log %s\n", com_openLogFilename);
}

void Com_CloseLogIfOpen(void)
{
    if (com_openLogFile != NULL) {
        Com_CloseLog();
    }
}

void Com_LogPrintf(const char *format, ...)
{
    va_list ap;

    if (com_openLogFile == NULL) {
        return;
    }

    va_start(ap, format);
    vfprintf(com_openLogFile, format, ap);
    va_end(ap);
    fflush(com_openLogFile);
}

FILE *Com_LogFile(void)
{
    return com_openLogFile;
}

void Com_FlushLog(void)
{
    if (com_openLogFile != NULL) {
        fflush(com_openLogFile);
    }
}
