#include <stdint.h>

#include "coduo_engine_structs.h"
#include "script_compile_private.h"
#include "script_memory_private.h"
#include "script_runtime_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_BUILTIN_FUNCTION_CALL = 0x21,
    SCRIPT_OPCODE_BUILTIN_METHOD_CALL = 0x22,
    SCRIPT_OPCODE_DEVELOPER_COMMAND = 0x57,
    SCRIPT_BUILTIN_PARAM_LIMIT = 0xff,
    SCRIPT_BUILTIN_LOCAL_DEPTH_STACK = 1
};

uint16_t
GetBuiltin(coduo_scr_ast_node_t *callNode)
{
    if (callNode->kind != SCR_AST_KIND_FUNCTION_CALL) {
        return 0;
    }

    coduo_scr_ast_node_t *callee = callNode->payload.call.callee;
    if (callee->kind != SCR_AST_KIND_SCRIPT_FUNCTION_NAME) {
        return 0;
    }

    coduo_scr_ast_node_t *nameNode = callee->payload.namedCall.nameNode;
    if (nameNode->kind != SCR_AST_KIND_FUNCTION_REF) {
        return 0;
    }

    uint16_t name =
        SCR_AST_STRING_HANDLE(nameNode->payload.functionRef.name);
    if (FindVariable(script_currentFunctionRoot, name) != 0) {
        return 0;
    }

    return name;
}

/* NOT_FROM_ORIGINAL_SOURCE: shared developer-command setup for call compilers. */
static int coduomp_script_begin_developer_command(uint32_t sourcePos,
                                                  qboolean emitDropTop)
{
    if (script_codegenMode != SCRIPT_CODEGEN_MODE_INTERN_STRINGS) {
        return 0;
    }

    if (emitDropTop == qfalse) {
        CompileError(
            sourcePos,
            "developer command can only be used as a statement if not in a /# ... #/ comment");
    }

    if (script_runtimeDeveloperScriptFlag == 0) {
        script_codegenMode = SCRIPT_CODEGEN_MODE_RELEASE_STRINGS;
        return 1;
    }

    if (script_codeNeedsDeferredCheck == qfalse) {
        EmitOpcode(SCRIPT_OPCODE_DEVELOPER_COMMAND, 0, 0);
        script_codeRelocationStart = script_codeEmitCursor;
    }

    script_codegenMode = SCRIPT_CODEGEN_MODE_RECORD_STRING_FIXUPS;
    return 1;
}

/* NOT_FROM_ORIGINAL_SOURCE: shared developer-command teardown for call compilers. */
static void coduomp_script_end_developer_command(int developerOnly,
                                                 uint8_t *tempMark,
                                                 uint32_t savedChecksum)
{
    if (developerOnly == 0) {
        return;
    }

    script_codegenMode = SCRIPT_CODEGEN_MODE_INTERN_STRINGS;
    if (script_runtimeDeveloperScriptFlag == 0) {
        TempMemorySetPos(tempMark);
        script_codeChecksum = savedChecksum;
        return;
    }

    script_codeNeedsDeferredCheck = qtrue;
    script_codeChecksum = savedChecksum;
}

void EmitCall(coduo_scr_ast_node_t *callNode,
              coduo_scr_ast_list_t *args,
              qboolean emitDropTop)
{
    uint16_t builtinName =
        GetBuiltin(callNode);

    if (builtinName == 0) {
        EmitPreFunctionCall(callNode);
        int32_t argumentCount = EmitExpressionList(args);
        EmitPostFunctionCall(callNode, argumentCount, qfalse);
        AddExpressionListOpcodePos(args);
        if (emitDropTop != qfalse) {
            EmitDecTop();
        }
        return;
    }

    const char *builtinText = SL_ConvertToString(builtinName);
    int developerOnly = 0;
    coduo_script_function_callback_fn function =
        Scr_GetFunction(&builtinText, &developerOnly);
    uint32_t savedChecksum = script_codeChecksum;
    uint32_t callSourcePos = callNode->payload.call.sourcePos;
    uint8_t *tempMark = TempMalloc(0);

    if (developerOnly != 0) {
        developerOnly =
            coduomp_script_begin_developer_command(callSourcePos, emitDropTop);
    }

    int32_t argumentCount = EmitExpressionList(args);
    /* ORIGINAL_BINARY_BUG: the one-byte operand correctly limits builtin
     * calls to 255 arguments, but stock rejects 256 while reporting that the
     * count "exceeds 256"; see original-bugs/coduomp-script-call-argument-limit-diagnostic.md. */
    if (SCRIPT_BUILTIN_PARAM_LIMIT < argumentCount) {
        CompileRemoveRefToString(builtinName);
        CompileError(callSourcePos, "parameter count exceeds 256");
    }

    if (function == NULL) {
        CompileError(callSourcePos, "unknown (builtin) function '%s'",
                              builtinText);
        CompileRemoveRefToString(builtinName);
    } else {
        CompileRemoveRefToString(builtinName);
        EmitOpcode(SCRIPT_OPCODE_BUILTIN_FUNCTION_CALL,
                   1 - argumentCount,
                   SCRIPT_BUILTIN_LOCAL_DEPTH_STACK);
        EmitByte((uint8_t)argumentCount);
        coduomp_emit_builtin_function_payload(function);
        AddOpcodePos(callSourcePos);
    }

    AddExpressionListOpcodePos(args);
    if (emitDropTop != qfalse) {
        EmitDecTop();
    }

    coduomp_script_end_developer_command(developerOnly, tempMark,
                                         savedChecksum);
}

void EmitMethod(coduo_scr_ast_node_t *objectNode,
                coduo_scr_ast_node_t *callNode,
                coduo_scr_ast_list_t *args,
                uint32_t methodSourcePos,
                qboolean emitDropTop)
{
    uint16_t builtinName =
        GetBuiltin(callNode);

    if (builtinName == 0) {
        EmitPreFunctionCall(callNode);
        int32_t argumentCount = EmitExpressionList(args);
        EmitPrimitiveExpression(objectNode);
        EmitPostFunctionCall(callNode, argumentCount, qtrue);
        AddOpcodePos(methodSourcePos);
        AddExpressionListOpcodePos(args);
        if (emitDropTop != qfalse) {
            EmitDecTop();
        }
        return;
    }

    const char *builtinText = SL_ConvertToString(builtinName);
    int developerOnly = 0;
    coduo_script_method_callback_fn method =
        Scr_GetMethod(&builtinText, &developerOnly);
    uint32_t savedChecksum = script_codeChecksum;
    uint32_t callSourcePos = callNode->payload.call.sourcePos;
    uint8_t *tempMark = TempMalloc(0);

    if (developerOnly != 0) {
        developerOnly =
            coduomp_script_begin_developer_command(callSourcePos, emitDropTop);
    }

    int32_t argumentCount = EmitExpressionList(args);
    EmitPrimitiveExpression(objectNode);
    /* ORIGINAL_BINARY_BUG: the method path has the same diagnostic boundary
     * mismatch as EmitCall; see the shared report above. */
    if (SCRIPT_BUILTIN_PARAM_LIMIT < argumentCount) {
        CompileRemoveRefToString(builtinName);
        CompileError(callSourcePos, "parameter count exceeds 256");
    }

    if (method == NULL) {
        CompileError(callSourcePos, "unknown (builtin) method '%s'",
                              builtinText);
        CompileRemoveRefToString(builtinName);
    } else {
        CompileRemoveRefToString(builtinName);
        EmitOpcode(SCRIPT_OPCODE_BUILTIN_METHOD_CALL,
                   -argumentCount,
                   SCRIPT_BUILTIN_LOCAL_DEPTH_STACK);
        EmitByte((uint8_t)argumentCount);
        coduomp_emit_builtin_method_payload(method);
        AddOpcodePos(callSourcePos);
    }

    AddOpcodePos(methodSourcePos);
    AddExpressionListOpcodePos(args);
    if (emitDropTop != qfalse) {
        EmitDecTop();
    }

    coduomp_script_end_developer_command(developerOnly, tempMark,
                                         savedChecksum);
}
