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

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

enum {
    SCRIPT_ERROR_PRINT_CHANNEL_DEFAULT = 0,
    SCRIPT_ERROR_PRINT_CHANNEL_WARNING = 4,
    SCRIPT_ERROR_DROP = 1,
    SCRIPT_ERROR_SCRIPT_RUNTIME = 6,
    SCRIPT_ERROR_FORMAT_BUFFER_SIZE = 1024
};

void PrintSourcePos(int32_t channel,
                    const char *filename,
                    const char *sourceText,
                    uint32_t sourcePos)
{
    const char *lineStart = sourceText;
    const char *sourceCursor = sourceText;
    int32_t line = 1;

    for (uint32_t remaining = sourcePos; remaining != 0;
         --remaining) {
        if (*sourceCursor == '\0') {
            lineStart = sourceCursor + 1;
            line++;
        }
        sourceCursor++;
    }

    size_t lineLength = strlen(lineStart);
    /* ORIGINAL_BINARY_BUG: stock reserves the complete input-derived source
     * line and marker widths on the stack with no policy limit. */
    char lineText[lineLength + 1];
    for (size_t index = 0; index <= lineLength; ++index) {
        char ch = lineStart[index];
        lineText[index] = ch == '\t' ? ' ' : ch;
    }

    const char *savegameSuffix =
        script_savedSourceFiles == NULL ? "" : " (savegame)";
    Com_PrintMessage(channel,
                     va("(file '%s'%s, line %d)\n", filename,
                        savegameSuffix, line));
    Com_PrintMessage(channel, va("%s\n", lineText));

    int32_t markerOffset = (int32_t)(sourceCursor - lineStart);
    char marker[markerOffset + 2];
    Com_Memset(marker, ' ', markerOffset);
    marker[markerOffset] = '*';
    marker[markerOffset + 1] = '\0';
    Com_PrintMessage(channel, va("%s\n", marker));
}

void Scr_PrintPrevCodePos(int32_t channel,
                          coduo_script_codepos_t codePos,
                          int32_t sourcePosOffset)
{
    if (script_runtimeDebugReportFlag == 0) {
        Com_PrintMessage(
            channel,
            va("@ %d\n", (int32_t)(codePos - script_codeBase)));
        return;
    }

    qboolean loadedCodePos = Scr_IsInOpcodeMemory(codePos);
    int32_t fileIndex = (int32_t)script_sourceFileCount;
    do {
        fileIndex--;
        if (fileIndex < 1) {
            break;
        }

        uint8_t *fileCodeStart =
            loadedCodePos == qfalse
                ? script_sourceFiles[fileIndex].relocatedCodeStart
                : script_sourceFiles[fileIndex].normalCodeStart;
        if (fileCodeStart != NULL && codePos > fileCodeStart) {
            break;
        }
    } while (qtrue);

    uint32_t sourcePos =
        GetPrevSourcePos(codePos, sourcePosOffset);
    PrintSourcePos(
        channel, script_sourceFiles[fileIndex].name,
        script_sourceFiles[fileIndex].source, sourcePos);
}

void CompileError(uint32_t sourcePos,
                  const char *format, ...)
{
    char message[SCRIPT_ERROR_FORMAT_BUFFER_SIZE];
    va_list args;

    Com_Printf("\n");
    Com_Printf("******* script compile error *******\n");

    va_start(args, format);
    /* ORIGINAL_BINARY_BUG: stock vsprintf has no bound for this 1,024-byte
     * stack destination. */
    vsprintf(message, format, args);
    va_end(args);

    if (script_runtimeDebugReportFlag == 0) {
        Com_Printf("%s\n", message);
    } else {
        Com_Printf("%s: ", message);
        PrintSourcePos(
            SCRIPT_ERROR_PRINT_CHANNEL_DEFAULT, script_sourceFilename,
            script_sourcePos, sourcePos);
    }

    Com_Printf("************************************\n");
    Com_Error(SCRIPT_ERROR_DROP,
              "\x15" "script compile error\n(see console for details)");
}

void CompileError2(coduo_script_codepos_t codePos,
                   const char *format, ...)
{
    char message[SCRIPT_ERROR_FORMAT_BUFFER_SIZE];
    va_list args;

    Com_Printf("\n");
    Com_Printf("******* script compile error *******\n");

    va_start(args, format);
    /* ORIGINAL_BINARY_BUG: stock vsprintf has no bound for this 1,024-byte
     * stack destination. */
    vsprintf(message, format, args);
    va_end(args);

    Com_Printf("%s: ", message);
    Scr_PrintPrevCodePos(SCRIPT_ERROR_PRINT_CHANNEL_DEFAULT, codePos, 0);
    Com_Printf("************************************\n");
    Com_Error(SCRIPT_ERROR_DROP,
              "\x15" "script compile error\n(see console for details)");
}

void RuntimeErrorInternal(int32_t channel,
                          coduo_script_codepos_t codePos,
                          int32_t sourcePosOffset,
                          const char *message)
{
    Com_PrintMessage(
        channel,
        va("\n******* script runtime error *******\n%s: ", message));
    Scr_PrintPrevCodePos(channel, codePos, sourcePosOffset);

    for (int32_t stackIndex = script_callStackDepth - 1; stackIndex >= 0;
         --stackIndex) {
        Com_PrintMessage(channel, "called from:\n");
        Scr_PrintPrevCodePos(channel, script_callStackCodepos[stackIndex],
                                 0);
    }

    Com_PrintMessage(channel, "************************************\n");
}

void RuntimeError(coduo_script_codepos_t codePos,
                  int32_t sourcePosOffset,
                  const char *message,
                  const char *detail)
{
    if (script_runtimeDebugReportFlag == 0 &&
        script_forceErrorReport == qfalse) {
        return;
    }

    qboolean fatal =
        script_runtimeDeveloperFlag != 0 || script_forceErrorReport != qfalse;
    int32_t channel =
        fatal == qfalse ? SCRIPT_ERROR_PRINT_CHANNEL_WARNING
                        : SCRIPT_ERROR_PRINT_CHANNEL_DEFAULT;
    RuntimeErrorInternal(channel, codePos, sourcePosOffset, message);

    if (fatal == qfalse) {
        return;
    }

    const char *detailPrefix = "\n";
    if (detail == NULL) {
        detailPrefix = "";
        detail = "";
    }

    int32_t errorCode =
        script_forceErrorReport == qfalse ? SCRIPT_ERROR_SCRIPT_RUNTIME
                                          : SCRIPT_ERROR_DROP;
    Com_Error(errorCode,
              "\x15" "script runtime error\n(see console for details)%s%s",
              detailPrefix, detail);
}
