/*
 * dboxmem Shared Memory PCI device.
 *
 * Copyright (C) 2018 Virtual Open Systems
 *
 * Author:
 *      Alvise Rigo <a.rigo@virtualopensystems.com>
 *
 * Heavily based On: ivshmem.c
 *
 * This code is licensed under the GNU GPL v2.
 *
 * Contributions after 2012-01-13 are licensed under the terms of the
 * GNU GPL, version 2 or (at your option) any later version.
 */
#include "qemu/osdep.h"
#include "qapi/error.h"
#include "qemu/cutils.h"
#include "hw/hw.h"
#include "hw/pci/pci.h"
#include "hw/pci/msi.h"
#include "hw/pci/msix.h"
#include "sysemu/kvm.h"
#include "migration/blocker.h"
#include "qemu/error-report.h"
#include "qemu/event_notifier.h"
#include "qom/object_interfaces.h"
#include "chardev/char-fe.h"
#include "sysemu/hostmem.h"
#include "sysemu/qtest.h"
#include "qapi/visitor.h"

#include <sys/mman.h>
#include <sys/types.h>
#include <limits.h>

#include "sysemu/hostmem-dboxshm.h"

#define PCI_VENDOR_ID_DBOXMEM   PCI_VENDOR_ID_REDHAT_QUMRANET
#define PCI_DEVICE_ID_DBOXMEM   0x1110

#define DBOXMEM_REG_BAR_SIZE 0x100

//#define DEBUG_DBOXMEM
#ifdef DEBUG_DBOXMEM
#define DBOXMEM_DPRINTF(fmt, ...)        \
    do {printf("DBOXMEM: " fmt, ## __VA_ARGS__); } while (0)
#else
#define DBOXMEM_DPRINTF(fmt, ...)
#endif

#define TYPE_DBOXMEM "dboxmem"
#define DBOXMEM(obj) \
    OBJECT_CHECK(DBoxMemState, (obj), TYPE_DBOXMEM)

/* Interrupt status values */
typedef enum dboxmem_lock_status {
    DBOXMEM_IRQ_LOCK_TAKEN = 1,
    DBOXMEM_IRQ_LOCK_RELEASED,
    DBOXMEM_IRQ_LOCK_REQUEST_PENDING,
} dboxmem_lock_status;

typedef struct DBoxMemState {
    /*< private >*/
    PCIDevice parent_obj;
    /*< public >*/

    HostMemoryBackend *hostmem;
    uint32_t intrmask;
    uint32_t intrstatus;

    MemoryRegion dboxmem_mmio;

    /* We might need to register the BAR before we actually have the memory.
     * So prepare a container MemoryRegion for the BAR immediately and
     * add a subregion when we have the memory.
     */
    MemoryRegion bar;
    MemoryRegion dboxmem;
    uint64_t dboxmem_size; /* size of shared memory region */
    uint32_t dboxmem_64bit;

    Error *migration_blocker;

    char * sizearg;

    QemuThread dboxshm_lock_thread;
    QemuCond dboxshm_lock_cond;
    QemuMutex dboxshm_lock_mutex;
    dboxmem_lock_status lock_status;
} DBoxMemState;

/* registers for the Inter-VM shared memory device */
enum dboxmem_registers {
    INTRMASK = 0,
    INTRSTATUS = 4,
    JOIN_SHM = 8,
    ALLOC_SHM = 12,
    UUID = 16, /* UUID [16, 31] */
    LOCK = 32,
    UNLOCK = 36,
    RELEASE = 40,
};

/* accessing registers - based on rtl8139 */
static void dboxmem_update_irq(DBoxMemState *s)
{
    PCIDevice *d = PCI_DEVICE(s);
    int isr;
    isr = (s->intrstatus & s->intrmask) & 0xffffffff;

    /* don't print ISR resets */
    if (isr) {
        DBOXMEM_DPRINTF("Set IRQ to %d (%04x %04x)\n",
                        isr ? 1 : 0, s->intrstatus, s->intrmask);
    }

    pci_set_irq(d, (isr != 0));
}

static void dboxmem_IntrMask_write(DBoxMemState *s, uint32_t val)
{
    DBOXMEM_DPRINTF("IntrMask write(w) val = 0x%04x\n", val);

    s->intrmask = val;

    dboxmem_update_irq(s);
}

static uint32_t dboxmem_IntrMask_read(DBoxMemState *s)
{
    uint32_t ret = s->intrmask;

    DBOXMEM_DPRINTF("intrmask read(w) val = 0x%04x\n", ret);

    return ret;
}

static void dboxmem_IntrStatus_write(DBoxMemState *s, uint32_t val)
{
    DBOXMEM_DPRINTF("IntrStatus write(w) val = 0x%04x\n", val);

    s->intrstatus = val;

    dboxmem_update_irq(s);
}

static uint32_t dboxmem_IntrStatus_read(DBoxMemState *s)
{
    uint32_t ret = s->intrstatus;

    /* reading ISR clears all interrupts */
    s->intrstatus = 0;

    dboxmem_update_irq(s);

    return ret;
}

static void *wait_for_lock_fn(void *opaque)
{
    DBoxMemState *s = (DBoxMemState *)opaque;

    DBOXMEM_DPRINTF("[SHM LOCK THREAD] Started\n");

    qemu_mutex_lock(&s->dboxshm_lock_mutex);

    while (true) {
        DboxShmMemoryBackend *shmb = NULL;

        /* Wait for a new lock request */
        qemu_cond_wait(&s->dboxshm_lock_cond, &s->dboxshm_lock_mutex);

        if (s->lock_status != DBOXMEM_IRQ_LOCK_REQUEST_PENDING) {
            /* Spurious wake up */
            g_warning("[SHM LOCK THREAD] Warning: Spurious wake-up\n");
            continue;
        }

        shmb = MEMORY_BACKEND_DBOXSHM(s->hostmem);
        assert(shmb);

        DBOXMEM_DPRINTF("[SHM LOCK THREAD] Awake\n");

        /* Take the lock of the shared memory */
        /* XXX: We should make sure that no modification is done to the device
         *      state */
        assert(shmb->shm_handle.data_ptr);

        DBOXMEM_DPRINTF("[SHM LOCK THREAD] New lock request, waiting\n");

        /* This might block */
        dboxshmem_lock(&shmb->shm_handle);

        DBOXMEM_DPRINTF("[SHM LOCK THREAD] Lock taken\n");

        /* N.B.: We do not trigger this function through the usual guest MMIO
         * path, hence, we need to take the global lock. */
        qemu_mutex_lock_iothread();
        dboxmem_IntrStatus_write(s, DBOXMEM_IRQ_LOCK_TAKEN);
        qemu_mutex_unlock_iothread();

        s->lock_status = DBOXMEM_IRQ_LOCK_TAKEN;
    }

    qemu_thread_exit(NULL);
}

static void dboxmem_io_write(void *opaque, hwaddr addr,
                             uint64_t val, unsigned size)
{
    DBoxMemState *s = opaque;
    HostMemoryBackendClass *bc = MEMORY_BACKEND_GET_CLASS(s->hostmem);
    DboxShmMemoryBackend *shmb = MEMORY_BACKEND_DBOXSHM(s->hostmem);
    DboxShmMemoryBackendClass *dmbc = DBOXSHM_MEMORY_BACKEND_CLASS(bc);

    addr &= 0xfc;

    DBOXMEM_DPRINTF("writing to addr " TARGET_FMT_plx "\n", addr);
    switch (addr)
    {
        case INTRMASK:
            dboxmem_IntrMask_write(s, val);
            break;

        case INTRSTATUS:
            dboxmem_IntrStatus_write(s, val);
            break;

        case JOIN_SHM:
        {
            dbox_shmem_handle shm_handle;
            /* Attach to an existing one */
            shm_handle = dmbc->shm_get(s->hostmem, true);

            if (shm_handle.size != s->hostmem->size) {
                goto dbox_size_error;
            }
        }
            break;

        case ALLOC_SHM:
        {
            dbox_shmem_handle shm_handle;

            /* Allocate a new one */
            shm_handle = dmbc->shm_get(s->hostmem, false);

            if (shm_handle.size != s->hostmem->size) {
                goto dbox_size_error;
            }
        }
            break;

        case UUID:
        case UUID + 4:
        case UUID + 8:
        case UUID + 12:
            {
                hwaddr off = addr - UUID;
                uint32_t *ptr = (uint32_t *)shmb->secid_uuid.data;
                char uuid_str[37];

                /* Copy the 1/4 of UUID into the uuid_t struct */
                memcpy((char *)shmb->secid_uuid.data + off, &val, 4);

                qemu_uuid_unparse(&shmb->secid_uuid, uuid_str);

                /* Check if the UUID is now complete */
                if (!!ptr[0] + !!ptr[1] + !!ptr[2] + !!ptr[3] == 4) {
                    /* The UUID has been completely set. We don't care about the
                     * order in which the four pieces are written into the
                     * register. */
                    qemu_uuid_unparse(&shmb->secid_uuid, shmb->secid_char);
                    DBOXMEM_DPRINTF("UUID %s\n", shmb->secid_char);
                }
            }
            break;

        case LOCK:
            qemu_mutex_lock(&s->dboxshm_lock_mutex);

            if (s->lock_status == DBOXMEM_IRQ_LOCK_TAKEN) {
                g_warning("Locking shmem with lock taken\n");
            }
            s->lock_status = DBOXMEM_IRQ_LOCK_REQUEST_PENDING;

            /* Signal the thread to process the request */
            qemu_cond_signal(&s->dboxshm_lock_cond);
            qemu_mutex_unlock(&s->dboxshm_lock_mutex);

            break;

        case UNLOCK:
            qemu_mutex_lock(&s->dboxshm_lock_mutex);

            if (s->lock_status == DBOXMEM_IRQ_LOCK_RELEASED) {
                g_warning("Unlocking shmem without lock taken\n");
            }

            dboxshmem_unlock(&shmb->shm_handle);
            s->lock_status = DBOXMEM_IRQ_LOCK_RELEASED;

            qemu_mutex_unlock(&s->dboxshm_lock_mutex);

            /* Inform the guest we released the lock */
            dboxmem_IntrStatus_write(s, DBOXMEM_IRQ_LOCK_RELEASED);

            break;

        case RELEASE:
            DBOXMEM_DPRINTF("Freeing UUID %s\n", shmb->secid_char);
            /* Release the memory and restore the device state. */
            dmbc->shm_release(s->hostmem, &shmb->shm_handle);
            /* Clear UUID */
            memset(&shmb->secid_uuid.data, 0, sizeof(QemuUUID));
            memset(shmb->secid_char, 0, strlen(UUID_NONE));
            if (s->lock_status == DBOXMEM_IRQ_LOCK_TAKEN) {
                g_warning("Releasing shmem with lock taken\n");
            }
            /* We already generated a warning, now we can reset the lock status
             * */
            s->lock_status = DBOXMEM_IRQ_LOCK_RELEASED;
            break;

        default:
            DBOXMEM_DPRINTF("Unhandled write " TARGET_FMT_plx "\n", addr);
    }

    return;

dbox_size_error:
    /* Make sure that the two sizes match. The guest library will rely on the
     * size of the BAR2, which is equal to s->hostmem->size.  If this value
     * does not match the size returned by the {join,init}_shmem(),
     * catastrophic errors may happen. */
    hw_error("dboxmem: Please instantiate the dboxmem device with the correct "
             "memory size: ...-object memory-backend-dbox-shm,size=<SIZE>,...");
}

static uint64_t dboxmem_io_read(void *opaque, hwaddr addr,
                                unsigned size)
{

    DBoxMemState *s = opaque;
    DboxShmMemoryBackend *shmb = MEMORY_BACKEND_DBOXSHM(s->hostmem);
    uint32_t ret = 0;

    DBOXMEM_DPRINTF("reading from addr " TARGET_FMT_plx "\n", addr);
    switch (addr)
    {
        case INTRMASK:
            ret = dboxmem_IntrMask_read(s);
            break;

        case INTRSTATUS:
            ret = dboxmem_IntrStatus_read(s);
            break;

        case ALLOC_SHM:
            break;

        case UUID:
        case UUID + 4:
        case UUID + 8:
        case UUID + 12:
            {
                /* This register is used if the operation (alloc or join)
                 * succeeded. If it did not succeeded, the returned value
                 * will be always 0xffff. */
                if (shmb->shm_handle.data_ptr == NULL) {
                    ret = (uint32_t)-1;
                } else {
                    hwaddr off = addr - UUID;
                    ret = *((uint32_t *)(shmb->secid_uuid.data + off));
                }
                DBOXMEM_DPRINTF("%s: returning 0x%x\n", __func__, ret);
            }
            break;

        default:
            DBOXMEM_DPRINTF("why are we reading " TARGET_FMT_plx "\n", addr);
            ret = 0;
    }

    return ret;
}

static const MemoryRegionOps dboxmem_mmio_ops = {
    .read = dboxmem_io_read,
    .write = dboxmem_io_write,
    .endianness = DEVICE_NATIVE_ENDIAN,
    .impl = {
        .min_access_size = 4,
        .max_access_size = 4,
    },
};

static void dboxmem_reset(DeviceState *d)
{
    DBoxMemState *s = DBOXMEM(d);

    s->intrstatus = 0;
    s->intrmask = 0;
}

static void dboxmem_write_config(PCIDevice *pdev, uint32_t address,
                                 uint32_t val, int len)
{
    pci_default_write_config(pdev, address, val, len);
}

static void pci_dboxmem_realize(PCIDevice *dev, Error **errp)
{
    DBoxMemState *s = DBOXMEM(dev);
    MemoryRegion *mr;
    uint8_t *pci_conf;
    uint8_t attr = PCI_BASE_ADDRESS_SPACE_MEMORY |
        PCI_BASE_ADDRESS_MEM_PREFETCH;

    if (!!s->hostmem != 1) {
        error_setg(errp,
                   "You must specify 'x-memdev'");
        return;
    }

    if (s->sizearg) {
        g_warning("size argument ignored with hostmem");
    }

    mr = host_memory_backend_get_memory(s->hostmem, errp);
    s->dboxmem_size = memory_region_size(mr);

    pci_conf = dev->config;
    pci_conf[PCI_COMMAND] = PCI_COMMAND_IO | PCI_COMMAND_MEMORY;

    pci_config_set_interrupt_pin(pci_conf, 1);

    memory_region_init_io(&s->dboxmem_mmio, OBJECT(s), &dboxmem_mmio_ops, s,
                          "dboxmem-mmio", DBOXMEM_REG_BAR_SIZE);

    /* region for registers*/
    pci_register_bar(dev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY,
                     &s->dboxmem_mmio);

    memory_region_init(&s->bar, OBJECT(s), "dboxmem-bar2-container", s->dboxmem_size);
    if (s->dboxmem_64bit) {
        attr |= PCI_BASE_ADDRESS_MEM_TYPE_64;
    }

    mr = host_memory_backend_get_memory(MEMORY_BACKEND(s->hostmem), errp);
    vmstate_register_ram(mr, DEVICE(s));
    memory_region_add_subregion(&s->bar, 0, mr);
    pci_register_bar(PCI_DEVICE(s), 2, attr, &s->bar);
}

static void pci_dboxmem_exit(PCIDevice *dev)
{
    DBoxMemState *s = DBOXMEM(dev);

    if (s->migration_blocker) {
        migrate_del_blocker(s->migration_blocker);
        error_free(s->migration_blocker);
    }

    if (memory_region_is_mapped(&s->dboxmem)) {
        if (!s->hostmem) {
            void *addr = memory_region_get_ram_ptr(&s->dboxmem);

            if (munmap(addr, s->dboxmem_size) == -1) {
                error_report("Failed to munmap shared memory %s",
                             strerror(errno));
            }
        }

        vmstate_unregister_ram(&s->dboxmem, DEVICE(dev));
        memory_region_del_subregion(&s->bar, &s->dboxmem);
    }
}

static int dboxmem_pre_load(void *opaque)
{
    return 0;
}

static int dboxmem_post_load(void *opaque, int version_id)
{
    return 0;
}

static int dboxmem_load_old(QEMUFile *f, void *opaque, int version_id)
{
    DBoxMemState *s = opaque;
    PCIDevice *pdev = PCI_DEVICE(s);
    int ret;

    DBOXMEM_DPRINTF("dboxmem_load_old\n");

    if (version_id != 0) {
        return -EINVAL;
    }

    ret = pci_device_load(pdev, f);
    if (ret) {
        return ret;
    }

    s->intrstatus = qemu_get_be32(f);
    s->intrmask = qemu_get_be32(f);

    return 0;
}

static const VMStateDescription dboxmem_vmsd = {
    .name = "dboxmem",
    .version_id = 1,
    .minimum_version_id = 1,
    .pre_load = dboxmem_pre_load,
    .post_load = dboxmem_post_load,
    .fields = (VMStateField[]) {
        VMSTATE_PCI_DEVICE(parent_obj, DBoxMemState),

        VMSTATE_END_OF_LIST()
    },
    .load_state_old = dboxmem_load_old,
    .minimum_version_id_old = 0
};

static Property dboxmem_properties[] = {
    DEFINE_PROP_STRING("size", DBoxMemState, sizearg),
    DEFINE_PROP_UINT32("use64", DBoxMemState, dboxmem_64bit, 1),
    DEFINE_PROP_END_OF_LIST(),
};

static void dboxmem_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);
    PCIDeviceClass *k = PCI_DEVICE_CLASS(klass);

    k->realize = pci_dboxmem_realize;
    k->exit = pci_dboxmem_exit;
    k->config_write = dboxmem_write_config;
    k->vendor_id = PCI_VENDOR_ID_DBOXMEM;
    k->device_id = PCI_DEVICE_ID_DBOXMEM;
    k->class_id = PCI_CLASS_MEMORY_RAM;
    dc->reset = dboxmem_reset;
    dc->props = dboxmem_properties;
    dc->vmsd = &dboxmem_vmsd;
    set_bit(DEVICE_CATEGORY_MISC, dc->categories);
    dc->desc = "Inter-VM over disaggregated shared memory";
}

static void dboxmem_check_memdev_is_busy(Object *obj, const char *name,
                                         Object *val, Error **errp)
{
    MemoryRegion *mr;

    mr = host_memory_backend_get_memory(MEMORY_BACKEND(val), errp);
    if (memory_region_is_mapped(mr)) {
        char *path = object_get_canonical_path_component(val);
        error_setg(errp, "can't use already busy memdev: %s", path);
        g_free(path);
    } else {
        qdev_prop_allow_set_link_before_realize(obj, name, val, errp);
    }
}

static void dboxmem_init(Object *obj)
{
    DBoxMemState *s = DBOXMEM(obj);

    object_property_add_link(obj, "x-memdev", TYPE_MEMORY_BACKEND,
                             (Object **)&s->hostmem,
                             dboxmem_check_memdev_is_busy,
                             OBJ_PROP_LINK_UNREF_ON_RELEASE,
                             &error_abort);

    qemu_cond_init(&s->dboxshm_lock_cond);
    qemu_mutex_init(&s->dboxshm_lock_mutex);
    s->lock_status = DBOXMEM_IRQ_LOCK_RELEASED;

    /* Create a new thread and lock */
    qemu_thread_create(&s->dboxshm_lock_thread, "wait-for-dboxshm-lock",
                       (void *)wait_for_lock_fn, (void *)s,
                       QEMU_THREAD_JOINABLE);
}

static const TypeInfo dboxmem_info = {
    .name          = TYPE_DBOXMEM,
    .parent        = TYPE_PCI_DEVICE,
    .instance_size = sizeof(DBoxMemState),
    .instance_init = dboxmem_init,
    .class_init    = dboxmem_class_init,
    .interfaces = (InterfaceInfo[]) {
        { INTERFACE_CONVENTIONAL_PCI_DEVICE },
        { },
    },
};

static void dboxmem_register_types(void)
{
    type_register_static(&dboxmem_info);
}

type_init(dboxmem_register_types)
