// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT

// cSpell: ignore blits DEXT Parameteriv
use ash::vk;
use glow::HasContext;

use crate::gl_bindings as gl;

use gl::Gles2 as Gl;

use slint::wgpu_30::wgpu::{self, hal};

#[derive(thiserror::Error, Debug)]
pub enum VulkanTextureError {
    #[error("{0:?}")]
    Surfman(surfman::Error),
    #[error("{0}")]
    Vulkan(#[from] ash::vk::Result),
    #[error("No surface returned when the surface was unbound from the context")]
    NoSurface,
    #[error("The surface didn't have a framebuffer object")]
    NoFramebuffer,
    #[error("Wgpu is not using the vulkan backend")]
    WgpuNotVulkan,
    #[error("{0}")]
    OpenGL(String),
}

impl super::GPURenderingContext {
    /// Imports Vulkan surface as a WGPU texture for rendering on Linux.
    /// Creates a Vulkan image with external memory, imports to OpenGL, blits content, then wraps as WGPU texture.
    pub fn get_wgpu_texture_from_vulkan(
        &self,
        wgpu_device: &wgpu::Device,
    ) -> Result<wgpu::Texture, VulkanTextureError> {
        let device = &self.surfman_rendering_info.device.borrow();
        let mut context = self.surfman_rendering_info.context.borrow_mut();

        let surface = device
            .unbind_surface_from_context(&mut context)
            .map_err(VulkanTextureError::Surfman)?
            .ok_or(VulkanTextureError::NoSurface)?;

        device.make_context_current(&mut context).map_err(VulkanTextureError::Surfman)?;

        let surface_info = device.surface_info(&surface);

        let size = self.size.get();

        let texture = unsafe {
            let hal_device = wgpu_device
                .as_hal::<wgpu::wgc::api::Vulkan>()
                .ok_or(VulkanTextureError::WgpuNotVulkan)?;
            let vulkan_device = hal_device.raw_device().clone();
            let vulkan_instance = hal_device.shared_instance().raw_instance();

            // Create Vulkan image with external memory for sharing with OpenGL

            let mut external_memory_image_info = vk::ExternalMemoryImageCreateInfo::default()
                .handle_types(vk::ExternalMemoryHandleTypeFlags::OPAQUE_FD);

            let vulkan_image = vulkan_device.create_image(
                &vk::ImageCreateInfo::default()
                    .image_type(vk::ImageType::TYPE_2D)
                    .format(vk::Format::R8G8B8A8_UNORM)
                    .extent(vk::Extent3D { width: size.width, height: size.height, depth: 1 })
                    .mip_levels(1)
                    .array_layers(1)
                    .samples(vk::SampleCountFlags::TYPE_1)
                    .tiling(vk::ImageTiling::OPTIMAL)
                    .usage(vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::COLOR_ATTACHMENT)
                    .sharing_mode(vk::SharingMode::EXCLUSIVE)
                    .initial_layout(vk::ImageLayout::UNDEFINED)
                    .push_next(&mut external_memory_image_info),
                None,
            )?;

            // Allocate dedicated Vulkan memory and bind to the created image

            let memory_requirements = vulkan_device.get_image_memory_requirements(vulkan_image);

            let mut dedicated_allocate_info =
                vk::MemoryDedicatedAllocateInfo::default().image(vulkan_image);

            let mut export_info = vk::ExportMemoryAllocateInfo::default()
                .handle_types(vk::ExternalMemoryHandleTypeFlags::OPAQUE_FD);

            let memory = vulkan_device.allocate_memory(
                &vk::MemoryAllocateInfo::default()
                    .allocation_size(memory_requirements.size)
                    // todo: required?
                    //.memory_type_index(mem_type_index as _)
                    .push_next(&mut dedicated_allocate_info)
                    .push_next(&mut export_info),
                None,
            )?;

            vulkan_device.bind_image_memory(vulkan_image, memory, 0)?;

            // Export Vulkan memory as a file descriptor for OpenGL import

            let external_memory_fd_api =
                ash::khr::external_memory_fd::Device::new(&vulkan_instance, &vulkan_device);

            let memory_handle = external_memory_fd_api.get_memory_fd(
                &vk::MemoryGetFdInfoKHR::default()
                    .memory(memory)
                    .handle_type(vk::ExternalMemoryHandleTypeFlags::OPAQUE_FD),
            )?;

            // Import Vulkan memory into OpenGL using EXT_external_objects

            let gl = &self.surfman_rendering_info.glow_gl;

            let gl_with_extensions =
                Gl::load_with(|function_name| device.get_proc_address(&context, function_name));

            let mut memory_object = 0;
            gl_with_extensions.CreateMemoryObjectsEXT(1, &mut memory_object);
            // We're using a dedicated allocation.
            // todo: taken from https://bxt.rs/blog/fast-half-life-video-recording-with-vulkan/, not sure if required.
            gl_with_extensions.MemoryObjectParameterivEXT(
                memory_object,
                gl::DEDICATED_MEMORY_OBJECT_EXT,
                &1,
            );
            gl_with_extensions.ImportMemoryFdEXT(
                memory_object,
                memory_requirements.size,
                gl::HANDLE_TYPE_OPAQUE_FD_EXT,
                memory_handle,
            );
            // Create a texture and bind it to the imported memory.
            let texture = gl.create_texture().map_err(VulkanTextureError::OpenGL)?;
            gl.bind_texture(gl::TEXTURE_2D, Some(texture));
            gl_with_extensions.TexStorageMem2DEXT(
                gl::TEXTURE_2D,
                1,
                gl::RGBA8,
                size.width as i32,
                size.height as i32,
                memory_object,
                0,
            );

            // Blit Servo's framebuffer to the imported texture

            let draw_framebuffer = gl.create_framebuffer().map_err(VulkanTextureError::OpenGL)?;
            let read_framebuffer =
                surface_info.framebuffer_object.ok_or(VulkanTextureError::NoFramebuffer)?;
            // todo: tried using gl.named_framebuffer_texture instead but it errored.
            gl.bind_framebuffer(gl::DRAW_FRAMEBUFFER, Some(draw_framebuffer));
            gl.framebuffer_texture_2d(
                gl::DRAW_FRAMEBUFFER,
                gl::COLOR_ATTACHMENT0,
                gl::TEXTURE_2D,
                Some(texture),
                0,
            );

            gl.bind_framebuffer(gl::READ_FRAMEBUFFER, Some(read_framebuffer));
            gl.bind_framebuffer(gl::DRAW_FRAMEBUFFER, Some(draw_framebuffer));

            gl.blit_framebuffer(
                0,
                0,
                size.width as i32,
                size.height as i32,
                // Flip vertically - OpenGL origin is bottom-left, texture origin is top-left
                0,
                size.height as i32,
                size.width as i32,
                0,
                gl::COLOR_BUFFER_BIT,
                gl::NEAREST,
            );
            gl.flush();
            // Delete all the opengl objects. Seems to be required to prevent memory leaks
            // according to `amdgpu_top`.
            gl.delete_framebuffer(draw_framebuffer);
            gl.delete_texture(texture);
            gl_with_extensions.DeleteMemoryObjectsEXT(1, &memory_object);

            wgpu_device.create_texture_from_hal::<wgpu::wgc::api::Vulkan>(
                hal_device.texture_from_raw(
                    vulkan_image,
                    &hal::TextureDescriptor {
                        label: None,
                        size: wgpu::Extent3d {
                            width: size.width,
                            height: size.height,
                            depth_or_array_layers: 1,
                        },
                        format: wgpu::TextureFormat::Rgba8Unorm,
                        dimension: wgpu::TextureDimension::D2,
                        mip_level_count: 1,
                        sample_count: 1,
                        usage: wgpu::TextureUses::RESOURCE | wgpu::TextureUses::COLOR_TARGET,
                        view_formats: Vec::new(),
                        memory_flags: hal::MemoryFlags::empty(),
                    },
                    Some(Box::new(move || {
                        // Images aren't cleaned up by wgpu-hal if theres a drop callback set so do it manually
                        vulkan_device.destroy_image(vulkan_image, None);
                        // Free the memory
                        vulkan_device.free_memory(memory, None);
                    })),
                    hal::vulkan::TextureMemory::External,
                ),
                &wgpu::TextureDescriptor {
                    label: None,
                    size: wgpu::Extent3d {
                        width: size.width,
                        height: size.height,
                        depth_or_array_layers: 1,
                    },
                    format: wgpu::TextureFormat::Rgba8Unorm,
                    dimension: wgpu::TextureDimension::D2,
                    mip_level_count: 1,
                    sample_count: 1,
                    usage: wgpu::TextureUsages::TEXTURE_BINDING
                        | wgpu::TextureUsages::RENDER_ATTACHMENT,
                    view_formats: &[],
                },
                // UNINITIALIZED is the tracker state wgpu 29 implicitly assigned to hal-imported
                // textures; it only seeds barrier tracking and does not cause a zero-init clear.
                wgpu::TextureUses::UNINITIALIZED,
            )
        };

        device.bind_surface_to_context(&mut context, surface).map_err(|(err, mut surface)| {
            let _ = device.destroy_surface(&mut context, &mut surface);
            VulkanTextureError::Surfman(err)
        })?;

        Ok(texture)
    }
}
