#define _POSIX_C_SOURCE 200809L

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

#include "../animation/dobj_private.h"
#include "../core_command/cmd_private.h"
#include "../filesystem/fs_private.h"
#include "../model_assets/xmodel_asset_private.h"
#include "../scripting/script_runtime_private.h"
#include "../server/sv_init_shutdown_private.h"
#include "core_runtime_private.h"

#define COM_ERROR_FATAL 0
#define COM_ERROR_DROP 1
#define COM_ERROR_DISCONNECT 2
#define COM_ERROR_SCRIPT 3
#define COM_ERROR_SERVER_CD 4
#define COM_ERROR_END_GAME 5
#define COM_ERROR_LOCALIZED 6
#define COM_ERROR_LOCALIZED_NO_SOUND 7
#define COM_ERROR_BURST_WINDOW_MSEC 100
#define COM_ERROR_BURST_FATAL_COUNT 4
#define COM_ERROR_RECOVERABLE_LONGJMP_VALUE -1
#define COM_ERROR_CVAR_FLAGS CODUO_CVAR_ROM
#define COM_ERROR_BUFFER_COPY_SIZE (CODUO_COM_ERROR_MESSAGE_SIZE + 8)
#define COM_WEAPON_INFO_SERVER_STATE 1
#define COM_WEAPON_INFO_CLIENT_STATE 2
#define COM_ERROR_KEEP_WEAPON_MEMORY qfalse
#define COM_FS_RESTART_CLEAR_LOOKUPS qtrue

coduo_com_error_message_t com_errorMessage;
int32_t com_errorEntered;

static int32_t com_lastErrorTime;
static int32_t com_errorCount;
static int32_t com_recoverableErrorCode;

void Com_ClearTempMemory(void)
{
    DObjBumpSkelCacheKey();
    Hunk_ClearTempMemory();
    Hunk_ClearTempMemoryHigh();
}

void Com_ClearServerFrameRunningFlag(void)
{
    host_sv_frame_running_flag = qfalse;
}

void Com_SetErrorMessage(const char *message)
{
    Cvar_Get("com_errorMessage", "", COM_ERROR_CVAR_FLAGS);
    Cvar_Set("com_errorMessage", message);
}

void Com_ErrorCleanup(void)
{
    if (com_recoverableErrorCode == COM_ERROR_DISCONNECT) {
        Com_Shutdown("EXE_DISCONNECTEDFROMOWNLISTENSERVER");
        com_errorEntered = qfalse;
    } else if (com_recoverableErrorCode == COM_ERROR_END_GAME) {
        Com_Shutdown("EXE_ENDOFGAME");
        com_errorEntered = qfalse;
    } else if (com_recoverableErrorCode == COM_ERROR_DROP ||
               com_recoverableErrorCode == COM_ERROR_SCRIPT) {
        Com_Printf("********************\nERROR: %s\n********************\n",
                   com_errorMessage);
        Com_Shutdown(com_errorMessage);
        com_errorEntered = qfalse;

        cvar_t *rVcCompile = Cvar_Get("r_vc_compile", "0", 0);
        if (com_recoverableErrorCode == COM_ERROR_DROP &&
            rVcCompile->integer == COM_WEAPON_INFO_CLIENT_STATE) {
            Com_Quit_f();
        }
    } else if (com_recoverableErrorCode == COM_ERROR_SERVER_CD) {
        Com_Shutdown("EXE_SERVERDIDNTHAVECD");
        if (host_cvar_cl_running_pointer_slot == NULL ||
            host_cvar_cl_running_pointer_slot->integer == 0) {
            Com_Printf("Server didn't have CD\n");
        } else {
            com_errorEntered = qfalse;
            CL_CDDialog();
        }
    }

    Com_InitServerRuntimePools();
}

void Com_Error(int32_t code, const char *format, ...)
{
    char shutdownMessage[COM_ERROR_BUFFER_COPY_SIZE];
    va_list args;

    if (com_errorEntered != qfalse) {
        Sys_Error("recursive error after: %s", com_errorMessage);
    }

    Com_ClearTempMemory();

    va_start(args, format);
    /* ORIGINAL_BINARY_BUG: stock uses unbounded vsprintf in the 4096-byte
     * global and then unbounded strcpy into fixed stack storage.  Preserved
     * per original-bugs/coduomp-com-error-remote-format-string.md. */
    vsprintf(com_errorMessage, format, args);
    va_end(args);

    strcpy(shutdownMessage, com_errorMessage);

    if (code == COM_ERROR_LOCALIZED ||
        code == COM_ERROR_LOCALIZED_NO_SOUND) {
        code = COM_ERROR_DROP;
    }

    com_errorEntered = qtrue;
    FS_PureServerSetLoadedPaks("", "");

    if (code != COM_ERROR_SCRIPT && code != COM_ERROR_SERVER_CD &&
        code != COM_ERROR_END_GAME) {
        Com_SetErrorMessage(com_errorMessage);
    }

    if (code != COM_ERROR_SCRIPT) {
        Scr_Abort();
    }

    Com_ClearServerFrameRunningFlag();
    Com_ResetParseSessions();
    FS_ResetFiles();

    if (code == COM_ERROR_DROP) {
        Cbuf_Init();
    }

    FreeWeaponInfoMemory(COM_WEAPON_INFO_SERVER_STATE,
                         COM_ERROR_KEEP_WEAPON_MEMORY);
    FreeWeaponInfoMemory(COM_WEAPON_INFO_CLIENT_STATE,
                         COM_ERROR_KEEP_WEAPON_MEMORY);

    int32_t now = Sys_Milliseconds();
    if (now - com_lastErrorTime < COM_ERROR_BURST_WINDOW_MSEC) {
        com_errorCount++;
        if (com_errorCount >= COM_ERROR_BURST_FATAL_COUNT) {
            code = COM_ERROR_FATAL;
        }
    } else {
        com_errorCount = 0;
    }
    com_lastErrorTime = now;

    if (code != COM_ERROR_DISCONNECT && code != COM_ERROR_END_GAME &&
        code != COM_ERROR_DROP && code != COM_ERROR_SCRIPT &&
        code != COM_ERROR_SERVER_CD) {
        CL_Shutdown();
        SV_Shutdown(va("EXE_SERVER_FATAL_CRASHED\x15 \x14%s",
                       shutdownMessage));
        Hunk_Clear();
        Com_Close();
        Sys_Error("%s", com_errorMessage);
        return;
    }

    com_recoverableErrorCode = code;
    longjmp(com_frameAbortContext, COM_ERROR_RECOVERABLE_LONGJMP_VALUE);
}
