#include <stdint.h>
#include <string.h>

#include "../core_memory/core_memory_private.h"
#include "../core_runtime/core_runtime_private.h"
#include "../filesystem/fs_private.h"
#include "coduo_engine_structs.h"
#include "script_compile_private.h"
#include "script_source_positions_private.h"

enum {
    SCRIPT_SOURCE_POS_INITIAL_CAPACITY = 0x10000,
    SCRIPT_SOURCE_FILE_INITIAL_CAPACITY = 0x10,
    SCRIPT_CODEGEN_MODE_RELEASE_STRINGS = 2,
    SCRIPT_CODEGEN_MODE_RELOCATED = 1
};

typedef void (*script_source_io_fn)(void *buffer, int32_t byteCount);

qboolean Scr_IsInOpcodeMemory(coduo_script_codepos_t codePos)
{
    const uintptr_t codeAddress = (uintptr_t)codePos;
    const uintptr_t codeBaseAddress = (uintptr_t)script_codeBase;

    return codeBaseAddress <= codeAddress &&
                   codeAddress <
                       codeBaseAddress + (uintptr_t)script_codeSize
               ? qtrue
               : qfalse;
}

qboolean Scr_IsInDeveloperOpcodeMemory(coduo_script_codepos_t codePos)
{
    const uintptr_t codeAddress = (uintptr_t)codePos;

    return (uintptr_t)script_developerOpBuffer <= codeAddress &&
                   codeAddress < (uintptr_t)script_codeRelocationEnd
               ? qtrue
               : qfalse;
}

qboolean ScriptCode_IsNormalCodePos(coduo_script_codepos_t codePos)
{
    const uintptr_t codeAddress = (uintptr_t)codePos;

    return (uintptr_t)script_codeBase <= codeAddress &&
                   codeAddress < (uintptr_t)script_codeEnd
               ? qtrue
               : qfalse;
}

void InitOpcodeLookup(void)
{
    if (script_runtimeDebugReportFlag == 0) {
        return;
    }

    for (int32_t tableIndex = 0; tableIndex < SCRIPT_SOURCE_POS_TABLE_COUNT;
         ++tableIndex) {
        script_sourcePosTableCapacity[tableIndex] =
            SCRIPT_SOURCE_POS_INITIAL_CAPACITY;
        script_sourcePosTableCount[tableIndex] = 0;
        script_sourcePosTables[tableIndex] =
            Z_Malloc(script_sourcePosTableCapacity[tableIndex] *
                     sizeof(script_sourcePosTables[tableIndex][0]));
        memset(script_sourcePosTables[tableIndex], 0,
               script_sourcePosTableCapacity[tableIndex] *
                   sizeof(script_sourcePosTables[tableIndex][0]));
    }

    script_sourcePosPoolCapacity = SCRIPT_SOURCE_POS_INITIAL_CAPACITY;
    script_sourcePosPoolCount = 0;
    script_sourcePosPool =
        Z_Malloc(script_sourcePosPoolCapacity *
                 sizeof(script_sourcePosPool[0]));
    script_sourcePosLastCodePos = NULL;
    script_sourcePosCountForLastCodePos = 0;

    script_sourceFileCapacity = SCRIPT_SOURCE_FILE_INITIAL_CAPACITY;
    script_sourceFileCount = 0;
    script_sourceFiles =
        Z_Malloc(script_sourceFileCapacity *
                 sizeof(script_sourceFiles[0]));
}

void ShutdownOpcodeLookup(void)
{
    for (uint32_t tableIndex = 0; tableIndex < SCRIPT_SOURCE_POS_TABLE_COUNT;
         ++tableIndex) {
        if (script_sourcePosTables[tableIndex] != NULL) {
            Z_Free(script_sourcePosTables[tableIndex]);
            script_sourcePosTables[tableIndex] = NULL;
        }
    }

    if (script_sourcePosPool != NULL) {
        Z_Free(script_sourcePosPool);
        script_sourcePosPool = NULL;
    }

    if (script_sourceFiles != NULL) {
        for (uint32_t index = 0; index < script_sourceFileCount; ++index) {
            Z_Free(script_sourceFiles[index].name);
        }
        Z_Free(script_sourceFiles);
        script_sourceFiles = NULL;
    }

    if (script_savedSourceFiles != NULL) {
        for (int32_t index = 0; index < script_savedSourceFileCount; ++index) {
            if (script_savedSourceFiles[index].source != NULL) {
                Z_Free(script_savedSourceFiles[index].source);
            }
        }
        Z_Free(script_savedSourceFiles);
        script_savedSourceFileCount = SCRIPT_SAVED_SOURCE_FILE_COUNT_NONE;
        script_savedSourceFiles = NULL;
    }
}

void AddOpcodePos(uint32_t sourcePos)
{
    if (script_runtimeDebugReportFlag == 0 ||
        script_codegenMode == SCRIPT_CODEGEN_MODE_RELEASE_STRINGS) {
        return;
    }

    uint32_t tableIndex =
        script_codegenMode == SCRIPT_CODEGEN_MODE_RELOCATED ? 1U : 0U;

    if (script_sourcePosTableCapacity[tableIndex] <=
        script_sourcePosTableCount[tableIndex]) {
        uint32_t oldCapacity = script_sourcePosTableCapacity[tableIndex];
        script_sourcePosTableCapacity[tableIndex] *= 2;
        script_source_pos_record_t *newTable = Z_Malloc(
            script_sourcePosTableCapacity[tableIndex] * sizeof(newTable[0]));
        memset(newTable, 0,
               script_sourcePosTableCapacity[tableIndex] *
                   sizeof(newTable[0]));
        Com_Memcpy(newTable, script_sourcePosTables[tableIndex],
                     oldCapacity * sizeof(newTable[0]));
        Z_Free(script_sourcePosTables[tableIndex]);
        script_sourcePosTables[tableIndex] = newTable;
    }

    if (script_sourcePosPoolCapacity <= script_sourcePosPoolCount) {
        uint32_t oldCapacity = script_sourcePosPoolCapacity;
        script_sourcePosPoolCapacity *= 2;
        uint32_t *newPool =
            Z_Malloc(script_sourcePosPoolCapacity *
                     sizeof(newPool[0]));
        Com_Memcpy(newPool, script_sourcePosPool,
                     oldCapacity * sizeof(newPool[0]));
        Z_Free(script_sourcePosPool);
        script_sourcePosPool = newPool;
    }

    if (script_codeLastOpcodePos == script_sourcePosLastCodePos) {
        script_sourcePosTableCount[tableIndex]--;
    } else {
        script_sourcePosCountForLastCodePos = 0;
        script_sourcePosLastCodePos = script_codeLastOpcodePos;
        script_sourcePosTables[tableIndex]
            [script_sourcePosTableCount[tableIndex]]
                .sourcePosIndex = script_sourcePosPoolCount;
    }

    script_source_pos_record_t *record =
        &script_sourcePosTables[tableIndex]
                              [script_sourcePosTableCount[tableIndex]];
    if (script_codegenMode == SCRIPT_CODEGEN_MODE_RELOCATED) {
        record->codePos =
            script_codeLastOpcodePos +
            (script_codeRelocationEnd - script_codeRelocationStart);
    } else {
        record->codePos = script_codeLastOpcodePos;
    }

    script_sourcePosPool[record->sourcePosIndex +
                         script_sourcePosCountForLastCodePos] = sourcePos;
    script_sourcePosTableCount[tableIndex]++;
    script_sourcePosCountForLastCodePos++;
    script_sourcePosPoolCount++;
}

uint32_t GetPrevSourcePos(coduo_script_codepos_t codePos,
                          int32_t sourcePosOffset)
{
    uint32_t tableIndex =
        Scr_IsInOpcodeMemory(codePos) == qfalse ? 1U : 0U;
    int32_t low = 0;
    int32_t high = (int32_t)script_sourcePosTableCount[tableIndex] - 1;

    while (low <= high) {
        int32_t middle = (low + high) / 2;
        if (script_sourcePosTables[tableIndex][middle].codePos < codePos) {
            low = middle + 1;
            if (low == (int32_t)script_sourcePosTableCount[tableIndex] ||
                codePos <=
                    script_sourcePosTables[tableIndex][low].codePos) {
                return script_sourcePosPool
                    [script_sourcePosTables[tableIndex][middle]
                         .sourcePosIndex +
                     sourcePosOffset];
            }
        } else {
            high = middle - 1;
        }
    }

    return 0;
}

qboolean Scr_HasSourceFiles(void)
{
    return script_runtimeDebugReportFlag;
}

void Scr_SaveSource(script_source_io_fn write)
{
    write(&script_sourceFileCount, sizeof(script_sourceFileCount));

    for (uint32_t index = 0; index < script_sourceFileCount; ++index) {
        write(&script_sourceFiles[index].sourceLen,
              sizeof(script_sourceFiles[index].sourceLen));
        if (script_sourceFiles[index].sourceLen > 0) {
            write(script_sourceFiles[index].source,
                  script_sourceFiles[index].sourceLen);
        }
    }
}

void Scr_LoadSource(script_source_io_fn read)
{
    /* ORIGINAL_BINARY_BUG: the source callback controls the signed record
     * count and lengths; stock applies no stream or allocation bounds. */
    read(&script_savedSourceFileCount,
         sizeof(script_savedSourceFileCount));
    script_savedSourceFiles =
        Z_Malloc(script_savedSourceFileCount *
                 sizeof(script_savedSourceFiles[0]));

    for (int32_t index = script_savedSourceFileCount - 1; index >= 0;
         --index) {
        read(&script_savedSourceFiles[index].sourceLen,
             sizeof(script_savedSourceFiles[index].sourceLen));

        if (script_savedSourceFiles[index].sourceLen < 1) {
            script_savedSourceFiles[index].source = NULL;
            continue;
        }

        script_savedSourceFiles[index].source =
            Z_Malloc(script_savedSourceFiles[index].sourceLen);
        read(script_savedSourceFiles[index].source,
             script_savedSourceFiles[index].sourceLen);
    }
}

void Scr_SkipSource(script_source_io_fn read)
{
    int32_t sourceFileCount;

    /* ORIGINAL_BINARY_BUG: stock trusts the callback-supplied count and every
     * positive length passed to the null-destination skip callback. */
    read(&sourceFileCount, sizeof(sourceFileCount));
    for (int32_t index = sourceFileCount - 1; index >= 0; --index) {
        int32_t sourceLen;

        read(&sourceLen, sizeof(sourceLen));
        if (sourceLen > 0) {
            read(NULL, sourceLen);
        }
    }
}

script_source_file_record_t *Scr_GetNewSourceBuffer(void)
{
    if (script_sourceFileCapacity <= script_sourceFileCount) {
        script_sourceFileCapacity *= 2;
        script_source_file_record_t *newFiles =
            Z_Malloc(script_sourceFileCapacity *
                     sizeof(script_sourceFiles[0]));

        Com_Memcpy(newFiles, script_sourceFiles,
                     script_sourceFileCount * sizeof(script_sourceFiles[0]));
        Z_Free(script_sourceFiles);
        script_sourceFiles = newFiles;
    }

    return &script_sourceFiles[script_sourceFileCount++];
}

char *Scr_AddSourceBuffer(const char *filename,
                          uint8_t *normalCodeStart,
                          uint8_t *relocatedCodeStart)
{
    int32_t sourceLen;
    char *loadedSource;

    if (script_savedSourceFiles == NULL) {
        int32_t handle;

        sourceLen = FS_FOpenFileByMode(filename, &handle, CODUO_FS_READ);
        if (sourceLen < 0) {
            goto missing_source;
        }

        loadedSource = Hunk_AllocateTempMemoryHighInternal(sourceLen + 1);
        FS_Read(loadedSource, sourceLen, handle);
        loadedSource[sourceLen] = '\0';
        FS_FCloseFile(handle);
    } else {
        /* ORIGINAL_BINARY_BUG: stock decrements the saved-record count before
         * indexing it and does not check that any records remain. */
        script_savedSourceFileCount--;
        sourceLen =
            script_savedSourceFiles[script_savedSourceFileCount].sourceLen;
        if (sourceLen < 0) {
            goto missing_source;
        }

        loadedSource = Hunk_AllocateTempMemoryHighInternal(sourceLen + 1);
        const char *savedSource =
            script_savedSourceFiles[script_savedSourceFileCount].source;
        for (int32_t index = 0; index < sourceLen; ++index) {
            char ch = savedSource[index];
            loadedSource[index] = ch == '\0' ? '\n' : ch;
        }
        loadedSource[sourceLen] = '\0';

        if (script_savedSourceFiles[script_savedSourceFileCount].source !=
            NULL) {
            Z_Free(script_savedSourceFiles[script_savedSourceFileCount].source);
        }
    }

    if (script_sourceFiles == NULL) {
        script_sourcePos = NULL;
        return loadedSource;
    }

    size_t filenameLen = strlen(filename) + 1;
    size_t trackedLen = (size_t)sourceLen + filenameLen + 2;
    char *tracked = Z_Malloc(trackedLen);
    strcpy(tracked, filename);

    char *trackedSource = tracked + filenameLen;
    for (int32_t index = 0; index <= sourceLen; ++index) {
        char ch = loadedSource[index];
        trackedSource[index] = ch == '\n' ? '\0' : ch;
    }

    script_source_file_record_t *record =
        Scr_GetNewSourceBuffer();
    record->normalCodeStart = normalCodeStart;
    record->relocatedCodeStart = relocatedCodeStart;
    record->name = tracked;
    record->source = trackedSource;
    record->sourceLen = sourceLen;
    script_sourcePos = trackedSource;

    return loadedSource;

missing_source:
    if (script_sourceFiles != NULL) {
        script_source_file_record_t *record =
            Scr_GetNewSourceBuffer();
        record->normalCodeStart = NULL;
        record->relocatedCodeStart = NULL;
        record->name = NULL;
        record->source = NULL;
        record->sourceLen = -1;
    }

    return NULL;
}
