use std::collections::HashMap;
use std::ffi::{CString, c_char};
use std::ops::Deref;
use std::sync::Arc;

use ash::vk;
use parking_lot::Mutex;

use crate::*;

static APPLICATION_NAME: &std::ffi::CStr = c"qdrant";

/// `Instance` is a Vulkan instance wrapper.
/// It's a root structure for all Vulkan operations and provides access the API.
/// It also manages all Vulkan API layers and API extensions.
pub struct Instance {
    /// Vulkan API entry point. It should be kept alive while the instance is alive.
    _entry: ash::Entry,

    /// Native Vulkan instance handle.
    vk_instance: ash::Instance,

    /// List of physical devices found by the Vulkan instance.
    vk_physical_devices: Vec<PhysicalDevice>,

    /// CPU allocator.
    allocation_callbacks: Option<Box<dyn AllocationCallbacks>>,

    /// Validation layer handler.
    vk_debug_utils_loader: Option<ash::ext::debug_utils::Instance>,

    /// Validation layer messenger.
    vk_debug_messenger: vk::DebugUtilsMessengerEXT,

    /// Shader compiler.
    compiler: Mutex<shaderc::Compiler>,

    /// Debug messenger for the instance. It contains validation error callbacks.
    /// Should be kept alive while the instance is alive because it contains raw pointers to callbacks.
    _debug_messenger: Option<Box<dyn DebugMessenger>>,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
/// Hardware type of the physical device.
pub enum PhysicalDeviceType {
    /// Discrete GPU like Nvidia or AMD.
    Discrete,
    /// Integrated graphics like Intel HD Graphics.
    Integrated,
    /// Other types of hardware like software emulated GPU.
    Other,
}

#[derive(Clone)]
pub struct PhysicalDevice {
    pub vk_physical_device: vk::PhysicalDevice,
    pub name: String,
    pub device_type: PhysicalDeviceType,
}

#[derive(Default)]
pub struct InstanceBuilder {
    debug_messenger: Option<Box<dyn DebugMessenger>>,
    allocation_callbacks: Option<Box<dyn AllocationCallbacks>>,
    dump_api: bool,
}

impl InstanceBuilder {
    pub fn new() -> Self {
        Self::default()
    }

    /// Set debug messenger for the instance.
    pub fn with_debug_messenger(mut self, debug_messenger: Box<dyn DebugMessenger>) -> Self {
        self.debug_messenger = Some(debug_messenger);
        self
    }

    /// Set CPU allocation callbacks for the instance.
    pub fn with_allocation_callbacks(
        mut self,
        allocation_callbacks: Box<dyn AllocationCallbacks>,
    ) -> Self {
        self.allocation_callbacks = Some(allocation_callbacks);
        self
    }

    // Enable API dump layer.
    pub fn with_dump_api(mut self, dump_api: bool) -> Self {
        self.dump_api = dump_api;
        self
    }

    pub fn build(self) -> GpuResult<Arc<Instance>> {
        Instance::new(
            self.debug_messenger,
            self.allocation_callbacks,
            self.dump_api,
        )
    }
}

impl Instance {
    pub fn builder() -> InstanceBuilder {
        InstanceBuilder::new()
    }

    fn new(
        debug_messenger: Option<Box<dyn DebugMessenger>>,
        allocation_callbacks: Option<Box<dyn AllocationCallbacks>>,
        dump_api: bool,
    ) -> GpuResult<Arc<Self>> {
        // Create a shader compiler before we start.
        // It's used to compile GLSL into SPIR-V.
        let compiler = Mutex::new(
            shaderc::Compiler::new()
                .map_err(|_| GpuError::Other("Failed to create shaderc compiler".to_string()))?,
        );

        // Create Vulkan API entry point.
        let entry = unsafe {
            ash::Entry::load().map_err(|e| {
                GpuError::Other(format!("Failed to create Vulkan API entry point {e:?}"))
            })?
        };

        // Collect Vulkan application info.
        // It contains application name and required Vulkan API version.
        let app_info = vk::ApplicationInfo::default()
            .application_name(APPLICATION_NAME)
            .application_version(0)
            .engine_name(APPLICATION_NAME)
            .engine_version(0)
            .api_version(vk::make_api_version(0, 1, 3, 0));

        // Collect Vulkan API extensions and convert it in raw pointers.
        let extensions = Self::extensions_list(debug_messenger.is_some());
        // Check presence of all required extensions.
        Self::check_extensions_list(&entry, &extensions)?;
        let extensions_cstr: Vec<CString> = extensions
            .iter()
            .filter_map(|s| CString::new(s.clone().into_bytes()).ok())
            .collect();
        let extension_names_raw: Vec<*const c_char> = extensions_cstr
            .iter()
            .map(|raw_name| raw_name.as_ptr())
            .collect();

        // Collect Vulkan API layers and convert it in raw pointers.
        let layers = Self::layers_list(debug_messenger.is_some(), dump_api);
        // Check presence of all required layers.
        Self::check_layers_list(&entry, &layers)?;
        let layers_cstr: Vec<CString> = layers
            .iter()
            .filter_map(|s| CString::new(s.clone().into_bytes()).ok())
            .collect();
        let layers_raw: Vec<*const c_char> = layers_cstr
            .iter()
            .map(|raw_name| raw_name.as_ptr())
            .collect();

        // If we provide debug messenger, we need to create a debug messenger info.
        let mut debug_utils_create_info = debug_messenger
            .as_deref()
            .map(Self::debug_messenger_create_info);

        let create_flags = if cfg!(any(target_os = "macos")) {
            // On MacOS we need to enable portability extension to enable MoltenVK.
            vk::InstanceCreateFlags::ENUMERATE_PORTABILITY_KHR
        } else {
            vk::InstanceCreateFlags::default()
        };

        // Collect all parameters together and create Vulkan instance.
        let mut create_info = vk::InstanceCreateInfo::default()
            .flags(create_flags)
            .application_info(&app_info)
            .enabled_layer_names(&layers_raw)
            .enabled_extension_names(&extension_names_raw);

        if let Some(debug_utils_create_info) = &mut debug_utils_create_info {
            create_info = create_info.push_next(debug_utils_create_info);
        }

        // Get CPU allocation callbacks if they are provided.
        let vk_allocation_callbacks = allocation_callbacks
            .as_ref()
            .map(|a| a.allocation_callbacks());

        // Finally, create Vulkan instance.
        let vk_instance: ash::Instance =
            unsafe { entry.create_instance(&create_info, vk_allocation_callbacks)? };

        // Find all available physical GPU devices.
        let vk_physical_devices_result = unsafe { vk_instance.enumerate_physical_devices() };
        let vk_physical_devices = match vk_physical_devices_result {
            Ok(vk_physical_devices) => vk_physical_devices,
            Err(e) => {
                // Don't forget to destroy the instance if we failed to find any physical devices.
                unsafe {
                    vk_instance.destroy_instance(vk_allocation_callbacks);
                }
                return Err(GpuError::from(e));
            }
        };

        let vk_physical_devices = vk_physical_devices
            .iter()
            .map(|&vk_physical_device| {
                let device_properties =
                    unsafe { vk_instance.get_physical_device_properties(vk_physical_device) };
                let device_name =
                    unsafe { ::std::ffi::CStr::from_ptr(device_properties.device_name.as_ptr()) };
                let device_name = device_name.to_str().unwrap_or("Unnamed GPU").to_owned();

                let device_type = match device_properties.device_type {
                    vk::PhysicalDeviceType::DISCRETE_GPU => PhysicalDeviceType::Discrete,
                    vk::PhysicalDeviceType::INTEGRATED_GPU => PhysicalDeviceType::Integrated,
                    _ => PhysicalDeviceType::Other,
                };

                log::info!("Found GPU device: {device_name}");
                PhysicalDevice {
                    vk_physical_device,
                    name: device_name,
                    device_type,
                }
            })
            .collect::<Vec<_>>();

        if vk_physical_devices.is_empty() {
            // Don't forget to destroy the instance if we failed to find any physical devices.
            unsafe {
                vk_instance.destroy_instance(vk_allocation_callbacks);
            }
            return Err(GpuError::Other(
                "No Vulkan physical devices found".to_string(),
            ));
        }

        // If we have a debug messenger, we need to create it.
        let (vk_debug_utils_loader, vk_debug_messenger) = if let Some(debug_messenger) =
            debug_messenger.as_ref()
        {
            let debug_utils_loader = ash::ext::debug_utils::Instance::new(&entry, &vk_instance);
            let messenger_create_info = Self::debug_messenger_create_info(debug_messenger.deref());
            let utils_messenger_result = unsafe {
                debug_utils_loader
                    .create_debug_utils_messenger(&messenger_create_info, vk_allocation_callbacks)
            };

            let utils_messenger = match utils_messenger_result {
                Ok(messenger) => messenger,
                Err(e) => {
                    // Don't forget to destroy the instance if we failed to create a debug messenger.
                    unsafe {
                        vk_instance.destroy_instance(vk_allocation_callbacks);
                    }
                    return Err(GpuError::from(e));
                }
            };
            (Some(debug_utils_loader), utils_messenger)
        } else {
            (None, vk::DebugUtilsMessengerEXT::null())
        };

        Ok(Arc::new(Self {
            _entry: entry,
            vk_instance,
            vk_physical_devices,
            allocation_callbacks,
            vk_debug_utils_loader,
            vk_debug_messenger,
            compiler,
            _debug_messenger: debug_messenger,
        }))
    }

    fn debug_messenger_create_info(
        debug_messenger: &dyn DebugMessenger,
    ) -> vk::DebugUtilsMessengerCreateInfoEXT<'_> {
        vk::DebugUtilsMessengerCreateInfoEXT::default()
            .flags(vk::DebugUtilsMessengerCreateFlagsEXT::empty())
            .message_severity(debug_messenger.severity_flags())
            .message_type(debug_messenger.message_type_flags())
            .pfn_user_callback(debug_messenger.callback())
    }

    pub fn cpu_allocation_callbacks(&self) -> Option<&vk::AllocationCallbacks<'_>> {
        self.allocation_callbacks
            .as_ref()
            .map(|alloc| alloc.allocation_callbacks())
    }

    pub fn vk_instance(&self) -> &ash::Instance {
        &self.vk_instance
    }

    pub fn physical_devices(&self) -> &[PhysicalDevice] {
        &self.vk_physical_devices
    }

    pub fn compile_shader(
        &self,
        shader: &str,
        shader_name: &str,
        defines: Option<&HashMap<String, Option<String>>>,
        includes: Option<&HashMap<String, String>>,
    ) -> GpuResult<Vec<u8>> {
        let mut options = shaderc::CompileOptions::new()
            .map_err(|_| GpuError::Other("Failed to create shaderc compile options".to_string()))?;
        options.set_optimization_level(shaderc::OptimizationLevel::Performance);
        options.set_target_env(
            shaderc::TargetEnv::Vulkan,
            shaderc::EnvVersion::Vulkan1_3 as u32,
        );
        options.set_target_spirv(shaderc::SpirvVersion::V1_3);

        if let Some(defines) = defines {
            for (define, value) in defines {
                match value {
                    Some(value) => {
                        options.add_macro_definition(define, Some(value));
                    }
                    None => {
                        options.add_macro_definition(define, None);
                    }
                }
            }
        }

        if let Some(includes) = includes {
            options.set_include_callback(|filename, _, _, _| {
                if let Some(code) = includes.get(filename) {
                    Ok(shaderc::ResolvedInclude {
                        resolved_name: filename.to_string(),
                        content: code.to_owned(),
                    })
                } else {
                    Err(format!("Include file not found: {filename}"))
                }
            });
        }

        let compiler = self.compiler.lock();
        let result = compiler
            .compile_into_spirv(
                shader,
                shaderc::ShaderKind::Compute,
                shader_name,
                "main",
                Some(&options),
            )
            .map_err(|e| GpuError::Other(format!("Failed to compile shader: {e:?}")))?;
        Ok(result.as_binary_u8().to_owned())
    }

    fn layers_list(validation: bool, dump_api: bool) -> Vec<String> {
        let mut result = Vec::new();
        if validation {
            result.push("VK_LAYER_KHRONOS_validation".to_owned());
        }
        if dump_api {
            result.push("VK_LAYER_LUNARG_api_dump".to_owned());
        }
        result
    }

    fn extensions_list(validation: bool) -> Vec<String> {
        let mut extensions_list = Vec::new();
        if validation && let Ok(ext) = ash::ext::debug_utils::NAME.to_str() {
            extensions_list.push(ext.to_string());
        }

        #[cfg(target_os = "macos")]
        {
            if let Ok(ext) = ash::khr::portability_enumeration::NAME.to_str() {
                extensions_list.push(ext.to_string());
            }
            if let Ok(ext) = ash::khr::get_physical_device_properties2::NAME.to_str() {
                extensions_list.push(ext.to_string());
            }
        }
        extensions_list
    }

    fn check_extensions_list(entry: &ash::Entry, extensions: &[String]) -> GpuResult<()> {
        let extension_properties = unsafe { entry.enumerate_instance_extension_properties(None)? };
        for extension in extensions {
            let extension_found = extension_properties.iter().any(|ep| {
                let name = unsafe { ::std::ffi::CStr::from_ptr(ep.extension_name.as_ptr()) };
                name.to_str().unwrap_or("") == extension
            });
            if !extension_found {
                return Err(GpuError::Other(format!("Extension {extension} not found")));
            }
        }
        Ok(())
    }

    fn check_layers_list(entry: &ash::Entry, layers: &[String]) -> GpuResult<()> {
        let layer_properties = unsafe { entry.enumerate_instance_layer_properties()? };
        for layer in layers {
            let layer_found = layer_properties.iter().any(|lp| {
                let name = unsafe { ::std::ffi::CStr::from_ptr(lp.layer_name.as_ptr()) };
                name.to_str().unwrap_or("") == layer
            });
            if !layer_found {
                return Err(GpuError::Other(format!("Layer {layer} not found")));
            }
        }
        Ok(())
    }
}

impl Drop for Instance {
    fn drop(&mut self) {
        let allocation_callbacks = self.cpu_allocation_callbacks();
        unsafe {
            // Destroy first debug messenger if it's present.
            if let Some(loader) = &self.vk_debug_utils_loader
                && self.vk_debug_messenger != vk::DebugUtilsMessengerEXT::null()
            {
                loader.destroy_debug_utils_messenger(self.vk_debug_messenger, allocation_callbacks);
            }

            // Last step after all drops of all GPU resources: destroy vulkan instance.
            self.vk_instance.destroy_instance(allocation_callbacks);
        }
    }
}
