#include <stddef.h>
#include <stdint.h>

#include "../core_runtime/core_runtime_private.h"
#include "coduo_engine_structs.h"
#include "script_compile_private.h"
#include "script_memory_private.h"
#include "script_source_positions_private.h"
#include "script_variable_private.h"

enum {
    SCRIPT_CODEGEN_MODE_INTERN_STRINGS = 0,
    SCRIPT_CODEGEN_MODE_RECORD_STRING_FIXUPS = 1,
    SCRIPT_CODEGEN_MODE_RELEASE_STRINGS = 2,
    SCRIPT_OPCODE_END = 0,
    SCRIPT_OPCODE_FUNCTION_PARAMETERS_DONE = 0x1f,
    SCRIPT_STRING_USAGE_FUNCTION = 2,
    SCRIPT_FUNCTION_LOCAL_SLOT_WIDTH = 0x20,
    SCRIPT_FUNCTION_OPERAND_STACK_LIMIT = 0x7ff
};

void EmitFormalParameterListRef(coduo_scr_ast_list_t *parameters,
                                uint32_t sourcePos)
{
    EmitFormalParameterListRefInternal(parameters->head);
    EmitOpcode(SCRIPT_OPCODE_FUNCTION_PARAMETERS_DONE, 0, 0);
    AddOpcodePos(sourcePos);
}

void SpecifyThread(coduo_scr_ast_node_t *node)
{
    if (node->kind == SCR_AST_KIND_FUNCTION_DEFINITION) {
        uint16_t functionName =
            SCR_AST_STRING_HANDLE(node->payload.functionDefinition.name);
        uint16_t functionObject =
            GetObject(GetVariable(
                script_currentFunctionRoot,
                functionName));
        SpecifyThreadPosition(functionObject,
                              node->payload.functionDefinition.sourcePos);
        return;
    }

    if (node->kind == SCR_AST_KIND_DEVELOPER_FUNCTION_DEFINITION) {
        uint16_t functionName =
            SCR_AST_STRING_HANDLE(node->payload.functionDefinition.name);
        uint16_t functionObject =
            GetObject(GetVariable(
                script_currentFunctionRoot,
                functionName));
        SpecifyDeveloperThreadPosition(
            functionObject, node->payload.functionDefinition.sourcePos);
    }
}

void EmitThreadInternal(coduo_scr_ast_node_t *node,
                        uint32_t sourcePos)
{
    AddOpcodePos(sourcePos);
    script_codeStackDepth = 0;
    script_codeMaxStackDepth = 0;
    script_codeMaxLocalDepth = 0;
    CompileTransferRefToString(
        SCR_AST_STRING_HANDLE(node->payload.functionDefinition.name),
                               SCRIPT_STRING_USAGE_FUNCTION);
    EmitFormalParameterListRef(node->payload.functionDefinition.parameters,
                               sourcePos);
    EmitStatementList(node->payload.functionDefinition.body);
    EmitOpcode(SCRIPT_OPCODE_END, 0, 0);

    if (SCRIPT_FUNCTION_OPERAND_STACK_LIMIT <
        script_codeMaxLocalDepth * SCRIPT_FUNCTION_LOCAL_SLOT_WIDTH +
            script_codeMaxStackDepth) {
        CompileError(sourcePos, "function exceeds operand stack size");
    }
}

void EmitNormalThread(coduo_scr_ast_node_t *node,
                      uint32_t sourcePos)
{
    uint16_t functionName =
        SCR_AST_STRING_HANDLE(node->payload.functionDefinition.name);
    uint16_t functionObject =
        FindObject(FindVariable(
            script_currentFunctionRoot, functionName));

    SetThreadPosition(functionObject,
                      node->payload.functionDefinition.sourcePos);
    EmitThreadInternal(node, sourcePos);
}

void EmitDeveloperThread(coduo_scr_ast_node_t *node,
                         uint32_t sourcePos)
{
    if (script_codegenMode != SCRIPT_CODEGEN_MODE_INTERN_STRINGS) {
        CompileError(sourcePos, "cannot recurse /#");
    }

    script_codeRelocationStart = TempMalloc(0);
    uint32_t savedChecksum = script_codeChecksum;

    if (script_runtimeDeveloperScriptFlag == 0) {
        script_codegenMode = SCRIPT_CODEGEN_MODE_RELEASE_STRINGS;
        EmitThreadInternal(node, sourcePos);
    } else {
        script_codegenMode = SCRIPT_CODEGEN_MODE_RECORD_STRING_FIXUPS;

        uint16_t functionName =
            SCR_AST_STRING_HANDLE(node->payload.functionDefinition.name);
        uint16_t functionObject =
            FindObject(FindVariable(
                script_currentFunctionRoot,
                functionName));
        SetDeveloperThreadPosition(
            functionObject, node->payload.functionDefinition.sourcePos);
        EmitThreadInternal(node, sourcePos);
        Scr_TransferToDeveloperBuffer();
    }

    script_codegenMode = SCRIPT_CODEGEN_MODE_INTERN_STRINGS;
    TempMemorySetPos(script_codeRelocationStart);
    script_codeChecksum = savedChecksum;
}

void EmitThread(coduo_scr_ast_node_t *node,
                uint32_t sourcePos)
{
    if (node->kind == SCR_AST_KIND_DEVELOPER_FUNCTION_DEFINITION) {
        EmitDeveloperThread(node, sourcePos);
        return;
    }

    if (node->kind == SCR_AST_KIND_FUNCTION_DEFINITION) {
        EmitNormalThread(node, sourcePos);
        return;
    }

    if (node->kind == SCR_AST_KIND_USING_ANIMTREE) {
        uint16_t animTreeNameHandle =
            SCR_AST_STRING_HANDLE(node->payload.usingAnimTree.name);
        const char *animTreeName =
            SL_ConvertToString(animTreeNameHandle);
        Scr_UsingTree(animTreeName,
                                   node->payload.usingAnimTree.sourcePos);
        CompileRemoveRefToString(animTreeNameHandle);
    }
}

void EmitThreadList(coduo_scr_ast_script_entry_block_t *block)
{
    for (coduo_scr_ast_list_item_t *item = block->list->head; item != NULL;
         item = item->next) {
        SpecifyThread(item->entry->node);
    }

    for (coduo_scr_ast_list_item_t *item = block->list->head; item != NULL;
         item = item->next) {
        EmitThread(item->entry->node, item->entry->sourcePos);
    }
}

void ScriptCompile(coduo_scr_ast_node_t *script,
                   uint16_t currentFunctionRoot)
{
    script_currentFunctionRoot = currentFunctionRoot;
    script_codeEmitCursor = TempMalloc(0);
    script_codeOwnsStrings = qfalse;
    script_activeAnimTreeHandle = 0;
    script_codegenMode = SCRIPT_CODEGEN_MODE_INTERN_STRINGS;
    script_codeNeedsDeferredCheck = qfalse;

    int32_t pendingLoadCapacity = script_pendingScriptLoadCount;
    coduo_scr_script_load_record_t *pendingLoads = NULL;
    if (pendingLoadCapacity != 0) {
        pendingLoads = __builtin_alloca((size_t)pendingLoadCapacity *
                                        sizeof(*pendingLoads));
    }
    script_pendingScriptLoadCursor = pendingLoads;

    EmitOpcode(SCRIPT_OPCODE_END, 0, 0);
    EmitThreadList(script->payload.scriptRoot.entries);
    script_codeSize = (size_t)(TempMalloc(0) - script_codeBase);
    AdjustFunctionAddresses();
    Hunk_CommitTempMemory();
    Hunk_ClearTempMemoryHigh();

    int32_t pendingLoadCount = script_pendingScriptLoadCount;
    for (int32_t index = 0; index < pendingLoadCount; ++index) {
        const char *filename =
            SL_ConvertToString(pendingLoads[index].filename);

        if (Scr_LoadScript(filename) == qfalse) {
            const char *errorFilename =
                SL_ConvertToString(pendingLoads[index].filename);
            CompileError(pendingLoads[index].sourcePos,
                                  "Could not find script '%s'", errorFilename);
        }

        SL_RemoveRefToString(pendingLoads[index].filename);
    }
}
