#define _POSIX_C_SOURCE 200809L

#include <setjmp.h>
#include <stdint.h>

#include "coduo_x87emu.h" /* defines EMULATE_X87; x87 shim when it is 1 */
#include "coduo_native_x87.h"
#include "core_runtime_private.h"
#include "../animation/dobj_private.h"
#include "../animation/xanim_private.h"
#include "../core_cvar/cvar_private.h"
#include "../core_command/cmd_private.h"
#include "../core_math/core_math_private.h"
#include "../filesystem/fs_private.h"
#include "../scripting/script_runtime_private.h"
#include "../server/server_private.h"

#define COM_CONFIG_ARGC 2
#define COM_CONFIG_FILENAME_ARG 1
#define COM_CONFIG_NAME_BUFFER_SIZE 64
#define COM_CONFIG_NAME_EXTENSION_LIMIT 64
#define COM_CONFIG_DEFAULT_EXTENSION ".cfg"
#define COM_CONFIG_AUTOWRITE_MASK 0x00000001
#define COM_CONFIG_AUTOWRITE_NAME "uoconfig_mp_server.cfg"
#define COM_GENERATED_CONFIG_HEADER \
    "// generated by Call of Duty, do not modify\n"
#define COM_TIMESCALE_RESET_VALUE "0"
#define COM_DEDICATED_FRAME_CLAMP_MSEC 5000
#define COM_SERVER_FRAME_CLAMP_MSEC 200
#define COM_HITCH_WARNING_MIN_MSEC 500
#define COM_HITCH_WARNING_MAX_MSEC 500000
#define COM_FRAME_MIN_MSEC 1
#define COM_FRAME_MSEC_BASE 1000
#define COM_FRAME_DEFAULT_MSEC 1
#define COM_FRAME_LONGJMP_SAVE_MASK 0

qboolean com_configAutowriteEnabled;
int32_t com_frameLastTime;
coduo_jump_buffer_t com_frameAbortContext;

void Com_WriteConfigToFile(const char *filename)
{
    int32_t handle;

    handle = FS_FOpenFileWrite(filename);
    if (handle == 0) {
        Com_Printf("Couldn't write %s.\n", filename);
        return;
    }

    FS_Printf(handle, COM_GENERATED_CONFIG_HEADER);
    Key_WriteBindings(handle);
    Cvar_WriteVariables(handle);
    FS_FCloseFile(handle);
}

void Com_WriteDefaultsToFile(const char *filename)
{
    int32_t handle;

    handle = FS_FOpenFileWrite(filename);
    if (handle == 0) {
        Com_Printf("Couldn't write %s.\n", filename);
        return;
    }

    FS_Printf(handle, COM_GENERATED_CONFIG_HEADER);
    Cvar_WriteDefaults(handle);
    FS_FCloseFile(handle);
}

void Com_WriteConfiguration(void)
{
    if (com_configAutowriteEnabled != qfalse &&
        (cvar_modifiedFlags & COM_CONFIG_AUTOWRITE_MASK) != 0) {
        cvar_modifiedFlags &= ~COM_CONFIG_AUTOWRITE_MASK;
        Com_WriteConfigToFile(COM_CONFIG_AUTOWRITE_NAME);
    }
}

void Com_WriteConfig_f(void)
{
    char filename[COM_CONFIG_NAME_BUFFER_SIZE];

    if (Cmd_Argc() != COM_CONFIG_ARGC) {
        Com_Printf("Usage: writeconfig <filename>\n");
        return;
    }

    Q_strncpyz(filename, Cmd_Argv(COM_CONFIG_FILENAME_ARG),
               COM_CONFIG_NAME_EXTENSION_LIMIT);
    Com_DefaultExtension(filename, COM_CONFIG_NAME_EXTENSION_LIMIT,
                         COM_CONFIG_DEFAULT_EXTENSION);
    Com_Printf("Writing %s.\n", filename);
    Com_WriteConfigToFile(filename);
}

void Com_WriteDefaults_f(void)
{
    char filename[COM_CONFIG_NAME_BUFFER_SIZE];

    if (Cmd_Argc() != COM_CONFIG_ARGC) {
        Com_Printf("Usage: writedefaults <filename>\n");
        return;
    }

    Q_strncpyz(filename, Cmd_Argv(COM_CONFIG_FILENAME_ARG),
               COM_CONFIG_NAME_EXTENSION_LIMIT);
    Com_DefaultExtension(filename, COM_CONFIG_NAME_EXTENSION_LIMIT,
                         COM_CONFIG_DEFAULT_EXTENSION);
    Com_Printf("Writing %s.\n", filename);
    Com_WriteDefaultsToFile(filename);
}

int32_t Com_ModifyMsec(int32_t msec)
{
    if (host_cvar_timescale_pointer_slot->value < 0.0f) {
        Cvar_Set("timescale", COM_TIMESCALE_RESET_VALUE);
    }

    if (host_cvar_fixedtime_pointer_slot->integer == 0) {
        /* Stock VA 0x8071d75..0x8071d92 feeds msec through bare fild, keeps
         * the product live in x87, and truncates it directly with fistp. The
         * native non-x87 expression would round to binary32 first. */
#if EMULATE_X87
        msec = x87f_store_i32_trunc(x87f_mul(
            x87f_load_i32(msec),
            x87f_load_f32(host_cvar_timescale_pointer_slot->value)));
#elif defined(__x86_64__)
        msec = CODUO_X87_TRUNCATE_I32(
            (long double)msec *
            (long double)host_cvar_timescale_pointer_slot->value);
#else
        msec = (int32_t)(msec * host_cvar_timescale_pointer_slot->value);
#endif
    } else {
        msec = host_cvar_fixedtime_pointer_slot->integer;
    }

    if (msec < COM_FRAME_MIN_MSEC &&
        host_cvar_timescale_pointer_slot->value != 0.0f) {
        msec = COM_FRAME_MIN_MSEC;
    }

    return msec;
}

int32_t Com_ClampMsec(int32_t msec)
{
    int32_t maxMsec;

    if (dedicated->integer == 0) {
        if (host_cvar_sv_running_pointer_slot->integer == 0) {
            maxMsec = COM_DEDICATED_FRAME_CLAMP_MSEC;
        } else {
            maxMsec = COM_SERVER_FRAME_CLAMP_MSEC;
        }
    } else {
        if (msec > COM_HITCH_WARNING_MIN_MSEC &&
            msec < COM_HITCH_WARNING_MAX_MSEC) {
            Com_Printf("Hitch warning: %i msec frame time\n", msec);
        }
        maxMsec = COM_DEDICATED_FRAME_CLAMP_MSEC;
    }

    if (maxMsec < msec) {
        msec = maxMsec;
    }

    return msec;
}

void Com_Frame(void)
{
    int32_t frameStartTime = 0;
    int32_t eventTime = 0;
    int32_t serverTime = 0;
    int32_t clientStartTime = 0;
    int32_t frameEndTime = 0;
    int32_t rawMsec;
    int32_t scaledMsec;
    int32_t minMsec;

    if (CODUO_SETJMP(com_frameAbortContext, COM_FRAME_LONGJMP_SAVE_MASK) != 0) {
        Com_ErrorCleanup();
        return;
    }

    Com_WriteConfiguration();
    if (host_cvar_viewlog_pointer_slot->modified != 0) {
        if (dedicated->value == 0.0f) {
            Sys_ShowConsole(host_cvar_viewlog_pointer_slot->integer, qfalse);
        }
        host_cvar_viewlog_pointer_slot->modified = qfalse;
    }

    SetAnimCheck(host_cvar_com_animCheck_pointer_slot->integer);

    if (host_cvar_com_speeds_pointer_slot->integer != 0) {
        frameStartTime = Sys_Milliseconds();
    }

    minMsec = COM_FRAME_DEFAULT_MSEC;
    if (host_cvar_com_maxfps_pointer_slot->integer > 0 &&
        dedicated->integer == 0) {
        minMsec =
            COM_FRAME_MSEC_BASE / host_cvar_com_maxfps_pointer_slot->integer;
    }

    do {
        host_sv_serverId_source = Com_EventLoop();
        if (host_sv_serverId_source < com_frameLastTime) {
            com_frameLastTime = host_sv_serverId_source;
        }
        rawMsec = host_sv_serverId_source - com_frameLastTime;
    } while (rawMsec < minMsec);

    Cbuf_Execute();
    com_frameLastTime = host_sv_serverId_source;

    rawMsec = Com_ClampMsec(rawMsec);
    scaledMsec = Com_ModifyMsec(rawMsec);

    /* ORIGINAL_BINARY_BUG: if Cbuf_Execute enables com_speeds after the entry
     * sample was skipped, stock leaves frameStartTime at zero but takes and
     * prints the later samples; preserved per
     * original-bugs/engine-com-speeds-enable-frame-sample.md. */
    if (host_cvar_com_speeds_pointer_slot->integer != 0) {
        eventTime = Sys_Milliseconds();
    }

    SV_Frame(rawMsec);
    if (host_cvar_com_speeds_pointer_slot->integer != 0) {
        serverTime = Sys_Milliseconds();
    }

    if (dedicated->integer == 0) {
        Com_EventLoop();
        Cbuf_Execute();
        if (host_cvar_com_speeds_pointer_slot->integer != 0) {
            clientStartTime = Sys_Milliseconds();
        }
        CL_Frame(rawMsec, scaledMsec);
        if (host_cvar_com_speeds_pointer_slot->integer != 0) {
            frameEndTime = Sys_Milliseconds();
        }
    } else if (host_cvar_com_speeds_pointer_slot->integer != 0) {
        frameEndTime = Sys_Milliseconds();
        clientStartTime = frameEndTime;
    }

    if (host_cvar_com_speeds_pointer_slot->integer != 0) {
        Com_Printf("frame:%i all:%3i sv:%3i ev:%3i cl:%3i gm:%3i rf:%3i "
                   "bk:%3i\n",
                   com_frameNumber, frameEndTime - eventTime,
                   (serverTime - eventTime) - com_timeGame,
                   ((eventTime - frameStartTime) + clientStartTime) -
                       serverTime,
                   (frameEndTime - clientStartTime) -
                       (com_timeFrontend + com_timeBackend),
                   com_timeGame, com_timeFrontend, com_timeBackend);
    }

    ++com_frameNumber;
}

void Com_Close(void)
{
    DObjShutdownPool();
    XAnimShutdownSmallTreePool();
    XAnimShutdown();
    Scr_Shutdown();

    if (com_logFileHandle != 0) {
        FS_FCloseFile(com_logFileHandle);
        com_logFileHandle = 0;
    }

    if (com_journalFile != 0) {
        FS_FCloseFile(com_journalFile);
        com_journalFile = 0;
    }

    /* ORIGINAL_BINARY_BUG: stock never closes or clears
     * com_journalDataFile; preserving that omission leaks the journal data
     * handle through normal shutdown.  See
     * original-bugs/engine-journal-data-file-close-leak.md. */
}
