#include "coduo_engine_structs.h"
#include "../abi/vm_private.h"
#include "../core_command/cmd_private.h"
#include "../core_cvar/cvar_private.h"
#include "../core_runtime/core_runtime_private.h"
#include "../filesystem/fs_private.h"
#include "../networking/msg_private.h"
#include "../networking/netchan_private.h"
#include "../networking/sv_snapshot_private.h"
#include "sv_globals_private.h"
#include "server_private.h"

#include <stdlib.h>
#include <string.h>

enum {
    SV_CLIENT_GAMESTATE_CONFIGSTRING_OPCODE = 3,
    SV_CLIENT_GAMESTATE_BASELINE_OPCODE = 4,
    SV_CLIENT_GAMESTATE_OPCODE = 2,
    SV_DOWNLOAD_BLOCK_SIZE = 0x800,
    SV_DOWNLOAD_MESSAGE_BUFFER_SIZE = 0x400,
    SV_DOWNLOAD_REDIRECT_URL_SIZE = 0x100,
    SV_DOWNLOAD_STALL_RETRANSMIT_MS = 1000,
    SV_DOWNLOAD_ID_PAK_PREFIX = 0,
    SV_DOWNLOAD_MAX_RATE_MINIMUM = 1000,
    SV_CLIENT_PURE_STATE_PENDING = 0
};

qboolean SV_CheckFallbackURL(client_t *client, msg_t *msg)
{
    (void)client;
    (void)msg;
    return qtrue;
}

void SV_FreeClientScriptPers(void)
{
    client_t *client = svs.clients;

    for (int32_t clientNum = 0;
         clientNum < host_cvar_sv_maxclients_pointer_slot->integer;
         ++clientNum) {
        if (client->state > CODUO_CS_ZOMBIE) {
            SV_FreeClientScriptId(client);
            client->scriptId = coduomp_sv_alloc_client_script_pers();
        }

        ++client;
    }
}

void SV_DropClient(client_t *client, const char *dropReason)
{
    char name[40];

    if (client->state == CODUO_CS_ZOMBIE) {
        return;
    }

    client->deferredDropReason = NULL;
    strcpy(name, client->name);
    PB_DropClient(client);
    Com_DPrintf("Going to CS_ZOMBIE for %s\n", name);
    client->state = CODUO_CS_ZOMBIE;

    if (client->gentity == NULL) {
        for (int32_t challengeNum = 0;
             challengeNum < CODUO_HOST_CHALLENGE_COUNT;
             ++challengeNum) {
            challenge_t *challenge = &host_svs_challenges[challengeNum];

            if (NET_CompareAdr(client->netchan.remoteAddress, challenge->adr) !=
                qfalse) {
                challenge->connected = qfalse;
                break;
            }
        }
    }

    /*
     * ORIGINAL_BINARY_BUG: stock passes dropReason directly to Q_stricmp
     * without rejecting NULL. See original-bugs/
     * coduomp-dropclient-null-reason-dereference.md.
     */
    if (Q_stricmp(dropReason, "EXE_DISCONNECTED") != 0) {
        SV_SendServerCommand(NULL, qfalse, "e \"\x15%s^7 \x14%s\"", name,
                             dropReason);
    }

    Com_Printf("%i:%s %s\n", (int)(client - svs.clients), name,
               dropReason);
    SV_SendServerCommand(client, qtrue, va("w \"%s\"", dropReason));

    int32_t activeClient;
    for (activeClient = 0;
         activeClient < host_cvar_sv_maxclients_pointer_slot->integer &&
         svs.clients[activeClient].state < CODUO_CS_CONNECTED;
         ++activeClient) {
    }

    if (activeClient == host_cvar_sv_maxclients_pointer_slot->integer) {
        SV_Heartbeat_f();
    }
}

void SV_SetClientDeferredDropReason(client_t *client,
                                    const char *dropReason)
{
    if (client->state != CODUO_CS_ZOMBIE &&
        client->deferredDropReason == NULL) {
        client->deferredDropReason = dropReason;
    }
}

void SV_DropClientNum(int32_t clientNum, const char *dropReason)
{
    SV_DropClient(&svs.clients[clientNum], dropReason);
}

void SV_SendClientGameState(client_t *client)
{
    uint8_t msgData[CODUO_MAX_MSGLEN];
    msg_t msg;
    coduo_host_entity_state_snapshot_t nullEntityState;

    while (client->state != CODUO_CS_FREE &&
           client->netchan.unsentFragments != qfalse) {
        SV_Netchan_TransmitNextFragment(&client->netchan);
    }

    Com_DPrintf("SV_SendClientGameState() for %s\n", client->name);
    Com_DPrintf("Going from CS_CONNECTED to CS_PRIMED for %s\n",
                client->name);

    client->state = CODUO_CS_PRIMED;
    client->pureAuthState = SV_CLIENT_PURE_STATE_PENDING;
    client->gamestateMessageNum = client->netchan.outgoingSequence;

    MSG_Init(&msg, msgData, sizeof(msgData));
    MSG_WriteLong(&msg, client->lastClientCommand);
    SV_UpdateServerCommandsToClient(client, &msg);

    MSG_WriteByte(&msg, SV_CLIENT_GAMESTATE_OPCODE);
    MSG_WriteLong(&msg, client->reliableSequence);

    for (int configIndex = 0; configIndex < CODUO_MAX_CONFIGSTRINGS;
         ++configIndex) {
        if (host_sv_configstrings[configIndex][0] != '\0') {
            MSG_WriteByte(&msg, SV_CLIENT_GAMESTATE_CONFIGSTRING_OPCODE);
            MSG_WriteShort(&msg, configIndex);
            MSG_WriteBigString(&msg, host_sv_configstrings[configIndex]);
        }
    }

    memset(&nullEntityState, 0, sizeof(nullEntityState));
    for (int baselineIndex = 0; baselineIndex < CODUO_MAX_GENTITIES;
         ++baselineIndex) {
        coduo_host_entity_baseline_t *baseline =
            &sv_entityLinks[baselineIndex].baseline;

        if (baseline->entityState.s_number != 0) {
            MSG_WriteByte(&msg, SV_CLIENT_GAMESTATE_BASELINE_OPCODE);
            MSG_WriteDeltaEntity(&msg, &nullEntityState,
                                 &baseline->entityState, qtrue);
        }
    }

    MSG_WriteByte(&msg, CODUO_SVC_EOF);
    MSG_WriteLong(&msg, (int32_t)(client - svs.clients));
    MSG_WriteLong(&msg, sv.gamestateChecksumFeed);
    MSG_WriteByte(&msg, CODUO_SVC_EOF);

    Com_DPrintf("Sending %i bytes in gamestate to client: %i\n",
                msg.cursize, (int)(client - svs.clients));
    SV_SendMessageToClient(&msg, client);

    if (host_cvar_timescale_pointer_slot->integer == 0 &&
        client->state != CODUO_CS_FREE &&
        client->netchan.unsentFragments != qfalse) {
        SV_Netchan_TransmitNextFragment(&client->netchan);
    }
}

void SV_ClientEnterWorld(client_t *client, usercmd_t *cmd)
{
    int32_t clientNum =
        (int32_t)(client - svs.clients);
    sharedEntity_t *gentity;

    Com_DPrintf("Going from CS_PRIMED to CS_ACTIVE for %s\n", client->name);
    client->state = CODUO_CS_ACTIVE;

    gentity = SV_GentityNum(clientNum);
    gentity->entityState.s_number = clientNum;
    client->gentity = gentity;
    client->deltaMessage = CODUO_CLIENT_DELTA_MESSAGE_NONE;
    client->nextSnapshotTime = svs.timeSecondary;
    client->usercmd = *cmd;

    VM_Call(sv_gameVM, CODUO_GAME_CLIENT_BEGIN, clientNum);
}

void SV_ClearClientDownload(client_t *client)
{
    if (client->download.fileHandle != 0) {
        FS_FCloseFile(client->download.fileHandle);
    }

    client->download.fileHandle = 0;
    client->download.fileName[0] = '\0';

    for (int blockIndex = 0;
         blockIndex < CODUO_HOST_CLIENT_DOWNLOAD_BLOCK_COUNT;
         ++blockIndex) {
        if (client->download.blockData[blockIndex] != NULL) {
            Z_Free(client->download.blockData[blockIndex]);
            client->download.blockData[blockIndex] = NULL;
        }
    }
}

void SV_ClientCommand_Stopdl(client_t *client)
{
    if (client->download.fileName[0] != '\0') {
        Com_DPrintf("clientDownload: %d : file \"%s\" aborted\n",
                    (int)(client - svs.clients),
                    client->download.fileName);
    }

    SV_ClearClientDownload(client);
}

void SV_ClientCommand_Donedl(client_t *client)
{
    Com_DPrintf("clientDownload: %s Done\n", client->name);
    SV_SendClientGameState(client);
}

void SV_ClientCommand_Retransdl(client_t *client)
{
    const char *blockText = Cmd_Argv(1);
    int block = atoi(blockText);

    if (block == client->download.nextAcknowledgmentBlock) {
        client->download.nextTransmitBlock =
            client->download.nextAcknowledgmentBlock;
    }
}

void SV_ClientCommand_Nextdl(client_t *client)
{
    const char *blockText = Cmd_Argv(1);
    int acknowledgedBlock = atoi(blockText);

    if (acknowledgedBlock != client->download.nextAcknowledgmentBlock) {
        SV_DropClient(client, "broken download");
        return;
    }

    Com_DPrintf("clientDownload: %d : client acknowledge of block %d\n",
                (int)(client - svs.clients), acknowledgedBlock);

    int windowIndex =
        client->download.nextAcknowledgmentBlock %
        CODUO_HOST_CLIENT_DOWNLOAD_BLOCK_COUNT;
    if (client->download.blockByteCounts[windowIndex] == 0) {
        Com_Printf("clientDownload: %d : file \"%s\" completed\n",
                   (int)(client - svs.clients),
                   client->download.fileName);
        SV_ClearClientDownload(client);
        return;
    }

    client->download.lastBlockActivityTime = svs.timeSecondary;
    ++client->download.nextAcknowledgmentBlock;
}

void SV_ClientCommand_Download(client_t *client)
{
    SV_ClearClientDownload(client);
    /*
     * ORIGINAL_BINARY_BUG: the client-supplied qpath is retained without
     * rejecting absolute or parent-relative components before the later
     * FS_SV_FOpenFileRead. See original-bugs/
     * coduomp-host-filesystem-unsanitized-paths.md.
     */
    Q_strncpyz(client->download.fileName, Cmd_Argv(1),
               CODUO_HOST_CLIENT_DOWNLOAD_NAME_SIZE);
}

void SV_ClientCommand_Wwwdl(client_t *client)
{
    const char *subcommand = Cmd_Argv(1);

    if (client->download.redirectActive == qfalse) {
        Com_Printf("SV_WWWDownload: unexpected wwwdl '%s' for client '%s'\n",
                   subcommand, client->name);
        SV_DropClient(client, "GMI_EXE_UNEXPECTEDWWWDOWLOADMESSAGE");
        return;
    }

    if (Q_stricmp(subcommand, "ack") == 0) {
        if (client->download.redirectAcknowledged != qfalse) {
            Com_Printf("WARNING: dupe wwwdl ack from client '%s'\n",
                       client->name);
        }
        client->download.redirectAcknowledged = qtrue;
        return;
    }

    if (Q_stricmp(subcommand, "bbl8r") == 0) {
        SV_DropClient(client, "GMI_EXE_DOWNLOADDISCONNECTED");
        return;
    }

    if (client->download.redirectAcknowledged == qfalse) {
        Com_Printf("SV_WWWDownload: unexpected wwwdl '%s' for client '%s'\n",
                   subcommand, client->name);
        SV_DropClient(client, "GMI_EXE_UNEXPECTEDWWWDOWLOADMESSAGE");
        return;
    }

    if (Q_stricmp(subcommand, "done") == 0) {
        client->download.fileHandle = 0;
        client->download.fileName[0] = '\0';
        client->download.redirectAcknowledged = qfalse;
        return;
    }

    if (Q_stricmp(subcommand, "fail") == 0) {
        client->download.fileHandle = 0;
        client->download.fileName[0] = '\0';
        client->download.redirectAcknowledged = qfalse;
        client->download.redirectFailed = qtrue;
        SV_SendClientGameState(client);
        return;
    }

    if (Q_stricmp(subcommand, "chkfail") == 0) {
        Com_Printf("WARNING: client '%s' reports that the redirect download "
                   "for '%s' had wrong checksum.\n",
                   client->name, client->download.fileName);
        Com_Printf("         you should check your download redirect "
                   "configuration.\n");
        client->download.fileHandle = 0;
        client->download.fileName[0] = '\0';
        client->download.redirectAcknowledged = qfalse;
        client->download.redirectFailed = qtrue;
        SV_SendClientGameState(client);
        return;
    }

    Com_Printf("SV_WWWDownload: unknown wwwdl subcommand '%s' for client '%s'\n",
               subcommand, client->name);
    SV_DropClient(client, "GMI_EXE_UNEXPECTEDWWWDOWLOADMESSAGE");
}

void SV_WriteDownloadFailureHeader(client_t *client,
                                   msg_t *msg)
{
    MSG_WriteByte(msg, CODUO_SVC_DOWNLOAD);
    MSG_WriteShort(msg, 0);
    MSG_WriteLong(msg, -1);
    client->download.fileName[0] = '\0';
}

void SV_WriteDownloadToClient(client_t *client, msg_t *msg)
{
    char errorText[SV_DOWNLOAD_MESSAGE_BUFFER_SIZE];

    if (client->download.fileName[0] == '\0' ||
        client->download.redirectAcknowledged != qfalse) {
        return;
    }

    if (client->download.fileHandle == 0) {
        qboolean idPak;

        Com_DPrintf("clientDownload: %d : begining \"%s\"\n",
                    (int)(client - svs.clients),
                    client->download.fileName);

        idPak = FS_idPak(
            client->download.fileName, "main",
            host_cvar_fs_basegame_pointer_slot->string);
        if (host_cvar_sv_allowDownload_pointer_slot->integer == 0 ||
            idPak != qfalse) {
            if (idPak == qfalse) {
                if (host_cvar_sv_allowDownload_pointer_slot->integer == 0) {
                    Com_Printf("clientDownload: %d : \"%s\" download disabled",
                               (int)(client - svs.clients),
                               client->download.fileName);
                    if (host_cvar_sv_pure_pointer_slot->integer == 0) {
                        Com_sprintf(errorText, sizeof(errorText),
                                     "EXE_AUTODL_SERVERDISABLED\x15%s",
                                     client->download.fileName);
                    } else {
                        Com_sprintf(errorText, sizeof(errorText),
                                     "EXE_AUTODL_SERVERDISABLED_PURE\x15%s",
                                     client->download.fileName);
                    }
                } else {
                    Com_Printf("clientDownload: %d : \"%s\" file not found "
                               "on server\n",
                               (int)(client - svs.clients),
                               client->download.fileName);
                    Com_sprintf(errorText, sizeof(errorText),
                                 "EXE_AUTODL_FILENOTONSERVER\x15%s",
                                 client->download.fileName);
                }
            } else {
                Com_Printf("clientDownload: %d : \"%s\" cannot download id "
                           "pk3 files\n",
                           (int)(client - svs.clients),
                           client->download.fileName);
                Com_sprintf(errorText, sizeof(errorText),
                             "EXE_CANTAUTODLGAMEPAK\x15%s",
                             client->download.fileName);
            }

            MSG_WriteByte(msg, CODUO_SVC_DOWNLOAD);
            MSG_WriteShort(msg, 0);
            MSG_WriteLong(msg, -1);
            MSG_WriteString(msg, errorText);
            client->download.fileName[0] = '\0';
            return;
        }

        if (host_cvar_sv_wwwDownload_pointer_slot->integer != 0) {
            if (client->download.redirectAllowedByClient == qfalse) {
                if (SV_CheckFallbackURL(client, msg) != qfalse) {
                    return;
                }
                Com_Printf("Client '%s' is not configured for www download\n",
                           client->name);
            } else if (client->download.redirectFailed == qfalse) {
                int32_t redirectHandle;
                int redirectSize = FS_SV_FOpenFileRead(
                    client->download.fileName, &redirectHandle);
                if (redirectSize != 0) {
                    FS_FCloseFile(redirectHandle);
                    Q_strncpyz(
                        client->download.redirectUrl,
                        va("%s/%s",
                           host_cvar_sv_wwwBaseURL_pointer_slot->string,
                           client->download.fileName),
                        SV_DOWNLOAD_REDIRECT_URL_SIZE);
                    Com_DPrintf("Redirecting client '%s' to %s\n",
                                client->name,
                                client->download.redirectUrl);
                    client->download.redirectActive = qtrue;
                    MSG_WriteByte(msg, CODUO_SVC_DOWNLOAD);
                    MSG_WriteShort(msg, -1);
                    MSG_WriteString(msg, client->download.redirectUrl);
                    MSG_WriteLong(msg, redirectSize);
                    MSG_WriteLong(
                        msg,
                        host_cvar_sv_wwwDlDisconnected_pointer_slot->integer !=
                            0);
                    return;
                }

                /* ORIGINAL_BINARY_BUG: each of these two-conversion formats
                 * receives only the download filename; see original-bugs/
                 * coduomp-download-size-diagnostic-missing-argument.md. */
                Com_Printf("ERROR: Client '%s': couldn't extract file size "
                           "for %s\n",
                           client->download.fileName);
            } else {
                client->download.redirectFailed = qfalse;
                if (SV_CheckFallbackURL(client, msg) != qfalse) {
                    return;
                }
                /* ORIGINAL_BINARY_BUG: the same missing client-name argument
                 * is preserved in the fallback diagnostic. */
                Com_Printf("Client '%s': falling back to regular downloading "
                           "for failed file %s\n",
                           client->download.fileName);
            }
        }

        client->download.redirectActive = qfalse;
        client->download.fileSize = FS_SV_FOpenFileRead(
            client->download.fileName, &client->download.fileHandle);
        if (client->download.fileSize <= 0) {
            Com_Printf("clientDownload: %d : \"%s\" file not found on "
                       "server\n",
                       (int)(client - svs.clients),
                       client->download.fileName);
            Com_sprintf(errorText, sizeof(errorText),
                         "EXE_AUTODL_FILENOTONSERVER\x15%s",
                         client->download.fileName);
            SV_WriteDownloadFailureHeader(client, msg);
            MSG_WriteString(msg, errorText);
            return;
        }

        client->download.nextTransmitBlock = 0;
        client->download.nextAcknowledgmentBlock = 0;
        client->download.nextBufferedBlock = 0;
        client->download.bytesRead = 0;
        client->download.eofBlockQueued = qfalse;
    }

    while (client->download.nextBufferedBlock -
                   client->download.nextAcknowledgmentBlock <=
               CODUO_HOST_CLIENT_DOWNLOAD_BLOCK_COUNT - 1 &&
           client->download.fileSize != client->download.bytesRead) {
        int blockIndex =
            client->download.nextBufferedBlock %
            CODUO_HOST_CLIENT_DOWNLOAD_BLOCK_COUNT;
        if (client->download.blockData[blockIndex] == NULL) {
            client->download.blockData[blockIndex] =
                Z_Malloc(SV_DOWNLOAD_BLOCK_SIZE);
        }

        client->download.blockByteCounts[blockIndex] =
            FS_Read(client->download.blockData[blockIndex],
                    SV_DOWNLOAD_BLOCK_SIZE,
                    client->download.fileHandle);
        if (client->download.blockByteCounts[blockIndex] < 0) {
            client->download.bytesRead = client->download.fileSize;
            break;
        }

        client->download.bytesRead +=
            client->download.blockByteCounts[blockIndex];
        ++client->download.nextBufferedBlock;
    }

    if (client->download.bytesRead == client->download.fileSize &&
        client->download.eofBlockQueued == qfalse &&
        client->download.nextBufferedBlock -
                client->download.nextAcknowledgmentBlock <
            CODUO_HOST_CLIENT_DOWNLOAD_BLOCK_COUNT) {
        int blockIndex =
            client->download.nextBufferedBlock %
            CODUO_HOST_CLIENT_DOWNLOAD_BLOCK_COUNT;
        client->download.blockByteCounts[blockIndex] = 0;
        ++client->download.nextBufferedBlock;
        client->download.eofBlockQueued = qtrue;
    }

    int rate = client->rate;
    if (host_cvar_sv_maxRate_pointer_slot->integer != 0) {
        if (host_cvar_sv_maxRate_pointer_slot->integer <
            SV_DOWNLOAD_MAX_RATE_MINIMUM) {
            Cvar_Set("sv_MaxRate", "1000");
        }
        if (host_cvar_sv_maxRate_pointer_slot->integer < rate) {
            rate = host_cvar_sv_maxRate_pointer_slot->integer;
        }
    }

    int blockBudget;
    if (rate == 0) {
        blockBudget = 1;
    } else {
        int bytes = (rate * client->snapshotMsec) / 1000;
        int rounded = bytes + SV_DOWNLOAD_BLOCK_SIZE;
        if (rounded < 0) {
            rounded = bytes + (SV_DOWNLOAD_BLOCK_SIZE * 2 - 1);
        }
        blockBudget = rounded / SV_DOWNLOAD_BLOCK_SIZE;
    }

    if (blockBudget < 0) {
        blockBudget = 1;
    }

    while (--blockBudget != -1) {
        if (client->download.nextAcknowledgmentBlock ==
            client->download.nextBufferedBlock) {
            return;
        }

        if (client->download.nextTransmitBlock ==
            client->download.nextBufferedBlock) {
            if (svs.timeSecondary - client->download.lastBlockActivityTime <=
                SV_DOWNLOAD_STALL_RETRANSMIT_MS) {
                return;
            }
            client->download.nextTransmitBlock =
                client->download.nextAcknowledgmentBlock;
        }

        int blockIndex =
            client->download.nextTransmitBlock %
            CODUO_HOST_CLIENT_DOWNLOAD_BLOCK_COUNT;
        MSG_WriteByte(msg, CODUO_SVC_DOWNLOAD);
        MSG_WriteShort(msg, client->download.nextTransmitBlock);
        if (client->download.nextTransmitBlock == 0) {
            MSG_WriteLong(msg, client->download.fileSize);
        }
        MSG_WriteShort(msg, client->download.blockByteCounts[blockIndex]);
        if (client->download.blockByteCounts[blockIndex] != 0) {
            MSG_WriteData(msg, client->download.blockData[blockIndex],
                          client->download.blockByteCounts[blockIndex]);
        }

        Com_DPrintf("clientDownload: %d : writing block %d\n",
                    (int)(client - svs.clients),
                    client->download.nextTransmitBlock);
        ++client->download.nextTransmitBlock;
        client->download.lastBlockActivityTime = svs.timeSecondary;
    }
}
