#include <cassert>
#include <cstdlib>
#include <nng/nng.h>
#include <nng/protocol/pubsub0/pub.h>
#include <nng/protocol/pubsub0/sub.h>
#include <unistd.h>

#include "dp_common.h"

typedef struct DoomContext DoomContext;

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

typedef DEF_DOOM_STATE_FUNC(DoomStateFunc);

typedef struct DoomContext
{
    nng_socket pub;
    nng_socket sub;
    doomid_t id;
    DP_DoomState state;
    DoomStateFunc *f;
} DoomContext;

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);

static const useconds_t IdleSleep = 0;

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);

        usleep(IdleSleep);
        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 = {};

    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);
    usleep(IdleSleep);
    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);

        usleep(IdleSleep);
        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 = {};

    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);
    usleep(IdleSleep);
    return res;
}

int doom_loop(DoomContext *ctx)
{
    log_debug("doom(%d) started", ctx->id);

    int res = 0;

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

    return res;
}

int main(int argc, char *argv[])
{
    (void) argc;
    (void) argv;
#ifndef NDEBUG
    log_set_level(LOG_DEBUG);
#else
    log_set_level(LOG_INFO);
#endif

    log_info("doomsim starting");

    dp_nng_init_limits(1, 1, 1);

    auto pubSock = make_doom_pub(DoomUrlIpc);
    auto subSock = make_doom_sub(CtrlUrlIpc);

    DoomContext ctx
    {
        pubSock,
        subSock,
        getpid(),
        DP_DS_Ready,
        do_doom_ready,
    };

    int ret = doom_loop(&ctx);

    nng_close(pubSock);
    nng_close(subSock);

    return ret;
}
