#include "coduo_engine_structs.h"
#include "sv_snapshot_private.h"

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

void Com_Error(int32_t code, const char *format, ...);
void MSG_Init(msg_t *msg, uint8_t *data, int32_t length);
int32_t MSG_ReadBit(msg_t *msg);
int32_t MSG_ReadBits(msg_t *msg,
                                    int32_t bits);
int32_t MSG_ReadLong(msg_t *msg);
void MSG_WriteBit0(msg_t *msg);
void MSG_WriteBit1(msg_t *msg);
void MSG_WriteBits(msg_t *msg, int32_t value,
                   int32_t bits);
void MSG_WriteLong(msg_t *msg, int32_t value);
void MSG_WriteDeltaEntity(msg_t *msg,
                          const coduo_host_entity_state_snapshot_t *from,
                          const coduo_host_entity_state_snapshot_t *to,
                          qboolean force);
void MSG_WriteDeltaArchivedEntity(msg_t *msg,
                                  const coduo_host_entity_baseline_t *from,
                                  const coduo_cached_snapshot_entity_t *to,
                                  qboolean force);
void MSG_WriteDeltaClient(msg_t *msg,
                          const coduo_host_client_snapshot_t *from,
                          const coduo_host_client_snapshot_t *to,
                          qboolean force);
void MSG_WriteDeltaPlayerstate(msg_t *msg,
                               const playerState_t *from,
                               const playerState_t *to);
sharedEntity_t *SV_GentityNum(int32_t entityNum);
playerState_t *SV_GameClientNum(int32_t clientNum);
svEntity_t *
SV_SvEntityForGentity(const sharedEntity_t *gentity);
int32_t CM_PointLeafnum(const vec3_t point);
int32_t CM_LeafArea(int32_t leafnum);
int32_t
CM_LeafCluster(int32_t leafnum);
const uint8_t *CM_ClusterPVS(int32_t cluster);
qboolean CM_AreasConnected(int32_t area1,
                                 int32_t area2);
int32_t
CM_BoxLeafnums(const vec3_t mins, const vec3_t maxs,
               int32_t *list,
               int32_t listSize,
               int32_t *lastLeaf);
qboolean BoxDistSqrdExceeds(const vec3_t mins, const vec3_t maxs,
                            const vec3_t origin, float opaqueDistSq);
/* Mac MP symbols and the recovered game module establish this shared helper's
 * float signature. The i386 ABI carries both float arguments as 32-bit words. */
void AddLeanToPosition(vec3_t origin, float yaw, float leanFraction,
                       float scale, float length);
void MSG_ReadDeltaClient(msg_t *msg,
                         const coduo_host_client_snapshot_t *from,
                         coduo_host_client_snapshot_t *to,
                         int32_t clientNum);
void MSG_ReadDeltaPlayerstate(msg_t *msg, const playerState_t *from,
                              playerState_t *to);
qboolean MSG_ReadDeltaArchivedEntity(msg_t *msg, const void *from,
                                     void *to, int32_t number);

enum {
    CODUO_SVF_NOCLIENT = 0x0001,
    CODUO_SVF_VISIBILITY_BYPASS_MASK = 0x0018,
    CODUO_SVF_SINGLECLIENT = 0x0800,
    CODUO_SVF_NOTSINGLECLIENT = 0x2000,
    CODUO_ENTITYNUM_NONE = 1023,
    CODUO_CLIENT_NUM_SENTINEL = 99999,
    CODUO_SNAPSHOT_COUNTER_WRAP_LIMIT = 0x7ffffffd,
    CODUO_PLAYERSTATE_SELF_ENTITY_HIDE_FLAG = 0x40000,
    CODUO_ARCHIVE_MSG_BUFFER_SIZE = 0x20000
};

/* NOT_FROM_ORIGINAL_SOURCE: ring index helper for recovered archive arrays.
 * The bias/SAR sequences at 0x0809733a..0x08097361 and their sibling ring
 * sites implement C's signed remainder.  Keep the operator itself: spelling
 * the bias followed by C division would truncate a still-negative adjusted
 * value toward zero and fail below one negative ring length. */
static int32_t
coduomp_sv_ring_index_power_of_two(int32_t index, int32_t ringCount)
{
    return index % ringCount;
}

/* NOT_FROM_ORIGINAL_SOURCE: typed accessor for archived snapshot frames. */
static archivedSnapshotFrameIndex_t *
coduomp_sv_archived_snapshot_frames(void)
{
    return svs.archivedSnapshotFrames;
}

/* NOT_FROM_ORIGINAL_SOURCE: typed accessor for archived snapshot bytes. */
static uint8_t *coduomp_sv_archived_snapshot_buffer(void)
{
    return svs.archivedSnapshotBuffer;
}

/* NOT_FROM_ORIGINAL_SOURCE: typed accessor for cached snapshot entities. */
static coduo_cached_snapshot_entity_t *
coduomp_sv_cached_snapshot_entities(void)
{
    return svs.cachedSnapshotEntities;
}

/* NOT_FROM_ORIGINAL_SOURCE: typed accessor for cached snapshot clients. */
static serverCachedSnapshotClient_t *
coduomp_sv_cached_snapshot_clients(void)
{
    return svs.cachedSnapshotClients;
}

/* NOT_FROM_ORIGINAL_SOURCE: typed accessor for cached snapshot frames. */
static serverCachedSnapshotFrame_t *
coduomp_sv_cached_snapshot_frames(void)
{
    return svs.cachedSnapshotFrames;
}

/* NOT_FROM_ORIGINAL_SOURCE: typed accessor for live entity snapshots. */
static coduo_host_snapshot_entity_record_t *
coduomp_sv_entity_state_snapshots(void)
{
    return svs.entityStateSnapshots;
}

/* NOT_FROM_ORIGINAL_SOURCE: typed accessor for live client snapshots. */
static coduo_host_client_snapshot_t *coduomp_sv_client_snapshots(void)
{
    return svs.clientSnapshots;
}

/* NOT_FROM_ORIGINAL_SOURCE: typed accessor for host clients. */
static client_t *coduomp_sv_clients(void)
{
    return svs.clients;
}

/* NOT_FROM_ORIGINAL_SOURCE: typed cached entity ring accessor. */
static coduo_cached_snapshot_entity_t *
coduomp_sv_cached_snapshot_entity_at(int32_t index)
{
    return &coduomp_sv_cached_snapshot_entities()
                [coduomp_sv_ring_index_power_of_two(
                    index, CODUO_HOST_CACHED_SNAPSHOT_ENTITY_RING_COUNT)];
}

/* NOT_FROM_ORIGINAL_SOURCE: typed cached client ring accessor. */
static serverCachedSnapshotClient_t *
coduomp_sv_cached_snapshot_client_at(int32_t index)
{
    return &coduomp_sv_cached_snapshot_clients()
                [coduomp_sv_ring_index_power_of_two(
                    index, CODUO_HOST_CACHED_SNAPSHOT_CLIENT_RING_COUNT)];
}

/* NOT_FROM_ORIGINAL_SOURCE: typed cached frame ring accessor. */
static serverCachedSnapshotFrame_t *
coduomp_sv_cached_snapshot_frame_at(int32_t index)
{
    return &coduomp_sv_cached_snapshot_frames()
                [coduomp_sv_ring_index_power_of_two(
                    index, CODUO_HOST_CACHED_SNAPSHOT_FRAME_RING_COUNT)];
}

/* NOT_FROM_ORIGINAL_SOURCE: shared recovered counter-wrap guard. */
static void
coduomp_sv_error_if_counter_wrapped(int32_t counter, const char *message)
{
    if (counter > CODUO_SNAPSHOT_COUNTER_WRAP_LIMIT) {
        Com_Error(0, message);
    }
}

/* NOT_FROM_ORIGINAL_SOURCE: typed VM-call wrapper for fog distance bits. */
static float coduomp_sv_get_fog_opaque_dist_sq(void)
{
    uint32_t resultBits = (uint32_t)
        VM_Call(sv_gameVM, CODUO_GAME_GET_FOG_OPAQUE_DIST_SQ_BITS);
    float result;

    memcpy(&result, &resultBits, sizeof(result));
    if (result == FLT_MAX) {
        result = 0.0f;
    }

    return result;
}

/* NOT_FROM_ORIGINAL_SOURCE: bitset predicate for snapshot PVS checks. */
static qboolean coduomp_sv_pvs_contains_cluster(
    const uint8_t *pvs,
    int32_t cluster)
{
    return (pvs[cluster >> 3] >> (cluster & 7)) & 1;
}

/* NOT_FROM_ORIGINAL_SOURCE: factored snapshot PVS bounds visibility check. */
static qboolean
coduomp_sv_box_visible_in_pvs(const vec3_t mins, const vec3_t maxs,
                              int32_t viewArea, const uint8_t *pvs)
{
    int32_t leaves[128];
    int32_t lastLeaf;
    int32_t leafCount;
    int32_t leafIndex;

    leafCount = CM_BoxLeafnums(mins, maxs, leaves, 128, &lastLeaf);
    if (leafCount == 0) {
        return 0;
    }

    leafIndex = 0;
    while (leafIndex < leafCount) {
        if (CM_AreasConnected(viewArea, CM_LeafArea(leaves[leafIndex])) != 0) {
            break;
        }
        ++leafIndex;
    }

    if (leafIndex == leafCount) {
        return 0;
    }

    leafIndex = 0;
    while (leafIndex < leafCount) {
        const int32_t cluster =
            CM_LeafCluster(leaves[leafIndex]);

        if (cluster != -1 &&
            coduomp_sv_pvs_contains_cluster(pvs, cluster) != 0) {
            return 1;
        }
        ++leafIndex;
    }

    return 0;
}

/* NOT_FROM_ORIGINAL_SOURCE: factored live-entity PVS cluster predicate. */
static qboolean coduomp_sv_live_entity_clusters_visible(
    const svEntity_t *svEntity,
    const uint8_t *pvs)
{
    int32_t cluster = 0;
    int32_t clusterIndex = 0;

    while (clusterIndex < svEntity->numClusters) {
        cluster = svEntity->clusterNums[clusterIndex];
        if (coduomp_sv_pvs_contains_cluster(pvs, cluster) != 0) {
            return 1;
        }
        ++clusterIndex;
    }

    if (clusterIndex == svEntity->numClusters && svEntity->lastCluster != 0) {
        /* 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 (cluster <= svEntity->lastCluster) {
            if (coduomp_sv_pvs_contains_cluster(pvs, cluster) == 0) {
                ++cluster;
            } else {
                break;
            }
        }
        if (cluster != svEntity->lastCluster) {
            return 1;
        }
    }

    return clusterIndex != svEntity->numClusters;
}

/* NOT_FROM_ORIGINAL_SOURCE: factored archived entity time rebasing. */
static void
coduomp_sv_adjust_entity_snapshot_times(
    coduo_host_entity_state_snapshot_t *es, int32_t deltaTime)
{
    if (es->pos.trTime != 0) {
        es->pos.trTime += deltaTime;
    }
    if (es->apos.trTime != 0) {
        es->apos.trTime += deltaTime;
    }
    if (es->entityStateTime != 0) {
        es->entityStateTime += deltaTime;
    }
    if (es->entityStateTime2 != 0) {
        es->entityStateTime2 += deltaTime;
    }
}

/* NOT_FROM_ORIGINAL_SOURCE: factored archived player-state time rebasing. */
static void coduomp_sv_adjust_archived_player_state_times(
    playerState_t *playerState,
    int32_t deltaTime)
{
    int32_t i;

    if (playerState->commandTime != 0) {
        playerState->commandTime += deltaTime;
    }
    if (playerState->pmTime != 0) {
        playerState->pmTime += deltaTime;
    }
    if (playerState->foliageSoundTime != 0) {
        playerState->foliageSoundTime += deltaTime;
    }
    if (playerState->fatigueSoundTime != 0) {
        playerState->fatigueSoundTime += deltaTime;
    }
    if (playerState->lastJumpCommandTime != 0) {
        playerState->lastJumpCommandTime += deltaTime;
    }
    if (playerState->viewHeightLerpTime != 0) {
        playerState->viewHeightLerpTime += deltaTime;
    }
    if (playerState->motionState.shellshock.time != 0) {
        playerState->motionState.shellshock.time += deltaTime;
    }

    for (i = 0; i < PLAYERSTATE_HUD_ELEM_COUNT; ++i) {
        hudElem_t *hud = &playerState->hudArchival[i];

        if (hud->timerValue != 0) {
            hud->timerValue += deltaTime;
        }
        if (hud->fadeStartTime != 0) {
            hud->fadeStartTime += deltaTime;
        }
        if (hud->scaleStartTime != 0) {
            hud->scaleStartTime += deltaTime;
        }
        if (hud->moveStartTime != 0) {
            hud->moveStartTime += deltaTime;
        }
    }

    playerState->deltaTime += deltaTime;
}

/* NOT_FROM_ORIGINAL_SOURCE: named player-state copy helper. */
static void
coduomp_sv_copy_player_state(playerState_t *to, const playerState_t *from)
{
    memcpy(to, from, sizeof(*to));
}

/* NOT_FROM_ORIGINAL_SOURCE: named client-snapshot copy helper. */
static void
coduomp_sv_copy_client_snapshot(
    coduo_host_client_snapshot_t *to,
    const coduo_host_client_snapshot_t *from)
{
    memcpy(to, from, sizeof(*to));
}

/* NOT_FROM_ORIGINAL_SOURCE: typed player-state view origin reader. */
static void
coduomp_sv_read_view_origin_from_player_state(
    const playerState_t *playerState, vec3_t origin)
{
    memcpy(origin, playerState->psOrigin, sizeof(vec3_t));
    origin[2] += playerState->viewHeightCurrent;
}

void SV_AddEntitiesVisibleFromPoint(
    const vec3_t origin,
    int32_t clientNum,
    snapshotEntityNumbers_t *entityNumbers)
{
    int32_t viewLeaf;
    int32_t viewArea;
    int32_t viewCluster;
    const uint8_t *pvs;
    float fogOpaqueDistSq;
    int32_t entityNum;

    viewLeaf = CM_PointLeafnum(origin);
    viewArea = CM_LeafArea(viewLeaf);
    if (viewArea < 0) {
        return;
    }

    viewCluster = CM_LeafCluster(viewLeaf);
    pvs = CM_ClusterPVS(viewCluster);
    fogOpaqueDistSq = coduomp_sv_get_fog_opaque_dist_sq();

    for (entityNum = 0; entityNum < sv_gameData.numGEntities; ++entityNum) {
        sharedEntity_t *gentity = SV_GentityNum(entityNum);
        svEntity_t *svEntity;

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

        if (gentity->entityState.s_eType == 9 &&
            gentity->entityState.vehicleEntityNum != CODUO_ENTITYNUM_NONE) {
            gentity = SV_GentityNum(gentity->entityState.vehicleEntityNum);
        }

        if ((gentity->svFlags & CODUO_SVF_NOCLIENT) != 0) {
            continue;
        }
        if ((gentity->svFlags & CODUO_SVF_SINGLECLIENT) != 0 &&
            gentity->ownerNum != clientNum) {
            continue;
        }
        if ((gentity->svFlags & CODUO_SVF_NOTSINGLECLIENT) != 0 &&
            gentity->ownerNum == clientNum) {
            continue;
        }
        if (entityNum == clientNum) {
            continue;
        }

        if ((gentity->svFlags & CODUO_SVF_VISIBILITY_BYPASS_MASK) != 0) {
            SV_AddEntToSnapshot(entityNum, entityNumbers);
            continue;
        }

        if (gentity->soundTime != 0) {
            if (gentity->soundTime < 0 ||
                gentity->soundTime - svs.time >= 0) {
                SV_AddEntToSnapshot(entityNum, entityNumbers);
                continue;
            }
            gentity->soundTime = 0;
        }

        svEntity = SV_SvEntityForGentity(gentity);

        if (gentity->entityState.s_eType == 1 &&
            (gentity->entityState.s_flags & 0x100000) != 0 &&
            SV_GentityNum(gentity->traceOwnerNum)->entityState.eventParm ==
                2 &&
            (gentity->entityState.eventParm & 7) == 1) {
            continue;
        }

        if (CM_AreasConnected(viewArea, svEntity->areaNum) == 0 &&
            CM_AreasConnected(viewArea, svEntity->areaNum2) == 0) {
            continue;
        }

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

        if (coduomp_sv_live_entity_clusters_visible(svEntity, pvs) == 0) {
            continue;
        }

        if (fogOpaqueDistSq == 0.0f ||
            BoxDistSqrdExceeds(gentity->absMin, gentity->absMax, origin,
                         fogOpaqueDistSq) == 0) {
            SV_AddEntToSnapshot(entityNum, entityNumbers);
        }
    }
}

void SV_AddCachedEntitiesVisibleFromPoint(
    int32_t numEntities,
    int32_t firstEntity,
    const vec3_t origin,
    int32_t clientNum,
    snapshotEntityNumbers_t *entityNumbers,
    const clientSnapshot_t *frame)
{
    int32_t viewLeaf;
    int32_t viewArea;
    int32_t viewCluster;
    const uint8_t *pvs;
    float fogOpaqueDistSq;
    int32_t entityIndex;

    viewLeaf = CM_PointLeafnum(origin);
    viewArea = CM_LeafArea(viewLeaf);
    if (viewArea < 0) {
        return;
    }

    viewCluster = CM_LeafCluster(viewLeaf);
    pvs = CM_ClusterPVS(viewCluster);
    fogOpaqueDistSq = coduomp_sv_get_fog_opaque_dist_sq();

    for (entityIndex = 0; entityIndex < numEntities; ++entityIndex) {
        coduo_cached_snapshot_entity_t *entity =
            coduomp_sv_cached_snapshot_entity_at(firstEntity + entityIndex);

        if ((entity->flags & CODUO_SVF_SINGLECLIENT) != 0 &&
            entity->ownerNum != clientNum) {
            continue;
        }
        if ((entity->flags & CODUO_SVF_NOTSINGLECLIENT) != 0 &&
            entity->ownerNum == clientNum) {
            continue;
        }
        if (entity->entityState.s_number == clientNum &&
            (frame->playerState.playerStateFlags &
             CODUO_PLAYERSTATE_SELF_ENTITY_HIDE_FLAG) != 0) {
            continue;
        }

        if ((entity->flags & CODUO_SVF_VISIBILITY_BYPASS_MASK) != 0) {
            SV_AddArchivedEntToSnapshot(entityIndex, entityNumbers);
            continue;
        }

        if (coduomp_sv_box_visible_in_pvs(
                entity->clipMins, entity->clipMaxs, viewArea, pvs) == 0) {
            continue;
        }

        if (fogOpaqueDistSq == 0.0f ||
            BoxDistSqrdExceeds(entity->clipMins, entity->clipMaxs, origin,
                         fogOpaqueDistSq) == 0) {
            SV_AddArchivedEntToSnapshot(entityIndex, entityNumbers);
        }
    }
}

serverCachedSnapshotFrame_t *
SV_GetCachedSnapshotInternal(int32_t archivedFrameIndex)
{
    archivedSnapshotFrameIndex_t *archivedFrame;
    serverCachedSnapshotFrame_t *cachedFrame = NULL;
    int32_t scanMin;
    int32_t scanIndex;
    uint8_t archivePayload[CODUO_ARCHIVE_MSG_BUFFER_SIZE];
    msg_t msg;
    int32_t byteCount;
    /* 0x8097404-0x809743a: signed remainder idiom and signed jg prove
     * the original wrap math is int32, not size_t. */
    int32_t firstByte;
    int32_t wrappedFirstByte;
    int32_t firstCopyBytes;
    qboolean fullSnapshot;
    int32_t clientNum;
    int32_t oldClientIndex;
    int32_t oldClientNum;
    serverCachedSnapshotClient_t *oldClient;

    archivedFrame =
        &coduomp_sv_archived_snapshot_frames()
             [archivedFrameIndex %
              CODUO_HOST_ARCHIVED_SNAPSHOT_FRAME_INDEX_COUNT];
    if (archivedFrame->firstByte <
        svs.nextArchivedSnapshotBuffer -
            CODUO_HOST_ARCHIVED_SNAPSHOT_BUFFER_SIZE) {
        return NULL;
    }

    scanMin =
        svs.nextCachedSnapshotFrames -
        CODUO_HOST_CACHED_SNAPSHOT_FRAME_RING_COUNT;
    if (scanMin < 0) {
        scanMin = 0;
    }

    scanIndex = svs.nextCachedSnapshotFrames - 1;
    while (scanIndex >= scanMin) {
        cachedFrame = coduomp_sv_cached_snapshot_frame_at(scanIndex);
        if (cachedFrame->archivedFrameIndex == archivedFrameIndex) {
            if (cachedFrame->firstEntity >=
                    svs.nextCachedSnapshotEntities -
                        CODUO_HOST_CACHED_SNAPSHOT_ENTITY_RING_COUNT &&
                cachedFrame->firstClient >=
                    svs.nextCachedSnapshotClients -
                        CODUO_HOST_CACHED_SNAPSHOT_CLIENT_RING_COUNT) {
                return cachedFrame;
            }
            break;
        }
        --scanIndex;
    }

    MSG_Init(&msg, archivePayload, CODUO_ARCHIVE_MSG_BUFFER_SIZE);

    byteCount = archivedFrame->byteCount;
    firstByte = archivedFrame->firstByte;
    wrappedFirstByte = firstByte % CODUO_HOST_ARCHIVED_SNAPSHOT_BUFFER_SIZE;

    firstCopyBytes =
        CODUO_HOST_ARCHIVED_SNAPSHOT_BUFFER_SIZE - wrappedFirstByte;
    if (firstCopyBytes < byteCount) {
        /* 0x08097434..0x0809749f compares signed dwords, then passes their
         * raw 32-bit values as the i386 memcpy counts. */
        memcpy(archivePayload,
               &coduomp_sv_archived_snapshot_buffer()[wrappedFirstByte],
               (size_t)(uint32_t)firstCopyBytes);
        memcpy(&archivePayload[firstCopyBytes],
               coduomp_sv_archived_snapshot_buffer(),
               (size_t)(uint32_t)(byteCount - firstCopyBytes));
    } else {
        memcpy(archivePayload,
               &coduomp_sv_archived_snapshot_buffer()[wrappedFirstByte],
               (size_t)(uint32_t)byteCount);
    }

    fullSnapshot = MSG_ReadBit(&msg);
    if (fullSnapshot == 0) {
        int32_t deltaFrameIndex;
        serverCachedSnapshotFrame_t *oldFrame;

        deltaFrameIndex = MSG_ReadLong(&msg);
        if (deltaFrameIndex <
            svs.nextArchivedSnapshotFrames -
                CODUO_HOST_ARCHIVED_SNAPSHOT_FRAME_INDEX_COUNT) {
            return NULL;
        }

        archivedFrame =
            &coduomp_sv_archived_snapshot_frames()
                 [deltaFrameIndex %
                  CODUO_HOST_ARCHIVED_SNAPSHOT_FRAME_INDEX_COUNT];
        if (archivedFrame->firstByte <
            svs.nextArchivedSnapshotBuffer -
                CODUO_HOST_ARCHIVED_SNAPSHOT_BUFFER_SIZE) {
            return NULL;
        }

        oldFrame = SV_GetCachedSnapshotInternal(deltaFrameIndex);
        if (oldFrame == NULL) {
            return NULL;
        }

        cachedFrame =
            coduomp_sv_cached_snapshot_frame_at(
                svs.nextCachedSnapshotFrames);
        cachedFrame->archivedFrameIndex = archivedFrameIndex;
        cachedFrame->numEntities = 0;
        cachedFrame->firstEntity = svs.nextCachedSnapshotEntities;
        cachedFrame->numClients = 0;
        cachedFrame->firstClient = svs.nextCachedSnapshotClients;
        cachedFrame->decodedFromDelta = qtrue;
        cachedFrame->messageTime = MSG_ReadLong(&msg);

        oldClientIndex = 0;
        oldClient = NULL;
        if (oldFrame->numClients < 1) {
            oldClientNum = CODUO_CLIENT_NUM_SENTINEL;
        } else {
            oldClient =
                coduomp_sv_cached_snapshot_client_at(oldFrame->firstClient);
            oldClientNum = oldClient->snapshot.clientNum;
        }

        while (MSG_ReadBit(&msg) != 0) {
            serverCachedSnapshotClient_t *newClient;

            clientNum = MSG_ReadBits(&msg, 6);
            if (msg.readcount > byteCount) {
                Com_Error(1, "\x15" "SV_GetCachedSnapshot: end of message");
            }

            while (oldClientNum < clientNum) {
                ++oldClientIndex;
                if (oldClientIndex < oldFrame->numClients) {
                    oldClient = coduomp_sv_cached_snapshot_client_at(
                        oldFrame->firstClient + oldClientIndex);
                    oldClientNum = oldClient->snapshot.clientNum;
                } else {
                    oldClient = NULL;
                    oldClientNum = CODUO_CLIENT_NUM_SENTINEL;
                }
            }

            newClient = coduomp_sv_cached_snapshot_client_at(
                svs.nextCachedSnapshotClients);
            if (oldClientNum == clientNum) {
                MSG_ReadDeltaClient(&msg, &oldClient->snapshot,
                                    &newClient->snapshot, clientNum);
                newClient->playerStateValid = MSG_ReadBit(&msg);
                if (newClient->playerStateValid != 0) {
                    /* ORIGINAL_BINARY_BUG: stock does not test the old row's
                     * validity before using its resident player-state bytes
                     * as the delta baseline. See original-bugs/
                     * coduomp-archive-stale-playerstate-delta-baseline.md. */
                    MSG_ReadDeltaPlayerstate(&msg, &oldClient->playerState,
                                             &newClient->playerState);
                }
                ++oldClientIndex;
                if (oldClientIndex < oldFrame->numClients) {
                    oldClient = coduomp_sv_cached_snapshot_client_at(
                        oldFrame->firstClient + oldClientIndex);
                    oldClientNum = oldClient->snapshot.clientNum;
                } else {
                    oldClient = NULL;
                    oldClientNum = CODUO_CLIENT_NUM_SENTINEL;
                }
            } else {
                MSG_ReadDeltaClient(&msg, NULL, &newClient->snapshot,
                                    clientNum);
                newClient->playerStateValid = MSG_ReadBit(&msg);
                if (newClient->playerStateValid != 0) {
                    MSG_ReadDeltaPlayerstate(&msg, NULL,
                                             &newClient->playerState);
                }
            }

            ++svs.nextCachedSnapshotClients;
            coduomp_sv_error_if_counter_wrapped(
                svs.nextCachedSnapshotClients,
                "\x15" "svs.nextCachedSnapshotClients wrapped");
            ++cachedFrame->numClients;
        }
    } else {
        cachedFrame =
            coduomp_sv_cached_snapshot_frame_at(
                svs.nextCachedSnapshotFrames);
        cachedFrame->archivedFrameIndex = archivedFrameIndex;
        cachedFrame->numEntities = 0;
        cachedFrame->firstEntity = svs.nextCachedSnapshotEntities;
        cachedFrame->numClients = 0;
        cachedFrame->firstClient = svs.nextCachedSnapshotClients;
        cachedFrame->decodedFromDelta = qfalse;
        cachedFrame->messageTime = MSG_ReadLong(&msg);

        while (MSG_ReadBit(&msg) != 0) {
            serverCachedSnapshotClient_t *newClient;

            clientNum = MSG_ReadBits(&msg, 6);
            if (msg.readcount > byteCount) {
                Com_Error(1, "\x15" "SV_GetCachedSnapshot: end of message");
            }

            newClient = coduomp_sv_cached_snapshot_client_at(
                svs.nextCachedSnapshotClients);
            MSG_ReadDeltaClient(&msg, NULL, &newClient->snapshot, clientNum);
            newClient->playerStateValid = MSG_ReadBit(&msg);
            if (newClient->playerStateValid != 0) {
                MSG_ReadDeltaPlayerstate(&msg, NULL,
                                         &newClient->playerState);
            }

            ++svs.nextCachedSnapshotClients;
            coduomp_sv_error_if_counter_wrapped(
                svs.nextCachedSnapshotClients,
                "\x15" "svs.nextCachedSnapshotClients wrapped");
            ++cachedFrame->numClients;
        }
    }

    while ((clientNum = MSG_ReadBits(&msg, 10)) != CODUO_ENTITYNUM_NONE) {
        coduo_cached_snapshot_entity_t *entity;

        if (msg.readcount > byteCount) {
            Com_Error(1, "\x15" "SV_GetCachedSnapshot: end of message");
        }

        entity = coduomp_sv_cached_snapshot_entity_at(
            svs.nextCachedSnapshotEntities);
        MSG_ReadDeltaArchivedEntity(&msg, &sv_entityLinks[clientNum].baseline,
                                    entity, clientNum);
        ++svs.nextCachedSnapshotEntities;
        coduomp_sv_error_if_counter_wrapped(
            svs.nextCachedSnapshotEntities,
            "\x15" "svs.nextCachedSnapshotEntities wrapped");
        ++cachedFrame->numEntities;
    }

    ++svs.nextCachedSnapshotFrames;
    coduomp_sv_error_if_counter_wrapped(
        svs.nextCachedSnapshotFrames,
        "\x15" "svs.nextCachedSnapshotFrames wrapped");

    return cachedFrame;
}

serverCachedSnapshotFrame_t *
SV_GetCachedSnapshot(int32_t *archiveTime)
{
    int32_t archivedFrameIndex;

    if (svs.archiveEnabled == 0 || *archiveTime <= 0) {
        return NULL;
    }

    archivedFrameIndex =
        svs.nextArchivedSnapshotFrames -
        (*archiveTime * sv_fps->integer) / 1000;

    if (archivedFrameIndex <
        svs.nextArchivedSnapshotFrames -
            CODUO_HOST_ARCHIVED_SNAPSHOT_FRAME_INDEX_COUNT) {
        archivedFrameIndex =
            svs.nextArchivedSnapshotFrames -
            CODUO_HOST_ARCHIVED_SNAPSHOT_FRAME_INDEX_COUNT;
        *archiveTime =
            ((svs.nextArchivedSnapshotFrames - archivedFrameIndex) * 1000) /
            sv_fps->integer;
    }

    if (archivedFrameIndex < 0) {
        archivedFrameIndex = 0;
        *archiveTime = (svs.nextArchivedSnapshotFrames * 1000) /
                       sv_fps->integer;
    }

    while (archivedFrameIndex < svs.nextArchivedSnapshotFrames) {
        serverCachedSnapshotFrame_t *frame =
            SV_GetCachedSnapshotInternal(archivedFrameIndex);

        if (frame != NULL) {
            return frame;
        }
        ++archivedFrameIndex;
    }

    *archiveTime = 0;
    return NULL;
}

qboolean
SV_GetArchivedClientInfo(int32_t clientNum,
                         int32_t *archiveTime,
                         playerState_t *playerStateOut,
                         coduo_archived_client_info_meta_t *clientSnapshotOut)
{
    serverCachedSnapshotFrame_t *frame;
    int32_t deltaTime;
    int32_t clientIndex;

    frame = SV_GetCachedSnapshot(archiveTime);
    if (frame == NULL) {
        if (*archiveTime < 1) {
            return SV_GetCurrentClientInfo(clientNum, playerStateOut,
                                           clientSnapshotOut);
        }
        return 0;
    }

    deltaTime = svs.time - frame->messageTime;
    for (clientIndex = 0; clientIndex < frame->numClients; ++clientIndex) {
        serverCachedSnapshotClient_t *client =
            coduomp_sv_cached_snapshot_client_at(
                frame->firstClient + clientIndex);

        if (client->snapshot.clientNum == clientNum) {
            if (client->playerStateValid == 0) {
                break;
            }

            coduomp_sv_copy_player_state(playerStateOut,
                                         &client->playerState);
            coduomp_sv_copy_client_snapshot(clientSnapshotOut,
                                            &client->snapshot);
            coduomp_sv_adjust_archived_player_state_times(
                playerStateOut, deltaTime);
            return 1;
        }
    }

    return 0;
}

void SV_BuildClientSnapshot(client_t *client)
{
    clientSnapshot_t *snapshotFrame;
    int32_t clientNum;
    int32_t archiveTime;
    serverCachedSnapshotFrame_t *archiveFrame;
    int32_t deltaTime;
    vec3_t viewOrigin;
    int32_t clientEntityNum;
    snapshotEntityNumbers_t visibleEntities;
    int32_t i;

    snapshotFrame =
        &client->snapshotFrames[client->netchan.outgoingSequence &
                                (CODUO_HOST_CLIENT_SNAPSHOT_FRAME_COUNT - 1)];
    snapshotFrame->numEntities = 0;
    snapshotFrame->numClients = 0;

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

    snapshotFrame->firstEntity = svs.nextEntityStateSnapshot;
    snapshotFrame->firstClient = svs.nextClientSnapshot;
    if (sv.state != CODUO_SS_GAME) {
        return;
    }

    visibleEntities.count = 0;
    clientNum = (int32_t)(client - coduomp_sv_clients());
    archiveTime = SV_GetClientArchiveTime(clientNum);
    archiveFrame = SV_GetCachedSnapshot(&archiveTime);
    SV_SetClientArchiveTime(clientNum, archiveTime);
    deltaTime = 0;
    if (archiveFrame != NULL) {
        deltaTime = svs.time - archiveFrame->messageTime;
    }

    coduomp_sv_copy_player_state(&snapshotFrame->playerState,
                                 SV_GameClientNum(clientNum));
    clientEntityNum = snapshotFrame->playerState.psClientNum;
    if (clientEntityNum < 0 || clientEntityNum > CODUO_ENTITYNUM_NONE) {
        Com_Error(1, "\x15" "SV_BuildClientSnapshot: bad gEnt");
    }

    coduomp_sv_read_view_origin_from_player_state(
        &snapshotFrame->playerState, viewOrigin);
    AddLeanToPosition(viewOrigin, snapshotFrame->playerState.viewAngles[1],
                      snapshotFrame->playerState.leanFraction, 16.0f, 20.0f);

    if (archiveFrame == NULL) {
        SV_AddEntitiesVisibleFromPoint(viewOrigin, clientEntityNum,
                                       &visibleEntities);
        for (i = 0; i < visibleEntities.count; ++i) {
            sharedEntity_t *gentity =
                SV_GentityNum(visibleEntities.numbers[i]);
            coduo_host_snapshot_entity_record_t *dest =
                &coduomp_sv_entity_state_snapshots()
                     [svs.nextEntityStateSnapshot %
                      svs.numEntityStateSnapshots];

            memcpy(&dest->entityState, &gentity->entityState,
                   sizeof(dest->entityState));
            ++svs.nextEntityStateSnapshot;
            coduomp_sv_error_if_counter_wrapped(
                svs.nextEntityStateSnapshot,
                "\x15" "svs.nextSnapshotEntities wrapped");
            ++snapshotFrame->numEntities;
        }

        for (i = 0; i < sv_maxclients->integer; ++i) {
            const client_t *snapshotClient = &coduomp_sv_clients()[i];

            if (snapshotClient->state > CODUO_CS_ZOMBIE) {
                coduo_host_client_snapshot_t *dest =
                    &coduomp_sv_client_snapshots()
                         [svs.nextClientSnapshot % svs.numClientSnapshots];

                coduomp_sv_copy_client_snapshot(dest, SV_GetClientState(i));
                ++svs.nextClientSnapshot;
                coduomp_sv_error_if_counter_wrapped(
                    svs.nextClientSnapshot,
                    "\x15" "svs.nextSnapshotClients wrapped");
                ++snapshotFrame->numClients;
            }
        }
    } else {
        SV_AddCachedEntitiesVisibleFromPoint(
            archiveFrame->numEntities, archiveFrame->firstEntity, viewOrigin,
            clientEntityNum, &visibleEntities, snapshotFrame);
        for (i = 0; i < visibleEntities.count; ++i) {
            coduo_cached_snapshot_entity_t *source =
                coduomp_sv_cached_snapshot_entity_at(
                    archiveFrame->firstEntity + visibleEntities.numbers[i]);
            coduo_host_snapshot_entity_record_t *dest =
                &coduomp_sv_entity_state_snapshots()
                     [svs.nextEntityStateSnapshot %
                      svs.numEntityStateSnapshots];

            memcpy(&dest->entityState, &source->entityState,
                   sizeof(dest->entityState));
            coduomp_sv_adjust_entity_snapshot_times(
                &dest->entityState, deltaTime);
            ++svs.nextEntityStateSnapshot;
            coduomp_sv_error_if_counter_wrapped(
                svs.nextEntityStateSnapshot,
                "\x15" "svs.nextSnapshotEntities wrapped");
            ++snapshotFrame->numEntities;
        }

        for (i = 0; i < archiveFrame->numClients; ++i) {
            serverCachedSnapshotClient_t *source =
                coduomp_sv_cached_snapshot_client_at(
                    archiveFrame->firstClient + i);
            coduo_host_client_snapshot_t *dest =
                &coduomp_sv_client_snapshots()
                     [svs.nextClientSnapshot % svs.numClientSnapshots];

            coduomp_sv_copy_client_snapshot(dest, &source->snapshot);
            ++svs.nextClientSnapshot;
            coduomp_sv_error_if_counter_wrapped(
                svs.nextClientSnapshot,
                "\x15" "svs.nextSnapshotClients wrapped");
            ++snapshotFrame->numClients;
        }
    }
}

void SV_ArchiveSnapshot(void)
{
    msg_t msg;
    uint8_t archivePayload[CODUO_ARCHIVE_MSG_BUFFER_SIZE];
    serverCachedSnapshotFrame_t *deltaFrame;
    int32_t scanMin;
    int32_t minArchivedFrame;
    int32_t scanIndex;
    qboolean useDelta;
    serverCachedSnapshotFrame_t *cachedFrame;
    int32_t maxClients;
    int32_t clientIndex;
    int32_t oldClientIndex;
    int32_t oldClientNum;
    coduo_archived_client_info_player_state_t playerState;
    int32_t entityNum;
    archivedSnapshotFrameIndex_t *archivedFrame;
    int32_t wrappedFirstByte;
    int32_t firstCopyBytes;

    if (sv.state != CODUO_SS_GAME || svs.archiveEnabled == 0) {
        return;
    }

    MSG_Init(&msg, archivePayload, CODUO_ARCHIVE_MSG_BUFFER_SIZE);

    scanMin =
        svs.nextCachedSnapshotFrames -
        CODUO_HOST_CACHED_SNAPSHOT_FRAME_RING_COUNT;
    if (scanMin < 0) {
        scanMin = 0;
    }

    minArchivedFrame = svs.nextArchivedSnapshotFrames - sv_fps->integer;
    deltaFrame = NULL;
    useDelta = 0;
    scanIndex = svs.nextCachedSnapshotFrames - 1;
    while (scanIndex >= scanMin) {
        deltaFrame = coduomp_sv_cached_snapshot_frame_at(scanIndex);
        if (deltaFrame->archivedFrameIndex >= minArchivedFrame &&
            deltaFrame->decodedFromDelta == qfalse) {
            if (deltaFrame->firstEntity >=
                    svs.nextCachedSnapshotEntities -
                        CODUO_HOST_CACHED_SNAPSHOT_ENTITY_RING_COUNT &&
                deltaFrame->firstClient >=
                    svs.nextCachedSnapshotClients -
                        CODUO_HOST_CACHED_SNAPSHOT_CLIENT_RING_COUNT) {
                useDelta = 1;
            }
            break;
        }
        --scanIndex;
    }

    if (useDelta != 0) {
        MSG_WriteBit0(&msg);
        MSG_WriteLong(&msg, deltaFrame->archivedFrameIndex);
        MSG_WriteLong(&msg, svs.time);

        maxClients = sv_maxclients->integer;
        clientIndex = 0;
        oldClientIndex = 0;
        oldClientNum = 0;

        while (clientIndex < maxClients || oldClientIndex < deltaFrame->numClients) {
            serverCachedSnapshotClient_t *oldClient = NULL;

            if (clientIndex < maxClients &&
                coduomp_sv_clients()[clientIndex].state <=
                    CODUO_CS_ZOMBIE) {
                ++clientIndex;
                continue;
            }

            if (oldClientIndex < deltaFrame->numClients) {
                oldClient =
                    coduomp_sv_cached_snapshot_client_at(
                        deltaFrame->firstClient + oldClientIndex);
                oldClientNum = oldClient->snapshot.clientNum;
            } else {
                oldClientNum = CODUO_CLIENT_NUM_SENTINEL;
            }

            if (clientIndex == oldClientNum) {
                MSG_WriteDeltaClient(&msg, &oldClient->snapshot,
                                     SV_GetClientState(clientIndex), 1);
                if (SV_GetFollowPlayerState(
                        clientIndex,
                        &playerState) == 0) {
                    MSG_WriteBit0(&msg);
                } else {
                    MSG_WriteBit1(&msg);
                    /* ORIGINAL_BINARY_BUG: oldClient->playerState is used
                     * without checking oldClient->playerStateValid. See
                     * original-bugs/
                     * coduomp-archive-stale-playerstate-delta-baseline.md. */
                    MSG_WriteDeltaPlayerstate(
                        &msg, &oldClient->playerState, &playerState);
                }
                ++oldClientIndex;
                ++clientIndex;
            } else if (clientIndex < oldClientNum) {
                MSG_WriteDeltaClient(&msg, NULL,
                                     SV_GetClientState(clientIndex), 1);
                if (SV_GetFollowPlayerState(
                        clientIndex,
                        &playerState) == 0) {
                    MSG_WriteBit0(&msg);
                } else {
                    MSG_WriteBit1(&msg);
                    MSG_WriteDeltaPlayerstate(
                        &msg, NULL, &playerState);
                }
                ++clientIndex;
            } else if (oldClientNum < clientIndex) {
                ++oldClientIndex;
            }
        }

        MSG_WriteBit0(&msg);

        for (entityNum = 0; entityNum < sv_gameData.numGEntities;
             ++entityNum) {
            sharedEntity_t *gentity = SV_GentityNum(entityNum);

            if (gentity->linked != 0 &&
                (gentity->svFlags & CODUO_SVF_NOCLIENT) == 0) {
                svEntity_t *svEntity =
                    SV_SvEntityForGentity(gentity);

                if ((gentity->svFlags & CODUO_SVF_VISIBILITY_BYPASS_MASK) !=
                        0 ||
                    svEntity->numClusters != 0 || gentity->soundTime != 0) {
                    coduo_cached_snapshot_entity_t archiveEntity;

                    memcpy(&archiveEntity.entityState, &gentity->entityState,
                           sizeof(archiveEntity.entityState));
                    archiveEntity.flags = gentity->svFlags;
                    if (gentity->soundTime != 0) {
                        archiveEntity.flags |= 8;
                    }
                    archiveEntity.ownerNum = gentity->ownerNum;
                    memcpy(archiveEntity.clipMins, gentity->absMin,
                           sizeof(archiveEntity.clipMins));
                    memcpy(archiveEntity.clipMaxs, gentity->absMax,
                           sizeof(archiveEntity.clipMaxs));

                    MSG_WriteDeltaArchivedEntity(
                        &msg,
                        &sv_entityLinks[gentity->entityState.s_number]
                             .baseline,
                        &archiveEntity, qtrue);
                }
            }
        }
    } else {
        MSG_WriteBit1(&msg);
        MSG_WriteLong(&msg, svs.time);

        cachedFrame =
            coduomp_sv_cached_snapshot_frame_at(
                svs.nextCachedSnapshotFrames);
        cachedFrame->archivedFrameIndex = svs.nextArchivedSnapshotFrames;
        cachedFrame->numEntities = 0;
        cachedFrame->firstEntity = svs.nextCachedSnapshotEntities;
        cachedFrame->numClients = 0;
        cachedFrame->firstClient = svs.nextCachedSnapshotClients;
        cachedFrame->decodedFromDelta = qfalse;
        cachedFrame->messageTime = svs.time;

        {
            const client_t *client = coduomp_sv_clients();

            for (clientIndex = 0; clientIndex < sv_maxclients->integer;
                 ++clientIndex, ++client) {
                if (client->state > CODUO_CS_ZOMBIE) {
                    serverCachedSnapshotClient_t *newClient =
                        coduomp_sv_cached_snapshot_client_at(
                            svs.nextCachedSnapshotClients);

                    coduomp_sv_copy_client_snapshot(
                        &newClient->snapshot,
                        SV_GetClientState(clientIndex));
                    MSG_WriteDeltaClient(&msg, NULL, &newClient->snapshot, 1);

                    newClient->playerStateValid = SV_GetFollowPlayerState(
                        clientIndex, &newClient->playerState);
                    if (newClient->playerStateValid == 0) {
                        MSG_WriteBit0(&msg);
                    } else {
                        MSG_WriteBit1(&msg);
                        MSG_WriteDeltaPlayerstate(
                            &msg, NULL, &newClient->playerState);
                    }

                    ++svs.nextCachedSnapshotClients;
                    coduomp_sv_error_if_counter_wrapped(
                        svs.nextCachedSnapshotClients,
                        "\x15svs.nextCachedSnapshotClients wrapped");
                    ++cachedFrame->numClients;
                }
            }
        }

        MSG_WriteBit0(&msg);

        for (entityNum = 0; entityNum < sv_gameData.numGEntities;
             ++entityNum) {
            sharedEntity_t *gentity = SV_GentityNum(entityNum);

            if (gentity->linked != 0 &&
                (gentity->svFlags & CODUO_SVF_NOCLIENT) == 0) {
                svEntity_t *svEntity =
                    SV_SvEntityForGentity(gentity);

                if ((gentity->svFlags & CODUO_SVF_VISIBILITY_BYPASS_MASK) !=
                        0 ||
                    svEntity->numClusters != 0 || gentity->soundTime != 0) {
                    coduo_cached_snapshot_entity_t *archiveEntity =
                        coduomp_sv_cached_snapshot_entity_at(
                            svs.nextCachedSnapshotEntities);

                    memcpy(&archiveEntity->entityState, &gentity->entityState,
                           sizeof(archiveEntity->entityState));
                    archiveEntity->flags = gentity->svFlags;
                    if (gentity->soundTime != 0) {
                        archiveEntity->flags |= 8;
                    }
                    archiveEntity->ownerNum = gentity->ownerNum;
                    memcpy(archiveEntity->clipMins, gentity->absMin,
                           sizeof(archiveEntity->clipMins));
                    memcpy(archiveEntity->clipMaxs, gentity->absMax,
                           sizeof(archiveEntity->clipMaxs));

                    MSG_WriteDeltaArchivedEntity(
                        &msg,
                        &sv_entityLinks[gentity->entityState.s_number]
                             .baseline,
                        archiveEntity, qtrue);

                    ++svs.nextCachedSnapshotEntities;
                    coduomp_sv_error_if_counter_wrapped(
                        svs.nextCachedSnapshotEntities,
                        "\x15svs.nextCachedSnapshotEntities wrapped");
                    ++cachedFrame->numEntities;
                }
            }
        }

        ++svs.nextCachedSnapshotFrames;
        coduomp_sv_error_if_counter_wrapped(
            svs.nextCachedSnapshotFrames,
            "\x15svs.nextCachedSnapshotFrames wrapped");
    }

    MSG_WriteBits(&msg, CODUO_ENTITYNUM_NONE, 10);

    archivedFrame =
        &coduomp_sv_archived_snapshot_frames()
             [svs.nextArchivedSnapshotFrames %
              CODUO_HOST_ARCHIVED_SNAPSHOT_FRAME_INDEX_COUNT];
    archivedFrame->firstByte = svs.nextArchivedSnapshotBuffer;
    archivedFrame->byteCount = msg.cursize;

    wrappedFirstByte =
        svs.nextArchivedSnapshotBuffer % CODUO_HOST_ARCHIVED_SNAPSHOT_BUFFER_SIZE;

    svs.nextArchivedSnapshotBuffer += msg.cursize;
    coduomp_sv_error_if_counter_wrapped(
        svs.nextArchivedSnapshotBuffer,
        "\x15svs.nextArchivedSnapshotBuffer wrapped");

    firstCopyBytes =
        CODUO_HOST_ARCHIVED_SNAPSHOT_BUFFER_SIZE - wrappedFirstByte;
    if (firstCopyBytes < msg.cursize) {
        /* 0x0809995b..0x080999fb retains the same signed-dword arithmetic;
         * only the libc boundary widens on a native 64-bit host. */
        memcpy(&coduomp_sv_archived_snapshot_buffer()[wrappedFirstByte],
               archivePayload, (size_t)(uint32_t)firstCopyBytes);
        memcpy(coduomp_sv_archived_snapshot_buffer(),
               &archivePayload[firstCopyBytes],
               (size_t)(uint32_t)(msg.cursize - firstCopyBytes));
    } else {
        memcpy(&coduomp_sv_archived_snapshot_buffer()[wrappedFirstByte],
               archivePayload, (size_t)(uint32_t)msg.cursize);
    }

    ++svs.nextArchivedSnapshotFrames;
    coduomp_sv_error_if_counter_wrapped(
        svs.nextArchivedSnapshotFrames,
        "\x15svs.nextArchivedSnapshotFrames wrapped");
}
