/*
 * OpenBIOS virtio-1.0 virtio-blk driver
 *
 * Copyright (c) 2013 Alexander Graf <agraf@suse.de>
 * Copyright (c) 2018 Mark Cave-Ayland <mark.cave-ayland@ilande.co.uk>
 *
 * 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 "config.h"
#include "libc/byteorder.h"
#include "libc/vsprintf.h"
#include "libopenbios/bindings.h"
#include "libopenbios/ofmem.h"
#include "kernel/kernel.h"
#include "drivers/drivers.h"

#include "virtio.h"

#define VRING_WAIT_REPLY_TIMEOUT 10000

static uint8_t virtio_cfg_read8(uint64_t cfg_addr, int addr)
{
    return in_8((uint8_t *)(uintptr_t)(cfg_addr + addr));
}

static void virtio_cfg_write8(uint64_t cfg_addr, int addr, uint8_t value)
{
    out_8((uint8_t *)(uintptr_t)(cfg_addr + addr), value);
}

static uint16_t virtio_cfg_read16(uint64_t cfg_addr, int addr)
{
    return in_le16((uint16_t *)(uintptr_t)(cfg_addr + addr));
}

static void virtio_cfg_write16(uint64_t cfg_addr, int addr, uint16_t value)
{
    out_le16((uint16_t *)(uintptr_t)(cfg_addr + addr), value);
}

static uint32_t virtio_cfg_read32(uint64_t cfg_addr, int addr)
{
    return in_le32((uint32_t *)(uintptr_t)(cfg_addr + addr));
}

static void virtio_cfg_write32(uint64_t cfg_addr, int addr, uint32_t value)
{
    out_le32((uint32_t *)(uintptr_t)(cfg_addr + addr), value);
}

static uint64_t virtio_cfg_read64(uint64_t cfg_addr, int addr)
{
    uint64_t q = ((uint64_t)virtio_cfg_read32(cfg_addr + 4, addr) << 32);
    q |= virtio_cfg_read32(cfg_addr, addr);

    return q;
}

static void virtio_cfg_write64(uint64_t cfg_addr, int addr, uint64_t value)
{
    virtio_cfg_write32(cfg_addr, addr, (value & 0xffffffff));
    virtio_cfg_write32(cfg_addr, addr + 4, ((value >> 32) & 0xffffffff));
}

static long virtio_notify(VDev *vdev, int vq_idx, long cookie)
{
    uint16_t notify_offset = virtio_cfg_read16(vdev->common_cfg, VIRTIO_PCI_COMMON_Q_NOFF);

    virtio_cfg_write16(vdev->notify_base, notify_offset +
                        vq_idx * vdev->notify_mult, vq_idx);

    return 0;
}

/***********************************************
 *             Virtio functions                *
 ***********************************************/

static void vring_init(VRing *vr, VqInfo *info)
{
    void *p = (void *) (uintptr_t)info->queue;

    vr->id = info->index;
    vr->num = info->num;
    vr->desc = p;
    vr->avail = (void *)((uintptr_t)p + info->num * sizeof(VRingDesc));
    vr->used = (void *)(((unsigned long)&vr->avail->ring[info->num]
               + info->align - 1) & ~(info->align - 1));

    /* Zero out all relevant field */
    vr->avail->flags = __cpu_to_le16(0);
    vr->avail->idx = __cpu_to_le16(0);

    /* We're running with interrupts off anyways, so don't bother */
    vr->used->flags = __cpu_to_le16(VRING_USED_F_NO_NOTIFY);
    vr->used->idx = __cpu_to_le16(0);
    vr->used_idx = 0;
    vr->next_idx = 0;
    vr->cookie = 0;
}

static int vring_notify(VDev *vdev, VRing *vr)
{
    return virtio_notify(vdev, vr->id, vr->cookie);
}

static void vring_send_buf(VRing *vr, uint64_t p, int len, int flags)
{
    /* For follow-up chains we need to keep the first entry point */
    if (!(flags & VRING_HIDDEN_IS_CHAIN)) {
        vr->avail->ring[__le16_to_cpu(vr->avail->idx) % vr->num] = __cpu_to_le16(vr->next_idx);
    }

    vr->desc[vr->next_idx].addr = __cpu_to_le64(p);
    vr->desc[vr->next_idx].len = __cpu_to_le32(len);
    vr->desc[vr->next_idx].flags = __cpu_to_le16(flags & ~VRING_HIDDEN_IS_CHAIN);
    vr->desc[vr->next_idx].next = __cpu_to_le16(vr->next_idx);
    vr->desc[vr->next_idx].next = __cpu_to_le16(__le16_to_cpu(vr->desc[vr->next_idx].next) + 1);
    vr->next_idx++;

    /* Chains only have a single ID */
    if (!(flags & VRING_DESC_F_NEXT)) {
        vr->avail->idx = __cpu_to_le16(__le16_to_cpu(vr->avail->idx) + 1);
    }
}

static int vr_poll(VDev *vdev, VRing *vr)
{
    if (__le16_to_cpu(vr->used->idx) == vr->used_idx) {
        vring_notify(vdev, vr);
        return 0;
    }

    vr->used_idx = __le16_to_cpu(vr->used->idx);
    vr->next_idx = 0;
    vr->desc[0].len = __cpu_to_le32(0);
    vr->desc[0].flags = __cpu_to_le16(0);
    return 1; /* vr has been updated */
}

/*
 * Wait for the host to reply.
 *
 * timeout is in msecs if > 0.
 *
 * Returns 0 on success, 1 on timeout.
 */
static int vring_wait_reply(VDev *vdev)
{
    ucell target_ms, get_ms;

    fword("get-msecs");
    target_ms = POP();
    target_ms += vdev->wait_reply_timeout;

    /* Wait for any queue to be updated by the host */
    do {
        int i, r = 0;

        for (i = 0; i < vdev->nr_vqs; i++) {
            r += vr_poll(vdev, &vdev->vrings[i]);
        }

        if (r) {
            return 0;
        }

        fword("get-msecs");
        get_ms = POP();

    } while (!vdev->wait_reply_timeout || (get_ms < target_ms));

    return 1;
}

static uint64_t vring_addr_translate(VDev *vdev, void *p)
{
    ucell mode;
    uint64_t iova;

    iova = ofmem_translate(pointer2cell(p), &mode);
    return iova;
}

/***********************************************
 *               Virtio block                  *
 ***********************************************/

static int virtio_blk_read_many(VDev *vdev,
                                uint64_t offset, void *load_addr, int len)
{
    VirtioBlkOuthdr out_hdr;
    u8 status;
    VRing *vr = &vdev->vrings[vdev->cmd_vr_idx];
    uint8_t discard[VIRTIO_SECTOR_SIZE];

    uint64_t start_sector = offset / virtio_get_block_size(vdev);
    int head_len = offset & (virtio_get_block_size(vdev) - 1);
    uint64_t end_sector = (offset + len + virtio_get_block_size(vdev) - 1) /
                            virtio_get_block_size(vdev);
    int tail_len = end_sector * virtio_get_block_size(vdev) - (offset + len);

    /* Tell the host we want to read */
    out_hdr.type = __cpu_to_le32(VIRTIO_BLK_T_IN);
    out_hdr.ioprio = __cpu_to_le32(99);
    out_hdr.sector = __cpu_to_le64(virtio_sector_adjust(vdev, start_sector));

    vring_send_buf(vr, vring_addr_translate(vdev, &out_hdr), sizeof(out_hdr),
                   VRING_DESC_F_NEXT);

    /* Discarded head */
    if (head_len) {
        vring_send_buf(vr, vring_addr_translate(vdev, &discard), head_len,
                       VRING_DESC_F_WRITE | VRING_HIDDEN_IS_CHAIN |
                       VRING_DESC_F_NEXT);
    }

    /* This is where we want to receive data */
    vring_send_buf(vr, vring_addr_translate(vdev, load_addr), len,
                   VRING_DESC_F_WRITE | VRING_HIDDEN_IS_CHAIN |
                   VRING_DESC_F_NEXT);

    /* Discarded tail */
    if (tail_len) {
        vring_send_buf(vr, vring_addr_translate(vdev, &discard), tail_len,
                       VRING_DESC_F_WRITE | VRING_HIDDEN_IS_CHAIN |
                       VRING_DESC_F_NEXT);
    }

    /* status field */
    vring_send_buf(vr, vring_addr_translate(vdev, &status), sizeof(u8),
                   VRING_DESC_F_WRITE | VRING_HIDDEN_IS_CHAIN);

    /* Now we can tell the host to read */
    vring_wait_reply(vdev);

    return status;
}

int virtio_read_many(VDev *vdev, uint64_t offset, void *load_addr, int len)
{
    switch (vdev->senseid) {
    case VIRTIO_ID_BLOCK:
        return virtio_blk_read_many(vdev, offset, load_addr, len);
    }
    return -1;
}

static int virtio_read(VDev *vdev, uint64_t offset, void *load_addr, int len)
{
    return virtio_read_many(vdev, offset, load_addr, len);
}

int virtio_get_block_size(VDev *vdev)
{
    switch (vdev->senseid) {
    case VIRTIO_ID_BLOCK:
        return vdev->config.blk.blk_size << vdev->config.blk.physical_block_exp;
    }
    return 0;
}

static void
ob_virtio_configure_device(VDev *vdev)
{
    uint32_t feature;
    uint8_t status;
    int i;

    /* Indicate we recognise the device */
    status = virtio_cfg_read8(vdev->common_cfg, VIRTIO_PCI_COMMON_STATUS);
    status |= VIRTIO_CONFIG_S_ACKNOWLEDGE | VIRTIO_CONFIG_S_DRIVER;
    virtio_cfg_write8(vdev->common_cfg, VIRTIO_PCI_COMMON_STATUS, status);

    /* Negotiate features: acknowledge VIRTIO_F_VERSION_1 for 1.0 specification
       little-endian access */
    virtio_cfg_write32(vdev->common_cfg, VIRTIO_PCI_COMMON_DFSELECT, 0x1);
    virtio_cfg_write32(vdev->common_cfg, VIRTIO_PCI_COMMON_GFSELECT, 0x1);
    feature = virtio_cfg_read32(vdev->common_cfg, VIRTIO_PCI_COMMON_DF);
    feature &= (1ULL << (VIRTIO_F_VERSION_1 - 32));
    virtio_cfg_write32(vdev->common_cfg, VIRTIO_PCI_COMMON_GF, feature);

    status = virtio_cfg_read8(vdev->common_cfg, VIRTIO_PCI_COMMON_STATUS);
    status |= VIRTIO_CONFIG_S_FEATURES_OK;
    virtio_cfg_write8(vdev->common_cfg, VIRTIO_PCI_COMMON_STATUS, status);

    vdev->senseid = VIRTIO_ID_BLOCK;
    vdev->nr_vqs = 1;
    vdev->cmd_vr_idx = 0;
    vdev->wait_reply_timeout = VRING_WAIT_REPLY_TIMEOUT;
    vdev->scsi_block_size = VIRTIO_SCSI_BLOCK_SIZE;
    vdev->blk_factor = 1;

    for (i = 0; i < vdev->nr_vqs; i++) {
        VqInfo info = {
            .queue = (uintptr_t) vdev->ring_area + (i * VIRTIO_RING_SIZE),
            .align = VIRTIO_PCI_VRING_ALIGN,
            .index = i,
            .num = 0,
        };

        virtio_cfg_write16(vdev->common_cfg, VIRTIO_PCI_COMMON_Q_SELECT, i);
        info.num = virtio_cfg_read16(vdev->common_cfg, VIRTIO_PCI_COMMON_Q_SIZE);

        vring_init(&vdev->vrings[i], &info);

        /* Set block information */
        vdev->guessed_disk_nature = VIRTIO_GDN_NONE;
        vdev->config.blk.blk_size = VIRTIO_SECTOR_SIZE;
        vdev->config.blk.physical_block_exp = 0;

        /* Read sectors */
        vdev->config.blk.capacity = virtio_cfg_read64(vdev->device_cfg, 0);

        /* Set queue addresses */
        virtio_cfg_write64(vdev->common_cfg, VIRTIO_PCI_COMMON_Q_DESCLO,
                            vring_addr_translate(vdev, &vdev->vrings[i].desc[0]));
        virtio_cfg_write64(vdev->common_cfg, VIRTIO_PCI_COMMON_Q_AVAILLO,
                            vring_addr_translate(vdev, &vdev->vrings[i].avail[0]));
        virtio_cfg_write64(vdev->common_cfg, VIRTIO_PCI_COMMON_Q_USEDLO,
                            vring_addr_translate(vdev, &vdev->vrings[i].used[0]));

        /* Enable queue */
        virtio_cfg_write16(vdev->common_cfg, VIRTIO_PCI_COMMON_Q_ENABLE, 1);
    }

    /* Initialisation complete */
    status |= VIRTIO_CONFIG_S_DRIVER_OK;
    virtio_cfg_write8(vdev->common_cfg, VIRTIO_PCI_COMMON_STATUS, status);

    vdev->configured = 1;
}

static void
ob_virtio_disk_open(VDev **_vdev)
{
    VDev *vdev;
    phandle_t ph;

    PUSH(find_ih_method("vdev", my_self()));
    fword("execute");
    *_vdev = cell2pointer(POP());
    vdev = *_vdev;

    vdev->pos = 0;

    if (!vdev->configured) {
       ob_virtio_configure_device(vdev);
    }

    /* interpose disk-label */
    ph = find_dev("/packages/disk-label");
    fword("my-args");
    PUSH_ph( ph );
    fword("interpose");

    RET(-1);
}

static void
ob_virtio_disk_close(VDev **_vdev)
{
    return;
}

/* ( pos.d -- status ) */
static void
ob_virtio_disk_seek(VDev **_vdev)
{
    VDev *vdev = *_vdev;
    uint64_t pos;

    pos = ((uint64_t)POP()) << 32;
    pos |= POP();

    /* Make sure we are within the physical limits */
    if (pos < (vdev->config.blk.capacity * virtio_get_block_size(vdev))) {
        vdev->pos = pos;
        PUSH(0);
    } else {
        PUSH(1);
    }

    return;
}

/* ( addr len -- actual ) */
static void
ob_virtio_disk_read(VDev **_vdev)
{
    VDev *vdev = *_vdev;
    ucell len = POP();
    uint8_t *addr = (uint8_t *)POP();

    virtio_read(vdev, vdev->pos, addr, len);

    vdev->pos += len;

    PUSH(len);
}

static void set_virtio_alias(const char *path, int idx)
{
    phandle_t aliases;
    char name[9];

    aliases = find_dev("/aliases");

    snprintf(name, sizeof(name), "virtio%d", idx);

    set_property(aliases, name, path, strlen(path) + 1);
}

DECLARE_UNNAMED_NODE(ob_virtio_disk, 0, sizeof(VDev *));

NODE_METHODS(ob_virtio_disk) = {
    { "open",      ob_virtio_disk_open          },
    { "close",     ob_virtio_disk_close         },
    { "seek",      ob_virtio_disk_seek          },
    { "read",      ob_virtio_disk_read          },
};

static void
ob_virtio_open(VDev **_vdev)
{
    PUSH(-1);
}

static void
ob_virtio_close(VDev **_vdev)
{
    return;
}

static void
ob_virtio_dma_alloc(__attribute__((unused)) VDev **_vdev)
{
    call_parent_method("dma-alloc");
}

static void
ob_virtio_dma_free(__attribute__((unused)) VDev **_vdev)
{
    call_parent_method("dma-free");
}

static void
ob_virtio_dma_map_in(__attribute__((unused)) VDev **_vdev)
{
    call_parent_method("dma-map-in");
}

static void
ob_virtio_dma_map_out(__attribute__((unused)) VDev **_vdev)
{
    call_parent_method("dma-map-out");
}

static void
ob_virtio_dma_sync(__attribute__((unused)) VDev **_vdev)
{
    call_parent_method("dma-sync");
}

DECLARE_UNNAMED_NODE(ob_virtio, 0, sizeof(VDev *));

NODE_METHODS(ob_virtio) = {
    { "open",          ob_virtio_open        },
    { "close",         ob_virtio_close       },
    { "dma-alloc",     ob_virtio_dma_alloc   },
    { "dma-free",      ob_virtio_dma_free    },
    { "dma-map-in",    ob_virtio_dma_map_in  },
    { "dma-map-out",   ob_virtio_dma_map_out },
    { "dma-sync",      ob_virtio_dma_sync    },
};

void ob_virtio_init(const char *path, const char *dev_name, uint64_t common_cfg,
                    uint64_t device_cfg, uint64_t notify_base, uint32_t notify_mult,
                    int idx)
{
    char buf[256];
    ucell addr;
    VDev *vdev;

    /* Open ob_virtio */
    BIND_NODE_METHODS(get_cur_dev(), ob_virtio);

    vdev = malloc(sizeof(VDev));
    vdev->common_cfg = common_cfg;
    vdev->device_cfg = device_cfg;
    vdev->notify_base = notify_base;
    vdev->notify_mult = notify_mult;
    vdev->configured = 0;

    PUSH(pointer2cell(vdev));
    feval("value vdev");

    PUSH(sizeof(VRing) * VIRTIO_MAX_VQS);
    feval("dma-alloc");
    addr = POP();
    vdev->vrings = cell2pointer(addr);

    PUSH(VIRTIO_RING_SIZE * VIRTIO_MAX_VQS);
    feval("dma-alloc");
    addr = POP();
    vdev->ring_area = cell2pointer(addr);

    fword("new-device");
    push_str("disk");
    fword("device-name");
    push_str("block");
    fword("device-type");

    PUSH(pointer2cell(vdev));
    feval("value vdev");

    BIND_NODE_METHODS(get_cur_dev(), ob_virtio_disk);
    fword("finish-device");

    snprintf(buf, sizeof(buf), "%s/disk", path);
    set_virtio_alias(buf, idx);
}
