// Virtio.HC

//
// PCI virtio I/O registers.
//

#define VIRTIO_PCI_HOST_FEATURES 0  // Features supported by the host
#define VIRTIO_PCI_GUEST_FEATURES 4 // Features activated by the guest
#define VIRTIO_PCI_QUEUE_PFN 8      // PFN for the currently selected queue
#define VIRTIO_PCI_QUEUE_SIZE 12    // Queue size for the currently selected queue
#define VIRTIO_PCI_QUEUE_SEL 14     // Queue selector
#define VIRTIO_PCI_QUEUE_NOTIFY 16  // Queue notifier
#define VIRTIO_PCI_STATUS 18        // Device status register
#define VIRTIO_PCI_ISR 19           // Interrupt status register
#define VIRTIO_PCI_CONFIG 20        // Configuration data block

//
// PCI virtio status register bits
//

#define VIRTIO_CONFIG_S_ACKNOWLEDGE 1
#define VIRTIO_CONFIG_S_DRIVER 2
#define VIRTIO_CONFIG_S_DRIVER_OK 4
#define VIRTIO_CONFIG_S_FAILED 0x80

//
// Ring descriptor flags
//

#define VRING_DESC_F_NEXT 1     // Buffer continues via the next field
#define VRING_DESC_F_WRITE 2    // Buffer is write-only (otherwise read-only)
#define VRING_DESC_F_INDIRECT 4 // Buffer contains a list of buffer descriptors

class @virtio_queue_buf
{
    U64 address;
    U32 length;
    U16 flags;
    U16 next;
};
class @virtio_avail
{
    U16 flags;
    U16 index;
    U16 ring[256];
    U16 int_index;
};
class @virtio_used_item
{
    U32 index;
    U32 length;
};
class @virtio_used
{
    U16 flags;
    U16 index;
    @virtio_used_item ring[256];
    U16 int_index;
};
class @virtio_queue
{
    @virtio_queue_buf buffers[256];
    @virtio_avail available;
    U8 padding[3578];
    @virtio_used used;
};

class @virtio_avail_buf
{
    U32 index;
    U64 address;
    U32 length;
};

class @virtio_buf_info
{
    U8* buffer;
    U64 size;
    U8 flags;

    // If the user wants to keep same buffer as passed in this struct, use "true".
    // otherwise, the supplied buffer will be copied in the queues' buffer
    Bool copy;
};

// Virtio-blk.HC

#define BDT_VIRTIO_BLK 10

#define VIRTIO_BLK_T_IN 0
#define VIRTIO_BLK_T_OUT 1
#define VIRTIO_BLK_T_FLUSH 4

#define VIRTIO_BLK_MAX_BLK 0x400000 // Limit blkdev to 2G max, set to NULL to use entire disk (not recommended for RedSea)

class @virtio_blk
{
    U16 port;
    U32 blks;
    @virtio_queue* vq;
    I64 vq_size;
    I64 vq_index;
    U8 status;
};

class @virtio_blk_request
{
    U32 type;
    U32 priority;
    U64 sector;
};

@virtio_blk virtio_blk;
MemSet(&virtio_blk, 0, sizeof(@virtio_blk));

I64 VirtioBlkInit()
{
    I64 j;

    // Scan for device
    j = PCIClassFind(0x010000, 0);
    if (j < 0) {
        "\n[virtio-blk] No device found\n";
        return -1;
    }
    virtio_blk.port = PCIReadU32(j.u8[2],
                          j.u8[1], j.u8[0], 0x10)
        & 0xFFFFFFFC;

    virtio_blk.blks = InU32(virtio_blk.port + VIRTIO_PCI_CONFIG);

    // Reset Device
    OutU8(virtio_blk.port + VIRTIO_PCI_STATUS, 0);

    // Found Driver
    OutU8(virtio_blk.port + VIRTIO_PCI_STATUS, InU8(virtio_blk.port + VIRTIO_PCI_STATUS) | VIRTIO_CONFIG_S_ACKNOWLEDGE | VIRTIO_CONFIG_S_DRIVER);

    // Set up virt queue
    OutU16(virtio_blk.port + VIRTIO_PCI_QUEUE_SEL, 0);
    virtio_blk.vq_size = InU16(virtio_blk.port + VIRTIO_PCI_QUEUE_SIZE); // 256
    virtio_blk.vq = CAllocAligned(sizeof(@virtio_queue), 4096, erythros_mem_task->code_heap);
    OutU32(virtio_blk.port + VIRTIO_PCI_QUEUE_PFN, virtio_blk.vq / 4096);

    // Init OK
    OutU8(virtio_blk.port + VIRTIO_PCI_STATUS, InU8(virtio_blk.port + VIRTIO_PCI_STATUS) | VIRTIO_CONFIG_S_DRIVER_OK);
    virtio_blk.vq_index = 0;
}

// DskVIO.HC

U0 VIOFlush()
{
    I64 j;
    I64 vq_idx;
    @virtio_blk_request* brq = CAlloc(sizeof(@virtio_blk_request), erythros_mem_task);
    brq->type = VIRTIO_BLK_T_FLUSH;
    brq->sector = NULL;
    vq_idx = virtio_blk.vq->available.index % 256;
    virtio_blk.vq->buffers[virtio_blk.vq_index % 256].address = brq;
    virtio_blk.vq->buffers[virtio_blk.vq_index % 256].length = sizeof(@virtio_blk_request);
    virtio_blk.vq->buffers[virtio_blk.vq_index % 256].flags = VRING_DESC_F_NEXT;
    virtio_blk.vq->buffers[virtio_blk.vq_index % 256].next = (virtio_blk.vq_index + 1) % 256;
    virtio_blk.vq->buffers[(virtio_blk.vq_index + 1) % 256].address = &virtio_blk.status;
    virtio_blk.vq->buffers[(virtio_blk.vq_index + 1) % 256].length = 1;
    virtio_blk.vq->buffers[(virtio_blk.vq_index + 1) % 256].flags = VRING_DESC_F_WRITE;
    virtio_blk.vq->buffers[(virtio_blk.vq_index + 1) % 256].next = 0;
    virtio_blk.vq->available.ring[vq_idx] = virtio_blk.vq_index % 256;
    virtio_blk.vq_index += 2;
    j = virtio_blk.vq->used.index;
    virtio_blk.vq->available.index++;
    OutU16(virtio_blk.port + VIRTIO_PCI_QUEUE_NOTIFY, 0);
    while (j == virtio_blk.vq->used.index) {
        Yield;
    }
    Free(brq);
}

Bool VIORBlks(CDrv* dv, U8* buf, I64 blk, I64 cnt)
{
    no_warn dv;
    I64 i, j;
    I64 vq_idx;
    U64 addr;
    @virtio_blk_request* brq = CAlloc(sizeof(@virtio_blk_request), erythros_mem_task);
    for (i = 0; i < cnt; i++) {
        brq->type = VIRTIO_BLK_T_IN;
        brq->sector = blk + i;
        vq_idx = virtio_blk.vq->available.index % 256;
        addr = buf + (BLK_SIZE * i);
        virtio_blk.vq->buffers[virtio_blk.vq_index % 256].address = brq;
        virtio_blk.vq->buffers[virtio_blk.vq_index % 256].length = sizeof(@virtio_blk_request);
        virtio_blk.vq->buffers[virtio_blk.vq_index % 256].flags = VRING_DESC_F_NEXT;
        virtio_blk.vq->buffers[virtio_blk.vq_index % 256].next = (virtio_blk.vq_index + 1) % 256;
        virtio_blk.vq->buffers[(virtio_blk.vq_index + 1) % 256].address = addr;
        virtio_blk.vq->buffers[(virtio_blk.vq_index + 1) % 256].length = BLK_SIZE;
        virtio_blk.vq->buffers[(virtio_blk.vq_index + 1) % 256].flags = VRING_DESC_F_WRITE | VRING_DESC_F_NEXT;
        virtio_blk.vq->buffers[(virtio_blk.vq_index + 1) % 256].next = (virtio_blk.vq_index + 2) % 256;
        virtio_blk.vq->buffers[(virtio_blk.vq_index + 2) % 256].address = &virtio_blk.status;
        virtio_blk.vq->buffers[(virtio_blk.vq_index + 2) % 256].length = 1;
        virtio_blk.vq->buffers[(virtio_blk.vq_index + 2) % 256].flags = VRING_DESC_F_WRITE;
        virtio_blk.vq->buffers[(virtio_blk.vq_index + 2) % 256].next = 0;
        virtio_blk.vq->available.ring[vq_idx] = virtio_blk.vq_index % 256;
        virtio_blk.vq_index += 3;
        j = virtio_blk.vq->used.index;
        virtio_blk.vq->available.index++;
        OutU16(virtio_blk.port + VIRTIO_PCI_QUEUE_NOTIFY, 0);
        while (j == virtio_blk.vq->used.index) {
            Yield;
        }
    }
    Free(brq);
    return TRUE;
}

Bool VIOWBlks(CDrv* dv, U8* buf, I64 blk, I64 cnt)
{
    no_warn dv;
    I64 i, j;
    I64 vq_idx;
    U64 addr;
    @virtio_blk_request* brq = CAlloc(sizeof(@virtio_blk_request), erythros_mem_task);
    for (i = 0; i < cnt; i++) {
        brq->type = VIRTIO_BLK_T_OUT;
        brq->sector = blk + i;
        vq_idx = virtio_blk.vq->available.index % 256;
        addr = buf + (BLK_SIZE * i);
        virtio_blk.vq->buffers[virtio_blk.vq_index % 256].address = brq;
        virtio_blk.vq->buffers[virtio_blk.vq_index % 256].length = sizeof(@virtio_blk_request);
        virtio_blk.vq->buffers[virtio_blk.vq_index % 256].flags = VRING_DESC_F_NEXT;
        virtio_blk.vq->buffers[virtio_blk.vq_index % 256].next = (virtio_blk.vq_index + 1) % 256;
        virtio_blk.vq->buffers[(virtio_blk.vq_index + 1) % 256].address = addr;
        virtio_blk.vq->buffers[(virtio_blk.vq_index + 1) % 256].length = BLK_SIZE;
        virtio_blk.vq->buffers[(virtio_blk.vq_index + 1) % 256].flags = VRING_DESC_F_NEXT;
        virtio_blk.vq->buffers[(virtio_blk.vq_index + 1) % 256].next = (virtio_blk.vq_index + 2) % 256;
        virtio_blk.vq->buffers[(virtio_blk.vq_index + 2) % 256].address = &virtio_blk.status;
        virtio_blk.vq->buffers[(virtio_blk.vq_index + 2) % 256].length = 1;
        virtio_blk.vq->buffers[(virtio_blk.vq_index + 2) % 256].flags = VRING_DESC_F_WRITE;
        virtio_blk.vq->buffers[(virtio_blk.vq_index + 2) % 256].next = 0;
        virtio_blk.vq->available.ring[vq_idx] = virtio_blk.vq_index % 256;
        virtio_blk.vq_index += 3;
        j = virtio_blk.vq->used.index;
        virtio_blk.vq->available.index++;
        OutU16(virtio_blk.port + VIRTIO_PCI_QUEUE_NOTIFY, 0);
        while (j == virtio_blk.vq->used.index) {
            Yield;
        }
    }
    Free(brq);
    VIOFlush;
    return TRUE;
}

U0 RedSeaTryInit(CDrv* dv)
{
    CRedSeaBoot br;
    Bool unlock;
    try {
        unlock = DrvLock(dv);
        BlkRead(dv, &br, dv->drv_offset, 1);
        if (br.signature != MBR_PT_REDSEA || br.signature2 != 0xAA55)
            return;
        dv->fs_type = FSt_REDSEA;
        CallExtStr("RedSeaFreeFreeLst", dv);
        dv->spc = 1;
        dv->size = br.sects;
        dv->data_area = dv->drv_offset + br.bitmap_sects;
        dv->root_clus = br.root_clus;
        dv->fat1 = dv->fat2 = dv->drv_offset + 1;
        CallExtStr("DrvFATBlkAlloc", dv);
        if (unlock)
            DrvUnlock(dv);
    } catch if (unlock)
        DrvUnlock(dv);
}

U8 MountVirtioBlk()
{ // Mount Virtio-blk device
    CDrv* dv = DrvMakeFreeSlot(DrvNextFreeLet('A'));
    CBlkDev* bd = BlkDevNextFreeSlot(dv->drv_let, BDT_RAM);
    CRedSeaBoot* bs = CAlloc(BLK_SIZE, erythros_mem_task);
    bd->max_blk = 512;
    BlkDevAdd(bd, , TRUE, TRUE);
    bd->type = BDT_VIRTIO_BLK;
    if (VIRTIO_BLK_MAX_BLK) {
        bd->max_blk = Min(VIRTIO_BLK_MAX_BLK, virtio_blk.blks);
    } else {
        bd->max_blk = virtio_blk.blks;
    }
    Free(bd->RAM_dsk);
    dv->size = bd->max_blk + 1 - bd->drv_offset;
    VIORBlks(dv, bs, 0, 1);
    dv->root_clus = bs->root_clus;
    dv->data_area = bs->bitmap_sects;
    dv->next_free = NULL;
    dv->last_free = NULL;
    Free(bs);
    RedSeaTryInit(dv);
    return dv->drv_let;
}

// DskBlk2.HC

Bool BlkRead2(CDrv* dv, U8* buf, I64 blk, I64 cnt)
{ // Read blk cnt from Drv to buf.
    Bool res = TRUE, unlock;
    CBlkDev* bd = dv->bd;
    if (cnt <= 0)
        return TRUE;
    DrvChk(dv);
    try {
        unlock = DrvLock(dv);
        CallExtStr("BlkDevInit", bd);
        if (dv->drv_offset && blk < dv->drv_offset || blk + cnt > dv->drv_offset + dv->size)
            throw('Drv');
        if (bd->flags & BDF_READ_CACHE)
            CallExtStr("RCache", dv, &buf, &blk, &cnt);
        if (cnt > 0) {
            switch (bd->type) {
            case BDT_RAM:
                MemCpy(buf, bd->RAM_dsk + blk << BLK_SIZE_BITS, cnt << BLK_SIZE_BITS);
                break;
            case BDT_ISO_FILE_READ:
            case BDT_ISO_FILE_WRITE:
                FBlkRead(bd->file_dsk, buf, blk, cnt);
                break;
            case BDT_ATA:
            case BDT_ATAPI:
                res = CallExtStr("ATARBlks", dv, buf, blk, cnt);
                break;
            case BDT_VIRTIO_BLK:
                res = VIORBlks(dv, buf, blk, cnt);
                break;
            }
            bd->last_time = tS;
            if (bd->flags & BDF_READ_CACHE)
                CallExtStr("DskCacheAdd", dv, buf, blk, cnt);
        }
        if (unlock)
            DrvUnlock(dv);
    } catch if (unlock)
        DrvUnlock(dv);
    return res;
}

Bool BlkWrite2(CDrv* dv, U8* buf, I64 blk, I64 cnt)
{ // Write blk cnt from buf to Drv.
    Bool res = TRUE, unlock;
    CBlkDev* bd = dv->bd;
    if (cnt <= 0)
        return TRUE;
    DrvChk(dv);
    try {
        unlock = DrvLock(dv);
        CallExtStr("BlkDevInit", bd);
        if (bd->flags & BDF_READ_ONLY && !(bd->flags & BDF_READ_ONLY_OVERRIDE))
            throw('BlkDev');
        if (dv->drv_offset && blk < dv->drv_offset || blk + cnt > dv->drv_offset + dv->size)
            throw('Drv');
        if (cnt > 0) {
            switch (bd->type) {
            case BDT_RAM:
                MemCpy(bd->RAM_dsk + blk << BLK_SIZE_BITS, buf, cnt << BLK_SIZE_BITS);
                break;
            case BDT_ISO_FILE_READ:
            case BDT_ISO_FILE_WRITE:
                FBlkWrite(bd->file_dsk, buf, blk, cnt);
                break;
            case BDT_ATA:
            case BDT_ATAPI:
                res = CallExtStr("ATAWBlks", dv, buf, blk, cnt);
                break;
            case BDT_VIRTIO_BLK:
                res = VIOWBlks(dv, buf, blk, cnt);
                break;
            }
            bd->last_time = tS;
            if (bd->flags & BDF_READ_CACHE)
                CallExtStr("DskCacheAdd", dv, buf, blk, cnt);
        }
        if (unlock)
            DrvUnlock(dv);
    } catch if (unlock)
        DrvUnlock(dv);
    return res;
}

@patch_jmp_rel32(&BlkRead, &BlkRead2);
@patch_jmp_rel32(&BlkWrite, &BlkWrite2);

// DskBlkDev2.HC

CBlkDev* BlkDevChk2(CBlkDev* bd, Bool except = TRUE)
{ // Check for valid BlkDev. Throw exception.
    if (bd->type == BDT_VIRTIO_BLK)
        return bd;
    if (!bd || bd->bd_signature != BD_SIGNATURE_VAL || !(BDT_NULL < bd->type < BDT_TYPES_NUM)) {
        if (except)
            throw('BlkDev');
        else
            return NULL;
    } else
        return bd;
}

@patch_jmp_rel32(&BlkDevChk, &BlkDevChk2);

// DskDrv2.HC

DefineLstLoad("ST_BLKDEV_TYPES2",
    "NULL\0RAM\0ATA\0FILE_READ\0FILE_WRITE\0ATAPI\0NULL\0NULL\0NULL\0NULL\0VIRTIO\0");

U8 DrvTextAttrGet2(U8 drv_let = 0)
{ // Get color of drive.
    U8* blkdev_text_attr2 = blkdev_text_attr;
    U8* drv_text_attr2 = drv_text_attr;
    I64 dta_size = 3;
    drv_let = Let2Let(drv_let);
    if (drv_let == 'A')
        return BLACK << 4 | WHITE;
    if ('A' <= drv_let <= 'Z')
        return blkdev_text_attr2[Let2BlkDevType(drv_let)] << 4 | drv_text_attr2[drv_let % dta_size];
    else
        return BLACK << 4 | WHITE;
}

U0 DrvRep2()
{ // Drive report.
    CDrv* dv;
    CBlkDev* bd;
    I64 ch, i, drv_let, attr;
    U8* st;
    "\nDefined Drives:\n";
    for (i = 0, dv = blkdev.drvs; i < DRVS_NUM; i++, dv++) {
        if (dv->dv_signature == DRV_SIGNATURE_VAL) {
            bd = dv->bd;
            drv_let = Drv2Let(dv);
            if (Bt(&dv->fs_type, FStf_DISABLE))
                ch = '-';
            else if (drv_let == blkdev.boot_drv_let)
                ch = ':';
            else
                ch = '+';
            attr = DrvTextAttrGet(drv_let);
            "\dFG,%d\d\dBG,%d\d%C %-8Z %-10Z %04X %04X %02X\n",
                attr & 15, attr >> 4, drv_let, dv->fs_type &FSG_TYPE_MASK, "ST_DRV_TYPES",
                bd->type, "ST_BLKDEV_TYPES2", bd->base0, bd->base1, bd->unit;
            if (st = DrvModelNum(drv_let)) {
                "Model#:%s\n", st;
                Free(st);
            }
            if (st = DrvSerialNum(drv_let)) {
                "Serial#:%s\n", st;
                Free(st);
            }
            if (bd->type == BDT_ISO_FILE_READ || bd->type == BDT_ISO_FILE_WRITE)
                "File=\"%s\"\n", bd->file_dsk_name;
            "%016X-%016X\n\dFG\d\dBG\d", dv->drv_offset, dv->drv_offset + dv->size - 1;
        }
    }
    "Home Dir:\"%s\"\n", blkdev.home_dir;
}

@patch_jmp_rel32(&DrvTextAttrGet, &DrvTextAttrGet2);
@patch_jmp_rel32(&DrvRep, &DrvRep2);

VirtioBlkInit;
MountVirtioBlk;

if (Let2Drv('A', 0) && !Let2Drv('A')->root_clus) {
    "[virtio-blk] RedSea filesystem not initialized, formatting.\n";
    Fmt('A', , FALSE, FSt_REDSEA);
    Cd("M:/System/");
}

"virtio-blk ";
