/*
 * This work is licensed under the terms of the GNU GPL, version 2 or
 * (at your option) any later version.  See the COPYING file in the
 * top-level directory.
 */

#include "qemu/osdep.h"
#include "qapi/error.h"
#include "qemu/iov.h"
#include "qemu/module.h"
#include "trace.h"

#include "hw/virtio/virtio.h"
#include "hw/qdev-properties.h"
#include "hw/virtio/virtio-input.h"

#include "standard-headers/linux/input.h"

#define VIRTIO_INPUT_VM_VERSION 1

/* ----------------------------------------------------------------- */

void virtio_input_send(VirtIOInput *vinput, virtio_input_event *event)
{
    VirtQueueElement *elem;
    int i, len;

    if (!vinput->active) {
        return;
    }

    /* queue up events ... */
    if (vinput->qindex == vinput->qsize) {
        vinput->qsize++;
        vinput->queue = g_realloc(vinput->queue, vinput->qsize *
                                  sizeof(vinput->queue[0]));
    }
    vinput->queue[vinput->qindex++].event = *event;

    /* ... until we see a report sync ... */
    if (event->type != cpu_to_le16(EV_SYN) ||
        event->code != cpu_to_le16(SYN_REPORT)) {
        return;
    }

    /* ... then check available space ... */
    for (i = 0; i < vinput->qindex; i++) {
        elem = virtqueue_pop(vinput->evt, sizeof(VirtQueueElement));
        if (!elem) {
            while (--i >= 0) {
                virtqueue_unpop(vinput->evt, vinput->queue[i].elem, 0);
            }
            vinput->qindex = 0;
            trace_virtio_input_queue_full();
            return;
        }
        vinput->queue[i].elem = elem;
    }

    /* ... and finally pass them to the guest */
    for (i = 0; i < vinput->qindex; i++) {
        elem = vinput->queue[i].elem;
        len = iov_from_buf(elem->in_sg, elem->in_num,
                           0, &vinput->queue[i].event, sizeof(virtio_input_event));
        virtqueue_push(vinput->evt, elem, len);
        g_free(elem);
    }
    virtio_notify(VIRTIO_DEVICE(vinput), vinput->evt);
    vinput->qindex = 0;
}

static void virtio_input_handle_evt(VirtIODevice *vdev, VirtQueue *vq)
{
    /* nothing */
}

static void virtio_input_handle_sts(VirtIODevice *vdev, VirtQueue *vq)
{
    VirtIOInputClass *vic = VIRTIO_INPUT_GET_CLASS(vdev);
    VirtIOInput *vinput = VIRTIO_INPUT(vdev);
    virtio_input_event event;
    VirtQueueElement *elem;
    int len;

    for (;;) {
        elem = virtqueue_pop(vinput->sts, sizeof(VirtQueueElement));
        if (!elem) {
            break;
        }

        memset(&event, 0, sizeof(event));
        len = iov_to_buf(elem->out_sg, elem->out_num,
                         0, &event, sizeof(event));
        if (vic->handle_status) {
            vic->handle_status(vinput, &event);
        }
        virtqueue_push(vinput->sts, elem, len);
        g_free(elem);
    }
    virtio_notify(vdev, vinput->sts);
}

virtio_input_config *virtio_input_find_config(VirtIOInput *vinput,
                                              uint8_t select,
                                              uint8_t subsel)
{
    VirtIOInputConfig *cfg;

    QTAILQ_FOREACH(cfg, &vinput->cfg_list, node) {
        if (select == cfg->config.select &&
            subsel == cfg->config.subsel) {
            return &cfg->config;
        }
    }
    return NULL;
}

void virtio_input_add_config(VirtIOInput *vinput,
                             virtio_input_config *config)
{
    VirtIOInputConfig *cfg;

    if (virtio_input_find_config(vinput, config->select, config->subsel)) {
        /* should not happen */
        fprintf(stderr, "%s: duplicate config: %d/%d\n",
                __func__, config->select, config->subsel);
        abort();
    }

    cfg = g_new0(VirtIOInputConfig, 1);
    cfg->config = *config;
    QTAILQ_INSERT_TAIL(&vinput->cfg_list, cfg, node);
}

void virtio_input_init_config(VirtIOInput *vinput,
                              virtio_input_config *config)
{
    int i = 0;

    QTAILQ_INIT(&vinput->cfg_list);
    while (config[i].select) {
        virtio_input_add_config(vinput, config + i);
        i++;
    }
}

void virtio_input_idstr_config(VirtIOInput *vinput,
                               uint8_t select, const char *string)
{
    virtio_input_config id;

    if (!string) {
        return;
    }
    memset(&id, 0, sizeof(id));
    id.select = select;
    id.size = snprintf(id.u.string, sizeof(id.u.string), "%s", string);
    virtio_input_add_config(vinput, &id);
}

static void virtio_input_get_config(VirtIODevice *vdev, uint8_t *config_data)
{
    VirtIOInput *vinput = VIRTIO_INPUT(vdev);
    virtio_input_config *config;

    config = virtio_input_find_config(vinput, vinput->cfg_select,
                                      vinput->cfg_subsel);
    if (config) {
        memcpy(config_data, config, vinput->cfg_size);
    } else {
        memset(config_data, 0, vinput->cfg_size);
    }
}

static void virtio_input_set_config(VirtIODevice *vdev,
                                    const uint8_t *config_data)
{
    VirtIOInput *vinput = VIRTIO_INPUT(vdev);
    virtio_input_config *config = (virtio_input_config *)config_data;

    vinput->cfg_select = config->select;
    vinput->cfg_subsel = config->subsel;
    virtio_notify_config(vdev);
}

static uint64_t virtio_input_get_features(VirtIODevice *vdev, uint64_t f,
                                          Error **errp)
{
    return f;
}

static void virtio_input_set_status(VirtIODevice *vdev, uint8_t val)
{
    VirtIOInputClass *vic = VIRTIO_INPUT_GET_CLASS(vdev);
    VirtIOInput *vinput = VIRTIO_INPUT(vdev);

    if (val & VIRTIO_CONFIG_S_DRIVER_OK) {
        if (!vinput->active) {
            vinput->active = true;
            if (vic->change_active) {
                vic->change_active(vinput);
            }
        }
    }
}

static void virtio_input_reset(VirtIODevice *vdev)
{
    VirtIOInputClass *vic = VIRTIO_INPUT_GET_CLASS(vdev);
    VirtIOInput *vinput = VIRTIO_INPUT(vdev);

    if (vinput->active) {
        vinput->active = false;
        if (vic->change_active) {
            vic->change_active(vinput);
        }
    }
}

static int virtio_input_post_load(void *opaque, int version_id)
{
    VirtIOInput *vinput = opaque;
    VirtIOInputClass *vic = VIRTIO_INPUT_GET_CLASS(vinput);
    VirtIODevice *vdev = VIRTIO_DEVICE(vinput);

    vinput->active = vdev->status & VIRTIO_CONFIG_S_DRIVER_OK;
    if (vic->change_active) {
        vic->change_active(vinput);
    }
    return 0;
}

static void virtio_input_device_realize(DeviceState *dev, Error **errp)
{
    VirtIOInputClass *vic = VIRTIO_INPUT_GET_CLASS(dev);
    VirtIODevice *vdev = VIRTIO_DEVICE(dev);
    VirtIOInput *vinput = VIRTIO_INPUT(dev);
    VirtIOInputConfig *cfg;
    Error *local_err = NULL;

    if (vic->realize) {
        vic->realize(dev, &local_err);
        if (local_err) {
            error_propagate(errp, local_err);
            return;
        }
    }

    virtio_input_idstr_config(vinput, VIRTIO_INPUT_CFG_ID_SERIAL,
                              vinput->serial);

    QTAILQ_FOREACH(cfg, &vinput->cfg_list, node) {
        if (vinput->cfg_size < cfg->config.size) {
            vinput->cfg_size = cfg->config.size;
        }
    }
    vinput->cfg_size += 8;
    assert(vinput->cfg_size <= sizeof(virtio_input_config));

    virtio_init(vdev, "virtio-input", VIRTIO_ID_INPUT,
                vinput->cfg_size);
    vinput->evt = virtio_add_queue(vdev, 64, virtio_input_handle_evt);
    vinput->sts = virtio_add_queue(vdev, 64, virtio_input_handle_sts);
}

static void virtio_input_finalize(Object *obj)
{
    VirtIOInput *vinput = VIRTIO_INPUT(obj);
    VirtIOInputConfig *cfg, *next;

    QTAILQ_FOREACH_SAFE(cfg, &vinput->cfg_list, node, next) {
        QTAILQ_REMOVE(&vinput->cfg_list, cfg, node);
        g_free(cfg);
    }

    g_free(vinput->queue);
}

static void virtio_input_device_unrealize(DeviceState *dev, Error **errp)
{
    VirtIOInputClass *vic = VIRTIO_INPUT_GET_CLASS(dev);
    VirtIODevice *vdev = VIRTIO_DEVICE(dev);
    VirtIOInput *vinput = VIRTIO_INPUT(dev);
    Error *local_err = NULL;

    if (vic->unrealize) {
        vic->unrealize(dev, &local_err);
        if (local_err) {
            error_propagate(errp, local_err);
            return;
        }
    }
    virtio_delete_queue(vinput->evt);
    virtio_delete_queue(vinput->sts);
    virtio_cleanup(vdev);
}

static const VMStateDescription vmstate_virtio_input = {
    .name = "virtio-input",
    .minimum_version_id = VIRTIO_INPUT_VM_VERSION,
    .version_id = VIRTIO_INPUT_VM_VERSION,
    .fields = (VMStateField[]) {
        VMSTATE_VIRTIO_DEVICE,
        VMSTATE_END_OF_LIST()
    },
    .post_load = virtio_input_post_load,
};

static Property virtio_input_properties[] = {
    DEFINE_PROP_STRING("serial", VirtIOInput, serial),
    DEFINE_PROP_END_OF_LIST(),
};

static void virtio_input_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);
    VirtioDeviceClass *vdc = VIRTIO_DEVICE_CLASS(klass);

    device_class_set_props(dc, virtio_input_properties);
    dc->vmsd           = &vmstate_virtio_input;
    set_bit(DEVICE_CATEGORY_INPUT, dc->categories);
    vdc->realize      = virtio_input_device_realize;
    vdc->unrealize    = virtio_input_device_unrealize;
    vdc->get_config   = virtio_input_get_config;
    vdc->set_config   = virtio_input_set_config;
    vdc->get_features = virtio_input_get_features;
    vdc->set_status   = virtio_input_set_status;
    vdc->reset        = virtio_input_reset;
}

static const TypeInfo virtio_input_info = {
    .name          = TYPE_VIRTIO_INPUT,
    .parent        = TYPE_VIRTIO_DEVICE,
    .instance_size = sizeof(VirtIOInput),
    .class_size    = sizeof(VirtIOInputClass),
    .class_init    = virtio_input_class_init,
    .abstract      = true,
    .instance_finalize = virtio_input_finalize,
};

/* ----------------------------------------------------------------- */

static void virtio_register_types(void)
{
    type_register_static(&virtio_input_info);
}

type_init(virtio_register_types)
