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

#include "../core_command/cmd_private.h"
#include "../networking/netchan_private.h"
#include "../server/server_private.h"
#include "core_runtime_private.h"

enum {
    COM_JOURNAL_RECORD_BYTES = sizeof(coduo_journal_event_record_t),
    COM_JOURNAL_MODE_RECORD = 1,
    COM_JOURNAL_MODE_REPLAY = 2,
    COM_PUSH_EVENT_QUEUE_MASK = CODUO_SYS_EVENT_QUEUE_COUNT - 1,
    COM_PUSH_EVENT_QUEUE_LIMIT = CODUO_SYS_EVENT_QUEUE_COUNT,
    COM_EVENT_NONE = 0,
    COM_EVENT_KEY = 1,
    COM_EVENT_CHAR = 2,
    COM_EVENT_MOUSE = 3,
    COM_EVENT_JOYSTICK_AXIS = 4,
    COM_EVENT_CONSOLE = 5,
    COM_EVENT_PACKET = 6,
    COM_PACKET_EVENT_ADDRESS_BYTES = CODUO_NETADR_SIZE
};

static sysEvent_t com_pushEventQueue[CODUO_SYS_EVENT_QUEUE_COUNT];
static int32_t com_pushEventHead;
static int32_t com_pushEventTail;
static int32_t com_pushEventOverflowWarned;

/* NOT_FROM_ORIGINAL_SOURCE: host-pointer reconstruction from journal records.
 * The original i386 replay copies the sixth 32-bit word back into the pointer
 * member before replacing it when payloadLength is nonzero. Preserve that
 * exact operation for 32-bit builds. A 64-bit host cannot reconstruct a host
 * pointer from the fixed journal word, so leave it null; payload-bearing events
 * replace it with the separately allocated/read payload below. This is the
 * necessary native-width deviation from the original machine code. */
static void Com_CopyJournalEvent(sysEvent_t *event,
                                 const coduo_journal_event_record_t *record)
{
    event->time = record->time;
    event->type = record->type;
    event->value = record->value;
    event->value2 = record->value2;
    event->payloadLength = record->payloadLength;
    event->payload = NULL;
#if UINTPTR_MAX == UINT32_MAX
    event->payload = (void *)(uintptr_t)record->payload;
#endif
}

/* NOT_FROM_ORIGINAL_SOURCE: fixed-width journal serialization for native
 * pointers. The original i386 machine code writes the complete 0x18-byte
 * sysEvent_t, whose last word is the payload pointer. On 64-bit hosts that
 * pointer is wider and begins after alignment padding, while journal.dat must
 * retain the original six-word format. Payload bytes are serialized separately;
 * the pointer word is meaningful only to the original-width record operation. */
static void Com_CopyEventToJournalRecord(
    coduo_journal_event_record_t *record, const sysEvent_t *event)
{
    record->time = event->time;
    record->type = event->type;
    record->value = event->value;
    record->value2 = event->value2;
    record->payloadLength = event->payloadLength;
    record->payload = 0;
#if UINTPTR_MAX == UINT32_MAX
    record->payload = (uint32_t)(uintptr_t)event->payload;
#endif
}

sysEvent_t Com_GetRealEvent(void)
{
    coduo_journal_event_record_t record;
    sysEvent_t localEvent;
    int32_t bytes;

    if (com_journal->integer == COM_JOURNAL_MODE_REPLAY) {
        bytes = FS_Read(&record, COM_JOURNAL_RECORD_BYTES, com_journalFile);
        if (bytes != COM_JOURNAL_RECORD_BYTES) {
            Com_Error(0, "EXE_ERR_JOURNAL_FILE_READ");
        }

        Com_CopyJournalEvent(&localEvent, &record);
        /* ORIGINAL_BINARY_BUG: the stock i386 path trusts every nonzero
         * signed payload length as an allocation/read count and retains the
         * serialized process pointer when the length is zero.  The latter is
         * necessarily nulled by the existing native64 journal adapter.  See
         * original-bugs/coduomp-event-journal-unchecked-record.md. */
        if (localEvent.payloadLength != 0) {
            /* NATIVE64_ADAPTATION: stock passes one 32-bit size word. */
            localEvent.payload =
                Z_Malloc((uint32_t)localEvent.payloadLength);
            bytes = FS_Read(localEvent.payload, localEvent.payloadLength,
                            com_journalFile);
            if (bytes != localEvent.payloadLength) {
                Com_Error(0, "EXE_ERR_JOURNAL_FILE_READ");
            }
        }
    } else {
        localEvent = Sys_GetEvent();
        if (com_journal->integer == COM_JOURNAL_MODE_RECORD) {
            Com_CopyEventToJournalRecord(&record, &localEvent);
            bytes = FS_Write(&record, COM_JOURNAL_RECORD_BYTES,
                             com_journalFile);
            if (bytes != COM_JOURNAL_RECORD_BYTES) {
                Com_Error(0, "EXE_ERR_JOURNAL_FILE_WRITE");
            }
            if (localEvent.payloadLength != 0) {
                bytes = FS_Write(localEvent.payload, localEvent.payloadLength,
                                 com_journalFile);
                if (bytes != localEvent.payloadLength) {
                    Com_Error(0, "EXE_ERR_JOURNAL_FILE_WRITE");
                }
            }
        }
    }

    return localEvent;
}

void Com_InitPushEvent(void)
{
    memset(com_pushEventQueue, 0, sizeof(com_pushEventQueue));
    com_pushEventHead = 0;
    com_pushEventTail = 0;
}

void Com_PushEvent(const sysEvent_t *event)
{
    sysEvent_t *slot;

    slot = &com_pushEventQueue[com_pushEventHead & COM_PUSH_EVENT_QUEUE_MASK];
    if (com_pushEventHead - com_pushEventTail < COM_PUSH_EVENT_QUEUE_LIMIT) {
        com_pushEventOverflowWarned = qfalse;
    } else {
        if (com_pushEventOverflowWarned == qfalse) {
            com_pushEventOverflowWarned = qtrue;
            Com_Printf("WARNING: Com_PushEvent overflow\n");
        }
        if (slot->payload != NULL) {
            Z_Free(slot->payload);
        }
        ++com_pushEventTail;
    }

    *slot = *event;
    /* ORIGINAL_BINARY_BUG: the signed head/tail ordering fails when head
     * crosses INT32_MAX even though the low-byte ring index keeps wrapping.
     * See original-bugs/engine-push-event-counter-wrap.md. */
    ++com_pushEventHead;
}

void Com_FlushEventQueue(void)
{
    while (com_pushEventTail < com_pushEventHead) {
        sysEvent_t event =
            com_pushEventQueue[com_pushEventTail & COM_PUSH_EVENT_QUEUE_MASK];

        ++com_pushEventTail;
        if (event.payload != NULL) {
            Z_Free(event.payload);
        }
    }
}

sysEvent_t Com_GetEvent(void)
{
    if (com_pushEventTail < com_pushEventHead) {
        ++com_pushEventTail;
        return com_pushEventQueue[(com_pushEventTail - 1) &
                                  COM_PUSH_EVENT_QUEUE_MASK];
    }

    return Com_GetRealEvent();
}

void Com_RunAndTimeServerPacket(netadr_t *from, msg_t *msg)
{
    int32_t startTime;
    int32_t endTime;

    startTime = 0;
    if (host_cvar_com_speeds_pointer_slot->integer != 0) {
        startTime = Sys_Milliseconds();
    }

    SV_PacketEvent(*from, msg);

    if (host_cvar_com_speeds_pointer_slot->integer != 0) {
        endTime = Sys_Milliseconds();
        if (host_cvar_com_speeds_pointer_slot->integer == 3) {
            Com_Printf("SV_PacketEvent time: %i\n", endTime - startTime);
        }
    }
}

int32_t Com_EventLoop(void)
{
    uint8_t packetBuffer[CODUO_MAX_MSGLEN];
    netadr_t from;
    msg_t msg;
    sysEvent_t event;

    memset(packetBuffer, 0, sizeof(packetBuffer));
    MSG_Init(&msg, packetBuffer, sizeof(packetBuffer));

    for (;;) {
        event = Com_GetEvent();
        if (event.type == COM_EVENT_NONE) {
            break;
        }

        switch (event.type) {
        case COM_EVENT_KEY:
            CL_KeyEvent(event.value, event.value2, event.time);
            break;
        case COM_EVENT_CHAR:
            CL_CharEvent(event.value);
            break;
        case COM_EVENT_MOUSE:
            CL_MouseEvent(event.value, event.value2, event.time);
            break;
        case COM_EVENT_JOYSTICK_AXIS:
            CL_JoystickEvent(event.value, event.value2, event.time);
            break;
        case COM_EVENT_CONSOLE:
            /* ORIGINAL_BINARY_BUG: stock trusts the payload as a terminated
             * string.  Journal replay can supply a stale serialized pointer;
             * see original-bugs/coduomp-event-journal-unchecked-record.md. */
            Cbuf_AddText(event.payload);
            Z_Free(event.payload);
            Cbuf_AddText("\n");
            break;
        case COM_EVENT_PACKET:
            /* ORIGINAL_BINARY_BUG: stock copies the complete address before
             * proving payloadLength covers it; preserved per the report above. */
            memcpy(&from, event.payload, sizeof(from));
            msg.cursize = event.payloadLength - COM_PACKET_EVENT_ADDRESS_BYTES;
            if ((uint32_t)msg.maxsize < (uint32_t)msg.cursize) {
                Z_Free(event.payload);
                Com_Printf("Com_EventLoop: oversize packet\n");
            } else {
                memcpy(msg.data, (uint8_t *)event.payload +
                                     COM_PACKET_EVENT_ADDRESS_BYTES,
                       (size_t)msg.cursize);
                Z_Free(event.payload);
                if (host_cvar_sv_running_pointer_slot->integer == 0) {
                    CL_PacketEvent(from, &msg, event.time);
                } else {
                    Com_RunAndTimeServerPacket(&from, &msg);
                }
            }
            break;
        default:
            Com_Error(0, "\x15" "Com_EventLoop: bad event type %i",
                      event.type);
        }
    }

    while (NET_GetLoopPacket(NS_CLIENT, &from, &msg) != qfalse) {
        CL_PacketEvent(from, &msg, event.time);
    }

    while (NET_GetLoopPacket(NS_SERVER, &from, &msg) != qfalse) {
        if (host_cvar_sv_running_pointer_slot->integer != 0) {
            Com_RunAndTimeServerPacket(&from, &msg);
        }
    }

    return event.time;
}

int32_t Com_Milliseconds(void)
{
    sysEvent_t event;

    do {
        event = Com_GetRealEvent();
        if (event.type != COM_EVENT_NONE) {
            Com_PushEvent(&event);
        }
    } while (event.type != COM_EVENT_NONE);

    return event.time;
}

void Com_PumpMessageLoop(void)
{
    sysEvent_t event;

    for (;;) {
        event = Com_GetRealEvent();
        if (event.type == COM_EVENT_NONE) {
            break;
        }
        Com_PushEvent(&event);
    }
}
