// Emacs style mode select   -*- C++ -*-
//-----------------------------------------------------------------------------
//
// $Id:$
//
// Copyright (C) 1993-1996 by id Software, Inc.
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// $Log:$
//
// DESCRIPTION:
//	DOOM graphics stuff for X11, UNIX.
//
//-----------------------------------------------------------------------------

#include <signal.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#include "../doomstat.h"
#include "../i_system.h"
#include "../v_video.h"
#include "../m_argv.h"
#include "../d_main.h"
#include "../doomdef.h"
#include "../ib_video.h"

#include <assert.h>
#include <nng/nng.h>
#include <nng/protocol/pubsub0/pub.h>
#include <nng/protocol/pubsub0/sub.h>
#include <dp_common.h>
#include <unistd.h>
#include <SDL.h>

_Static_assert(sizeof(event_t) == sizeof(dp_doom_event_t),
    "Size mismatch between dooms event_t and doompannings dp_doom_event_t. Packing?");

_Static_assert(MAXEVENTS == DoomMaxEvents,
    "MAXEVENTS (from d_event.h) and DoomMaxEvents (from dp_common.h) are not equal!");

typedef struct DoomContext DoomContext;

#define DEF_DOOM_STATE_FUNC(fn) int fn(DoomContext *ctx)

typedef DEF_DOOM_STATE_FUNC(DoomStateFunc);

struct DoomContext
{
    nng_socket pub;
    nng_socket sub;
    doomid_t id;
    DP_DoomState state;
    u8 eventCount;
    dp_doom_event_t events[MAXEVENTS];
    DoomStateFunc *f;
};

static int publish_state(DoomContext *ctx)
{
    nng_msg *msg = NULL;
    int res = 0;

    if ((res = nng_msg_alloc(&msg, sizeof(MsgDoomState))))
        dp_nng_fatal("doom/nng_msg_alloc", res);

    MsgDoomState *dmsg = DP_NNG_BODY_AS(msg, MsgDoomState);
    dmsg->head.msgType = DP_MT_DoomState;
    dmsg->doomId = ctx->id;
    dmsg->doomState = ctx->state;

    res = nng_sendmsg(ctx->pub, msg, 0);

    return res;
}

DEF_DOOM_STATE_FUNC(do_doom_ready);
DEF_DOOM_STATE_FUNC(do_doom_running);

DEF_DOOM_STATE_FUNC(do_doom_ready)
{
    assert(ctx->state == DP_DS_Ready);

    int res = 0;

    if ((res = publish_state(ctx)))
        dp_nng_fatal("doom/publish_sate", res);

    nng_msg *msg = NULL;

    if ((res = dp_recv_new_msg_nonblock(ctx->sub, &msg)))
    {
        if (!dp_nng_is_timeout(res))
            dp_nng_fatal("doom/recvmsg", res);

        return 0;
    }

    log_trace("do_doom_ready received message of size %zu", nng_msg_len(msg));

    MessageBase *msgBase = DP_NNG_BODY_AS(msg, MessageBase);

    DP_DoomCommand cmd = DP_DC_Noop;

    if (msgBase->msgType == DP_MT_Command)
    {
        MsgCommand *msgCmd = DP_NNG_BODY_AS(msg, MsgCommand);

        if (msgCmd->doomId == ctx->id)
            cmd = msgCmd->cmd;
    }
    else if (msgBase->msgType == DP_MT_McstCommand)
    {
        MsgMcstCommand *msgCmd = DP_NNG_BODY_AS(msg, MsgMcstCommand);
        cmd = msgCmd->cmd;
    }

    if (cmd == DP_DC_RunDoom)
    {
        ctx->state = DP_DS_Running;
        ctx->f = do_doom_running;

        if ((res = publish_state(ctx)))
            dp_nng_fatal("doom/publish_sate", res);
    }
    else if (cmd == DP_DC_Endoom)
    {
        ctx->state = DP_DS_Endoom;

        if ((res = publish_state(ctx)))
            dp_nng_fatal("doom/publish_sate", res);
    }

    nng_msg_free(msg);
    return res;
}

DEF_DOOM_STATE_FUNC(do_doom_running)
{

    assert(ctx->state == DP_DS_Running);

    // Non-blocking receive of incoming messages.
    // Check if we should quit.
    // Handle incoming input.
    // Let doom render a new frame.
    // Publish the frame.

    int res = 0;
    nng_msg *msg = NULL;

    if ((res = dp_recv_new_msg_nonblock(ctx->sub, &msg)))
    {
        if (!dp_nng_is_timeout(res))
            dp_nng_fatal("doom/recvmsg", res);

        return 0;
    }

    log_trace("do_doom_running received message of size %zu", nng_msg_len(msg));

    MessageBase *msgBase = DP_NNG_BODY_AS(msg, MessageBase);

    DP_DoomCommand cmd = DP_DC_Noop;

    if (msgBase->msgType == DP_MT_Command)
    {
        MsgCommand *msgCmd = DP_NNG_BODY_AS(msg, MsgCommand);

        if (msgCmd->doomId == ctx->id)
            cmd = msgCmd->cmd;
    }
    else if (msgBase->msgType == DP_MT_McstCommand)
    {
        MsgMcstCommand *msgCmd = DP_NNG_BODY_AS(msg, MsgMcstCommand);
        cmd = msgCmd->cmd;
    }

    if (cmd == DP_DC_Endoom)
    {
        ctx->state = DP_DS_Endoom;

        if ((res = publish_state(ctx)))
            dp_nng_fatal("doom/publish_sate", res);
    }

    nng_msg_free(msg);
    return res;
}

static size_t g_frameBufferSize;
static uint8_t *g_frameBuffer;
static size_t g_pitch;
DoomContext g_doomContext;

//
// IB_StartTic
//
void IB_StartTic (void)
{
}

void IB_GetFramebuffer(unsigned char **pixels, size_t *pitch)
{
    *pixels = g_frameBuffer;
    *pitch = g_pitch;
}

void IB_FinishUpdate (void)
{
    DoomContext *ctx = &g_doomContext;

    assert(ctx->state == DP_DS_Running);

    static const u64 MaxReceiveTime_ms = 10;
    u64 tStart = SDL_GetTicks64();

    while (true)
    {
        int res = 0;
        nng_msg *msg = NULL;

        if (SDL_GetTicks64() - tStart >= MaxReceiveTime_ms)
            break;

        if ((res = dp_recv_new_msg_nonblock(ctx->sub, &msg)))
        {
            if (!dp_nng_is_timeout(res))
                dp_nng_fatal("doom/recvmsg", res);
            break; // timeout
        }
        else
        {
            MessageBase *msgBase = DP_NNG_BODY_AS(msg, MessageBase);

            DP_DoomCommand cmd = DP_DC_Noop;

            if (msgBase->msgType == DP_MT_Command)
            {
                MsgCommand *msgCmd = DP_NNG_BODY_AS(msg, MsgCommand);

                if (msgCmd->doomId == ctx->id)
                    cmd = msgCmd->cmd;
            }
            else if (msgBase->msgType == DP_MT_McstCommand)
            {
                MsgMcstCommand *msgCmd = DP_NNG_BODY_AS(msg, MsgMcstCommand);
                cmd = msgCmd->cmd;
            }
            else if (msgBase->msgType == DP_MT_Inputs)
            {
                MsgInputs *msgInputs = DP_NNG_BODY_AS(msg, MsgInputs);
                for (unsigned eventIndex=0; eventIndex<msgInputs->eventCount; ++eventIndex)
                {
                    // Can do this cast. _Static_assert() at the top gives a bit
                    // of ensurance.
                    D_PostEvent((event_t *) msgInputs->events + eventIndex);
                }
            }

            nng_msg_free(msg);

            if (cmd == DP_DC_Endoom)
            {
                log_info("Received Endoom from controller, quitting");
                ctx->state = DP_DS_Endoom;

                if ((res = publish_state(ctx)))
                    dp_nng_fatal("doom/publish_sate", res);

                I_Quit();
                return;
            }
        }
    }

    {
        int res = 0;
        nng_msg *msg = NULL;

        if ((res = nng_msg_alloc(&msg, sizeof(MsgDoomFrame))))
            dp_nng_fatal("doom/nng_msg_alloc", res);

        MsgDoomFrame *dpmsg = DP_NNG_BODY_AS(msg, MsgDoomFrame);
        dpmsg->head.msgType = DP_MT_DoomFrame;
        dpmsg->doomId = ctx->id;
        assert(g_frameBufferSize == DoomFrameSize);
        memcpy(dpmsg->frame, g_frameBuffer, g_frameBufferSize);

        if ((res = nng_sendmsg(ctx->pub, msg, 0)))
            dp_nng_fatal("ctrl/sendmsg", res);

        log_trace("doom(%d) published frame", ctx->id);
    }
}

void IB_GetColor(unsigned char *bytes, unsigned char red, unsigned char green, unsigned char blue)
{
#if DoomBytesPerPixel == 4
    bytes[0] = blue;
    bytes[1] = green;
    bytes[2] = red;
    bytes[3] = 0xff; // alpha?
#else
#error Unhandled DoomBytesPerPixel value. How to IB_GetColor?
#endif
}

static void I_Quit_Wrapper(int dummy)
{
    (void)dummy;

    log_info("Caught SIGINT, quitting");
    I_Quit();
}

void IB_InitGraphics(const char *title, size_t screen_width, size_t screen_height, size_t *bytes_per_pixel)
{
    (void) title;
    log_set_level(LOG_DEBUG);

    g_frameBufferSize = screen_width * screen_height * DoomBytesPerPixel;
    assert(g_frameBufferSize == DoomFrameSize);
    g_frameBuffer = malloc(g_frameBufferSize);
    g_pitch = screen_width * DoomBytesPerPixel;
    *bytes_per_pixel = DoomBytesPerPixel;

    log_debug("IB_InitGraphics: w=%zu, h=%zu -> bytesPerPixel=%zu, frameBufferSize=%zu",
        screen_width, screen_height, *bytes_per_pixel, g_frameBufferSize);

    signal(SIGINT, I_Quit_Wrapper);

    dp_nng_init_limits(1, 1, 1);

    int p = M_CheckParm("-dp-host");

    if (p)
    {
        // use tcp transport with the host given on the command line
        char url[1024];
        snprintf(url, 1024, "tcp://%s:%d", myargv[p+1], DoomPort);
        g_doomContext.pub = make_doom_pub(url);
        snprintf(url, 1024, "tcp://%s:%d", myargv[p+1], CtrlPort);
        g_doomContext.sub = make_doom_sub(url);
    }
    else
    {
        // use unix domain sockets
        g_doomContext.pub = make_doom_pub(DoomUrlIpc);
        g_doomContext.sub = make_doom_sub(CtrlUrlIpc);
    }

    g_doomContext.id = getpid();
    g_doomContext.state = DP_DS_Ready;
    g_doomContext.f = do_doom_ready;

    DoomContext *ctx = &g_doomContext;

    log_info("waiting for go from doompanning");

    while (true)
    {
        DP_DoomState prevState = ctx->state;
        ctx->f(ctx);
        if (prevState != ctx->state)
        {
            log_info("transition %s -> %s",
                doomstate_to_string(prevState), doomstate_to_string(ctx->state));
        }

        if (ctx->state != DP_DS_Endoom && ctx->state != DP_DS_Running)
            usleep(10 * 1000);
        else
            break;
    }

    if (ctx->state == DP_DS_Endoom)
    {
        log_info("Received Endoom from controller, quitting");
        I_Quit();
    }
}

void IB_ShutdownGraphics(void)
{
    nng_close(g_doomContext.pub);
    nng_close(g_doomContext.sub);

    free(g_frameBuffer);
}

void IB_GrabMouse(boolean grab)
{
    (void) grab;
}
