// Copyright (c) 2016 The vulkano developers
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT
// license <LICENSE-MIT or https://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.

//! Vulkan library loading system.
//!
//! Before Vulkano can do anything, it first needs to find a library containing an implementation
//! of Vulkan. A Vulkan implementation is defined as a single `vkGetInstanceProcAddr` function,
//! which can be accessed through the `Loader` trait.
//!
//! This module provides various implementations of the `Loader` trait.
//!
//! Once you have a type that implements `Loader`, you can create a `VulkanLibrary`
//! from it and use this `VulkanLibrary` struct to build an `Instance`.

pub use crate::fns::EntryFunctions;
use crate::{
    instance::{InstanceExtensions, LayerProperties},
    ExtensionProperties, OomError, SafeDeref, Version, VulkanError,
};
use libloading::{Error as LibloadingError, Library};
use std::{
    error::Error,
    ffi::{CStr, CString},
    fmt::{Debug, Display, Error as FmtError, Formatter},
    mem::transmute,
    os::raw::c_char,
    path::Path,
    ptr,
    sync::Arc,
};

/// A loaded library containing a valid Vulkan implementation.
#[derive(Debug)]
pub struct VulkanLibrary {
    loader: Box<dyn Loader>,
    fns: EntryFunctions,

    api_version: Version,
    extension_properties: Vec<ExtensionProperties>,
    supported_extensions: InstanceExtensions,
}

impl VulkanLibrary {
    /// Loads the default Vulkan library for this system.
    pub fn new() -> Result<Arc<Self>, LoadingError> {
        #[cfg(target_os = "ios")]
        #[allow(non_snake_case)]
        fn def_loader_impl() -> Result<Box<dyn Loader>, LoadingError> {
            let loader = crate::statically_linked_vulkan_loader!();

            Ok(Box::new(loader))
        }

        #[cfg(not(target_os = "ios"))]
        fn def_loader_impl() -> Result<Box<dyn Loader>, LoadingError> {
            #[cfg(windows)]
            fn get_path() -> &'static Path {
                Path::new("vulkan-1.dll")
            }
            #[cfg(all(unix, not(target_os = "android"), not(target_os = "macos")))]
            fn get_path() -> &'static Path {
                Path::new("libvulkan.so.1")
            }
            #[cfg(target_os = "macos")]
            fn get_path() -> &'static Path {
                Path::new("libvulkan.1.dylib")
            }
            #[cfg(target_os = "android")]
            fn get_path() -> &'static Path {
                Path::new("libvulkan.so")
            }

            let loader = unsafe { DynamicLibraryLoader::new(get_path())? };

            Ok(Box::new(loader))
        }

        def_loader_impl().and_then(VulkanLibrary::with_loader)
    }

    /// Loads a custom Vulkan library.
    pub fn with_loader(loader: impl Loader + 'static) -> Result<Arc<Self>, LoadingError> {
        let fns = EntryFunctions::load(|name| unsafe {
            loader
                .get_instance_proc_addr(ash::vk::Instance::null(), name.as_ptr())
                .map_or(ptr::null(), |func| func as _)
        });

        let api_version = unsafe { Self::get_api_version(&loader)? };
        let extension_properties = unsafe { Self::get_extension_properties(&fns, None)? };
        let supported_extensions = extension_properties
            .iter()
            .map(|property| property.extension_name.as_str())
            .collect();

        Ok(Arc::new(VulkanLibrary {
            loader: Box::new(loader),
            fns,
            api_version,
            extension_properties,
            supported_extensions,
        }))
    }

    unsafe fn get_api_version(loader: &impl Loader) -> Result<Version, VulkanError> {
        // Per the Vulkan spec:
        // If the vkGetInstanceProcAddr returns NULL for vkEnumerateInstanceVersion, it is a
        // Vulkan 1.0 implementation. Otherwise, the application can call vkEnumerateInstanceVersion
        // to determine the version of Vulkan.

        let name = CStr::from_bytes_with_nul_unchecked(b"vkEnumerateInstanceVersion\0");
        let func = loader.get_instance_proc_addr(ash::vk::Instance::null(), name.as_ptr());

        let version = if let Some(func) = func {
            let func: ash::vk::PFN_vkEnumerateInstanceVersion = transmute(func);
            let mut api_version = 0;
            func(&mut api_version).result().map_err(VulkanError::from)?;
            Version::from(api_version)
        } else {
            Version {
                major: 1,
                minor: 0,
                patch: 0,
            }
        };

        Ok(version)
    }

    unsafe fn get_extension_properties(
        fns: &EntryFunctions,
        layer: Option<&str>,
    ) -> Result<Vec<ExtensionProperties>, VulkanError> {
        let layer_vk = layer.map(|layer| CString::new(layer).unwrap());

        loop {
            let mut count = 0;
            (fns.v1_0.enumerate_instance_extension_properties)(
                layer_vk
                    .as_ref()
                    .map_or(ptr::null(), |layer| layer.as_ptr()),
                &mut count,
                ptr::null_mut(),
            )
            .result()
            .map_err(VulkanError::from)?;

            let mut output = Vec::with_capacity(count as usize);
            let result = (fns.v1_0.enumerate_instance_extension_properties)(
                layer_vk
                    .as_ref()
                    .map_or(ptr::null(), |layer| layer.as_ptr()),
                &mut count,
                output.as_mut_ptr(),
            );

            match result {
                ash::vk::Result::SUCCESS => {
                    output.set_len(count as usize);
                    return Ok(output.into_iter().map(Into::into).collect());
                }
                ash::vk::Result::INCOMPLETE => (),
                err => return Err(VulkanError::from(err)),
            }
        }
    }

    /// Returns pointers to the raw global Vulkan functions of the library.
    #[inline]
    pub fn fns(&self) -> &EntryFunctions {
        &self.fns
    }

    /// Returns the highest Vulkan version that is supported for instances.
    #[inline]
    pub fn api_version(&self) -> Version {
        self.api_version
    }

    /// Returns the extension properties reported by the core library.
    #[inline]
    pub fn extension_properties(&self) -> &[ExtensionProperties] {
        &self.extension_properties
    }

    /// Returns the extensions that are supported by the core library.
    #[inline]
    pub fn supported_extensions(&self) -> &InstanceExtensions {
        &self.supported_extensions
    }

    /// Returns the list of layers that are available when creating an instance.
    ///
    /// On success, this function returns an iterator that produces
    /// [`LayerProperties`](crate::instance::LayerProperties) objects. In order to enable a layer,
    /// you need to pass its name (returned by `LayerProperties::name()`) when creating the
    /// [`Instance`](crate::instance::Instance).
    ///
    /// > **Note**: The available layers may change between successive calls to this function, so
    /// > each call may return different results. It is possible that one of the layers enumerated
    /// > here is no longer available when you create the `Instance`. This will lead to an error
    /// > when calling `Instance::new`.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use vulkano::VulkanLibrary;
    ///
    /// let library = VulkanLibrary::new().unwrap();
    ///
    /// for layer in library.layer_properties().unwrap() {
    ///     println!("Available layer: {}", layer.name());
    /// }
    /// ```
    pub fn layer_properties(
        &self,
    ) -> Result<impl ExactSizeIterator<Item = LayerProperties>, OomError> {
        let fns = self.fns();

        let layer_properties = unsafe {
            loop {
                let mut count = 0;
                (fns.v1_0.enumerate_instance_layer_properties)(&mut count, ptr::null_mut())
                    .result()
                    .map_err(VulkanError::from)?;

                let mut properties = Vec::with_capacity(count as usize);
                let result = (fns.v1_0.enumerate_instance_layer_properties)(
                    &mut count,
                    properties.as_mut_ptr(),
                );

                match result {
                    ash::vk::Result::SUCCESS => {
                        properties.set_len(count as usize);
                        break properties;
                    }
                    ash::vk::Result::INCOMPLETE => (),
                    err => return Err(VulkanError::from(err).into()),
                }
            }
        };

        Ok(layer_properties
            .into_iter()
            .map(|p| LayerProperties { props: p }))
    }

    /// Returns the extension properties that are reported by the given layer.
    #[inline]
    pub fn layer_extension_properties(
        &self,
        layer: &str,
    ) -> Result<Vec<ExtensionProperties>, VulkanError> {
        unsafe { Self::get_extension_properties(&self.fns, Some(layer)) }
    }

    /// Returns the extensions that are supported by the given layer.
    #[inline]
    pub fn supported_layer_extensions(
        &self,
        layer: &str,
    ) -> Result<InstanceExtensions, VulkanError> {
        Ok(self
            .layer_extension_properties(layer)?
            .iter()
            .map(|property| property.extension_name.as_str())
            .collect())
    }

    /// Returns the union of the extensions that are supported by the core library and all
    /// the given layers.
    #[inline]
    pub fn supported_extensions_with_layers<'a>(
        &self,
        layers: impl IntoIterator<Item = &'a str>,
    ) -> Result<InstanceExtensions, VulkanError> {
        layers
            .into_iter()
            .try_fold(self.supported_extensions, |extensions, layer| {
                self.supported_layer_extensions(layer)
                    .map(|layer_extensions| extensions.union(&layer_extensions))
            })
    }

    /// Calls `get_instance_proc_addr` on the underlying loader.
    #[inline]
    pub unsafe fn get_instance_proc_addr(
        &self,
        instance: ash::vk::Instance,
        name: *const c_char,
    ) -> ash::vk::PFN_vkVoidFunction {
        self.loader.get_instance_proc_addr(instance, name)
    }
}

/// Implemented on objects that grant access to a Vulkan implementation.
pub unsafe trait Loader: Send + Sync {
    /// Calls the `vkGetInstanceProcAddr` function. The parameters are the same.
    ///
    /// The returned function must stay valid for as long as `self` is alive.
    unsafe fn get_instance_proc_addr(
        &self,
        instance: ash::vk::Instance,
        name: *const c_char,
    ) -> ash::vk::PFN_vkVoidFunction;
}

unsafe impl<T> Loader for T
where
    T: SafeDeref + Send + Sync,
    T::Target: Loader,
{
    unsafe fn get_instance_proc_addr(
        &self,
        instance: ash::vk::Instance,
        name: *const c_char,
    ) -> ash::vk::PFN_vkVoidFunction {
        (**self).get_instance_proc_addr(instance, name)
    }
}

impl Debug for dyn Loader {
    fn fmt(&self, _f: &mut Formatter<'_>) -> Result<(), FmtError> {
        Ok(())
    }
}

/// Implementation of `Loader` that loads Vulkan from a dynamic library.
pub struct DynamicLibraryLoader {
    _vk_lib: Library,
    get_instance_proc_addr: ash::vk::PFN_vkGetInstanceProcAddr,
}

impl DynamicLibraryLoader {
    /// Tries to load the dynamic library at the given path, and tries to
    /// load `vkGetInstanceProcAddr` in it.
    ///
    /// # Safety
    ///
    /// - The dynamic library must be a valid Vulkan implementation.
    ///
    pub unsafe fn new(path: impl AsRef<Path>) -> Result<DynamicLibraryLoader, LoadingError> {
        let vk_lib = Library::new(path.as_ref()).map_err(LoadingError::LibraryLoadFailure)?;

        let get_instance_proc_addr = *vk_lib
            .get(b"vkGetInstanceProcAddr")
            .map_err(LoadingError::LibraryLoadFailure)?;

        Ok(DynamicLibraryLoader {
            _vk_lib: vk_lib,
            get_instance_proc_addr,
        })
    }
}

unsafe impl Loader for DynamicLibraryLoader {
    #[inline]
    unsafe fn get_instance_proc_addr(
        &self,
        instance: ash::vk::Instance,
        name: *const c_char,
    ) -> ash::vk::PFN_vkVoidFunction {
        (self.get_instance_proc_addr)(instance, name)
    }
}

/// Expression that returns a loader that assumes that Vulkan is linked to the executable you're
/// compiling.
///
/// If you use this macro, you must linked to a library that provides the `vkGetInstanceProcAddr`
/// symbol.
///
/// This is provided as a macro and not as a regular function, because the macro contains an
/// `extern {}` block.
// TODO: should this be unsafe?
#[macro_export]
macro_rules! statically_linked_vulkan_loader {
    () => {{
        extern "C" {
            fn vkGetInstanceProcAddr(
                instance: ash::vk::Instance,
                pName: *const c_char,
            ) -> ash::vk::PFN_vkVoidFunction;
        }

        struct StaticallyLinkedVulkanLoader;
        unsafe impl Loader for StaticallyLinkedVulkanLoader {
            unsafe fn get_instance_proc_addr(
                &self,
                instance: ash::vk::Instance,
                name: *const c_char,
            ) -> ash::vk::PFN_vkVoidFunction {
                vkGetInstanceProcAddr(instance, name)
            }
        }

        StaticallyLinkedVulkanLoader
    }};
}

/// Error that can happen when loading a Vulkan library.
#[derive(Debug)]
pub enum LoadingError {
    /// Failed to load the Vulkan shared library.
    LibraryLoadFailure(LibloadingError),

    /// Not enough memory.
    OomError(OomError),
}

impl Error for LoadingError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            //Self::LibraryLoadFailure(err) => Some(err),
            Self::OomError(err) => Some(err),
            _ => None,
        }
    }
}

impl Display for LoadingError {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
        write!(
            f,
            "{}",
            match self {
                Self::LibraryLoadFailure(_) => "failed to load the Vulkan shared library",
                Self::OomError(_) => "not enough memory available",
            }
        )
    }
}

impl From<VulkanError> for LoadingError {
    fn from(err: VulkanError) -> Self {
        match err {
            err @ VulkanError::OutOfHostMemory => Self::OomError(OomError::from(err)),
            err @ VulkanError::OutOfDeviceMemory => Self::OomError(OomError::from(err)),
            _ => panic!("unexpected error: {:?}", err),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{DynamicLibraryLoader, LoadingError};

    #[test]
    fn dl_open_error() {
        unsafe {
            match DynamicLibraryLoader::new("_non_existing_library.void") {
                Err(LoadingError::LibraryLoadFailure(_)) => (),
                _ => panic!(),
            }
        }
    }
}
