#include <stdint.h>

#include "../core_runtime/core_runtime_private.h"
#include "coduo_engine_structs.h"
#include "script_memory_private.h"
#include "script_notify_private.h"
#include "script_runtime_private.h"
#include "script_string_private.h"
#include "script_variable_private.h"

enum {
    SCRIPT_VARIABLE_TYPE_MASK = 0x1f,
    SCRIPT_VARIABLE_NAME_SHIFT = 8,
    SCRIPT_VARIABLE_SHARED_OBJECT_REFCOUNT_EMPTY = 0,
    SCRIPT_VARIABLE_OBJECT_KEY_LIMIT = 0x20000,
    SCRIPT_VARIABLE_CHILD_COPY_OCCUPANCY_BITS = 0x60,
    SCRIPT_IMPORT_CLASS_FIELD_STRING_TYPE = 0x0f,
    SCRIPT_IMPORT_ENTITY_NUM_MASK = 0xffff,
};

#define SCRIPT_IMPORT_OFFSET_NOT_FOUND UINT32_MAX

void CopyEntity(uint16_t source, uint16_t dest);
uint16_t FindEntityId(int32_t entityNum, int32_t classNum);

void Scr_SetDynamicEntityField(
    int32_t entityNum, int32_t classNum,
    uint16_t fieldName)
{
    uint16_t entity =
        Scr_GetEntityId(entityNum, classNum);
    uint16_t field =
        GetVariableField(entity, fieldName);

    SetVariableFieldValue(field, script_valueStackTop);
    script_valueStackTop--;
    script_valueStackDepth = 0;
}

void Scr_CopyEntityNum(int32_t sourceEntityNum, int32_t destEntityNum,
                       int32_t classNum)
{
    uint16_t source =
        FindEntityId(sourceEntityNum, classNum);

    if (source == 0 || FindNextSibling(source) == 0) {
        return;
    }

    uint16_t dest =
        Scr_GetEntityId(destEntityNum, classNum);
    CopyEntity(source, dest);
}

void Scr_FreeEntityNum(int32_t entityNum, int32_t classNum)
{
    if (script_runtimeActive == qfalse) {
        return;
    }

    uint16_t classMapSlot =
        FindVariable(script_entityTypeClassMapRoot,
                                 (uint32_t)classNum);
    uint16_t classMap =
        FindObject(classMapSlot);
    uint16_t entitySlot =
        FindArrayVariable(classMap, entityNum);
    if (entitySlot == 0) {
        return;
    }

    uint16_t entityObject =
        script_variableNodes[entitySlot].payload.halves.valueOrRefCount;
    script_variableNodes[entityObject].packedTypeIndex &=
        ~(uint32_t)SCRIPT_VARIABLE_TYPE_MASK;
    script_variableNodes[entityObject].packedTypeIndex |=
        CODUO_SCRIPT_VAR_DEAD_ENTITY;

    AddRefToObject(entityObject);
    VM_TerminateNotifyList(entityObject);
    ClearObjectInternal(entityObject);
    RemoveRefToObject(entityObject);
    RemoveArrayVariable(classMap, entityNum);
}

uint16_t
Scr_GetEntityId(int32_t entityNum, int32_t classNum)
{
    uint16_t classMapSlot =
        FindVariable(script_entityTypeClassMapRoot,
                                 (uint32_t)classNum);
    uint16_t classMap =
        FindObject(classMapSlot);
    uint16_t entitySlot =
        GetArrayVariableBySignedIndex(classMap, entityNum);
    coduo_script_variable_node_t *slotNode = &script_variableNodes[entitySlot];

    if ((slotNode->packedTypeIndex & SCRIPT_VARIABLE_TYPE_MASK) ==
        CODUO_SCRIPT_VAR_UNDEFINED) {
        uint16_t entityObject =
            AllocEntity(classNum,
                                       (uint16_t)
                                           (entityNum &
                                            SCRIPT_IMPORT_ENTITY_NUM_MASK));

        slotNode->packedTypeIndex |= CODUO_SCRIPT_VAR_OBJECT;
        slotNode->payload.halves.valueOrRefCount = entityObject;
        return entityObject;
    }

    return slotNode->payload.halves.valueOrRefCount;
}

void Scr_SetClassMap(coduo_script_entity_type_usage_t *records,
                              uint32_t count)
{
    script_entityTypeUsageRecords = records;
    script_entityTypeUsageCount = count;

    for (uint32_t index = 0; index < count;
         ++index) {
        uint16_t entityMapSlot =
            GetVariable(script_entityTypeClassMapRoot, index);
        GetArray(entityMapSlot);

        uint16_t classFieldSlot =
            GetVariable(script_classMapRoot, index);
        records[index].rootHandle =
            GetArray(classFieldSlot);
    }
}

void Scr_RemoveClassMap(void)
{
    if (script_runtimeActive == qfalse) {
        return;
    }

    for (uint32_t index = 0;
         index < script_entityTypeUsageCount; ++index) {
        SafeRemoveVariable(script_classMapRoot, index);
        SafeRemoveVariable(script_entityTypeClassMapRoot,
                                           index);
    }
}

void Scr_AddClassField(uint16_t classRoot,
                       const char *name,
                       uint16_t offset)
{
    uint16_t tempHash = SL_FindCanonicalString(name);
    if (tempHash != 0) {
        uint16_t field =
            GetArrayVariableByUnsignedIndex(classRoot, tempHash);

        script_variableNodes[field].packedTypeIndex &=
            ~(uint32_t)SCRIPT_VARIABLE_TYPE_MASK;
        script_variableNodes[field].packedTypeIndex |= CODUO_SCRIPT_VAR_INT;
        script_variableNodes[field].payload.halves.parentHandle = offset;
    }

    uint16_t string =
        SL_GetString_(name, 0, SCRIPT_IMPORT_CLASS_FIELD_STRING_TYPE);
    uint16_t field =
        GetVariable(classRoot, string);
    SL_RemoveRefToString(string);

    script_variableNodes[field].packedTypeIndex &=
        ~(uint32_t)SCRIPT_VARIABLE_TYPE_MASK;
    script_variableNodes[field].packedTypeIndex |= CODUO_SCRIPT_VAR_INT;
    script_variableNodes[field].payload.halves.parentHandle = offset;
}

uint32_t Scr_GetOffset(uint16_t classRoot,
                                const char *name)
{
    uint16_t string = SL_ConvertFromString(name);
    uint16_t field =
        FindVariable(classRoot, string);

    if (field == 0) {
        return SCRIPT_IMPORT_OFFSET_NOT_FOUND;
    }

    return script_variableNodes[field].payload.halves.parentHandle;
}

uint16_t
FindEntityId(int32_t entityNum, int32_t classNum)
{
    uint16_t classMapSlot =
        FindVariable(script_entityTypeClassMapRoot,
                                 (uint32_t)classNum);
    uint16_t classMap =
        FindObject(classMapSlot);
    uint16_t entitySlot =
        FindArrayVariable(classMap, entityNum);

    if (entitySlot == 0) {
        return 0;
    }

    return script_variableNodes[entitySlot].payload.halves.valueOrRefCount;
}

uint16_t
FindArrayVariableByValue(uint16_t handle,
                         VariableValue *index)
{
    coduo_script_variable_node_t *node = &script_variableNodes[handle];
    coduo_script_variable_type_t type =
        (coduo_script_variable_type_t)(node->packedTypeIndex &
                                       SCRIPT_VARIABLE_TYPE_MASK);
    coduo_script_value_payload_t payload = node->payload.valuePayload;

    while (type == CODUO_SCRIPT_VAR_KEY_VALUE) {
        VariableValue resolved;

        GetEntityFieldValue(
            node->packedTypeIndex >> SCRIPT_VARIABLE_NAME_SHIFT,
            node->payload.halves.valueOrRefCount, node->payload.halves.parentHandle, index + 1);
        resolved = index[1];
        ScriptValue_ReleaseValue(&resolved);

        type = resolved.type;
        payload = resolved.payload;
    }

    if (type != CODUO_SCRIPT_VAR_OBJECT) {
        script_errorParameterIndex = 1;
        Scr_Error(
            va("%s is not an array", script_variableTypeNames[type]));
    }

    uint16_t object =
        (uint16_t)payload;
    coduo_script_variable_type_t objectType = GetVarType(object);
    if (objectType != CODUO_SCRIPT_VAR_ARRAY) {
        script_errorParameterIndex = 1;
        Scr_Error(
            va("%s is not an array", script_variableTypeNames[objectType]));
    }

    uint16_t child = 0;
    if (index->type == CODUO_SCRIPT_VAR_INT) {
        if (IsValidArrayIndex((int32_t)index->payload) !=
            qfalse) {
            uint16_t indirection =
                FindArrayVariableIndex(
                    object, (int32_t)index->payload);

            if (indirection != 0) {
                child = script_variableIndirections[indirection].valueIndex;
            }
        } else {
            Scr_Error(
                va("array index %d out of range", (int32_t)index->payload));
        }
    } else if (index->type == CODUO_SCRIPT_VAR_STRING) {
        uint16_t indirection =
            FindVariableIndexInternal(object, index->payload);

        if (indirection != 0) {
            SL_RemoveRefToString(
                (uint16_t)index->payload);
            child = script_variableIndirections[indirection].valueIndex;
        }
    } else {
        Scr_Error(
            va("%s is not an array index",
               script_variableTypeNames[index->type]));
    }

    if (child == 0) {
        Scr_Error("array index does not exist");
    }

    return child;
}

uint16_t
EvalArrayRef(uint16_t handle,
             VariableValue *index)
{
    coduo_script_variable_node_t *node = &script_variableNodes[handle];
    coduo_script_variable_type_t type =
        (coduo_script_variable_type_t)(node->packedTypeIndex &
                                       SCRIPT_VARIABLE_TYPE_MASK);
    coduo_script_value_payload_t payload = node->payload.valuePayload;

    if (type == CODUO_SCRIPT_VAR_UNDEFINED) {
        type = CODUO_SCRIPT_VAR_OBJECT;
        payload = Scr_AllocArray();
        node->packedTypeIndex |= CODUO_SCRIPT_VAR_OBJECT;
        node->payload.valuePayload = payload;
    } else {
        while (type == CODUO_SCRIPT_VAR_KEY_VALUE) {
            VariableValue resolved;

            GetEntityFieldValue(
                node->packedTypeIndex >> SCRIPT_VARIABLE_NAME_SHIFT,
                node->payload.halves.valueOrRefCount, node->payload.halves.parentHandle, index + 1);
            resolved = index[1];
            ScriptValue_ReleaseValue(&resolved);

            type = resolved.type;
            payload = resolved.payload;
        }

        if (type != CODUO_SCRIPT_VAR_OBJECT) {
            script_errorParameterIndex = 1;
            if (type == CODUO_SCRIPT_VAR_STRING) {
                Scr_Error(
                    "string characters cannot be individually changed");
            }
            if (type == CODUO_SCRIPT_VAR_STRING ||
                type == CODUO_SCRIPT_VAR_VECTOR) {
                Scr_Error(
                    "vector components cannot be individually changed");
            }

            Scr_Error(
                va("%s is not an array", script_variableTypeNames[type]));
        }
    }

    uint16_t object =
        (uint16_t)payload;
    coduo_script_variable_type_t objectType = GetVarType(object);
    if (objectType != CODUO_SCRIPT_VAR_ARRAY) {
        script_errorParameterIndex = 1;
        Scr_Error(
            va("%s is not an array", script_variableTypeNames[objectType]));
    }

    coduo_script_variable_node_t *objectNode = &script_variableNodes[object];
    if (objectNode->payload.halves.valueOrRefCount !=
        SCRIPT_VARIABLE_SHARED_OBJECT_REFCOUNT_EMPTY) {
        uint16_t oldObject = object;

        RemoveRefToObject(oldObject);
        object = Scr_AllocArray();
        CopyArray(oldObject, object);
        /* ORIGINAL_BINARY_BUG: stock 0x080a88e1..0x080a88e7 writes the full
         * zero-extended handle through the original reference node.  When
         * that node is an external KEY_VALUE field reference, this neither
         * updates the backing field nor preserves the field-reference
         * payload; see original-bugs/coduomp-field-array-cow-detaches.md. */
        node->payload.valuePayload = object;
    }

    if (index->type == CODUO_SCRIPT_VAR_INT) {
        if (IsValidArrayIndex((int32_t)index->payload) ==
            qfalse) {
            Scr_Error(
                va("array index %d out of range", (int32_t)index->payload));
        }

        uint16_t indirection =
            GetArrayVariableIndex(
                object, (int32_t)index->payload);

        return script_variableIndirections[indirection].valueIndex;
    }

    if (index->type == CODUO_SCRIPT_VAR_STRING) {
        uint16_t indirection =
            GetVariableIndexInternal(object, index->payload);

        SL_RemoveRefToString((uint16_t)index->payload);
        return script_variableIndirections[indirection].valueIndex;
    }

    Scr_Error(
        va("%s is not an array index", script_variableTypeNames[index->type]));
    return 0;
}

void ClearArray(uint16_t handle,
                VariableValue *index)
{
    coduo_script_variable_node_t *node = &script_variableNodes[handle];
    coduo_script_variable_type_t type =
        (coduo_script_variable_type_t)(node->packedTypeIndex &
                                       SCRIPT_VARIABLE_TYPE_MASK);
    coduo_script_value_payload_t payload = node->payload.valuePayload;

    while (type == CODUO_SCRIPT_VAR_KEY_VALUE) {
        VariableValue resolved;

        GetEntityFieldValue(
            node->packedTypeIndex >> SCRIPT_VARIABLE_NAME_SHIFT,
            node->payload.halves.valueOrRefCount, node->payload.halves.parentHandle, index + 1);
        resolved = index[1];
        ScriptValue_ReleaseValue(&resolved);

        type = resolved.type;
        payload = resolved.payload;
    }

    if (type != CODUO_SCRIPT_VAR_OBJECT) {
        script_errorParameterIndex = 1;
        Scr_Error(
            va("%s is not an array", script_variableTypeNames[type]));
    }

    uint16_t object =
        (uint16_t)payload;
    coduo_script_variable_type_t objectType = GetVarType(object);
    if (objectType != CODUO_SCRIPT_VAR_ARRAY) {
        script_errorParameterIndex = 1;
        Scr_Error(
            va("%s is not an array", script_variableTypeNames[objectType]));
    }

    coduo_script_variable_node_t *objectNode = &script_variableNodes[object];
    if (objectNode->payload.halves.valueOrRefCount !=
        SCRIPT_VARIABLE_SHARED_OBJECT_REFCOUNT_EMPTY) {
        uint16_t oldObject = object;

        RemoveRefToObject(oldObject);
        object = Scr_AllocArray();
        CopyArray(oldObject, object);
        /* ORIGINAL_BINARY_BUG: stock 0x080a8b18..0x080a8b1e keeps the
         * original node base and replaces its complete payload with the new
         * array handle.  An external KEY_VALUE field therefore loses its
         * reference metadata without receiving the detached array; see
         * original-bugs/coduomp-field-array-cow-detaches.md. */
        node->payload.valuePayload = object;
    }

    if (index->type == CODUO_SCRIPT_VAR_INT) {
        if (IsValidArrayIndex((int32_t)index->payload) !=
            qfalse) {
            SafeRemoveArrayVariable(
                object, (int32_t)index->payload);
        } else {
            Scr_Error(
                va("array index %d out of range", (int32_t)index->payload));
        }
        return;
    }

    if (index->type == CODUO_SCRIPT_VAR_STRING) {
        SafeRemoveVariable(object, index->payload);
        SL_RemoveRefToString((uint16_t)index->payload);
        return;
    }

    Scr_Error(
        va("%s is not an array index", script_variableTypeNames[index->type]));
}

void GetEmptyArray(VariableValue *value)
{
    value->type = CODUO_SCRIPT_VAR_OBJECT;
    value->payload = Scr_AllocArray();
}

uint16_t
GetEntnum(uint16_t handle)
{
    return script_variableNodes[handle].payload.halves.parentHandle;
}

void CopyEntity(uint16_t source, uint16_t dest)
{
    uint16_t child = FindNextSibling(source);

    while (child != 0) {
        coduo_script_variable_node_t *sourceNode =
            &script_variableNodes[child];
        uint32_t sourceName =
            sourceNode->packedTypeIndex >> SCRIPT_VARIABLE_NAME_SHIFT;

        if (sourceName != SCRIPT_VARIABLE_OBJECT_KEY_LIMIT) {
            uint16_t destIndirection =
                GetVariableIndexInternal(dest, sourceName);
            coduo_script_variable_node_t *destNode =
                &script_variableNodes[script_variableIndirections
                                          [destIndirection]
                                              .valueIndex];
            uint32_t copiedPackedType =
                sourceNode->packedTypeIndex &
                ~(uint32_t)SCRIPT_VARIABLE_CHILD_COPY_OCCUPANCY_BITS;
            coduo_script_variable_type_t copiedType =
                (coduo_script_variable_type_t)(copiedPackedType &
                                               SCRIPT_VARIABLE_TYPE_MASK);

            destNode->packedTypeIndex |= copiedPackedType;
            destNode->payload.valuePayload = sourceNode->payload.valuePayload;
            AddRefToValue(copiedType, sourceNode->payload.valuePayload);
        }

        child = FindNextSibling(child);
    }
}
