/*
 * External control interface
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

/*
 * This communicates with the outside world over a trivial, binary
 * protocol.  Both channels consist of a stream of packets.  The
 * stream consists of repeated tuples of the length of the packet
 * encoded as a be32 followed by the packet content.  The packet
 * format consists of a four character command string followed by the
 * argument to that command.  Everything is encoded using the QEMUFile
 * abstraction.  Communication is asynchronous, and has no built-in
 * notion of "responses" to commands.
 *
 * Recognized commands:
 * * run(bp : int64_t) - Run until TSC = bp and then send a "bp"
 *   response.  At most one breakpoint may be active at a time.
 * * quit() - Shutdown the VM and quit immediately.
 * * pkt(id : int64_t, size : int32_t, packet : uint8_t[size]) -
 *   Enqueue a packet in the NIC's incoming buffer.  Responds with a
 *   pok(tsc, id) or a perr(tsc, id)
 * * save() - Save the current VM.  When the save completes, a "save"
 *   is sent in response.
 * * load (handle : ?) - Load the VM identified by the handle.  When
 *   the load completes, a "load" is sent in response.
 *
 * Produced commands:
 * * bp(tsc : int64_t) - A breakpoint at time tsc has been reached.
 * * pkt(tsc : int64_t, mac : uint8_t[6], size : int32_t, packet :
 *   uint8_t[size]) - A packet was generated by the NIC at time tsc.
 *   This is not a response.  It just happens.
 * * pok(tsc : int64_t, id : int64_t) - Packet was delivered to the
 *   NIC successfully at time tsc.  id is the ID of the pkt command
 *   that initiated this queuing.
 * * perr(tsc : int64_t, id : int64_t) - Packet was not delivered to
 *   the NIC at time tsc.  This can occur because the NIC's buffer is
 *   full, or because the NIC is disabled (this happens, for example,
 *   when the interface is down in the ifconfig sense)
 * * save(handle : ?) - A requested save operation has completed.  The
 *   handle can be used as an argument to "load" to later restore.
 * * load() - A load operation has completed.
 */

#include "vl.h"
#include <assert.h>

static const int MAX_XCTL_CMD = 1024*1024*10;
static const int MAX_PACKET_SIZE = 1514;
static const int DEBUG_XCTL = 0;
static const int DEBUG_XCTL_CMDS = 0;
static const int DEBUG_XCTL_NET = 0;

#define CMD(a,b,c,d) (((a) << 24) | ((b) << 16) | ((c) << 8) | (d))

typedef struct xctl_state_t
{
    CharDriverState *hd;
    QEMUFile *buf;
    QEMUFile *arg_buf;
    QEMUFile *temp_buf;
    int cmd_len;
    int reading_cmd; // 0 if reading command size, 1 if reading
                     // commnad
    QEMUTimer *xctl_bp_timer;

    // Net-related
    // XXX(amdragon): What happens with multiple net interfaces?
    IOCanRWHandler *packet_can_read;
    IOReadHandler *packet_read;
    void *packet_opaque;

    // savevm-related
    int32_t save_extra_size;
    char *save_extra;
} xctl_state_t;

static void xctl_send(xctl_state_t *xs, uint32_t cmd)
{
    if (DEBUG_XCTL_CMDS)
        fprintf(stderr, "xctl -> %c%c%c%c\n",
                (cmd >> 24) & 0xFF, (cmd >> 16) & 0xFF,
                (cmd >> 8) & 0xFF, cmd & 0xFF);

    // Write length header
    qemu_fseek(xs->arg_buf, 0, SEEK_END);
    qemu_fseek(xs->temp_buf, 0, SEEK_SET);
    qemu_put_be32(xs->temp_buf, qemu_ftell(xs->arg_buf) + 4);

    // Write command
    qemu_put_byte(xs->temp_buf, (cmd >> 24) & 0xFF);
    qemu_put_byte(xs->temp_buf, (cmd >> 16) & 0xFF);
    qemu_put_byte(xs->temp_buf, (cmd >> 8) & 0xFF);
    qemu_put_byte(xs->temp_buf, cmd & 0xFF);
    qemu_chr_write_memfile(xs->hd, xs->temp_buf);

    // Write arguments
    qemu_fseek(xs->arg_buf, 0, SEEK_SET);
    qemu_chr_write_memfile(xs->hd, xs->arg_buf);
    qemu_ftruncate(xs->arg_buf, 0);

    // XXX(amdragon): I don't think this deals with partial sends
    // correctly
}

static void xctl_bp_timer_cb(void *opaque)
{
    xctl_state_t *xs = (xctl_state_t*)opaque;
    int64_t tsc = qemu_get_clock(vm_clock);

    if (DEBUG_XCTL)
        fprintf(stderr, "xctl: TSC breakpoint timer at %lld\n", tsc);

    vm_stop(0);
    // Send breakpoint reached
    qemu_put_be64(xs->arg_buf, tsc);
    xctl_send(xs, CMD('b','p',' ',' '));
}

static void xctl_process_command(xctl_state_t *xs)
{
    uint32_t cmd = CMD(qemu_get_byte(xs->buf), qemu_get_byte(xs->buf),
                       qemu_get_byte(xs->buf), qemu_get_byte(xs->buf));

    switch (cmd) {
    case CMD('r','u','n',' '):
        {
            int64_t tsc = qemu_get_be64(xs->buf);

            if (DEBUG_XCTL_CMDS)
                fprintf(stderr, "xctl <- run(%lld)\n", tsc);

            if (tsc == 0)
                qemu_del_timer(xs->xctl_bp_timer);
            else
                qemu_mod_timer(xs->xctl_bp_timer, tsc);
            vm_start();
        }
        break;
    case CMD('q','u','i','t'):
        {
            if (DEBUG_XCTL_CMDS)
                fprintf(stderr, "xctl <- quit()\n");

            qemu_system_shutdown_request();
            // We have to make sure the VM is running or this will go
            // unnoticed
            vm_start();
        }
        break;
    case CMD('p','k','t',' '):
        {
            int64_t id = qemu_get_be64(xs->buf);
            int32_t size = qemu_get_be32(xs->buf);
            char buf[MAX_PACKET_SIZE];
            int64_t tsc = qemu_get_clock(vm_clock);
            
            if (DEBUG_XCTL_CMDS)
                fprintf(stderr, "xctl <- pkt(%lld, [%d])\n", id, size);

            if (size > MAX_PACKET_SIZE) {
                fprintf(stderr, "xctl packet too big (%d > %d)\n",
                        size, MAX_PACKET_SIZE);
                exit(1);
            }

            if (qemu_get_buffer(xs->buf, buf, size) != size) {
                fprintf(stderr, "xctl couldn't read whole packet\n");
                exit(1);
            }

            if (DEBUG_XCTL_NET) {
                fprintf(stderr, "xctl input packet:\n");
                hex_dump(stderr, buf, size);
            }

            if (!xs->packet_can_read || !xs->packet_read) {
                fprintf(stderr, "xctl packet received, but no iface bound\n");
                exit(1);
            }

            qemu_put_be64(xs->arg_buf, tsc);
            qemu_put_be64(xs->arg_buf, id);
            if (xs->packet_can_read(xs->packet_opaque) >= size) {
                xs->packet_read(xs->packet_opaque, buf, size);
                xctl_send(xs, CMD('p','o','k',' '));
            } else {
                xctl_send(xs, CMD('p','e','r','r'));
            }
        }
        break;
    case CMD('s','a','v','e'):
        {
            int32_t incremental = qemu_get_be32(xs->buf);
            int32_t extra_size = qemu_get_be32(xs->buf);

            if (extra_size > 0) {
                xs->save_extra = qemu_malloc(extra_size);
                if (!xs->save_extra) {
                    fprintf(stderr,
                            "Failed to allocate %d bytes for save extra\n",
                            extra_size);
                    exit(1);
                }

                if (qemu_get_buffer(xs->buf,
                                    xs->save_extra,
                                    extra_size) != extra_size) {
                    fprintf(stderr, "Failed to read %d bytes for save extra\n",
                            extra_size);
                    exit(1);
                }
            } else {
                xs->save_extra = NULL;
            }

            xs->save_extra_size = extra_size;

            snapdesc desc;
            savevm(&desc, incremental);

            if (xs->save_extra)
                qemu_free(xs->save_extra);
            xs->save_extra = NULL;
            xs->save_extra_size = -1;

            qemu_put_be32(xs->arg_buf, sizeof(snapdesc));
            qemu_put_buffer(xs->arg_buf, (char*)&desc, sizeof(snapdesc));
            xctl_send(xs, CMD('s','a','v','e'));
        }
        break;
    case CMD('l','o','a','d'):
        {
            snapdesc desc;
            int32_t saveid_len = qemu_get_be32(xs->buf);
            assert(saveid_len == sizeof(snapdesc));
            qemu_get_buffer(xs->buf, (char*)&desc, saveid_len);

            loadvm(&desc);

            int64_t tsc = qemu_get_clock(vm_clock);
            qemu_put_be64(xs->arg_buf, tsc);
            qemu_put_be32(xs->arg_buf, xs->save_extra_size);
            if (xs->save_extra_size > 0)
                qemu_put_buffer(xs->arg_buf,
                                xs->save_extra, xs->save_extra_size);
            xctl_send(xs, CMD('l','o','a','d'));

            if (xs->save_extra)
                qemu_free(xs->save_extra);
            xs->save_extra = NULL;
            xs->save_extra_size = -1;
        }
        break;
    }
}

static void xctl_read_char(xctl_state_t *xs, uint8_t c)
{
    qemu_put_byte(xs->buf, c);

    if (xs->reading_cmd == 0) {
        if (qemu_ftell(xs->buf) == 4) {
            // The entire command size is in the buffer
            qemu_fseek(xs->buf, 0, SEEK_SET);
            xs->cmd_len = qemu_get_be32(xs->buf);
            qemu_fseek(xs->buf, 0, SEEK_SET);
            qemu_ftruncate(xs->buf, 0);
            xs->reading_cmd = 1;
            if (xs->cmd_len > MAX_XCTL_CMD) {
                fprintf(stderr, "xctl command length too large (%d > %d)\n",
                        xs->cmd_len, MAX_XCTL_CMD);
                exit(1);
            }
        }
    } else {
        if (qemu_ftell(xs->buf) == xs->cmd_len) {
            // The entire command is in the buffer
            qemu_fseek(xs->buf, 0, SEEK_SET);
            xctl_process_command(xs);
            qemu_fseek(xs->buf, 0, SEEK_SET);
            qemu_ftruncate(xs->buf, 0);
            xs->reading_cmd = 0;
        }
    }
}

static int xctl_can_read(void *opaque)
{
    return 128;
}

static void xctl_read(void *opaque, const uint8_t *buf, int size)
{
    xctl_state_t *xs = (xctl_state_t*)opaque;
    int i;

    if (size == 0) {
        // Control process died
        fprintf(stderr, "xctl: Control process died.  Shutting down\n");
        qemu_system_shutdown_request();
        vm_start();
        return;
    }

    for (i = 0; i < size; ++i)
        xctl_read_char(xs, buf[i]);
}

static void xctl_save(QEMUFile *f, void *opaque)
{
    xctl_state_t *xs = (xctl_state_t*)opaque;

    if (xs->save_extra_size >= 0) {
        qemu_put_be32(f, xs->save_extra_size);
        qemu_put_buffer(f, xs->save_extra, xs->save_extra_size);
    } else {
        fprintf(stderr,
                "xctl: WARNING xctl_save without save extra buffer\n");
        qemu_put_be32(f, -1);
    }
}

static int xctl_load(QEMUFile *f, void *opaque, int version_id)
{
    xctl_state_t *xs = (xctl_state_t*)opaque;

    if (version_id != 0)
        return -EINVAL;

    int32_t extra_size = qemu_get_be32(f);
    if (extra_size > 0) {
        xs->save_extra = qemu_malloc(extra_size);
        if (!xs->save_extra) {
            fprintf(stderr,
                    "Failed to allocate %d bytes for save extra\n",
                    extra_size);
            exit(1);
        }
        if (qemu_get_buffer(f, xs->save_extra, extra_size) != extra_size) {
            fprintf(stderr,
                    "Failed to read %d bytes for save extra\n",
                    extra_size);
            exit(1);
        }
    }
    xs->save_extra_size = extra_size;

    return 0;
}

void *xctl_init(CharDriverState *hd)
{
    xctl_state_t *xs;

    xs = qemu_mallocz(sizeof(xctl_state_t));
    if (!xs) {
        fprintf(stderr, "Could not allocate xctl state\n");
        exit(1);
    }

    xs->hd = hd;
    qemu_chr_add_read_handler(hd, xctl_can_read, xctl_read, xs);

    xs->buf = qemu_fmemcreate(0);
    xs->arg_buf = qemu_fmemcreate(0);
    xs->temp_buf = qemu_fmemcreate(10);

    assert(vm_clock);
    xs->xctl_bp_timer = qemu_new_timer(vm_clock, xctl_bp_timer_cb, xs);

    register_savevm("xctl", 0, 0, xctl_save, xctl_load, xs);

    return xs;
}

static void xctl_send_packet(NetDriverState *nd, const uint8_t *buf, int size)
{
    // A packet is coming out of the network card
    xctl_state_t *xs = (xctl_state_t*)nd->opaque;
    int64_t tsc = qemu_get_clock(vm_clock);

    if (DEBUG_XCTL_NET) {
        fprintf(stderr, "xctl output packet at %lld:\n", tsc);
        hex_dump(stderr, buf, size);
    }

    if (xs == NULL) {
        fprintf(stderr, "net driver not yet bound to xctl\n");
        exit(1);
    }

    // Relay packet to the outside world
    int i;
    qemu_put_be64(xs->arg_buf, tsc);
    for (i = 0; i < 6; ++i)
        qemu_put_byte(xs->arg_buf, nd->macaddr[i]);
    qemu_put_be32(xs->arg_buf, size);
    qemu_put_buffer(xs->arg_buf, buf, size);
    xctl_send(xs, CMD('p','k','t',' '));
}

static void xctl_add_read_packet(NetDriverState *nd, 
                                 IOCanRWHandler *fd_can_read, 
                                 IOReadHandler *fd_read, void *opaque)
{
    xctl_state_t *xs = (xctl_state_t*)nd->opaque;

    if (xs == NULL) {
        fprintf(stderr, "net driver not yet bound to xctl\n");
        exit(1);
    }

    xs->packet_can_read = fd_can_read;
    xs->packet_read = fd_read;
    xs->packet_opaque = opaque;
}

void net_xctl_init(NetDriverState *nd)
{
    nd->send_packet = xctl_send_packet;
    nd->add_read_packet = xctl_add_read_packet;
    pstrcpy(nd->ifname, sizeof(nd->ifname), "xctlnet");
}

void net_xctl_bind(NetDriverState *nd, void *xctl)
{
    nd->opaque = xctl;
}
