#include "msg_private.h"
#include "netchan_private.h"
#include "sv_snapshot_private.h"
#include "../core_runtime/core_runtime_private.h"
#include "../server/sv_globals_private.h"
#include "../server/server_private.h"

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

enum {
    SV_SNAPSHOT_MESSAGE_HEADER_BYTES = sizeof(uint32_t)
};

/* NOT_FROM_ORIGINAL_SOURCE: local predicate for active/zombie snapshot builds. */
static qboolean coduomp_sv_client_needs_snapshot_build(
    const client_t *client)
{
    return client->state == CODUO_CS_ACTIVE || client->state == CODUO_CS_ZOMBIE;
}

void SV_SendMessageToClient(msg_t *msg, client_t *client)
{
    uint8_t packetData[CODUO_MAX_MSGLEN];
    uint8_t *msgData = msg->data;
    int32_t packetSize;
    clientSnapshot_t *frame =
        &client->snapshotFrames[client->netchan.outgoingSequence &
                                (CODUO_HOST_CLIENT_SNAPSHOT_FRAME_COUNT - 1)];

    memcpy(packetData, msgData, SV_SNAPSHOT_MESSAGE_HEADER_BYTES);
    packetSize = MSG_WriteBitsCompress(
        msgData + SV_SNAPSHOT_MESSAGE_HEADER_BYTES,
        packetData + SV_SNAPSHOT_MESSAGE_HEADER_BYTES,
        msg->cursize - SV_SNAPSHOT_MESSAGE_HEADER_BYTES);
    packetSize += SV_SNAPSHOT_MESSAGE_HEADER_BYTES;

    if (client->deferredDropReason != NULL) {
        SV_DropClient(client, client->deferredDropReason);
    }

    frame->messageSize = packetSize;
    frame->messageSentTime = svs.timeSecondary;
    frame->messageAckedTime = -1;

    SV_Netchan_Transmit(client, packetData, packetSize);

    if (client->netchan.remoteAddress.type == CODUO_NA_LOOPBACK ||
        Sys_IsLANAddress(client->netchan.remoteAddress) != 0) {
        client->nextSnapshotTime = svs.timeSecondary - 1;
        return;
    }

    int32_t rateMsec =
        SV_RateMsec(client, packetSize);
    if (rateMsec < client->snapshotMsec) {
        rateMsec = client->snapshotMsec;
        client->rateDelayed = 0;
    } else {
        client->rateDelayed = 1;
    }

    client->nextSnapshotTime = svs.timeSecondary + rateMsec;

    if (client->state != CODUO_CS_ACTIVE &&
        client->download.fileName[0] == '\0' &&
        client->nextSnapshotTime < svs.timeSecondary + 1000) {
        client->nextSnapshotTime = svs.timeSecondary + 1000;
    }

    sv_totalBytesSentThisFrame += packetSize;
}

void SV_SendClientSnapshot(client_t *client)
{
    msg_t msg;
    uint8_t msgData[CODUO_MAX_MSGLEN];

    if (coduomp_sv_client_needs_snapshot_build(client)) {
        SV_BuildClientSnapshot(client);
    }

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

    if (coduomp_sv_client_needs_snapshot_build(client)) {
        SV_UpdateServerCommandsToClient(client, &msg);
        SV_WriteSnapshotToClient(client, &msg);
    }

    if (client->state != CODUO_CS_ZOMBIE) {
        SV_WriteDownloadToClient(client, &msg);
    }

    MSG_WriteByte(&msg, 8);

    if (msg.overflowed != 0) {
        Com_Printf("WARNING: msg overflowed for %s, trying to recover\n",
                   client->name);

        if (coduomp_sv_client_needs_snapshot_build(client)) {
            SV_PrintServerCommandsForClient(client);
            MSG_Init(&msg, msgData, sizeof(msgData));
            MSG_WriteLong(&msg, client->lastClientCommand);
            SV_UpdateServerCommandsToClient_PreventOverflow(
                client, &msg, sizeof(msgData));
            MSG_WriteByte(&msg, 8);
        }

        if (msg.overflowed != 0) {
            Com_Printf("WARNING: client disconnected for msg overflow: %s\n",
                       client->name);
            NET_OutOfBandPrint(NS_SERVER, client->netchan.remoteAddress,
                               "disconnect");
            SV_DropClient(client, "EXE_SERVERMESSAGEOVERFLOW");
        }
    }

    SV_SendMessageToClient(&msg, client);
}

void SV_SendClientMessages(void)
{
    int sentClientCount = 0;
    client_t *client = svs.clients;

    sv_totalBytesSentThisFrame = 0;
    sv_totalUncompressedBytesThisFrame = 0;

    for (int clientNum = 0; clientNum < sv_maxclients->integer; clientNum++) {
        if (client->state != CODUO_CS_FREE &&
            client->nextSnapshotTime <= svs.timeSecondary) {
            ++sentClientCount;

            if (client->netchan.unsentFragments == 0) {
                SV_SendClientSnapshot(client);
            } else {
                client->nextSnapshotTime =
                    svs.timeSecondary +
                    SV_RateMsec(client,
                                client->netchan.unsentLength -
                                    client->netchan.unsentFragmentStart);
                SV_Netchan_TransmitNextFragment(&client->netchan);
            }
        }

        ++client;
    }

    if (sv_showAverageBPS->integer != 0 && sentClientCount > 0) {
        float compressedBytes = 0.0f;
        float uncompressedBytes = 0.0f;

        for (int windowIndex = 0; windowIndex < 19; windowIndex++) {
            sv_compressedBpsWindow[windowIndex] =
                sv_compressedBpsWindow[windowIndex + 1];
            /* 0x8099b4c: bare fild feeds faddp; no cast rounding. */
            compressedBytes += sv_compressedBpsWindow[windowIndex];

            sv_uncompressedBpsWindow[windowIndex] =
                sv_uncompressedBpsWindow[windowIndex + 1];
            uncompressedBytes += sv_uncompressedBpsWindow[windowIndex];
        }

        sv_compressedBpsWindow[19] = sv_totalBytesSentThisFrame;
        compressedBytes += sv_totalBytesSentThisFrame;

        sv_uncompressedBpsWindow[19] = sv_totalUncompressedBytesThisFrame;
        uncompressedBytes += sv_totalUncompressedBytesThisFrame;

        if (sv_compressedBpsMax <= sv_totalBytesSentThisFrame) {
            sv_compressedBpsMax = sv_totalBytesSentThisFrame;
        }
        if (sv_uncompressedBpsMax <= sv_totalUncompressedBytesThisFrame) {
            sv_uncompressedBpsMax = sv_totalUncompressedBytesThisFrame;
        }

        ++sv_averageBpsFrameCount;
        if (sv_averageBpsFrameCount > 19) {
            sv_averageBpsFrameCount = 0;
            compressedBytes /= 20.0f;
            uncompressedBytes /= 20.0f;

            float compressionRatio =
                (1.0f - compressedBytes / uncompressedBytes) * 100.0f;
            sv_averageCompressionRatioSum += compressionRatio;
            ++sv_averageCompressionRatioCount;

            Com_DPrintf(
                "bpspc(%2.0f) bps(%2.0f) pk(%i) ubps(%2.0f) upk(%i) "
                "cr(%2.2f) acr(%2.2f)\n",
                (double)(compressedBytes / sentClientCount),
                (double)compressedBytes, sv_compressedBpsMax,
                (double)uncompressedBytes, sv_uncompressedBpsMax,
                (double)compressionRatio,
                (double)(sv_averageCompressionRatioSum /
                         sv_averageCompressionRatioCount));
        }
    }
}
