// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0

// cSpell: ignore CRTC crtcs htotal vrefresh vtotal
use std::cell::{Cell, RefCell};
use std::os::fd::{AsFd, BorrowedFd, OwnedFd};
use std::rc::Rc;

use crate::DeviceOpener;
use drm::Device as DrmDevice;
use drm::buffer::Buffer;
use drm::control::Device;
use i_slint_core::platform::PlatformError;

// Wrapped needed because gbm::Device<T> wants T to be sized.
#[derive(Clone)]
pub struct SharedFd(Rc<OwnedFd>);
impl AsFd for SharedFd {
    fn as_fd(&self) -> BorrowedFd<'_> {
        self.0.as_fd()
    }
}

impl DrmDevice for SharedFd {}

impl drm::control::Device for SharedFd {}

#[derive(Default)]
enum PageFlipState {
    #[default]
    NoFrameBufferPosted,
    InitialBufferPosted,
    WaitingForPageFlip {
        _buffer_to_keep_alive_until_flip: Box<dyn Buffer>,
    },
    ReadyForNextBuffer,
}

pub struct DrmOutput {
    pub drm_device: SharedFd,
    pub connector: drm::control::connector::Info,
    mode: drm::control::Mode,
    crtc: drm::control::crtc::Handle,
    last_buffer: Cell<Option<Box<dyn Buffer>>>,
    page_flip_state: Rc<RefCell<PageFlipState>>,
}

impl DrmOutput {
    pub fn new(device_opener: &DeviceOpener) -> Result<Self, PlatformError> {
        let mut last_err = None;
        if let Ok(drm_devices) = std::fs::read_dir("/dev/dri/") {
            for device in drm_devices {
                if let Ok(device) = device.map_err(|e| format!("Error opening DRM device: {e}")) {
                    match Self::new_with_path(device_opener, &device.path()) {
                        Ok(dsp) => return Ok(dsp),
                        Err(e) => last_err = Some(e),
                    }
                }
            }
        }
        Err(last_err.unwrap_or_else(|| "Could not create an egl display".into()))
    }

    fn new_with_path(
        device_opener: &DeviceOpener,
        device: &std::path::Path,
    ) -> Result<Self, PlatformError> {
        let drm_device = SharedFd(device_opener(device)?);

        let resources = drm_device
            .resource_handles()
            .map_err(|e| format!("Error reading DRM resource handles: {e}"))?;

        let connector = if let Ok(requested_connector_name) = std::env::var("SLINT_DRM_OUTPUT") {
            let mut connectors = resources.connectors().iter().filter_map(|handle| {
                let connector = drm_device.get_connector(*handle, false).ok()?;
                let name =
                    format!("{}-{}", connector.interface().as_str(), connector.interface_id());
                let connected = connector.state() == drm::control::connector::State::Connected;
                Some((name, connector, connected))
            });

            if requested_connector_name.eq_ignore_ascii_case("list") {
                let names_and_status = connectors
                    .map(|(name, _, connected)| format!("{} (connected: {})", name, connected))
                    .collect::<Vec<_>>();
                // Can't return error here because newlines are escaped.
                eprintln!(
                    "\nDRM Output List Requested:\n{}\nPlease select an output with the SLINT_DRM_OUTPUT environment variable and re-run the program.",
                    names_and_status.join("\n")
                );
                std::process::exit(1);
            } else {
                let (_, connector, connected) =
                    connectors.find(|(name, _, _)| name == &requested_connector_name).ok_or_else(
                        || format!("No output with the name '{}' found", requested_connector_name),
                    )?;

                if !connected {
                    return Err(format!(
                        "Requested output '{}' is not connected",
                        requested_connector_name
                    )
                    .into());
                };

                connector
            }
        } else {
            resources
                .connectors()
                .iter()
                .find_map(|handle| {
                    let connector = drm_device.get_connector(*handle, false).ok()?;
                    (connector.state() == drm::control::connector::State::Connected)
                        .then_some(connector)
                })
                .ok_or_else(|| "No connected display connector found".to_string())?
        };

        let mode = std::env::var("SLINT_DRM_MODE").map_or_else(
            |_| {
                connector
                    .modes()
                    .iter()
                    .max_by(|current_mode, next_mode| {
                        let current = (
                            current_mode
                                .mode_type()
                                .contains(drm::control::ModeTypeFlags::PREFERRED),
                            current_mode.size().0 as u32 * current_mode.size().1 as u32,
                        );
                        let next = (
                            next_mode.mode_type().contains(drm::control::ModeTypeFlags::PREFERRED),
                            next_mode.size().0 as u32 * next_mode.size().1 as u32,
                        );

                        current.cmp(&next)
                    })
                    .cloned()
                    .ok_or_else(|| "No preferred or non-zero size display mode found".to_string())
            },
            |mode_str| {
                let mut modes_and_index = connector.modes().iter().cloned().enumerate();

                if mode_str.to_lowercase() == "list" {
                    let mode_names: Vec<String> = modes_and_index
                        .map(|(index, mode)| {
                            let (width, height) = mode.size();
                            format!(
                                "Index: {index} Width: {width} Height: {height} Refresh Rate: {}",
                                mode.vrefresh()
                            )
                        })
                        .collect();

                    // Can't return error here because newlines are escaped.
                    eprintln!("DRM Mode List Requested:\n{}\nPlease select a mode with the SLINT_DRM_MODE environment variable and re-run the program.", mode_names.join("\n"));
                    std::process::exit(1);
                }
                let mode_index: usize =
                    mode_str.parse().map_err(|_| {
                        format!(
                            "Invalid SLINT_DRM_MODE value '{mode_str}': expected a mode index or 'list' to list the available modes"
                        )
                    })?;
                modes_and_index.nth(mode_index).map_or_else(
                    || Err(format!("Mode index is out of bounds: {mode_index}")),
                    |(_, mode)| Ok(mode),
                )
            },
        )?;

        let encoder = connector
            .current_encoder()
            .filter(|current| connector.encoders().contains(current))
            .and_then(|current| drm_device.get_encoder(current).ok());

        let crtc = if let Some(encoder) = encoder {
            encoder.crtc().ok_or_else(|| "no crtc for encoder".to_string())?
        } else {
            // No crtc found for current encoder? Pick the first possible crtc
            // as described in https://manpages.debian.org/testing/libdrm-dev/drm-kms.7.en.html#CRTC/Encoder_Selection
            connector
                .encoders()
                .iter()
                .filter_map(|handle| drm_device.get_encoder(*handle).ok())
                .flat_map(|encoder| resources.filter_crtcs(encoder.possible_crtcs()))
                .find(|crtc_handle| drm_device.get_crtc(*crtc_handle).is_ok())
                .ok_or_else(|| {
                    format!(
                        "Could not find any crtc for any encoder connected to output {}-{}",
                        connector.interface().as_str(),
                        connector.interface_id()
                    )
                })?
        };

        //eprintln!("mode {}/{}", width, height);

        Ok(Self {
            drm_device,
            connector,
            mode,
            crtc,
            last_buffer: Cell::default(),
            page_flip_state: Default::default(),
        })
    }

    pub fn present(
        &self,
        front_buffer: impl Buffer + 'static,
        framebuffer_handle: drm::control::framebuffer::Handle,
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        if let Some(last_buffer) = self.last_buffer.replace(Some(Box::new(front_buffer))) {
            self.drm_device
                .page_flip(self.crtc, framebuffer_handle, drm::control::PageFlipFlags::EVENT, None)
                .map_err(|e| format!("Error presenting framebuffer on screen: {e}"))?;

            *self.page_flip_state.borrow_mut() =
                PageFlipState::WaitingForPageFlip { _buffer_to_keep_alive_until_flip: last_buffer };
        } else {
            self.drm_device
                .set_crtc(
                    self.crtc,
                    Some(framebuffer_handle),
                    (0, 0),
                    &[self.connector.handle()],
                    Some(self.mode),
                )
                .map_err(|e| format!("Error presenting framebuffer on screen: {e}"))?;
            *self.page_flip_state.borrow_mut() = PageFlipState::InitialBufferPosted;
        }

        Ok(())
    }

    pub fn wait_for_page_flip(&self) {
        if matches!(
            *self.page_flip_state.borrow(),
            PageFlipState::NoFrameBufferPosted
                | PageFlipState::InitialBufferPosted
                | PageFlipState::ReadyForNextBuffer
        ) {
            return;
        }

        loop {
            let Ok(mut event_it) = self.drm_device.receive_events() else {
                return;
            };

            if event_it.any(|event| matches!(event, drm::control::Event::PageFlip(..)))
                && let PageFlipState::WaitingForPageFlip { .. } =
                    self.page_flip_state.replace(PageFlipState::ReadyForNextBuffer)
            {
                return;
            }
        }
    }

    pub fn get_supported_formats(&self) -> Result<Vec<drm::buffer::DrmFourcc>, PlatformError> {
        // Try to set universal planes client capability if possible
        let _ = self.drm_device.set_client_capability(drm::ClientCapability::UniversalPlanes, true);

        let mut all_formats = std::collections::HashSet::new();

        // Iterate through all planes and collect formats from compatible ones
        if let Ok(plane_handles) = self.drm_device.plane_handles() {
            for &plane_handle in &plane_handles {
                if let Ok(plane) = self.drm_device.get_plane(plane_handle)
                    && plane.crtc() == Some(self.crtc)
                {
                    // Collect formats from this compatible plane
                    for &format_u32 in plane.formats() {
                        if let Ok(format) = drm::buffer::DrmFourcc::try_from(format_u32) {
                            all_formats.insert(format);
                        }
                    }
                }
            }
        }

        if all_formats.is_empty() {
            eprintln!(
                "No available formats found for any plane with CRTC {:?}. Falling back to XRGB8888 format",
                self.crtc
            );

            Ok(vec![drm::buffer::DrmFourcc::Xrgb8888])
        } else {
            Ok(all_formats.into_iter().collect())
        }
    }

    pub fn size(&self) -> (u32, u32) {
        let (width, height) = self.mode.size();
        (width as u32, height as u32)
    }

    /// Returns the refresh rate in millihertz, computed from the mode's pixel clock
    /// and timing parameters. This matches the precision used by Vulkan's
    /// VkDisplayModeParametersKHR::refreshRate.
    #[cfg(wgpu_surface)]
    pub fn refresh_rate_millihertz(&self) -> u32 {
        let clock = self.mode.clock() as u64; // in kHz
        let (_, _, htotal) = self.mode.hsync();
        let (_, _, vtotal) = self.mode.vsync();
        let htotal = htotal as u64;
        let vtotal = vtotal as u64;
        if htotal == 0 || vtotal == 0 {
            // Fallback to rounded vrefresh * 1000
            return self.mode.vrefresh() * 1000;
        }
        // clock is in kHz, so clock * 1_000_000 gives us millihertz * htotal * vtotal
        ((clock * 1_000_000 + (htotal * vtotal) / 2) / (htotal * vtotal)) as u32
    }

    #[cfg(skia_wgpu_29)]
    /// Creates a wgpu-29 DRM surface target from this output.
    pub fn wgpu_29_surface_target(
        &self,
    ) -> Result<
        (i_slint_core::graphics::wgpu_29::SurfaceTarget, i_slint_core::api::PhysicalSize),
        PlatformError,
    > {
        use i_slint_core::graphics::wgpu_29::wgpu;
        use std::os::fd::AsRawFd;
        let plane = self.find_compatible_plane()?;
        let (width, height) = self.size();
        let target =
            i_slint_core::graphics::wgpu_29::SurfaceTarget::Drm(wgpu::SurfaceTargetUnsafe::Drm {
                fd: self.drm_device.as_fd().as_raw_fd(),
                plane: plane.handle().into(),
                connector_id: self.connector.handle().into(),
                width,
                height,
                refresh_rate: self.refresh_rate_millihertz(),
            });
        Ok((target, i_slint_core::api::PhysicalSize::new(width, height)))
    }

    #[cfg(any(feature = "unstable-wgpu-30", feature = "renderer-femtovg-wgpu"))]
    /// Creates a wgpu-30 DRM surface target from this output.
    pub fn wgpu_30_surface_target(
        &self,
    ) -> Result<
        (i_slint_core::graphics::wgpu_30::SurfaceTarget, i_slint_core::api::PhysicalSize),
        PlatformError,
    > {
        use i_slint_core::graphics::wgpu_30::wgpu;
        use std::os::fd::AsRawFd;
        let plane = self.find_compatible_plane()?;
        let (width, height) = self.size();
        let target =
            i_slint_core::graphics::wgpu_30::SurfaceTarget::Drm(wgpu::SurfaceTargetUnsafe::Drm {
                fd: self.drm_device.as_fd().as_raw_fd(),
                plane: plane.handle().into(),
                connector_id: self.connector.handle().into(),
                width,
                height,
                refresh_rate: self.refresh_rate_millihertz(),
            });
        Ok((target, i_slint_core::api::PhysicalSize::new(width, height)))
    }

    // Iterate through all planes and collect formats from compatible ones
    #[cfg(wgpu_surface)]
    pub fn find_compatible_plane(&self) -> Result<drm::control::plane::Info, PlatformError> {
        let _ = self.drm_device.set_client_capability(drm::ClientCapability::UniversalPlanes, true);
        let plane_handles = self
            .drm_device
            .plane_handles()
            .map_err(|e| format!("Error obtaining drm plane handles: {e}"))?
            .into_iter()
            .filter(|plane_handle| {
                let Ok(plane_info) = self.drm_device.get_plane(*plane_handle) else {
                    return false;
                };
                self.drm_device
                    .resource_handles()
                    .unwrap()
                    .filter_crtcs(plane_info.possible_crtcs())
                    .contains(&self.crtc)
            });

        for plane_handle in plane_handles {
            if let Ok(plane) = self.drm_device.get_plane(plane_handle) {
                return Ok(plane);
            }
        }

        Err(PlatformError::Other("Could not find plane matching crtc".into()))
    }
}
