use std::ffi::{CStr, CString};

use ash::{
    Device, Entry, Instance,
    vk::{self},
};
use raw_window_handle::{HasDisplayHandle as _, HasWindowHandle as _};
use sdl2::{Sdl, video::Window};

use crate::{APP_NAME, Game, RenderAvailable, RenderCtx, Unstarted};

impl Game<Unstarted> {
    pub fn init_render(self, sdl_context: Sdl, window: Window) -> Game<RenderAvailable> {
        let (width, height) = window.size();

        let entry = Entry::linked();

        let instance = create_instance(&window, &entry);

        let surface = make_surface(&entry, &instance, &window);

        let pdev = find_pdev_dgpu(&instance);

        let queue_idx = find_rcs_queue_inx(&instance, &pdev).expect("No RCS queue found");

        let dev = make_dev(
            &instance,
            &pdev,
            queue_idx,
            vec![c"VK_KHR_swapchain", c"VK_EXT_extended_dynamic_state3"],
        );

        let queue = unsafe { dev.get_device_queue(queue_idx, 0) };

        let cmd_pool = make_command_pool(&dev, queue_idx);

        let cmd_buf = alloc_cmd_buf(&dev, cmd_pool);

        let host_vis_idx =
            find_host_visible(&instance, &pdev).expect("No host visible memory found");

        let host_invis_idx =
            find_host_invisible(&instance, &pdev).expect("No host invisible memory found");

        Game {
            state: RenderAvailable {
                sdl_context,
                window: crate::Window {
                    width,
                    height,
                    _sdl: window,
                },
                ctx: RenderCtx {
                    entry,
                    instance,
                    pdev,
                    dev,
                    surface,
                    queue,
                    queue_idx,
                    cmd_buf,
                    host_vis_idx,
                    host_invis_idx,
                },
            },
        }
    }
}

fn create_instance(window: &Window, entry: &Entry) -> Instance {
    let n_app_name = CString::new(APP_NAME).unwrap();
    let n_engine_name = CString::new(format!("{} Engine", APP_NAME)).unwrap();

    let appinfo = vk::ApplicationInfo::default()
        .application_name(n_app_name.as_c_str())
        .application_version(0)
        .engine_name(n_engine_name.as_c_str())
        .engine_version(0)
        .api_version(vk::make_api_version(0, 1, 3, 0));

    let default_exts = window
        .vulkan_instance_extensions()
        .expect("Failed to get list of required extensions from SDL2");

    let mut exts = vec![];
    exts.extend(default_exts);

    let exts: Vec<*const i8> = exts
        .iter()
        .map(|s| s.as_ptr() as *const i8)
        .collect::<Vec<_>>();
    let exts: &[*const i8] = exts.as_slice();

    let insinfo = vk::InstanceCreateInfo::default()
        .application_info(&appinfo)
        .enabled_extension_names(exts);

    let instance =
        unsafe { entry.create_instance(&insinfo, None) }.expect("Failed to create instance");
    println!("Created instance");
    instance
}

fn make_surface(entry: &Entry, instance: &Instance, window: &Window) -> vk::SurfaceKHR {
    let surface = unsafe {
        ash_window::create_surface(
            &entry,
            &instance,
            window.display_handle().unwrap().as_raw(),
            window.window_handle().unwrap().as_raw(),
            None,
        )
    }
    .expect("Failed to create surface");
    println!("Created surface");
    surface
}

fn find_pdev_dgpu(instance: &Instance) -> vk::PhysicalDevice {
    let mut pdevs =
        unsafe { instance.enumerate_physical_devices() }.expect("Failed to enumerate devices");
    for (i, d) in pdevs.iter().enumerate() {
        let props = unsafe { instance.get_physical_device_properties(*d) };
        if props.device_type == vk::PhysicalDeviceType::DISCRETE_GPU {
            // TODO this assumes that the first discrete GPU will have an RCS queue family
            // This is not guaranteed by the spec (example, a compute-only GPU)
            // Fix.
            println!(
                "Found discrete GPU: {}",
                unsafe { std::str::from_utf8(std::mem::transmute(props.device_name.as_slice())) }
                    .unwrap()
            );
            return pdevs.remove(i);
        }
    }
    println!("No discrete GPU found");
    assert!(pdevs.len() > 0, "No GPU found");
    return pdevs.remove(0);
}

// Using Intel GPU hardware terminology 🥴
fn find_rcs_queue_inx(instance: &Instance, pdev: &vk::PhysicalDevice) -> Option<u32> {
    let qfams = unsafe { instance.get_physical_device_queue_family_properties(*pdev) };
    for (inx, qfam) in qfams.iter().enumerate() {
        if qfam
            .queue_flags
            .contains(vk::QueueFlags::GRAPHICS | vk::QueueFlags::COMPUTE)
        {
            println!("Found RCS queue at index {}", inx);
            return Some(inx as u32);
        }
    }
    println!("No RCS queue found");
    return None;
}

fn make_dev(
    instance: &Instance,
    pdev: &vk::PhysicalDevice,
    rcs_queue_inx: u32,
    exts: Vec<&CStr>,
) -> Device {
    unsafe {
        let dev_queue = vec![
            vk::DeviceQueueCreateInfo::default()
                .queue_family_index(rcs_queue_inx)
                .queue_priorities(&[1.0]),
        ];

        let exts = exts.iter().map(|&s| s.as_ptr()).collect::<Vec<_>>();
        let exts = exts.as_slice();

        // Enable all features
        let features = instance.get_physical_device_features(*pdev);

        let dev_info = vk::DeviceCreateInfo::default()
            .queue_create_infos(&dev_queue)
            .enabled_extension_names(exts)
            .enabled_features(&features);

        let dev = instance
            .create_device(*pdev, &dev_info, None)
            .expect("Failed to create device");
        println!("Created device");
        dev
    }
}

fn make_command_pool(dev: &Device, rcs_queue_inx: u32) -> vk::CommandPool {
    unsafe {
        let pool_info = vk::CommandPoolCreateInfo::default()
            .queue_family_index(rcs_queue_inx)
            .flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER);
        let pool = dev
            .create_command_pool(&pool_info, None)
            .expect("Failed to create command pool");
        println!("Created command pool");
        pool
    }
}

fn alloc_cmd_buf(dev: &Device, pool: vk::CommandPool) -> vk::CommandBuffer {
    unsafe {
        let alloc_info = vk::CommandBufferAllocateInfo::default()
            .command_pool(pool)
            .level(vk::CommandBufferLevel::PRIMARY)
            .command_buffer_count(1);
        let cmd_buf = dev
            .allocate_command_buffers(&alloc_info)
            .expect("Failed to allocate command buffer")
            .remove(0);
        println!("Allocated command buffer");
        cmd_buf
    }
}

fn find_host_visible(instance: &Instance, pdev: &vk::PhysicalDevice) -> Option<u32> {
    let mem_props = unsafe { instance.get_physical_device_memory_properties(*pdev) };

    for (i, mem_type) in mem_props.memory_types.iter().enumerate() {
        if mem_type.property_flags.contains(
            vk::MemoryPropertyFlags::DEVICE_LOCAL
                | vk::MemoryPropertyFlags::HOST_VISIBLE
                | vk::MemoryPropertyFlags::HOST_COHERENT,
        ) {
            println!("Found host visible memory at index {}", i);
            return Some(i as u32);
        }
    }
    println!("No host visible memory found");
    return None;
}

fn find_host_invisible(instance: &Instance, pdev: &vk::PhysicalDevice) -> Option<u32> {
    let mem_props = unsafe { instance.get_physical_device_memory_properties(*pdev) };
    for (i, mem_type) in mem_props.memory_types.iter().enumerate() {
        if mem_type
            .property_flags
            .contains(vk::MemoryPropertyFlags::DEVICE_LOCAL)
            && !mem_type.property_flags.contains(
                vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT,
            )
        {
            println!("Found host invisible memory at index {}", i);
            return Some(i as u32);
        }
    }
    println!("No host invisible memory found");
    return None;
}
