use alloc::alloc::{GlobalAlloc, Layout};
use alloc::sync::Arc;
use core::slice;

use bitflags::*;
use device_tree::Node;
use device_tree::util::SliceRead;
use log::*;
use rcore_memory::PAGE_SIZE;
use rcore_memory::paging::PageTable;
use volatile::{ReadOnly, Volatile, WriteOnly};

use crate::arch::cpu;
use crate::HEAP_ALLOCATOR;
use crate::memory::active_table;
use crate::arch::consts::{KERNEL_OFFSET, MEMORY_OFFSET};
use crate::sync::SpinNoIrqLock as Mutex;

use super::super::{DeviceType, Driver, DRIVERS};
use super::super::bus::virtio_mmio::*;
use super::test::mandelbrot;

const VIRTIO_GPU_EVENT_DISPLAY : u32 = 1 << 0;

struct VirtIOGpu {
    interrupt_parent: u32,
    interrupt: u32,
    header: &'static mut VirtIOHeader,
    queue_buffer: [usize; 2],
    frame_buffer: usize,
    rect: VirtIOGpuRect,
    queues: [VirtIOVirtqueue; 2]
}

#[repr(C)]
#[derive(Debug)]
struct VirtIOGpuConfig {
    events_read: ReadOnly<u32>,
    events_clear: WriteOnly<u32>,
    num_scanouts: Volatile<u32>
}

bitflags! {
    struct VirtIOGpuFeature : u64 {
        const VIRGL = 1 << 0;
        const EDID = 1 << 1;
        // device independent
        const NOTIFY_ON_EMPTY = 1 << 24; // legacy
        const ANY_LAYOUT = 1 << 27; // legacy
        const RING_INDIRECT_DESC = 1 << 28;
        const RING_EVENT_IDX = 1 << 29;
        const UNUSED = 1 << 30; // legacy
        const VERSION_1 = 1 << 32; // detect legacy
        const ACCESS_PLATFORM = 1 << 33; // since virtio v1.1
        const RING_PACKED = 1 << 34; // since virtio v1.1
        const IN_ORDER = 1 << 35; // since virtio v1.1
        const ORDER_PLATFORM = 1 << 36; // since virtio v1.1
        const SR_IOV = 1 << 37; // since virtio v1.1
        const NOTIFICATION_DATA = 1 << 38; // since virtio v1.1
    }
}

const VIRTIO_GPU_CMD_GET_DISPLAY_INFO : u32 = 0x100;
const VIRTIO_GPU_CMD_RESOURCE_CREATE_2D : u32 = 0x101;
const VIRTIO_GPU_CMD_RESOURCE_UNREF : u32 = 0x102;
const VIRTIO_GPU_CMD_SET_SCANOUT : u32 = 0x103;
const VIRTIO_GPU_CMD_RESOURCE_FLUSH : u32 = 0x104;
const VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D : u32 = 0x105;
const VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING : u32 = 0x106;
const VIRTIO_GPU_CMD_RESOURCE_DETACH_BACKING : u32 = 0x107;
const VIRTIO_GPU_CMD_GET_CAPSET_INFO : u32 = 0x108;
const VIRTIO_GPU_CMD_GET_CAPSET : u32 = 0x109;
const VIRTIO_GPU_CMD_GET_EDID : u32 = 0x10a;

const VIRTIO_GPU_CMD_UPDATE_CURSOR : u32 = 0x300;
const VIRTIO_GPU_CMD_MOVE_CURSOR : u32 = 0x301;

const VIRTIO_GPU_RESP_OK_NODATA : u32 = 0x1100;
const VIRTIO_GPU_RESP_OK_DISPLAY_INFO : u32 = 0x1101;
const VIRTIO_GPU_RESP_OK_CAPSET_INFO : u32 = 0x1102;
const VIRTIO_GPU_RESP_OK_CAPSET : u32 = 0x1103;
const VIRTIO_GPU_RESP_OK_EDID : u32 = 0x1104;

const VIRTIO_GPU_RESP_ERR_UNSPEC : u32 = 0x1200;
const VIRTIO_GPU_RESP_ERR_OUT_OF_MEMORY : u32 = 0x1201;
const VIRTIO_GPU_RESP_ERR_INVALID_SCANOUT_ID : u32 = 0x1202;

const VIRTIO_GPU_FLAG_FENCE : u32 = 1 << 0;

#[repr(C)]
#[derive(Debug)]
struct VirtIOGpuCtrlHdr {
    hdr_type: u32,
    flags: u32,
    fence_id: u64,
    ctx_id: u32,
    padding: u32
}

impl VirtIOGpuCtrlHdr {
    fn with_type(hdr_type: u32) -> VirtIOGpuCtrlHdr {
        VirtIOGpuCtrlHdr {
            hdr_type,
            flags: 0,
            fence_id: 0,
            ctx_id: 0,
            padding: 0
        }
    }
}

#[repr(C)]
#[derive(Debug, Copy, Clone, Default)]
struct VirtIOGpuRect {
    x: u32,
    y: u32,
    width: u32,
    height: u32
}

#[repr(C)]
#[derive(Debug)]
struct VirtIOGpuRespDisplayInfo {
    header: VirtIOGpuCtrlHdr,
    rect: VirtIOGpuRect,
    enabled: u32,
    flags: u32
}

const VIRTIO_GPU_FORMAT_B8G8R8A8_UNORM: u32 = 1;

#[repr(C)]
#[derive(Debug)]
struct VirtIOGpuResourceCreate2D {
    header: VirtIOGpuCtrlHdr,
    resource_id: u32,
    format: u32,
    width: u32,
    height: u32
}

#[repr(C)]
#[derive(Debug)]
struct VirtIOGpuResourceAttachBacking {
    header: VirtIOGpuCtrlHdr,
    resource_id: u32,
    nr_entries: u32, // always 1
    addr: u64,
    length: u32,
    padding: u32
}

#[repr(C)]
#[derive(Debug)]
struct VirtIOGpuSetScanout {
    header: VirtIOGpuCtrlHdr,
    rect: VirtIOGpuRect,
    scanout_id: u32,
    resource_id: u32
}

#[repr(C)]
#[derive(Debug)]
struct VirtIOGpuTransferToHost2D {
    header: VirtIOGpuCtrlHdr,
    rect: VirtIOGpuRect,
    offset: u64,
    resource_id: u32,
    padding: u32
}

#[repr(C)]
#[derive(Debug)]
struct VirtIOGpuResourceFlush {
    header: VirtIOGpuCtrlHdr,
    rect: VirtIOGpuRect,
    resource_id: u32,
    padding: u32
}

const VIRTIO_QUEUE_TRANSMIT: usize = 0;
const VIRTIO_QUEUE_CURSOR: usize = 1;

const VIRTIO_BUFFER_TRANSMIT: usize = 0;
const VIRTIO_BUFFER_RECEIVE: usize = 1;

const VIRTIO_GPU_RESOURCE_ID: u32 = 0xbabe;

pub struct VirtIOGpuDriver(Mutex<VirtIOGpu>);

impl Driver for VirtIOGpuDriver {
    fn try_handle_interrupt(&self, _irq: Option<u32>) -> bool {
        // for simplicity
        if cpu::id() > 0 {
            return false
        }

        let mut driver = self.0.lock();

        // ensure header page is mapped
        // TODO: this should be mapped in all page table by default
        let header_addr = &mut driver.header as *mut _ as usize;
        active_table().map_if_not_exists(header_addr, header_addr);

        let interrupt = driver.header.interrupt_status.read();
        if interrupt != 0 {
            driver.header.interrupt_ack.write(interrupt);
            debug!("Got interrupt {:?}", interrupt);
            return true;
        }
        return false;
    }

    fn device_type(&self) -> DeviceType {
        DeviceType::Gpu
    }
}

fn request(driver: &mut VirtIOGpu) {
    let input = unsafe { slice::from_raw_parts(driver.queue_buffer[VIRTIO_BUFFER_RECEIVE] as *const u8, PAGE_SIZE) };
    let output = unsafe { slice::from_raw_parts(driver.queue_buffer[VIRTIO_BUFFER_TRANSMIT] as *const u8, PAGE_SIZE) };
    driver.queues[VIRTIO_QUEUE_TRANSMIT].add_and_notify(&[input], &[output], 0);
}

fn setup_framebuffer(driver: &mut VirtIOGpu) {
    // get display info
    let request_get_display_info = unsafe { &mut *(driver.queue_buffer[VIRTIO_BUFFER_TRANSMIT] as *mut VirtIOGpuCtrlHdr) };
    *request_get_display_info = VirtIOGpuCtrlHdr::with_type(VIRTIO_GPU_CMD_GET_DISPLAY_INFO);
    request(driver);
    driver.queues[VIRTIO_QUEUE_TRANSMIT].get_block();
    let response_get_display_info = unsafe { &mut *(driver.queue_buffer[VIRTIO_BUFFER_RECEIVE] as *mut VirtIOGpuRespDisplayInfo) };
    info!("response: {:?}", response_get_display_info);
    driver.rect = response_get_display_info.rect;

    // create resource 2d
    let request_resource_create_2d = unsafe { &mut *(driver.queue_buffer[VIRTIO_BUFFER_TRANSMIT] as *mut VirtIOGpuResourceCreate2D) };
    *request_resource_create_2d = VirtIOGpuResourceCreate2D {
        header: VirtIOGpuCtrlHdr::with_type(VIRTIO_GPU_CMD_RESOURCE_CREATE_2D),
        resource_id: VIRTIO_GPU_RESOURCE_ID,
        format: VIRTIO_GPU_FORMAT_B8G8R8A8_UNORM,
        width: response_get_display_info.rect.width,
        height: response_get_display_info.rect.height
    };
    request(driver);
    driver.queues[VIRTIO_QUEUE_TRANSMIT].get_block();
    let response_resource_create_2d = unsafe { &mut *(driver.queue_buffer[VIRTIO_BUFFER_RECEIVE] as *mut VirtIOGpuCtrlHdr) };
    info!("response: {:?}", response_resource_create_2d);

    // alloc continuous pages for the frame buffer
    let size = response_get_display_info.rect.width * response_get_display_info.rect.height * 4;
    let frame_buffer = unsafe {
        HEAP_ALLOCATOR.alloc_zeroed(Layout::from_size_align(size as usize, PAGE_SIZE).unwrap())
    } as usize;
    mandelbrot(driver.rect.width, driver.rect.height, frame_buffer as *mut u32);
    driver.frame_buffer = frame_buffer;
    let request_resource_attach_backing = unsafe { &mut *(driver.queue_buffer[VIRTIO_BUFFER_TRANSMIT] as *mut VirtIOGpuResourceAttachBacking) };
    *request_resource_attach_backing = VirtIOGpuResourceAttachBacking {
        header: VirtIOGpuCtrlHdr::with_type(VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING),
        resource_id: VIRTIO_GPU_RESOURCE_ID,
        nr_entries: 1,
        addr: (frame_buffer - KERNEL_OFFSET + MEMORY_OFFSET) as u64,
        length: size,
        padding: 0
    };
    request(driver);
    driver.queues[VIRTIO_QUEUE_TRANSMIT].get_block();
    let response_resource_attach_backing = unsafe { &mut *(driver.queue_buffer[VIRTIO_BUFFER_RECEIVE] as *mut VirtIOGpuCtrlHdr) };
    info!("response: {:?}", response_resource_attach_backing);

    // map frame buffer to screen
    let request_set_scanout = unsafe { &mut *(driver.queue_buffer[VIRTIO_BUFFER_TRANSMIT] as *mut VirtIOGpuSetScanout) };
    *request_set_scanout = VirtIOGpuSetScanout {
        header: VirtIOGpuCtrlHdr::with_type(VIRTIO_GPU_CMD_SET_SCANOUT),
        rect: response_get_display_info.rect,
        scanout_id: 0,
        resource_id: VIRTIO_GPU_RESOURCE_ID
    };
    request(driver);
    driver.queues[VIRTIO_QUEUE_TRANSMIT].get_block();
    let response_set_scanout = unsafe { &mut *(driver.queue_buffer[VIRTIO_BUFFER_RECEIVE] as *mut VirtIOGpuCtrlHdr) };
    info!("response: {:?}", response_set_scanout);

    flush_frame_buffer_to_screen(driver);
}

fn flush_frame_buffer_to_screen(driver: &mut VirtIOGpu) {
    // copy data from guest to host
    let request_transfer_to_host_2d = unsafe { &mut *(driver.queue_buffer[VIRTIO_BUFFER_TRANSMIT] as *mut VirtIOGpuTransferToHost2D) };
    *request_transfer_to_host_2d = VirtIOGpuTransferToHost2D {
        header: VirtIOGpuCtrlHdr::with_type(VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D),
        rect: driver.rect,
        offset: 0,
        resource_id: VIRTIO_GPU_RESOURCE_ID,
        padding: 0
    };
    request(driver);
    driver.queues[VIRTIO_QUEUE_TRANSMIT].get_block();
    let response_transfer_to_host_2d = unsafe { &mut *(driver.queue_buffer[VIRTIO_BUFFER_RECEIVE] as *mut VirtIOGpuCtrlHdr) };
    info!("response: {:?}", response_transfer_to_host_2d);

    // flush data to screen
    let request_resource_flush = unsafe { &mut *(driver.queue_buffer[VIRTIO_BUFFER_TRANSMIT] as *mut VirtIOGpuResourceFlush) };
    *request_resource_flush = VirtIOGpuResourceFlush {
        header: VirtIOGpuCtrlHdr::with_type(VIRTIO_GPU_CMD_RESOURCE_FLUSH),
        rect: driver.rect,
        resource_id: VIRTIO_GPU_RESOURCE_ID,
        padding: 0
    };
    request(driver);
    driver.queues[VIRTIO_QUEUE_TRANSMIT].get_block();
    let response_resource_flush = unsafe { &mut *(driver.queue_buffer[VIRTIO_BUFFER_RECEIVE] as *mut VirtIOGpuCtrlHdr) };
    info!("response: {:?}", response_resource_flush);
}

pub fn virtio_gpu_init(node: &Node) {
    let reg = node.prop_raw("reg").unwrap();
    let from = reg.as_slice().read_be_u64(0).unwrap();
    let header = unsafe { &mut *(from as *mut VirtIOHeader) };

    header.status.write(VirtIODeviceStatus::DRIVER.bits());

    let device_features_bits = header.read_device_features();
    let device_features = VirtIOGpuFeature::from_bits_truncate(device_features_bits);
    info!("Device features {:?}", device_features);

    // negotiate these flags only
    let supported_features = VirtIOGpuFeature::empty();
    let driver_features = (device_features & supported_features).bits();
    header.write_driver_features(driver_features);

    // read configuration space
    let config = unsafe { &mut *((from + VIRTIO_CONFIG_SPACE_OFFSET) as *mut VirtIOGpuConfig) };
    info!("Config: {:?}", config);

    // virtio 4.2.4 Legacy interface
    // configure two virtqueues: ingress and egress
    header.guest_page_size.write(PAGE_SIZE as u32); // one page

    let queue_num = 2;
    let queues = [
        VirtIOVirtqueue::new(header, VIRTIO_QUEUE_TRANSMIT, queue_num),
        VirtIOVirtqueue::new(header, VIRTIO_QUEUE_CURSOR, queue_num)
    ];
    let mut driver = VirtIOGpu {
        interrupt: node.prop_u32("interrupts").unwrap(),
        interrupt_parent: node.prop_u32("interrupt-parent").unwrap(),
        header,
        queue_buffer: [0, 0],
        frame_buffer: 0,
        rect: VirtIOGpuRect::default(),
        queues,
    };

    for buffer in 0..2 {
        // allocate a page for each buffer
        let page = unsafe {
            HEAP_ALLOCATOR.alloc_zeroed(Layout::from_size_align(PAGE_SIZE, PAGE_SIZE).unwrap())
        } as usize;
        driver.queue_buffer[buffer as usize] = page;
        debug!("buffer {} using page address {:#X}", buffer, page as usize);
    }

    driver.header.status.write(VirtIODeviceStatus::DRIVER_OK.bits());

    setup_framebuffer(&mut driver);

    let driver = Arc::new(VirtIOGpuDriver(Mutex::new(driver)));
    DRIVERS.write().push(driver);
}