"use strict";

// https://docs.oasis-open.org/virtio/virtio/v1.2/csd01/virtio-v1.2-csd01.html#x1-2900003


const VIRTIO_NET_F_MAC = 5;
const VIRTIO_NET_F_CTRL_VQ = 17;
const VIRTIO_NET_F_STATUS = 16;
const VIRTIO_NET_F_MQ = 22;
const VIRTIO_NET_F_CTRL_MAC_ADDR = 23;
const VIRTIO_NET_F_MTU = 3;

const VIRTIO_NET_CTRL_MQ_VQ_PAIRS_SET = 0;
const VIRTIO_NET_CTRL_MAC_ADDR_SET = 1;

/**
 * @constructor
 * @param {CPU} cpu
 * @param {BusConnector} bus
 * @param {Boolean} preserve_mac_from_state_image
 */
function VirtioNet(cpu, bus, preserve_mac_from_state_image)
{
    /** @const @type {BusConnector} */
    this.bus = bus;
    this.id = cpu.devices.net ? 1 : 0;
    this.pairs = 1;
    this.status = 1;
    this.preserve_mac_from_state_image = preserve_mac_from_state_image;
    this.mac = new Uint8Array([
        0x00, 0x22, 0x15,
        Math.random() * 255 | 0,
        Math.random() * 255 | 0,
        Math.random() * 255 | 0,
    ]);

    this.bus.send("net" + this.id + "-mac", format_mac(this.mac));

    const queues = [];

    for(let i = 0; i < this.pairs; ++i)
    {
        queues.push({size_supported: 32, notify_offset: 0});
        queues.push({size_supported: 32, notify_offset: 1});
    }
    queues.push({
        size_supported: 16,
        notify_offset: 2,
    });

    /** @type {VirtIO} */
    this.virtio = new VirtIO(cpu,
    {
        name: "virtio-net",
        pci_id: 0x0A << 3,
        device_id: 0x1041,
        subsystem_device_id: 1,
        common:
        {
            initial_port: 0xC800,
            queues: queues,
            features:
            [
                VIRTIO_NET_F_MAC,
                VIRTIO_NET_F_STATUS,
                VIRTIO_NET_F_MQ,
                VIRTIO_NET_F_MTU,
                VIRTIO_NET_F_CTRL_VQ,
                VIRTIO_NET_F_CTRL_MAC_ADDR,
                VIRTIO_F_VERSION_1,
            ],
            on_driver_ok: () => {},
        },
        notification:
        {
            initial_port: 0xC900,
            single_handler: false,
            handlers:
            [
                (queue_id) =>
                {
                    // TODO: Full buffer looks like an empty buffer so prevent it from filling
                    // The kernel gives us a prefilled one, so throw the first bufchain so
                    // it doesnt look filled.

                    const queue = this.virtio.queues[queue_id];

                    const desc_idx = queue.avail_get_entry(queue.avail_last_idx);
                    const bufchain = new VirtQueueBufferChain(queue, desc_idx);
                    queue.avail_last_idx = queue.avail_last_idx + 1 & queue.mask;
                    this.virtio.queues[0].push_reply(bufchain);
                    this.virtio.queues[0].flush_replies();
                },
                (queue_id) =>
                {
                    const queue = this.virtio.queues[queue_id];

                    while(queue.has_request())
                    {
                        const bufchain = queue.pop_request();
                        const buffer = new Uint8Array(bufchain.length_readable);
                        bufchain.get_next_blob(buffer);
                        this.bus.send("net" + this.id + "-send", buffer.subarray(12));
                        this.bus.send("eth-transmit-end", [buffer.length - 12]);
                        this.virtio.queues[queue_id].push_reply(bufchain);
                    }
                    this.virtio.queues[queue_id].flush_replies();
                },
                (queue_id) =>
                {
                    if(queue_id !== this.pairs * 2)
                    {
                        dbg_assert(false, "VirtioConsole Notified for wrong queue: " + queue_id +
                            " (expected queue_id of 3)");
                        return;
                    }
                    const queue = this.virtio.queues[queue_id];

                    while(queue.has_request())
                    {
                        const bufchain = queue.pop_request();
                        const buffer = new Uint8Array(bufchain.length_readable);
                        bufchain.get_next_blob(buffer);


                        const parts = marshall.Unmarshall(["b", "b"], buffer, { offset : 0 });
                        const xclass = parts[0];
                        const command = parts[1];


                        //this.Ack(queue_id, bufchain);

                        switch(xclass << 8 | command) {
                            case 4 << 8 | VIRTIO_NET_CTRL_MQ_VQ_PAIRS_SET:
                                const data =  marshall.Unmarshall(["h"], buffer, { offset : 2 });
                                dbg_assert(data[0] === 1);
                                this.Send(queue_id, bufchain, new Uint8Array([0]));
                                break;
                            case 1 << 8 | VIRTIO_NET_CTRL_MAC_ADDR_SET:
                                this.mac = buffer.subarray(2, 8);
                                this.Send(queue_id, bufchain, new Uint8Array([0]));
                                this.bus.send("net" + this.id + "-mac", format_mac(this.mac));
                                break;
                            default:
                                dbg_assert(false," VirtioConsole received unknown command: " + xclass + ":" + command);
                                this.Send(queue_id, bufchain, new Uint8Array([1]));
                                return;

                        }
                    }
                },
            ],
        },
        isr_status:
        {
            initial_port: 0xC700,
        },
        device_specific:
        {
            initial_port: 0xC600,
            struct:
            [0,1,2,3,4,5].map((v,k) => ({
                bytes: 1,
                name: "mac_" + k,
                read: () => this.mac[k],
                write: data => { /* read only */ },
            })).concat(
            [
                {
                    bytes: 2,
                    name: "status",
                    read: () => this.status,
                    write: data => { /* read only */ },
                },
                {
                    bytes: 2,
                    name: "max_pairs",
                    read: () => this.pairs,
                    write: data => { /* read only */ },
                },
                {
                    bytes: 2,
                    name: "mtu",
                    read: () => 1500,
                    write: data => {},
                }
           ])
        },
    });

    this.bus.register("net" + this.id + "-receive", data => {
        this.bus.send("eth-receive-end", [data.length]);
        const with_header = new Uint8Array(12 + data.byteLength);
        const view = new DataView(with_header.buffer, with_header.byteOffset, with_header.byteLength);
        view.setInt16(10, 1);
        with_header.set(data, 12);

        const queue = this.virtio.queues[0];
        if(queue.has_request()) {
            const bufchain = queue.pop_request();
            bufchain.set_next_blob(with_header);
            this.virtio.queues[0].push_reply(bufchain);
            this.virtio.queues[0].flush_replies();
        } else {
            console.log("No buffer to write into!");
        }
    }, this);

}


VirtioNet.prototype.get_state = function()
{
    const state = [];
    state[0] = this.virtio;
    state[1] = this.id;

    if(this.preserve_mac_from_state_image)
    {
        this.mac = state[2];
        this.bus.send("net" + this.id + "-mac", format_mac(this.mac));
    }

    return state;
};

VirtioNet.prototype.set_state = function(state)
{
    this.virtio.set_state(state[0]);
};

VirtioNet.prototype.reset = function() {
    this.virtio.reset();
};

VirtioNet.prototype.Send = function (queue_id, bufchain, blob)
{
    bufchain.set_next_blob(blob);
    this.virtio.queues[queue_id].push_reply(bufchain);
    this.virtio.queues[queue_id].flush_replies();
};

VirtioNet.prototype.Ack = function (queue_id, bufchain)
{
    //bufchain.set_next_blob(new Uint8Array(0));
    this.virtio.queues[queue_id].push_reply(bufchain);
    this.virtio.queues[queue_id].flush_replies();
};
