#include <ctype.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <strings.h>

#include "../core_memory/core_memory_private.h"
#include "../core_runtime/core_runtime_private.h"
#include "../filesystem/fs_private.h"
#include "coduo_ctype_compat.h"
#include "coduo_engine_structs.h"
#include "script_compile_private.h"
#include "script_memory_private.h"
#include "script_variable_private.h"

enum {
    SCRIPT_IMPORT_FIELD_FLOAT = 4,
    SCRIPT_IMPORT_FIELD_INT = 5,
    SCRIPT_IMPORT_FIELD_STRING = 1,
    SCRIPT_IMPORT_FIELD_QPATH_BUFFER_SIZE = 64,
    SCRIPT_IMPORT_FIELD_RECORD_TERMINATOR_BYTES = 1,
    SCRIPT_IMPORT_FIELD_ERROR_DROP = 1,
    SCRIPT_IMPORT_FIELD_NOT_FOUND = 0
};

/* NOT_FROM_ORIGINAL_SOURCE: local reader for packed field records. */
static uint8_t *ScriptImport_FieldRecordValue(uint8_t *cursor,
                                              uint16_t *offsetOut,
                                              int32_t *typeOut)
{
    memcpy(offsetOut, cursor, sizeof(*offsetOut));
    cursor += sizeof(*offsetOut);
    *typeOut = (int8_t)*cursor;
    cursor += sizeof(int8_t);
    return cursor;
}

/* NOT_FROM_ORIGINAL_SOURCE: local writer for packed field records. */
static uint8_t *ScriptImport_WriteFieldRecordValue(
    uint8_t *cursor, uint16_t offset, int32_t fieldType)
{
    uint16_t storedOffset = (uint16_t)offset;

    memcpy(cursor, &storedOffset, sizeof(storedOffset));
    cursor += sizeof(storedOffset);
    *cursor = (uint8_t)fieldType;
    cursor += sizeof(uint8_t);
    /* ORIGINAL_BINARY_BUG: stock 0x080aa471..0x080aa482 writes this sentinel
     * one byte beyond the extent just requested from TempMalloc. */
    *cursor = '\0';
    return cursor;
}

uint16_t Scr_FindField(const char *name,
                                                      int32_t *typeOut)
{
    uint8_t *cursor = script_importFieldBuffer;

    while (*cursor != '\0') {
        size_t nameSize = strlen((const char *)cursor) + 1;

        if (strcasecmp(name, (const char *)cursor) != 0) {
            cursor += nameSize + sizeof(uint16_t) + sizeof(int8_t);
            continue;
        }

        uint16_t offset;
        ScriptImport_FieldRecordValue(cursor + nameSize, &offset, typeOut);
        return offset;
    }

    return SCRIPT_IMPORT_FIELD_NOT_FOUND;
}

/* NOT_FROM_ORIGINAL_SOURCE: local mapping for field type parse tokens. */
static int32_t ScriptImport_FieldTypeForToken(const char *token,
                                              const char *filename)
{
    if (strcmp(token, "float") == 0) {
        return SCRIPT_IMPORT_FIELD_FLOAT;
    }

    if (strcmp(token, "int") == 0) {
        return SCRIPT_IMPORT_FIELD_INT;
    }

    if (strcmp(token, "string") == 0) {
        return SCRIPT_IMPORT_FIELD_STRING;
    }

    Com_Error(SCRIPT_IMPORT_FIELD_ERROR_DROP,
              va("\x15" "unknown type '%s' in '%s'", token, filename));
    return SCRIPT_IMPORT_FIELD_NOT_FOUND;
}

/* NOT_FROM_ORIGINAL_SOURCE: local in-place lowercase helper. */
static void ScriptImport_LowercaseToken(char *token)
{
    size_t nameSize = strlen(token) + 1;

    for (int32_t index = (int32_t)nameSize - 1; index >= 0; --index) {
        token[index] =
            (char)tolower(coduo_ctype_signed_byte_arg(token[index]));
    }
}

static void Scr_AddFieldsForFile(const char *filename)
{
    int32_t fileHandle;
    int32_t fileLength;
    char *fileText;
    char *parse;

    fileLength = FS_FOpenFileByMode(filename, &fileHandle, CODUO_FS_READ);
    if (fileLength < 0) {
        Com_Error(SCRIPT_IMPORT_FIELD_ERROR_DROP,
                  va("\x15" "cannot find '%s'", filename));
    }

    /* Stock passes the wrapped dword fileLength + 1 allocation extent. */
    fileText = Hunk_AllocateTempMemoryHighInternal(
        (size_t)(uint32_t)(fileLength + 1));
    FS_Read(fileText, fileLength, fileHandle);
    fileText[fileLength] = '\0';
    FS_FCloseFile(fileHandle);

    parse = fileText;
    Com_BeginParseSession("Scr_AddFields");

    for (;;) {
        char *token = Com_Parse(&parse);

        if (parse == NULL) {
            Com_EndParseSession();
            Hunk_ClearTempMemoryHigh();
            return;
        }

        int32_t fieldType = ScriptImport_FieldTypeForToken(token, filename);
        if (fieldType == SCRIPT_IMPORT_FIELD_NOT_FOUND) {
            Com_EndParseSession();
            Hunk_ClearTempMemoryHigh();
            return;
        }

        token = Com_Parse(&parse);
        if (parse == NULL) {
            Com_Error(SCRIPT_IMPORT_FIELD_ERROR_DROP,
                      va("\x15" "missing field name in '%s'", filename));
        }

        ScriptImport_LowercaseToken(token);
        uint16_t offset =
            SL_FindCanonicalString(token);
        if (offset == 0) {
            continue;
        }

        int32_t existingType;
        if (Scr_FindField(token, &existingType) != 0) {
            Com_Error(SCRIPT_IMPORT_FIELD_ERROR_DROP,
                      "\x15" "duplicate key '%s' in '%s'", token, filename);
        }

        size_t nameSize = strlen(token) + 1;
        uint8_t *fieldRecord = TempMalloc(
            nameSize + sizeof(uint16_t) + sizeof(int8_t));

        strcpy((char *)fieldRecord, token);
        ScriptImport_WriteFieldRecordValue(fieldRecord + nameSize, offset,
                                           fieldType);
    }
}

void Scr_AddFields(const char *path, const char *extension)
{
    int32_t fileCount;
    char **files = FS_ListFiles(path, extension, &fileCount);

    TempMemoryReset();
    script_importFieldBuffer = Hunk_AllocLow(0);
    /* ORIGINAL_BINARY_BUG: stock 0x080aa4d2..0x080aa4d7 writes the initial
     * sentinel before reserving even one byte at this low-hunk position. */
    script_importFieldBuffer[0] = '\0';

    for (int32_t fileIndex = 0; fileIndex < fileCount; ++fileIndex) {
        char qpath[SCRIPT_IMPORT_FIELD_QPATH_BUFFER_SIZE];

        /* ORIGINAL_BINARY_BUG: stock 0x080aa4fb passes this 64-byte array to
         * unbounded sprintf. */
        sprintf(qpath, "%s/%s", path, files[fileIndex]);
        Scr_AddFieldsForFile(qpath);
    }

    if (files != NULL) {
        FS_FreeFileList(files);
    }

    uint8_t *terminator =
        TempMalloc(SCRIPT_IMPORT_FIELD_RECORD_TERMINATOR_BYTES);
    *terminator = '\0';
    Hunk_CommitTempMemory();
}

void Scr_FreeValue(uint16_t handle)
{
    RemoveRefToObject(handle);
}
