#include "coduo_engine_structs.h"
#include "../abi/vm_private.h"
#include "../animation/xanim_private.h"
#include "../core_command/cmd_private.h"
#include "../core_math/core_math_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 "sv_connect_authorize_private.h"
#include "server_private.h"

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

enum {
    SV_CLIENT_COMMAND_RELIABLE_WINDOW = 64,
    SV_CLIENT_COMMAND_FLOOD_DELAY_MS = 800,
    SV_CLIENT_COMMAND_CHECK_DIGEST_LEN = 32,
    SV_MAX_CONFIGURE_ARGS = 1024,
    SV_BOT_CONNECT_PROTOCOL = 22,
    SV_CLIENT_USERCMD_TYPE_DELTA = 0,
    SV_CLIENT_USERCMD_TYPE_NODELTA = 1,
    SV_CLIENT_COMMAND_TYPE_RELIABLE = 2,
    SV_CLIENT_COMMAND_TYPE_EOF = 3,
    SV_CLIENT_PURE_STATE_VALID = 1,
    SV_CLIENT_PURE_STATE_INVALID = 2
};

static int32_t SV_ClientNumForHostClient(
    const client_t *client)
{
    /* NOT_FROM_ORIGINAL_SOURCE: local factoring for repeated stock index math. */
    return (int32_t)(client - svs.clients);
}

static qboolean SV_IsChallengeAdr(client_t *client,
                                  netadr_t from)
{
    /* NOT_FROM_ORIGINAL_SOURCE: local typed wrapper for the stock NET_CompareBaseAdr call. */
    return NET_CompareBaseAdr(from, client->netchan.remoteAddress);
}

void SV_ClientCommand_Disconnect(client_t *client)
{
    SV_DropClient(client, "EXE_DISCONNECTED");
}

void SV_ClientCommand_Cp(client_t *client)
{
    int expectedA = 0;
    int expectedB = 0;
    qboolean valid = qtrue;
    int argc;
    int argIndex;
    int checkCount;
    int valueCount;
    int i;
    int j;
    int configureArgs[SV_MAX_CONFIGURE_ARGS];
    int values[SV_MAX_CONFIGURE_ARGS + 1];

    valid = FS_FileIsInPAK(FS_ShiftStr("wqaeicogaoraz:80fnn", -2),
                           &expectedA) == 1;
    if (valid) {
        valid = FS_FileIsInPAK(FS_ShiftStr("ztdzndrud}=;3iqq", -5),
                               &expectedB) == 1;
    }

    argc = Cmd_Argc();
    if (valid) {
        if (argc < 6) {
            valid = qfalse;
        } else {
            argIndex = 1;
            const char *arg = Cmd_Argv(argIndex++);
            if (arg == NULL || arg[0] == '@' || atoi(arg) != expectedA) {
                valid = qfalse;
            }

            if (valid) {
                arg = Cmd_Argv(argIndex++);
                if (arg == NULL || arg[0] == '@' || atoi(arg) != expectedB) {
                    valid = qfalse;
                }
            }

            if (valid) {
                arg = Cmd_Argv(argIndex++);
                if (arg[0] != '@') {
                    valid = qfalse;
                }
            }

            valueCount = 0;
            while (valid && argIndex < argc) {
                values[valueCount++] = atoi(Cmd_Argv(argIndex++));
            }

            checkCount = valueCount - 1;
            for (i = 0; valid && i < checkCount; ++i) {
                for (j = 0; j < checkCount; ++j) {
                    if (i != j && values[i] == values[j]) {
                        valid = qfalse;
                        break;
                    }
                }
            }

            if (valid) {
                Cmd_TokenizeString(FS_LoadedPakPureChecksums());
                argc = Cmd_Argc();
                if (argc > SV_MAX_CONFIGURE_ARGS) {
                    argc = SV_MAX_CONFIGURE_ARGS;
                }
                for (i = 0; i < argc; ++i) {
                    configureArgs[i] = atoi(Cmd_Argv(i));
                }

                for (i = 0; valid && i < checkCount; ++i) {
                    for (j = 0; j < argc && values[i] != configureArgs[j]; ++j) {
                    }
                    if (j >= argc) {
                        valid = qfalse;
                    }
                }
            }

            if (valid) {
                int checksum = sv.gamestateChecksumFeed;
                for (i = 0; i < checkCount; ++i) {
                    checksum ^= values[i];
                }
                if ((checksum ^ checkCount) != values[checkCount]) {
                    valid = qfalse;
                }
            }
        }
    }

    client->pureAuthState =
        valid ? SV_CLIENT_PURE_STATE_VALID : SV_CLIENT_PURE_STATE_INVALID;
}

void SV_ClientCommand_Vdr(client_t *client)
{
    client->pureAuthState = 0;
}

void SV_ClientCommand_Userinfo(client_t *client)
{
    Q_strncpyz(client->userinfo, Cmd_Argv(1), sizeof(client->userinfo));
    SV_UserinfoChanged(client);
    VM_Call(sv_gameVM, CODUO_GAME_CLIENT_USERINFO_CHANGED,
            SV_ClientNumForHostClient(client));
}

void SV_ExecuteClientCommand(client_t *client, const char *text,
                             qboolean clientOK)
{
    coduo_sv_client_command_handler_t *handler;

    XAnimSetUser(XANIM_SECONDARY_POOL_PAYLOAD_SLOT);
    Cmd_TokenizeString(text);

    for (handler = sv_clientCommandHandlers; handler->command != NULL;
         ++handler) {
        if (strcmp(Cmd_Argv(0), handler->command) == 0) {
            handler->handler(client);
            break;
        }
    }

    if (clientOK && handler->command == NULL && sv.state == CODUO_SS_GAME) {
        VM_Call(sv_gameVM, CODUO_GAME_CLIENT_COMMAND,
                SV_ClientNumForHostClient(client));
    }
}

qboolean
SV_ParseClientCommand(client_t *client, msg_t *msg)
{
    int32_t sequence;
    const char *command;
    qboolean clientOK = qtrue;
    qboolean floodProtect = qtrue;

    sequence = MSG_ReadLong(msg);
    command = MSG_ReadString(msg);

    if (sequence <= client->lastClientCommand) {
        return qtrue;
    }

    if (host_cvar_sv_showCommands_pointer_slot->integer != 0) {
        Com_Printf("clientCommand: %i : %s\n", sequence, command);
    }

    if (sequence > client->lastClientCommand + 1) {
        /*
         * ORIGINAL_BINARY_BUG: the diagnostic adds one where the skipped
         * command count requires subtracting one, so it reports two too
         * many. See original-bugs/coduomp-client-command-lost-count.md.
         */
        Com_Printf("Client %s lost %i clientCommands\n", client->name,
                   (sequence - client->lastClientCommand) + 1);
        SV_DropClient(client, "EXE_LOSTRELIABLECOMMANDS");
        return qfalse;
    }

    if (Q_strncmp("team ", command, 5) == 0 ||
        Q_strncmp("score ", command, 6) == 0 ||
        Q_strncmp("mr ", command, 3) == 0) {
        floodProtect = qfalse;
    }

    if (host_cvar_cl_running_pointer_slot->integer == 0 &&
        client->state > CODUO_CS_PRIMED &&
        host_cvar_sv_floodProtect_pointer_slot->integer != 0 &&
        svs.timeSecondary < client->nextReliableTime && floodProtect) {
        clientOK = qfalse;
        Com_DPrintf("client text ignored for %s: %s\n", client->name,
                    Cmd_Argv(0));
    }

    if (floodProtect) {
        client->nextReliableTime =
            svs.timeSecondary + SV_CLIENT_COMMAND_FLOOD_DELAY_MS;
    }

    SV_ExecuteClientCommand(client, command, clientOK);
    client->lastClientCommand = sequence;
    Com_sprintf(client->lastClientCommandString,
                 sizeof(client->lastClientCommandString), "%s", command);
    return qtrue;
}

void SV_ClientThink(client_t *client, const usercmd_t *cmd)
{
    client->usercmd = *cmd;
    if (client->state == CODUO_CS_ACTIVE) {
        XAnimSetUser(XANIM_SECONDARY_POOL_PAYLOAD_SLOT);
        VM_Call(sv_gameVM, CODUO_GAME_CLIENT_THINK,
                SV_ClientNumForHostClient(client));
    }
}

void SV_ParseUsercmds(client_t *client, msg_t *msg,
                      int delta)
{
    int cmdCount;
    int key;
    usercmd_t nullcmd;
    usercmd_t cmds[CODUO_MAX_PACKET_USERCMDS];
    const usercmd_t *from;
    playerState_t *playerState;
    int i;

    client->deltaMessage =
        delta ? client->messageAcknowledge : CODUO_CLIENT_DELTA_MESSAGE_NONE;

    if (client->reliableSequence - client->reliableAcknowledge >=
        SV_CLIENT_COMMAND_RELIABLE_WINDOW) {
        return;
    }

    cmdCount = MSG_ReadByte(msg);
    if (cmdCount < 1) {
        Com_Printf("cmdCount < 1\n");
        return;
    }
    if (cmdCount > CODUO_MAX_PACKET_USERCMDS) {
        Com_Printf("cmdCount > MAX_PACKET_USERCMDS\n");
        return;
    }

    key = sv.gamestateChecksumFeed ^ client->messageAcknowledge;
    key ^= Com_HashKey(
        client->reliableCommands[client->reliableAcknowledge &
                                 (CODUO_MAX_RELIABLE_COMMANDS - 1)]
            .commandText,
        SV_CLIENT_COMMAND_CHECK_DIGEST_LEN);

    playerState = SV_GameClientNum(SV_ClientNumForHostClient(client));
    MSG_SetDefaultUserCmd(playerState, &nullcmd);
    from = &nullcmd;

    for (i = 0; i < cmdCount; ++i) {
        MSG_ReadDeltaUsercmdKey(msg, (uint32_t)key, from, &cmds[i]);
        if (VM_Call(sv_gameVM, CODUO_GAME_IS_VALID_WEAPON,
                    cmds[i].weapon) == 0) {
            cmds[i].weapon = (uint8_t)playerState->currentWeapon;
        }
        from = &cmds[i];
    }

    client->snapshotFrames[client->messageAcknowledge &
                           (CODUO_HOST_CLIENT_SNAPSHOT_FRAME_COUNT - 1)]
        .messageAckedTime = svs.timeSecondary;

    if (client->state == CODUO_CS_PRIMED) {
        XAnimSetUser(XANIM_SECONDARY_POOL_PAYLOAD_SLOT);
        SV_ClientEnterWorld(client, cmds);
    }

    if (host_cvar_sv_pure_pointer_slot->integer != 0 &&
        client->pureAuthState == 0) {
        SV_DropClient(client, "EXE_CANNOTVALIDATEPURECLIENT");
        return;
    }

    if (client->state != CODUO_CS_ACTIVE) {
        client->deltaMessage = CODUO_CLIENT_DELTA_MESSAGE_NONE;
        return;
    }

    for (i = 0; i < cmdCount; ++i) {
        if (cmds[i].commandTime <= cmds[cmdCount - 1].commandTime &&
            client->usercmd.commandTime < cmds[i].commandTime) {
            SV_ClientThink(client, &cmds[i]);
        }
    }
}

void SV_ParseClientMessage(client_t *client, msg_t *msg)
{
    uint8_t msgBuffer[CODUO_MAX_MSGLEN];
    msg_t decoded;
    int command;

    MSG_Init(&decoded, msgBuffer, sizeof(msgBuffer));
    const int32_t decodedSize = MSG_ReadBitsCompress(
        msg->data + msg->readcount, msgBuffer, msg->cursize - msg->readcount,
        (int32_t)sizeof(msgBuffer));
    /* ORIGINAL_BINARY_BUG: retail can overrun msgBuffer and has no
     * decode-failure result. SECURITY_PATCH, NOT_FROM_ORIGINAL_SOURCE: on the
     * new error result output may be partial; do not publish -1 as cursize or
     * parse it as client commands. See original-bugs/
     * coduomp-message-huffman-output-overflow.md. */
    if (decodedSize == HUFFMAN_TRANSFORM_ERROR)
        return;
    decoded.cursize = decodedSize;

    if (client->serverId == sv_serverId ||
        client->download.fileName[0] != '\0') {
        while ((command = MSG_ReadBits(&decoded, 2)) !=
                   SV_CLIENT_COMMAND_TYPE_EOF &&
               command == SV_CLIENT_COMMAND_TYPE_RELIABLE) {
            if (!SV_ParseClientCommand(client, &decoded)) {
                return;
            }
            if (client->state == CODUO_CS_ZOMBIE) {
                return;
            }
        }

        if (host_cvar_sv_pure_pointer_slot->integer != 0 &&
            client->pureAuthState == SV_CLIENT_PURE_STATE_INVALID) {
            client->nextSnapshotTime = -1;
            client->state = CODUO_CS_ACTIVE;
            SV_SendClientSnapshot(client);
            SV_DropClient(client, "EXE_UNPURECLIENTDETECTED");
        }

        if (command == SV_CLIENT_USERCMD_TYPE_DELTA) {
            SV_ParseUsercmds(client, &decoded, qtrue);
        } else if (command == SV_CLIENT_USERCMD_TYPE_NODELTA) {
            SV_ParseUsercmds(client, &decoded, qfalse);
        } else if (command != SV_CLIENT_COMMAND_TYPE_EOF) {
            Com_Printf("WARNING: bad command byte %i for client %i\n",
                       command, SV_ClientNumForHostClient(client));
        }
        return;
    }

    if ((client->serverId & 0xf0) == (sv_serverId & 0xf0)) {
        if (client->state == CODUO_CS_PRIMED) {
            XAnimSetUser(XANIM_SECONDARY_POOL_PAYLOAD_SLOT);
            SV_ClientEnterWorld(client, &client->usercmd);
        }
        return;
    }

    if (client->messageAcknowledge <= client->gamestateMessageNum) {
        return;
    }

    Com_DPrintf("%s : dropped gamestate, resending\n", client->name);
    SV_SendClientGameState(client);
#if !CODUO_DISABLE_SERVER_AUTH
    if ((net_lanauthorize->integer != 0 ||
        Sys_IsLANAddress(client->netchan.remoteAddress) == 0) &&
        !SV_AuthorizeIsCrackedMode()) {
        SV_AuthorizeRequest(client->netchan.remoteAddress, client->challenge);
    }
#endif
}

int SV_AddTestClient(void)
{
    client_t *client;
    netadr_t loopback;
    char connectCommand[CODUO_MAX_STRING_CHARS];
    usercmd_t cmd;
    int clientNum;

    client = svs.clients;
    for (clientNum = 0; clientNum < host_cvar_sv_maxclients_pointer_slot->integer;
         ++clientNum, ++client) {
        if (client->state == CODUO_CS_FREE) {
            break;
        }
    }

    if (clientNum == host_cvar_sv_maxclients_pointer_slot->integer) {
        return -1;
    }

    sprintf(connectCommand,
            "connect \"\\cl_guid\\unknown\\cg_predictItems\\1\\cl_punkbuster\\0"
            "\\cl_anonymous\\0\\handicap\\100\\color\\4\\head\\default"
            "\\model\\multi\\snaps\\20\\rate\\5000\\name\\bot%d"
            "\\protocol\\%d\"",
            sv_reconnectSequence, SV_BOT_CONNECT_PROTOCOL);
    Cmd_TokenizeString(connectCommand);

    memset(&loopback, 0, sizeof(loopback));
    loopback.port = (uint16_t)sv_reconnectSequence;
    ++sv_reconnectSequence;
    SVC_DirectConnect(loopback);

    client = svs.clients;
    for (clientNum = 0; clientNum < host_cvar_sv_maxclients_pointer_slot->integer;
         ++clientNum, ++client) {
        if (client->state != CODUO_CS_FREE &&
            SV_IsChallengeAdr(client, loopback)) {
            break;
        }
    }

    if (clientNum == host_cvar_sv_maxclients_pointer_slot->integer) {
        return -1;
    }

    XAnimSetUser(XANIM_SECONDARY_POOL_PAYLOAD_SLOT);
    client->bIsTestClient = qtrue;
    SV_SendClientGameState(client);
    memset(&cmd, 0, sizeof(cmd));
    SV_ClientEnterWorld(client, &cmd);
    return clientNum;
}
