#include <stdint.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_RECORD_STRING_FIXUPS = 1,
    SCRIPT_CODEGEN_MODE_RELEASE_STRINGS = 2,
    SCR_AST_STRING_USAGE_FUNCTION = 2,
    SCR_FUNCTION_REFERENCE_COUNT_CHILD = 0,
    SCR_FUNCTION_DEFINITION_CHILD = 1,
    SCR_FUNCTION_REFERENCE_FIRST_CHILD = 2
};

void EmitArrayVariable(coduo_scr_ast_node_t *objectNode,
                       coduo_scr_ast_node_t *indexNode,
                       uint32_t objectSourcePos,
                       uint32_t indexSourcePos)
{
    EmitExpression(indexNode);
    EmitPrimitiveExpression(objectNode);
    EmitEvalArray(objectSourcePos, indexSourcePos);
}

void EmitArrayVariableRef(coduo_scr_ast_node_t *arrayNode,
                          coduo_scr_ast_node_t *indexNode,
                          uint32_t arraySourcePos,
                          uint32_t indexSourcePos)
{
    EmitExpression(indexNode);
    EmitArrayExpressionRef(arrayNode, arraySourcePos);
    EmitEvalArrayRef(arraySourcePos, indexSourcePos);
}

void EmitClearArrayVariable(coduo_scr_ast_node_t *arrayNode,
                            coduo_scr_ast_node_t *indexNode,
                            uint32_t arraySourcePos,
                            uint32_t indexSourcePos)
{
    EmitExpression(indexNode);
    EmitArrayExpressionRef(arrayNode, arraySourcePos);
    EmitClearArray(arraySourcePos, indexSourcePos);
}

void EmitVariableExpression(coduo_scr_ast_node_t *node)
{
    if (node->kind == SCR_AST_KIND_OBJECT_INDEX_OBJECT_REF) {
        EmitArrayVariable(
            node->payload.objectIndexObjectRef.objectNode,
            node->payload.objectIndexObjectRef.indexNode,
            node->payload.objectIndexObjectRef.objectSourcePos,
            node->payload.objectIndexObjectRef.indexSourcePos);
        return;
    }

    if (node->kind == SCR_AST_KIND_STRING_REF) {
        EmitLocalVariable(
            SCR_AST_STRING_HANDLE(node->payload.stringRef.string));
        return;
    }

    if (node->kind == SCR_AST_KIND_OBJECT_STRING_REF) {
        EmitFieldVariable(
            node->payload.objectStringRef.objectNode,
            SCR_AST_STRING_HANDLE(node->payload.objectStringRef.string),
            node->payload.objectStringRef.sourcePos);
    }
}

int32_t EmitExpressionList(coduo_scr_ast_list_t *list)
{
    int32_t count = 0;

    for (coduo_scr_ast_list_item_t *item = list->head; item != NULL;
         item = item->next) {
        EmitExpression(item->entry->node);
        count++;
    }

    return count;
}

coduo_scr_ast_list_item_t *
GetSingleParameter(coduo_scr_ast_list_t *list)
{
    coduo_scr_ast_list_item_t *item = list->head;

    if (item == NULL || item->next != NULL) {
        return NULL;
    }

    return item;
}

void AddExpressionListOpcodePos(coduo_scr_ast_list_t *list)
{
    if (script_runtimeDebugReportFlag == 0) {
        return;
    }

    for (coduo_scr_ast_list_item_t *item = list->head; item != NULL;
         item = item->next) {
        AddOpcodePos(item->entry->sourcePos);
    }
}

uint16_t
AddFilePrecache(uint16_t filename,
                uint32_t sourcePos)
{
    SL_AddRefToString(filename);

    script_pendingScriptLoadCursor->filename = filename;
    script_pendingScriptLoadCursor->sourcePos = sourcePos;
    script_pendingScriptLoadCursor++;

    uint16_t scriptSlot =
        GetVariable(script_loadScriptHandleRoot, filename);
    return GetObject(scriptSlot);
}

void EmitFunction(coduo_scr_ast_node_t *node,
                  uint32_t sourcePos)
{
    if (script_codegenMode == SCRIPT_CODEGEN_MODE_RELEASE_STRINGS) {
        CompileRemoveRefToString(
            SCR_AST_STRING_HANDLE(node->payload.functionRef.name));
        if (node->kind == SCR_AST_KIND_SCRIPT_FUNCTION_REF) {
            CompileRemoveRefToString(
                SCR_AST_STRING_HANDLE(node->payload.scriptFunctionRef.name));
            script_pendingScriptLoadCount--;
        }
        return;
    }

    uint16_t functionObject = 0;

    if (node->kind == SCR_AST_KIND_FUNCTION_REF) {
        uint16_t functionName =
            SCR_AST_STRING_HANDLE(node->payload.functionRef.name);
        uint16_t functionHandle =
            FindVariable(script_currentFunctionRoot,
                                     functionName);
        CompileTransferRefToString(functionName,
                                   SCR_AST_STRING_USAGE_FUNCTION);
        if (functionHandle == 0) {
            CompileError(sourcePos, "unknown function");
            return;
        }
        functionObject = FindObject(functionHandle);
    } else if (node->kind == SCR_AST_KIND_SCRIPT_FUNCTION_REF) {
        uint16_t scriptFilename =
            SCR_AST_STRING_HANDLE(node->payload.scriptFunctionRef.filename);
        uint16_t scriptFunctionName =
            SCR_AST_STRING_HANDLE(node->payload.scriptFunctionRef.name);
        const char *scriptName = SL_ConvertToString(
            scriptFilename);
        uint16_t canonicalFilename =
            Scr_CreateCanonicalFilename(scriptName);

        CompileRemoveRefToString(scriptFilename);

        uint16_t loadedScript =
            FindVariable(script_loadScriptCodeRoot,
                                     canonicalFilename);
        uint16_t scriptRoot =
            AddFilePrecache(canonicalFilename, sourcePos);
        CompileRemoveRefToString(canonicalFilename);

        uint16_t functionHandle;
        if (loadedScript == 0) {
            functionHandle = GetVariable(
                scriptRoot, scriptFunctionName);
            CompileTransferRefToString(scriptFunctionName,
                                       SCR_AST_STRING_USAGE_FUNCTION);
        } else {
            functionHandle = FindVariable(
                scriptRoot, scriptFunctionName);
            CompileTransferRefToString(scriptFunctionName,
                                       SCR_AST_STRING_USAGE_FUNCTION);
            if (functionHandle == 0) {
                CompileError(sourcePos, "unknown function");
                return;
            }
        }

        VariableValue functionValue;
        GetVariableValue(functionHandle, &functionValue);
        if (functionValue.type == CODUO_SCRIPT_VAR_CODEPOS) {
            coduomp_emit_value_payload(functionValue.payload);
            return;
        }
        if (functionValue.type == CODUO_SCRIPT_VAR_INT) {
            if (script_codegenMode != SCRIPT_CODEGEN_MODE_RECORD_STRING_FIXUPS) {
                CompileError(
                    sourcePos,
                    "normal script cannot reference /# ... #/ comment");
            }
            coduomp_emit_value_payload(functionValue.payload);
            return;
        }
        functionObject = GetObject(functionHandle);
    }

    coduomp_emit_blank_value_payload();
    if (script_codegenMode == SCRIPT_CODEGEN_MODE_RELEASE_STRINGS) {
        return;
    }

    uint16_t countSlot =
        GetVariable(functionObject, 0);
    VariableValue countValue;
    GetVariableValue(countSlot, &countValue);
    if (countValue.type == CODUO_SCRIPT_VAR_UNDEFINED) {
        countValue.payload = 0;
        countValue.type = CODUO_SCRIPT_VAR_INT;
    }

    uint16_t referenceSlot =
        GetVariable(functionObject, countValue.payload + 2);
    VariableValue referenceValue;
    referenceValue.payload = (coduo_script_value_payload_t)script_codeEmitCursor;
    if (script_codegenMode == SCRIPT_CODEGEN_MODE_RECORD_STRING_FIXUPS) {
        referenceValue.type = CODUO_SCRIPT_VAR_INT;
        referenceValue.payload += (coduo_script_value_payload_t)(
            script_codeRelocationEnd - script_codeRelocationStart);
    } else {
        referenceValue.type = CODUO_SCRIPT_VAR_CODEPOS;
    }

    SetNewVariableValue(referenceSlot, &referenceValue);
    countValue.payload++;
    SetVariableValue(countSlot, &countValue);
    AddOpcodePos(sourcePos);
}

void AdjustFunctionAddresses(void)
{
    for (uint16_t functionSlot =
             FindNextSibling(script_currentFunctionRoot);
         functionSlot != 0;
         functionSlot = FindNextSibling(functionSlot)) {
        uint16_t functionObject =
            FindObject(functionSlot);

        uint16_t definitionSlot =
            FindVariable(functionObject,
                                     SCR_FUNCTION_DEFINITION_CHILD);
        VariableValue definitionValue = {
            .payload = 0,
            .type = CODUO_SCRIPT_VAR_UNDEFINED,
        };
        if (definitionSlot != 0) {
            GetVariableValue(definitionSlot, &definitionValue);
        }

        uint16_t referenceCountSlot =
            FindVariable(functionObject,
                                     SCR_FUNCTION_REFERENCE_COUNT_CHILD);
        if (referenceCountSlot != 0) {
            VariableValue referenceCountValue;
            GetVariableValue(referenceCountSlot, &referenceCountValue);

            for (int32_t index = 0;
                 index < (int32_t)referenceCountValue.payload; ++index) {
                uint16_t referenceSlot =
                    FindVariable(
                        functionObject,
                        index + SCR_FUNCTION_REFERENCE_FIRST_CHILD);
                VariableValue *referenceValue =
                    GetVariableValueAddress(referenceSlot);

                if (definitionValue.type == CODUO_SCRIPT_VAR_INT &&
                    GetVarType(referenceSlot) ==
                        CODUO_SCRIPT_VAR_CODEPOS) {
                    CompileError(
                        referenceValue->payload,
                        "normal script cannot reference /# ... #/ comment");
                } else if (definitionSlot == 0) {
                    CompileError(referenceValue->payload,
                                          "unknown function");
                } else {
                    *(coduo_script_value_payload_t *)referenceValue->payload =
                        definitionValue.payload;
                }
            }
        }

        if (definitionSlot != 0) {
            SetVariableValue(functionSlot, &definitionValue);
        }
    }
}

void SpecifyThreadPosition(uint16_t functionObject,
                           uint32_t sourcePos)
{
    uint16_t definitionSlot =
        GetVariable(functionObject, SCR_FUNCTION_DEFINITION_CHILD);
    VariableValue definitionValue;
    GetVariableValue(definitionSlot, &definitionValue);

    if (definitionValue.type == CODUO_SCRIPT_VAR_UNDEFINED) {
        definitionValue.type = CODUO_SCRIPT_VAR_CODEPOS;
        definitionValue.payload = 0;
        SetNewVariableValue(definitionSlot, &definitionValue);
        return;
    }

    CompileError(sourcePos, "function already defined");
}

void SpecifyDeveloperThreadPosition(uint16_t functionObject,
                                    uint32_t sourcePos)
{
    if (script_runtimeDeveloperScriptFlag == 0) {
        return;
    }

    uint16_t definitionSlot =
        GetVariable(functionObject, SCR_FUNCTION_DEFINITION_CHILD);
    VariableValue definitionValue;
    GetVariableValue(definitionSlot, &definitionValue);

    if (definitionValue.type == CODUO_SCRIPT_VAR_UNDEFINED) {
        definitionValue.type = CODUO_SCRIPT_VAR_INT;
        definitionValue.payload = 0;
        SetNewVariableValue(definitionSlot, &definitionValue);
        return;
    }

    CompileError(sourcePos, "function already defined");
}

void SetThreadPosition(uint16_t functionObject,
                       uint32_t sourcePos)
{
    (void)sourcePos;

    uint16_t definitionSlot =
        FindVariable(functionObject,
                                 SCR_FUNCTION_DEFINITION_CHILD);
    VariableValue *definitionValue = GetVariableValueAddress(definitionSlot);

    definitionValue->payload =
        (coduo_script_value_payload_t)TempMalloc(0);
}

void SetDeveloperThreadPosition(
    uint16_t functionObject,
    uint32_t sourcePos)
{
    (void)sourcePos;

    uint16_t definitionSlot =
        FindVariable(functionObject,
                                 SCR_FUNCTION_DEFINITION_CHILD);
    VariableValue *definitionValue = GetVariableValueAddress(definitionSlot);

    definitionValue->payload =
        (coduo_script_value_payload_t)(
            TempMalloc(0) +
            (script_codeRelocationEnd - script_codeRelocationStart));
}
