#include <float.h>
#include <stdint.h>

#include "coduo_engine_structs.h"
#include "../abi/vm_private.h"
#include "../animation/xanim_private.h"
#include "../core_cvar/cvar_private.h"
#include "../core_math/core_math_private.h"
#include "../core_memory/core_memory_private.h"
#include "../core_runtime/core_runtime_private.h"
#include "../filesystem/fs_private.h"
#include "../physics_collision/cm_world_sector_private.h"
#include "../scripting/script_runtime_private.h"
#include "../sound_alias/sound_alias_private.h"
#include "server_private.h"
#include "sv_globals_private.h"

enum {
    SV_GAME_COM_ERROR_FATAL = 0,
    SV_GAME_COM_ERROR_DROP = 1,
    SV_GAME_SOUND_ALIAS_SET_GAME = 2,
    SV_GAME_PRINT_CHANNEL_DEDICATED = 4
};

static const char svGameStringFormat[] = "%s";
static const char svGameModuleName[] = "game";
static const char svGameVmCreateFailure[] = "\x15VM_Create on game failed";
static const char svGameVmApiMismatch[] =
    "\x15game is version %d, expected %d";

void SV_GameSendServerCommand(int32_t clientNum,
                              qboolean reliable,
                              const char *command)
{
    if (clientNum == -1) {
        SV_SendServerCommand(NULL, reliable, svGameStringFormat, command);
        return;
    }

    if (clientNum >= 0 &&
        clientNum < host_cvar_sv_maxclients_pointer_slot->integer) {
        SV_SendServerCommand(&svs.clients[clientNum], reliable,
                             svGameStringFormat, command);
    }
}

void SV_GameDropClient(int32_t clientNum, const char *reason)
{
    if (clientNum >= 0 &&
        clientNum < host_cvar_sv_maxclients_pointer_slot->integer) {
        SV_DropClient(&svs.clients[clientNum], reason);
    }
}

int32_t
SV_inSnapshot(const vec3_t origin, int32_t entityNum)
{
    sharedEntity_t *gentity = SV_GentityNum(entityNum);
    svEntity_t *svEntity;
    int32_t leafnum;
    int32_t area;
    int32_t cluster;
    const uint8_t *pvs;
    int32_t testCluster = 0;
    int32_t clusterIndex;
    uint32_t fogOpaqueDistSqBits;
    float fogOpaqueDistSq;

    if (gentity->linked == 0) {
        return qfalse;
    }

    if ((gentity->svFlags & CODUO_SHARED_ENTITY_SVFLAG_NOCLIENT) != 0) {
        return qfalse;
    }

    if ((gentity->svFlags & CODUO_SHARED_ENTITY_SVFLAG_VISIBILITY_BYPASS_MASK) !=
            0 ||
        gentity->soundTime != 0) {
        return qtrue;
    }

    svEntity = SV_SvEntityForGentity(gentity);
    leafnum = CM_PointLeafnum(origin);
    area = CM_LeafArea(leafnum);

    if (CM_AreasConnected(area, svEntity->areaNum) == 0 &&
        CM_AreasConnected(area, svEntity->areaNum2) == 0) {
        return qfalse;
    }

    if (svEntity->numClusters == 0) {
        return qfalse;
    }

    cluster = CM_LeafCluster(leafnum);
    pvs = CM_ClusterPVS(cluster);
    clusterIndex = 0;
    while (clusterIndex < svEntity->numClusters) {
        testCluster = svEntity->clusterNums[clusterIndex];
        if (((pvs[testCluster >> 3] >> (testCluster & 7)) & 1) != 0) {
            break;
        }
        ++clusterIndex;
    }

    if (clusterIndex == svEntity->numClusters) {
        if (svEntity->lastCluster == 0) {
            return qfalse;
        }

        /*
         * ORIGINAL_BINARY_BUG: the inclusive scan followed by equality
         * rejects a terminal visible cluster but accepts a completely
         * invisible overflow range. See original-bugs/
         * coduomp-snapshot-overflow-cluster-terminal-test.md.
         */
        while (testCluster <= svEntity->lastCluster &&
               ((pvs[testCluster >> 3] >> (testCluster & 7)) & 1) == 0) {
            ++testCluster;
        }

        if (testCluster == svEntity->lastCluster) {
            return qfalse;
        }
    }

    fogOpaqueDistSqBits = (uint32_t)
        VM_Call(sv_gameVM, CODUO_GAME_GET_FOG_OPAQUE_DIST_SQ_BITS);
    memcpy(&fogOpaqueDistSq, &fogOpaqueDistSqBits, sizeof(fogOpaqueDistSq));
    if (fogOpaqueDistSq == FLT_MAX) {
        return qtrue;
    }

    return BoxDistSqrdExceeds(gentity->absMin, gentity->absMax, origin,
                              fogOpaqueDistSq) == 0;
}

void SV_LocateGameData(sharedEntity_t *gEnts,
                       int32_t numGEntities,
                       int32_t sizeofGEntity,
                       playerState_t *clients,
                       int32_t sizeofClient)
{
    sv_gameData.gentities = gEnts;
    sv_gameData.gentitySize = sizeofGEntity;
    sv_gameData.numGEntities = numGEntities;
    sv_gameData.clients = clients;
    sv_gameData.clientSize = sizeofClient;
}

int32_t FloatAsInt(float value)
{
    int32_t bits;

    memcpy(&bits, &value, sizeof(bits));
    return bits;
}

void MatrixTransformVector43(const vec3_t in,
                             const DObjSkelMat *matrix,
                             vec3_t out)
{
    out[0] = in[0] * matrix->axis[0][0] + in[1] * matrix->axis[1][0] +
             in[2] * matrix->axis[2][0] + matrix->origin[0];
    out[1] = in[0] * matrix->axis[0][1] + in[1] * matrix->axis[1][1] +
             in[2] * matrix->axis[2][1] + matrix->origin[1];
    out[2] = in[0] * matrix->axis[0][2] + in[1] * matrix->axis[1][2] +
             in[2] * matrix->axis[2][2] + matrix->origin[2];
}

qboolean SV_MapExists(const char *mapName)
{
    const char *cleanMapName = SV_GetMapBaseName(mapName);
    const char *bspPath = va("maps/mp/%s.bsp", cleanMapName);

    return FS_ReadFile(bspPath, NULL) >= 0;
}

void SV_ResetEntityParsePoint(void)
{
    sv_entityParsePoint = CM_EntityString();
}

void SV_ShutdownGameProgs(void)
{
    Com_UnloadSoundAliasSet(SV_GAME_SOUND_ALIAS_SET_GAME);

    if (sv_gameVM != NULL) {
        XAnimSetUser(XANIM_SECONDARY_POOL_PAYLOAD_SLOT);
        VM_Call(sv_gameVM, CODUO_GAME_SHUTDOWN, qfalse);
        Hunk_ClearToLowMark();
        VM_Free(sv_gameVM);
        sv_gameVM = NULL;
    }
}

void SV_CallGameInit(qboolean restart, qboolean cvarRestartGate)
{
    coduo_script_import_callback_t *engineCallbacks;
    coduo_script_export_callback_t *gameCallbacks;

    sv_entityParsePoint = CM_EntityString();
    engineCallbacks = Scr_NearHook(NULL);
    gameCallbacks = (coduo_script_export_callback_t *)VM_Call(
        sv_gameVM, CODUO_GAME_SCRIPT_FAR_HOOK, engineCallbacks);
    Scr_NearHook(gameCallbacks);

    Sys_LoadingKeepAlive();
    VM_Call(sv_gameVM, CODUO_GAME_INIT, svs.time, Com_Milliseconds(),
            (int)restart, (int)cvarRestartGate);
    Sys_LoadingKeepAlive();

    for (int32_t clientNum = 0;
         clientNum < host_cvar_sv_maxclients_pointer_slot->integer;
         ++clientNum) {
        svs.clients[clientNum].gentity = NULL;
    }

    if (dedicated->integer != 0) {
        Cvar_DumpToChannel(SV_GAME_PRINT_CHANNEL_DEDICATED);
    }
}

void SV_RestartGameProgs(qboolean cvarRestartGate)
{
    VM_Call(sv_gameVM, CODUO_GAME_SHUTDOWN, qtrue);
    Hunk_ClearToLowMark();
    SV_CallGameInit(qtrue, cvarRestartGate);
}

void SV_InitGameProgs(qboolean cvarRestartGate)
{
    intptr_t apiVersion;

    sv_gameVM = VM_Create(svGameModuleName, SV_GameSystemCalls);
    if (sv_gameVM == NULL) {
        Com_Error(SV_GAME_COM_ERROR_FATAL, svGameVmCreateFailure);
    }

    apiVersion = VM_Call(sv_gameVM, CODUO_GAME_GET_API_VERSION);
    if (apiVersion != CODUO_GAME_API_VERSION) {
        /* Native64 VM_Call returns intptr_t; the game API version is 32-bit. */
        Com_Error(SV_GAME_COM_ERROR_FATAL, svGameVmApiMismatch,
                  (int32_t)apiVersion,
                  CODUO_GAME_API_VERSION);
    }

    Hunk_SetLowMark();
    SV_CallGameInit(qfalse, cvarRestartGate);
}

qboolean SV_GameConsoleCommand(void)
{
    if (sv.state != CODUO_SS_GAME) {
        return qfalse;
    }

    XAnimSetUser(XANIM_SECONDARY_POOL_PAYLOAD_SLOT);
    return VM_Call(sv_gameVM, CODUO_GAME_CONSOLE_COMMAND);
}
