#include "msg_private.h"
#include "netchan_private.h"
#include "../core_command/cmd_private.h"
#include "../core_memory/core_memory_private.h"
#include "../core_runtime/core_runtime_private.h"

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

enum {
    NETCHAN_COM_ERROR_DROP = 1,
    NETCHAN_FIRST_OUTGOING_SEQUENCE = 1,
    NETCHAN_QPORT_CVAR_FLAGS = 0x10,
    NETCHAN_SHOWPACKETS_CVAR_FLAGS = 0x100,
    NETCHAN_SHOWDROP_CVAR_FLAGS = 0x100,
    NETCHAN_PROFILE_CVAR_FLAGS = 0x100,
    NETCHAN_SHOWPROFILE_CVAR_FLAGS = 0x100,
    NETCHAN_RELIABLE_SEQUENCE_FRAGMENT_BIT = INT32_MIN,
    NETCHAN_RELIABLE_SEQUENCE_MASK = INT32_MAX,
    NETCHAN_FRAGMENT_SIZE = 1300,
    NETCHAN_PACKET_BUFFER_SIZE = 1400,
    NETCHAN_SEQUENCE_HEADER_BYTES = 4,
    NET_LOOPBACK_SOCKET_PAIR_MASK = 1
};

enum {
    NET_OOB_MARKER = -1,
    NET_OOB_MARKER_BYTES = sizeof(int32_t),
    NET_OOB_COMPRESS_SKIP_BYTES = 12,
    NET_OOB_PRINT_BUFFER_SIZE = 32768,
    NET_OOB_DATA_BUFFER_SIZE = 65536,
    NET_STRING_TO_ADR_COPY_SIZE = 1024,
    NET_DEFAULT_PORT = 28960
};

void Net_DumpProfile_f(void)
{
    if (net_profileActiveMode == CODUO_NET_PROFILE_OFF) {
        Com_Printf("Network profiling is not on. Set net_profile to turn on network profiling\n");
    }
    else {
        SV_Netchan_PrintProfileStats(qtrue);
    }
}

void Netchan_Init(int32_t qport)
{
    qport &= UINT16_MAX;

    showpackets =
        Cvar_Get("showpackets", "0", NETCHAN_SHOWPACKETS_CVAR_FLAGS);
    showdrop = Cvar_Get("showdrop", "0", NETCHAN_SHOWDROP_CVAR_FLAGS);
    net_qport = Cvar_Get("net_qport", va("%i", qport),
                         NETCHAN_QPORT_CVAR_FLAGS);
    net_profile = Cvar_Get("net_profile", "0", NETCHAN_PROFILE_CVAR_FLAGS);
    net_showprofile =
        Cvar_Get("net_showprofile", "0", NETCHAN_SHOWPROFILE_CVAR_FLAGS);
    net_lanauthorize = Cvar_Get("net_lanauthorize", "0", 0);

    Cmd_AddCommand("net_dumpprofile", Net_DumpProfile_f);
}

void Netchan_Setup(netsrc_t sock, netchan_t *chan,
                   netadr_t adr, int32_t qport)
{
    memset(chan, 0, sizeof(*chan));

    chan->sock = sock;
    chan->remoteAddress = adr;
    chan->qport = qport;
    chan->incomingSequence = 0;
    chan->outgoingSequence = NETCHAN_FIRST_OUTGOING_SEQUENCE;

    NetProf_PrepProfiling(&chan->profile);
}

const char *NET_AdrToString(netadr_t adr)
{
    int16_t port;

    if (adr.type == CODUO_NA_LOOPBACK) {
        Com_sprintf(com_netadrString, sizeof(com_netadrString), "loopback");
    }
    else if (adr.type == CODUO_NA_IP) {
        port = (int16_t)BigShort(adr.port);
        Com_sprintf(com_netadrString, sizeof(com_netadrString),
                    "%i.%i.%i.%i:%i", adr.ip[0], adr.ip[1], adr.ip[2],
                    adr.ip[3], port);
    }
    else {
        port = (int16_t)BigShort(adr.port);
        Com_sprintf(com_netadrString, sizeof(com_netadrString),
                    "%02x%02x%02x%02x.%02x%02x%02x%02x%02x%02x:%i",
                    adr.ipx[0], adr.ipx[1], adr.ipx[2], adr.ipx[3],
                    adr.ipx[4], adr.ipx[5], adr.ipx[6], adr.ipx[7],
                    adr.ipx[8], adr.ipx[9], port);
    }

    return com_netadrString;
}

void NET_SendLoopPacket(netsrc_t sock,
                        int32_t length, const void *data)
{
    loopback_t *loop;
    int32_t packetIndex;

    loop = &net_loopbacks[sock ^ NET_LOOPBACK_SOCKET_PAIR_MASK];
    packetIndex = loop->send & (CODUO_NET_LOOPBACK_PACKET_COUNT - 1);
    /* ORIGINAL_BINARY_BUG: the signed queue counters briefly make a newly
     * wrapped queue appear empty and lose the oldest packets when it catches
     * up. See original-bugs/coduomp-loopback-counter-wrap-drop.md. */
    ++loop->send;

    /* ORIGINAL_BINARY_BUG: stock does not check length against the 1400-byte
     * ring payload. See original-bugs/
     * coduomp-network-packet-constructor-bounds.md. */
    memcpy(loop->msgs[packetIndex].data, data, (size_t)(uint32_t)length);
    loop->msgs[packetIndex].datalen = length;
}

void NET_SendPacket(netsrc_t sock,
                    int32_t length,
                    const void *data, netadr_t to)
{
    if (showpackets->integer != 0) {
        int32_t marker;

        /* ORIGINAL_BINARY_BUG: stock performs this dword read without first
         * requiring four packet bytes. memcpy preserves the unaligned load on
         * native hosts. See original-bugs/
         * coduomp-network-packet-constructor-bounds.md. */
        memcpy(&marker, data, sizeof(marker));
        if (marker == NET_OOB_MARKER) {
            Com_Printf("send packet %4i\n", length);
        }
    }

    if (to.type == CODUO_NA_LOOPBACK) {
        NET_SendLoopPacket(sock, length, data);
        return;
    }

    if (to.type != CODUO_NA_BOT && to.type != CODUO_NA_BAD) {
        Sys_SendPacket(length, data, to);
    }
}

void NET_OutOfBandPrint(netsrc_t sock, netadr_t adr,
                        const char *format, ...)
{
    uint8_t packet[NET_OOB_PRINT_BUFFER_SIZE];
    int32_t marker = NET_OOB_MARKER;
    va_list args;
    int32_t length;

    memcpy(packet, &marker, sizeof(marker));
    va_start(args, format);
    /* ORIGINAL_BINARY_BUG: stock supplies no capacity to vsprintf. See
     * original-bugs/coduomp-out-of-band-formatting-overflow.md. */
    vsprintf((char *)&packet[NET_OOB_MARKER_BYTES], format, args);
    va_end(args);

    length = (int32_t)strlen((const char *)packet);
    NET_SendPacket(sock, length, packet, adr);
    if (sock == NS_SERVER) {
        SV_Netchan_AddOOBProfilePacket(length);
    }
}

void NET_OutOfBandData(netsrc_t sock, netadr_t adr,
                       const void *data,
                       int32_t length)
{
    uint8_t packet[NET_OOB_DATA_BUFFER_SIZE];
    int32_t marker = NET_OOB_MARKER;
    msg_t msg;

    memcpy(packet, &marker, sizeof(marker));
    /* ORIGINAL_BINARY_BUG: positive length is not capped at the remaining
     * stack-array capacity. See original-bugs/
     * coduomp-network-packet-constructor-bounds.md. */
    for (int32_t index = 0; index < length;
         ++index) {
        packet[NET_OOB_MARKER_BYTES + index] =
            ((const uint8_t *)data)[index];
    }

    msg.data = packet;
    msg.cursize = length + NET_OOB_MARKER_BYTES;
    Huff_Compress(&msg, NET_OOB_COMPRESS_SKIP_BYTES);

    NET_SendPacket(sock, msg.cursize, msg.data, adr);
    if (sock == NS_SERVER) {
        SV_Netchan_AddOOBProfilePacket(msg.cursize);
    }
}

void NET_OutOfBandPbPacket(netsrc_t sock, netadr_t adr,
                           const void *data,
                           int32_t length)
{
    uint8_t packet[NET_OOB_DATA_BUFFER_SIZE];
    int32_t marker = NET_OOB_MARKER;
    int32_t packetLength;

    memcpy(packet, &marker, sizeof(marker));
    /* ORIGINAL_BINARY_BUG: stock does not cap length at the remaining stack
     * array. See original-bugs/
     * coduomp-network-packet-constructor-bounds.md. */
    memcpy(&packet[NET_OOB_MARKER_BYTES], data,
           (size_t)(uint32_t)length);

    packetLength = length + NET_OOB_MARKER_BYTES;
    NET_SendPacket(sock, packetLength, packet, adr);
    if (sock == NS_SERVER) {
        SV_Netchan_AddOOBProfilePacket(packetLength);
    }
}

qboolean NET_StringToAdr(const char *text, netadr_t *adr)
{
    char copy[NET_STRING_TO_ADR_COPY_SIZE];
    char *portText;

    if (strcmp(text, "localhost") == 0) {
        memset(adr, 0, sizeof(*adr));
        adr->type = CODUO_NA_LOOPBACK;
        return qtrue;
    }

    Q_strncpyz(copy, text, sizeof(copy));
    portText = strstr(copy, ":");
    if (portText != NULL) {
        *portText = '\0';
        ++portText;
    }

    if (Sys_StringToAdr(copy, adr) == qfalse ||
        (adr->ip[0] == UINT8_MAX && adr->ip[1] == UINT8_MAX &&
         adr->ip[2] == UINT8_MAX && adr->ip[3] == UINT8_MAX)) {
        adr->type = CODUO_NA_BOT;
        return qfalse;
    }

    if (portText == NULL) {
        adr->port = (uint16_t)BigShort(NET_DEFAULT_PORT);
    } else {
        adr->port =
            (uint16_t)BigShort((int16_t)atoi(portText));
    }

    return qtrue;
}

void Netchan_TransmitNextFragment(netchan_t *chan)
{
    msg_t msg;
    uint8_t msgData[NETCHAN_PACKET_BUFFER_SIZE];
    int32_t fragmentLength;

    NetProf_PrepProfiling(&chan->profile);
    MSG_Init(&msg, msgData, sizeof(msgData));
    MSG_WriteLong(&msg,
                  chan->outgoingSequence |
                      NETCHAN_RELIABLE_SEQUENCE_FRAGMENT_BIT);

    if (chan->sock == NS_CLIENT) {
        MSG_WriteShort(&msg, net_qport->integer);
    }

    fragmentLength = NETCHAN_FRAGMENT_SIZE;
    if (chan->unsentLength <
        chan->unsentFragmentStart + NETCHAN_FRAGMENT_SIZE) {
        fragmentLength = chan->unsentLength - chan->unsentFragmentStart;
    }

    MSG_WriteShort(&msg, chan->unsentFragmentStart);
    MSG_WriteShort(&msg, fragmentLength);
    MSG_WriteData(&msg, &chan->unsentBuffer[chan->unsentFragmentStart],
                  fragmentLength);

    NET_SendPacket(chan->sock, msg.cursize, msg.data, chan->remoteAddress);
    NetProf_NewSendPacket(chan, msg.cursize, qtrue);

    if (showpackets->integer != 0) {
        Com_Printf("%s send %4i : s=%i fragment=%i,%i\n",
                   net_profileSocketNames[chan->sock], msg.cursize,
                   chan->outgoingSequence - 1, chan->unsentFragmentStart,
                   fragmentLength);
    }

    chan->unsentFragmentStart += fragmentLength;
    if (chan->unsentFragmentStart == chan->unsentLength &&
        fragmentLength != NETCHAN_FRAGMENT_SIZE) {
        /* ORIGINAL_BINARY_BUG: the signed sequence is allowed to enter its
         * fragment-flag half-space. See original-bugs/
         * coduomp-netchan-sequence-wrap.md. */
        ++chan->outgoingSequence;
        chan->unsentFragments = qfalse;
    }
}

void Netchan_Transmit(netchan_t *chan,
                      int32_t length,
                      const void *data)
{
    msg_t msg;
    uint8_t msgData[NETCHAN_PACKET_BUFFER_SIZE];

    /* ORIGINAL_BINARY_BUG: this signed maximum check admits negative lengths,
     * which the ordinary path forwards to MSG_WriteData. See original-bugs/
     * coduomp-network-packet-constructor-bounds.md. */
    if (length > CODUO_MAX_MSGLEN) {
        Com_Error(NETCHAN_COM_ERROR_DROP,
                  "\x15" "Netchan_Transmit: length = %i", length);
    }

    chan->unsentFragmentStart = 0;
    if (length >= NETCHAN_FRAGMENT_SIZE) {
        chan->unsentFragments = qtrue;
        chan->unsentLength = length;
        Com_Memcpy(chan->unsentBuffer, data, (size_t)length);
        Netchan_TransmitNextFragment(chan);
        return;
    }

    NetProf_PrepProfiling(&chan->profile);
    MSG_Init(&msg, msgData, sizeof(msgData));
    MSG_WriteLong(&msg, chan->outgoingSequence);
    /* ORIGINAL_BINARY_BUG: after INT32_MAX this high bit is decoded as the
     * fragment flag rather than sequence data. See original-bugs/
     * coduomp-netchan-sequence-wrap.md. */
    ++chan->outgoingSequence;

    if (chan->sock == NS_CLIENT) {
        MSG_WriteShort(&msg, net_qport->integer);
    }

    MSG_WriteData(&msg, data, length);
    NET_SendPacket(chan->sock, msg.cursize, msg.data, chan->remoteAddress);
    NetProf_NewSendPacket(chan, msg.cursize, qfalse);

    if (showpackets->integer != 0) {
        Com_Printf("%s send %4i : s=%i ack=%i\n",
                   net_profileSocketNames[chan->sock], msg.cursize,
                   chan->outgoingSequence - 1, chan->incomingSequence);
    }
}

qboolean Netchan_Process(netchan_t *chan, msg_t *msg)
{
    qboolean fragmented;
    int32_t fragmentStart;
    int32_t fragmentLength;
    int32_t sequence;
    int32_t rewrittenSequence;

    NetProf_PrepProfiling(&chan->profile);
    MSG_BeginReading(msg);

    sequence = MSG_ReadLong(msg);
    /* ORIGINAL_BINARY_BUG: this flag consumes the high half of the unbounded
     * signed outgoing sequence space. See original-bugs/
     * coduomp-netchan-sequence-wrap.md. */
    fragmented =
        (sequence & NETCHAN_RELIABLE_SEQUENCE_FRAGMENT_BIT) != 0;
    if (fragmented) {
        sequence &= NETCHAN_RELIABLE_SEQUENCE_MASK;
    }

    if (chan->sock == NS_SERVER) {
        (void)MSG_ReadShort(msg);
    }

    if (fragmented) {
        fragmentStart = MSG_ReadShort(msg);
        fragmentLength = MSG_ReadShort(msg);
    }
    else {
        fragmentStart = 0;
        fragmentLength = 0;
    }

    NetProf_NewRecievePacket(chan, msg->cursize, fragmented);

    if (showpackets->integer != 0) {
        if (fragmented) {
            Com_Printf("%s recv %4i : s=%i fragment=%i,%i\n",
                       net_profileSocketNames[chan->sock], msg->cursize,
                       sequence, fragmentStart, fragmentLength);
        }
        else {
            Com_Printf("%s recv %4i : s=%i\n",
                       net_profileSocketNames[chan->sock], msg->cursize,
                       sequence);
        }
    }

    if (sequence <= chan->incomingSequence) {
        if (showdrop->integer != 0 || showpackets->integer != 0) {
            Com_Printf("%s:Out of order packet %i at %i\n",
                       NET_AdrToString(chan->remoteAddress), sequence,
                       chan->incomingSequence);
        }
        return qfalse;
    }

    chan->dropped = sequence - chan->incomingSequence - 1;
    if (chan->dropped > 0 &&
        (showdrop->integer != 0 || showpackets->integer != 0)) {
        Com_Printf("%s:Dropped %i packets at %i\n",
                   NET_AdrToString(chan->remoteAddress), chan->dropped,
                   sequence);
    }

    if (!fragmented) {
        chan->incomingSequence = sequence;
        return qtrue;
    }

    if (sequence != chan->fragmentSequence) {
        chan->fragmentSequence = sequence;
        chan->fragmentLength = 0;
    }

    if (fragmentStart != chan->fragmentLength) {
        if (showdrop->integer != 0 || showpackets->integer != 0) {
            Com_Printf("%s:Dropped a message fragment\n",
                       NET_AdrToString(chan->remoteAddress), sequence);
        }
        return qfalse;
    }

    if (fragmentLength < 0 || msg->cursize < msg->readcount + fragmentLength ||
        (uint32_t)chan->fragmentLength + (uint32_t)fragmentLength >
            (uint32_t)CODUO_MAX_MSGLEN) {
        if (showdrop->integer != 0 || showpackets->integer != 0) {
            Com_Printf("%s:illegal fragment length\n",
                       NET_AdrToString(chan->remoteAddress));
        }
        return qfalse;
    }

    memcpy(&chan->fragmentBuffer[chan->fragmentLength],
           &msg->data[msg->readcount], (size_t)fragmentLength);
    chan->fragmentLength += fragmentLength;

    if (fragmentLength == NETCHAN_FRAGMENT_SIZE) {
        return qfalse;
    }

    if (msg->maxsize < chan->fragmentLength) {
        Com_Printf("%s:fragmentLength %i > msg->maxsize\n",
                   NET_AdrToString(chan->remoteAddress),
                   chan->fragmentLength);
        return qfalse;
    }

    rewrittenSequence = FUN_08085130(sequence);
    memcpy(msg->data, &rewrittenSequence, sizeof(rewrittenSequence));
    memcpy(&msg->data[NETCHAN_SEQUENCE_HEADER_BYTES], chan->fragmentBuffer,
           (size_t)chan->fragmentLength);
    msg->cursize = chan->fragmentLength + NETCHAN_SEQUENCE_HEADER_BYTES;
    chan->fragmentLength = 0;

    MSG_BeginReading(msg);
    (void)MSG_ReadLong(msg);
    /* ORIGINAL_BINARY_BUG: unlike the unfragmented path, stock does not
     * publish sequence to incomingSequence after successful reassembly. The
     * same fragmented sequence can therefore be accepted again until a newer
     * sequence arrives. See original-bugs/
     * coduomp-netchan-fragment-sequence-replay.md. */
    return qtrue;
}

qboolean NET_GetLoopPacket(netsrc_t sock, netadr_t *net_from,
                           msg_t *net_message)
{
    loopback_t *loop;
    loopmsg_t *packet;
    int32_t packetIndex;
    qboolean hasPacket;

    loop = &net_loopbacks[sock];
    if (loop->send - loop->get > CODUO_NET_LOOPBACK_PACKET_COUNT) {
        loop->get = loop->send - CODUO_NET_LOOPBACK_PACKET_COUNT;
    }

    /* ORIGINAL_BINARY_BUG: this signed ordering rejects the first packets
     * after send wraps through INT32_MAX. See original-bugs/
     * coduomp-loopback-counter-wrap-drop.md. */
    hasPacket = loop->get < loop->send;
    if (hasPacket) {
        packetIndex = loop->get & (CODUO_NET_LOOPBACK_PACKET_COUNT - 1);
        ++loop->get;

        packet = &loop->msgs[packetIndex];
        /* ORIGINAL_BINARY_BUG: the stored signed length is copied without
         * checking the destination capacity. See original-bugs/
         * coduomp-network-packet-constructor-bounds.md. */
        memcpy(net_message->data, packet->data,
               (size_t)(uint32_t)packet->datalen);
        net_message->cursize = packet->datalen;

        memset(net_from, 0, sizeof(*net_from));
        net_from->type = CODUO_NA_LOOPBACK;
    }

    return hasPacket;
}
