use drm::control::Device as ControlDevice;
use drm::control::atomic::AtomicModeReq;
use drm::control::connector::Interface;
use drm::control::property::ValueType;
use drm::control::{
    AtomicCommitFlags, Mode, PlaneType, connector, crtc, dumbbuffer::DumbBuffer, framebuffer, plane, property,
};

#[cfg(debug_assertions)]
use std::collections::HashMap;
use std::collections::HashSet;
#[cfg(debug_assertions)]
use std::fmt;
use std::os::unix::io::AsRawFd;
use std::sync::{
    Arc, Mutex, RwLock,
    atomic::{AtomicBool, Ordering},
};

use crate::backend::drm::error::AccessError;
use crate::utils::{Coordinate, Rectangle, Transform};
use crate::{
    backend::{
        allocator::format::{get_bpp, get_depth},
        drm::{
            DrmDeviceFd,
            device::DrmDeviceInternal,
            device::atomic::{PropMapping, map_props},
            error::Error,
            plane_type,
        },
    },
    utils::DevPath,
};

use tracing::{debug, info, info_span, instrument, trace, warn};

use super::{PlaneConfig, PlaneState, VrrSupport};

#[derive(Debug, Clone)]
pub struct State {
    pub active: bool,
    pub mode: Mode,
    pub blob: property::Value<'static>,
    pub vrr: bool,
    pub connectors: HashSet<connector::Handle>,
}

impl PartialEq for State {
    #[inline]
    fn eq(&self, other: &Self) -> bool {
        self.active == other.active
            && self.mode == other.mode
            && self.vrr == other.vrr
            && self.connectors == other.connectors
    }
}

impl State {
    fn current_state<A: DevPath + ControlDevice>(
        fd: &A,
        crtc: crtc::Handle,
        prop_mapping: &mut PropMapping,
    ) -> Result<Self, Error> {
        let crtc_info = fd.get_crtc(crtc).map_err(|source| {
            Error::Access(AccessError {
                errmsg: "Error loading crtc info",
                dev: fd.dev_path(),
                source,
            })
        })?;

        // If we have no current mode, we create a fake one, which will not match (and thus gets overridden on the commit below).
        // A better fix would probably be making mode an `Option`, but that would mean
        // we need to be sure, we require a mode to always be set without relying on the compiler.
        // So we cheat, because it works and is easier to handle later.
        let current_mode = crtc_info.mode().unwrap_or_else(|| unsafe { std::mem::zeroed() });
        let current_blob = match crtc_info.mode() {
            Some(mode) => fd.create_property_blob(&mode).map_err(|source| {
                Error::Access(AccessError {
                    errmsg: "Failed to create Property Blob for mode",
                    dev: fd.dev_path(),
                    source,
                })
            })?,
            None => property::Value::Unknown(0),
        };

        let res_handles = fd.resource_handles().map_err(|source| {
            Error::Access(AccessError {
                errmsg: "Error loading drm resources",
                dev: fd.dev_path(),
                source,
            })
        })?;

        // the current set of connectors are those, that already have the correct `CRTC_ID` set.
        // so we collect them for `current_state` and set the user-given once in `pending_state`.
        //
        // If they don't match, `commit_pending` will return true and they will be changed on the next `commit`.
        let mut current_connectors = HashSet::new();
        // make sure the mapping is up to date
        map_props(fd, res_handles.connectors(), &mut prop_mapping.connectors)?;
        for conn in res_handles.connectors() {
            let crtc_prop = prop_mapping.conn_prop_handle(*conn, "CRTC_ID")?;
            if let (Ok(crtc_prop_info), Ok(props)) = (fd.get_property(crtc_prop), fd.get_properties(*conn)) {
                let (ids, vals) = props.as_props_and_values();
                for (&id, &val) in ids.iter().zip(vals.iter()) {
                    if id == crtc_prop {
                        if let property::Value::CRTC(Some(conn_crtc)) =
                            crtc_prop_info.value_type().convert_value(val)
                        {
                            if conn_crtc == crtc {
                                current_connectors.insert(*conn);
                            }
                        }
                        break;
                    }
                }
            }
        }

        // Get the current active (dpms) state and vrr state of the CRTC
        //
        // Changing a CRTC to active might require a modeset
        let mut active = None;
        let mut vrr = None;
        if let Ok(props) = fd.get_properties(crtc) {
            let active_prop = prop_mapping.crtcs.get(&crtc).and_then(|m| m.get("ACTIVE"));
            let vrr_prop = prop_mapping.crtcs.get(&crtc).and_then(|m| m.get("VRR_ENABLED"));
            let (ids, vals) = props.as_props_and_values();
            for (&id, &val) in ids.iter().zip(vals.iter()) {
                if Some(&id) == active_prop {
                    active = property::ValueType::Boolean.convert_value(val).as_boolean();
                    break;
                }
                if Some(&id) == vrr_prop {
                    vrr = property::ValueType::Boolean.convert_value(val).as_boolean();
                    break;
                }
            }
        }

        Ok(State {
            // If we don't know the active state we just assume off.
            // This is highly unlikely, but having a false negative should do no harm.
            active: active.unwrap_or(false),
            mode: current_mode,
            blob: current_blob,
            // If we don't know the VRR state, the driver doesn't support the property
            vrr: vrr.unwrap_or(false),
            connectors: current_connectors,
        })
    }

    fn clear(&mut self) {
        self.mode = unsafe { std::mem::zeroed() };
        self.blob = property::Value::Unknown(0);
        self.connectors.clear();
        self.active = false;
        self.vrr = false;
    }
}

#[derive(Debug)]
pub struct AtomicDrmSurface {
    pub(in crate::backend::drm) fd: Arc<DrmDeviceInternal>,
    pub(super) active: Arc<AtomicBool>,
    crtc: crtc::Handle,
    plane: plane::Handle,
    used_planes: Mutex<HashSet<plane::Handle>>,
    prop_mapping: Arc<RwLock<PropMapping>>,
    state: RwLock<State>,
    pending: RwLock<State>,
    pub(super) span: tracing::Span,
}

impl AtomicDrmSurface {
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        fd: Arc<DrmDeviceInternal>,
        active: Arc<AtomicBool>,
        crtc: crtc::Handle,
        plane: plane::Handle,
        prop_mapping: Arc<RwLock<PropMapping>>,
        mode: Mode,
        connectors: &[connector::Handle],
    ) -> Result<Self, Error> {
        let span = info_span!("drm_atomic", crtc = ?crtc);
        let _guard = span.enter();
        info!(
            "Initializing drm surface ({:?}:{:?}) with mode {:?} and connectors {:?}",
            crtc, plane, mode, connectors
        );

        let state = State::current_state(&*fd, crtc, &mut prop_mapping.write().unwrap())?;
        let blob = fd.create_property_blob(&mode).map_err(|source| {
            Error::Access(AccessError {
                errmsg: "Failed to create Property Blob for mode",
                dev: fd.dev_path(),
                source,
            })
        })?;
        let pending = State {
            active: true,
            mode,
            blob,
            vrr: false,
            connectors: connectors.iter().copied().collect(),
        };

        drop(_guard);
        let surface = AtomicDrmSurface {
            fd,
            active,
            crtc,
            plane,
            used_planes: Mutex::new(HashSet::new()),
            prop_mapping,
            state: RwLock::new(state),
            pending: RwLock::new(pending),
            span,
        };

        Ok(surface)
    }

    // we need a framebuffer to do test commits, which we use to verify our pending state.
    // here we create a dumbbuffer for that purpose.
    #[profiling::function]
    fn create_test_buffer(&self, size: (u16, u16), plane: plane::Handle) -> Result<TestBuffer, Error> {
        let (w, h) = size;
        let needs_alpha = plane_type(&*self.fd, plane)? != PlaneType::Primary;
        let format = if needs_alpha {
            crate::backend::allocator::Fourcc::Argb8888
        } else {
            crate::backend::allocator::Fourcc::Xrgb8888
        };

        let db = self
            .fd
            .create_dumb_buffer((w as u32, h as u32), format, get_bpp(format).unwrap() as u32)
            .map_err(|source| {
                Error::Access(AccessError {
                    errmsg: "Failed to create dumb buffer",
                    dev: self.fd.dev_path(),
                    source,
                })
            })?;
        let fb_result = self
            .fd
            .add_framebuffer(
                &db,
                get_depth(format).unwrap() as u32,
                get_bpp(format).unwrap() as u32,
            )
            .map_err(|source| {
                Error::Access(AccessError {
                    errmsg: "Failed to create framebuffer",
                    dev: self.fd.dev_path(),
                    source,
                })
            });

        match fb_result {
            Ok(fb) => Ok(TestBuffer {
                fd: self.fd.clone(),
                db,
                fb,
            }),
            Err(err) => {
                let _ = self.fd.destroy_dumb_buffer(db);
                Err(err)
            }
        }
    }

    pub fn current_connectors(&self) -> HashSet<connector::Handle> {
        self.state.read().unwrap().connectors.clone()
    }

    pub fn pending_connectors(&self) -> HashSet<connector::Handle> {
        self.pending.read().unwrap().connectors.clone()
    }

    pub fn current_mode(&self) -> Mode {
        self.state.read().unwrap().mode
    }

    pub fn pending_mode(&self) -> Mode {
        self.pending.read().unwrap().mode
    }

    fn ensure_props_known(&self, conns: &[connector::Handle]) -> Result<(), Error> {
        let mapping_exists = {
            let prop_mapping = self.prop_mapping.read().unwrap();
            conns
                .iter()
                .all(|conn| prop_mapping.connectors.contains_key(conn))
        };
        if !mapping_exists {
            map_props(
                &*self.fd,
                self.fd
                    .resource_handles()
                    .map_err(|source| {
                        Error::Access(AccessError {
                            errmsg: "Error loading connector info",
                            dev: self.fd.dev_path(),
                            source,
                        })
                    })?
                    .connectors(),
                &mut self.prop_mapping.write().unwrap().connectors,
            )?;
        }
        Ok(())
    }

    #[instrument(parent = &self.span, skip(self))]
    pub fn add_connector(&self, conn: connector::Handle) -> Result<(), Error> {
        if !self.active.load(Ordering::SeqCst) {
            return Err(Error::DeviceInactive);
        }

        self.ensure_props_known(&[conn])?;
        let info = self.fd.get_connector(conn, false).map_err(|source| {
            Error::Access(AccessError {
                errmsg: "Error loading connector info",
                dev: self.fd.dev_path(),
                source,
            })
        })?;

        let mut pending = self.pending.write().unwrap();

        // check if the connector can handle the current mode
        if info.modes().contains(&pending.mode) {
            let test_buffer = self.create_test_buffer(pending.mode.size(), self.plane)?;

            // check if config is supported
            let prop_mapping = self.prop_mapping.read().unwrap();
            let plane_state = PlaneState {
                handle: self.plane,
                config: Some(PlaneConfig {
                    src: Rectangle::from_size(pending.mode.size().into()).to_f64(),
                    dst: Rectangle::from_size(
                        (pending.mode.size().0 as i32, pending.mode.size().1 as i32).into(),
                    ),
                    transform: Transform::Normal,
                    alpha: 1.0,
                    damage_clips: None,
                    fb: test_buffer.fb,
                    fence: None,
                }),
            };

            let mut connectors = pending.connectors.clone();
            connectors.insert(conn);
            let req = AtomicRequest::build_request(
                &prop_mapping,
                self.crtc,
                Some(pending.blob),
                pending.vrr,
                &connectors,
                [],
                [&plane_state],
            )?;
            self.fd
                .atomic_commit(
                    AtomicCommitFlags::ALLOW_MODESET | AtomicCommitFlags::TEST_ONLY,
                    req.build()?,
                )
                .map_err(|_| Error::TestFailed(self.crtc))?;

            // seems to be, lets add the connector
            pending.connectors.insert(conn);

            Ok(())
        } else {
            Err(Error::ModeNotSuitable(pending.mode))
        }
    }

    #[instrument(parent = &self.span, skip(self))]
    pub fn remove_connector(&self, conn: connector::Handle) -> Result<(), Error> {
        if !self.active.load(Ordering::SeqCst) {
            return Err(Error::DeviceInactive);
        }

        let mut pending = self.pending.write().unwrap();

        // the test would also prevent this, but the error message is far less helpful
        if pending.connectors.contains(&conn) && pending.connectors.len() == 1 {
            return Err(Error::SurfaceWithoutConnectors(self.crtc));
        }

        // check if new config is supported (should be)
        let test_buffer = self.create_test_buffer(pending.mode.size(), self.plane)?;

        let prop_mapping = self.prop_mapping.read().unwrap();
        let plane_state = PlaneState {
            handle: self.plane,
            config: Some(PlaneConfig {
                src: Rectangle::from_size(pending.mode.size().into()).to_f64(),
                dst: Rectangle::from_size(
                    (pending.mode.size().0 as i32, pending.mode.size().1 as i32).into(),
                ),
                transform: Transform::Normal,
                alpha: 1.0,
                damage_clips: None,
                fb: test_buffer.fb,
                fence: None,
            }),
        };

        let mut connectors = pending.connectors.clone();
        connectors.remove(&conn);
        let req = AtomicRequest::build_request(
            &prop_mapping,
            self.crtc,
            Some(pending.blob),
            pending.vrr,
            &connectors,
            [&conn],
            [&plane_state],
        )?;
        self.fd
            .atomic_commit(
                AtomicCommitFlags::ALLOW_MODESET | AtomicCommitFlags::TEST_ONLY,
                req.build()?,
            )
            .map_err(|_| Error::TestFailed(self.crtc))?;

        // seems to be, lets remove the connector
        pending.connectors.remove(&conn);

        Ok(())
    }

    #[instrument(parent = &self.span, skip(self))]
    pub fn set_connectors(&self, connectors: &[connector::Handle]) -> Result<(), Error> {
        // the test would also prevent this, but the error message is far less helpful
        if connectors.is_empty() {
            return Err(Error::SurfaceWithoutConnectors(self.crtc));
        }

        if !self.active.load(Ordering::SeqCst) {
            return Err(Error::DeviceInactive);
        }

        let current = self.state.read().unwrap();
        let mut pending = self.pending.write().unwrap();

        self.ensure_props_known(connectors)?;
        let conns = connectors.iter().cloned().collect::<HashSet<_>>();
        let removed = current.connectors.difference(&conns);

        let test_buffer = self.create_test_buffer(pending.mode.size(), self.plane)?;

        let prop_mapping = self.prop_mapping.read().unwrap();
        let plane_state = PlaneState {
            handle: self.plane,
            config: Some(PlaneConfig {
                src: Rectangle::from_size(pending.mode.size().into()).to_f64(),
                dst: Rectangle::from_size(
                    (pending.mode.size().0 as i32, pending.mode.size().1 as i32).into(),
                ),
                transform: Transform::Normal,
                alpha: 1.0,
                damage_clips: None,
                fb: test_buffer.fb,
                fence: None,
            }),
        };
        let req = AtomicRequest::build_request(
            &prop_mapping,
            self.crtc,
            Some(pending.blob),
            pending.vrr,
            &conns,
            removed,
            [&plane_state],
        )?;

        self.fd
            .atomic_commit(
                AtomicCommitFlags::ALLOW_MODESET | AtomicCommitFlags::TEST_ONLY,
                req.build()?,
            )
            .map_err(|_| Error::TestFailed(self.crtc))?;

        pending.connectors = conns;

        Ok(())
    }

    #[instrument(level = "debug", parent = &self.span, skip(self))]
    pub fn use_mode(&self, mode: Mode) -> Result<(), Error> {
        if !self.active.load(Ordering::SeqCst) {
            return Err(Error::DeviceInactive);
        }

        let mut pending = self.pending.write().unwrap();

        // check if new config is supported
        let new_blob = self.fd.create_property_blob(&mode).map_err(|source| {
            Error::Access(AccessError {
                errmsg: "Failed to create Property Blob for mode",
                dev: self.fd.dev_path(),
                source,
            })
        })?;

        let test_buffer = self.create_test_buffer(mode.size(), self.plane)?;

        let prop_mapping = self.prop_mapping.read().unwrap();
        let plane_state = PlaneState {
            handle: self.plane,
            config: Some(PlaneConfig {
                src: Rectangle::from_size(mode.size().into()).to_f64(),
                dst: Rectangle::from_size((mode.size().0 as i32, mode.size().1 as i32).into()),
                transform: Transform::Normal,
                alpha: 1.0,
                damage_clips: None,
                fb: test_buffer.fb,
                fence: None,
            }),
        };
        let req = AtomicRequest::build_request(
            &prop_mapping,
            self.crtc,
            Some(new_blob),
            pending.vrr,
            pending.connectors.iter(),
            [],
            [&plane_state],
        )?;
        if let Err(err) = self
            .fd
            .atomic_commit(
                AtomicCommitFlags::ALLOW_MODESET | AtomicCommitFlags::TEST_ONLY,
                req.build()?,
            )
            .map_err(|_| Error::TestFailed(self.crtc))
        {
            let _ = self.fd.destroy_property_blob(new_blob.into());
            return Err(err);
        }

        // seems to be, lets change the mode
        pending.mode = mode;
        pending.blob = new_blob;

        Ok(())
    }

    pub fn vrr_supported(&self, conn: connector::Handle) -> Result<VrrSupport, Error> {
        if !self.active.load(Ordering::SeqCst) {
            return Err(Error::DeviceInactive);
        }

        let props = self.prop_mapping.read().unwrap();
        if self
            .prop_mapping
            .read()
            .unwrap()
            .crtc_prop_handle(self.crtc, "VRR_ENABLED")
            .is_err()
        {
            return Ok(VrrSupport::NotSupported);
        }

        if let Some(vrr_prop) = props
            .connectors
            .get(&conn)
            .and_then(|props| props.get("vrr_capable"))
        {
            for (prop, value) in self.fd.get_properties(conn).map_err(|source| {
                Error::Access(AccessError {
                    errmsg: "Error querying properties",
                    dev: self.fd.dev_path(),
                    source,
                })
            })? {
                if prop == *vrr_prop {
                    let interface = self
                        .fd
                        .get_connector(conn, false)
                        .map_err(|source| {
                            Error::Access(AccessError {
                                errmsg: "Error querying connector",
                                dev: self.fd.dev_path(),
                                source,
                            })
                        })?
                        .interface();

                    // see: https://gitlab.freedesktop.org/drm/amd/-/issues/2200#note_2159982
                    // Currently setting VRR for HDMI connectors will cause flickering despite not needing `ALLOW_MODESET`
                    // TODO: Once the kernel is fixed, do actual test commits with and without `ALLOW_MODESET`.
                    return Ok(
                        match ValueType::Boolean.convert_value(value).as_boolean().unwrap() {
                            true if interface == Interface::HDMIA || interface == Interface::HDMIB => {
                                VrrSupport::RequiresModeset
                            }
                            true => VrrSupport::Supported,
                            false => VrrSupport::NotSupported,
                        },
                    );
                }
            }
        }

        Ok(VrrSupport::NotSupported)
    }

    pub fn vrr_enabled(&self) -> bool {
        self.pending.read().unwrap().vrr
    }

    pub fn use_vrr(&self, value: bool) -> Result<(), Error> {
        let mut current = self.state.write().unwrap();
        let mut pending = self.pending.write().unwrap();
        if pending.vrr == value {
            return Ok(());
        }
        let prop_mapping = self.prop_mapping.read().unwrap();

        if value && prop_mapping.crtc_prop_handle(self.crtc, "VRR_ENABLED").is_err() {
            return Err(Error::UnknownProperty {
                handle: self.crtc.into(),
                name: "VRR_ENABLED",
            });
        }

        let test_buffer = self.create_test_buffer(pending.mode.size(), self.plane)?;
        let plane_config = PlaneState {
            handle: self.plane,
            config: Some(PlaneConfig {
                src: Rectangle::from_size(pending.mode.size().into()).to_f64(),
                dst: Rectangle::from_size(
                    (pending.mode.size().0 as i32, pending.mode.size().1 as i32).into(),
                ),
                transform: Transform::Normal,
                alpha: 1.0,
                damage_clips: None,
                fb: test_buffer.fb,
                fence: None,
            }),
        };

        let req = AtomicRequest::build_request(
            &prop_mapping,
            self.crtc,
            Some(pending.blob),
            value,
            &pending.connectors,
            &[],
            [&plane_config],
        )?;

        if *current == *pending {
            // Try a non modesetting commit
            if self
                .fd
                .atomic_commit(AtomicCommitFlags::TEST_ONLY, req.build()?)
                .is_ok()
            {
                pending.vrr = value;
                current.vrr = value;
                return Ok(());
            }
        }

        // Try a modeset commit
        self.fd
            .atomic_commit(
                AtomicCommitFlags::ALLOW_MODESET | AtomicCommitFlags::TEST_ONLY,
                req.build()?,
            )
            .map_err(|_| Error::TestFailed(self.crtc))?;

        pending.vrr = value;
        Ok(())
    }

    pub fn commit_pending(&self) -> bool {
        *self.pending.read().unwrap() != *self.state.read().unwrap()
    }

    #[instrument(level = "trace", parent = &self.span, skip(self, planes))]
    #[profiling::function]
    pub fn test_state<'a>(
        &self,
        planes: impl IntoIterator<Item = PlaneState<'a>>,
        allow_modeset: bool,
    ) -> Result<(), Error> {
        if !self.active.load(Ordering::SeqCst) {
            return Err(Error::DeviceInactive);
        }

        let current = self.state.read().unwrap();
        let pending = self.pending.read().unwrap();

        self.test_state_internal(planes, allow_modeset, &current, &pending)
    }

    fn test_state_internal<'a>(
        &self,
        planes: impl IntoIterator<Item = PlaneState<'a>>,
        allow_modeset: bool,
        current: &'_ State,
        pending: &'_ State,
    ) -> Result<(), Error> {
        let planes = planes.into_iter().collect::<Vec<_>>();

        let current_conns = current.connectors.clone();
        let pending_conns = pending.connectors.clone();
        let removed = current_conns.difference(&pending_conns);
        let prop_mapping = self.prop_mapping.read().unwrap();

        let req = AtomicRequest::build_request(
            &prop_mapping,
            self.crtc,
            Some(pending.blob),
            pending.vrr,
            &pending_conns,
            removed,
            &*planes,
        )?;

        let flags = if allow_modeset {
            AtomicCommitFlags::ALLOW_MODESET | AtomicCommitFlags::TEST_ONLY
        } else {
            AtomicCommitFlags::TEST_ONLY
        };
        self.fd.atomic_commit(flags, req.build()?).map_err(|source| {
            Error::Access(AccessError {
                errmsg: "Error testing state",
                dev: self.fd.dev_path(),
                source,
            })
        })
    }

    #[instrument(level = "trace", parent = &self.span, skip(self, planes))]
    #[profiling::function]
    pub fn commit<'a>(
        &self,
        planes: impl IntoIterator<Item = PlaneState<'a>>,
        event: bool,
    ) -> Result<(), Error> {
        if !self.active.load(Ordering::SeqCst) {
            return Err(Error::DeviceInactive);
        }

        let planes = planes.into_iter().collect::<Vec<_>>();
        let mut current = self.state.write().unwrap();
        let mut used_planes = self.used_planes.lock().unwrap();
        let pending = self.pending.read().unwrap();

        debug!(current = ?*current, pending = ?*pending, ?planes, "Preparing Commit",);

        // we need the differences to know, which connectors need to change properties
        let current_conns = current.connectors.clone();
        let pending_conns = pending.connectors.clone();
        let removed = current_conns.difference(&pending_conns);

        for conn in removed.clone() {
            if let Ok(info) = self.fd.get_connector(*conn, false) {
                info!("Removing connector: {:?}", info.interface());
            } else {
                info!("Removing unknown connector");
            }
        }

        for conn in &pending_conns {
            if let Ok(info) = self.fd.get_connector(*conn, false) {
                info!("Adding connector: {:?}", info.interface());
            } else {
                info!("Adding unknown connector");
            }
        }

        if current.mode != pending.mode {
            info!("Setting new mode: {:?}", pending.mode.name());
        }

        trace!("Testing screen config");

        // test the new config and return the request if it would be accepted by the driver.
        let prop_mapping = self.prop_mapping.read().unwrap();
        let req = {
            let req = AtomicRequest::build_request(
                &prop_mapping,
                self.crtc,
                Some(pending.blob),
                pending.vrr,
                &pending_conns,
                removed,
                &*planes,
            )?;

            if let Err(err) = self.fd.atomic_commit(
                AtomicCommitFlags::ALLOW_MODESET | AtomicCommitFlags::TEST_ONLY,
                req.build()?,
            ) {
                warn!("New screen configuration invalid!:\n\t{:?}\n\t{}\n", req, err);

                return Err(Error::TestFailed(self.crtc));
            } else {
                if current.mode != pending.mode {
                    if let Err(err) = self.fd.destroy_property_blob(current.blob.into()) {
                        warn!("Failed to destroy old mode property blob: {}", err);
                    }
                }

                // new config
                req
            }
        };

        debug!("Setting screen: {:?}", req);
        let result = self
            .fd
            .atomic_commit(
                if event {
                    // on the atomic api we can modeset and trigger a page_flip event on the same call!
                    AtomicCommitFlags::PAGE_FLIP_EVENT | AtomicCommitFlags::ALLOW_MODESET
                    // we also *should* not need to wait for completion, like with `set_crtc`,
                    // because we have tested this exact commit already, so we do not expect any errors later down the line.
                    //
                    // but there is always an exception and `amdgpu` can fail in interesting ways with this flag set...
                    // https://gitlab.freedesktop.org/drm/amd/-/issues?scope=all&utf8=%E2%9C%93&state=opened&search=drm_atomic_helper_wait_for_flip_done
                    //
                    // so we skip this flag:
                    // AtomicCommitFlags::Nonblock,
                } else {
                    AtomicCommitFlags::ALLOW_MODESET
                },
                req.build()?,
            )
            .map_err(|source| {
                Error::Access(AccessError {
                    errmsg: "Error setting crtc",
                    dev: self.fd.dev_path(),
                    source,
                })
            });

        if result.is_ok() {
            *current = pending.clone();
            for plane in planes.iter() {
                if plane.config.is_some() {
                    used_planes.insert(plane.handle);
                } else {
                    used_planes.remove(&plane.handle);
                }
            }
        }

        result
    }

    #[instrument(level = "trace", parent = &self.span, skip(self, planes))]
    #[profiling::function]
    pub fn page_flip<'a>(
        &self,
        planes: impl IntoIterator<Item = PlaneState<'a>>,
        event: bool,
    ) -> Result<(), Error> {
        if !self.active.load(Ordering::SeqCst) {
            return Err(Error::DeviceInactive);
        }

        let mut used_planes = self.used_planes.lock().unwrap();
        let planes = planes.into_iter().collect::<Vec<_>>();

        // page flips work just like commits with fewer parameters..
        let prop_mapping = self.prop_mapping.read().unwrap();
        let req = AtomicRequest::build_request(
            &prop_mapping,
            self.crtc,
            None,
            self.state.read().unwrap().vrr,
            [],
            [],
            &*planes,
        )?;

        // .. and without `AtomicCommitFlags::AllowModeset`.
        // If we would set anything here, that would require a modeset, this would fail,
        // indicating a problem in our assumptions.
        trace!(?planes, "Queueing page flip: {:?}", req);
        let res = self
            .fd
            .atomic_commit(
                if event {
                    AtomicCommitFlags::PAGE_FLIP_EVENT | AtomicCommitFlags::NONBLOCK
                } else {
                    AtomicCommitFlags::NONBLOCK
                },
                req.build()?,
            )
            .map_err(|source| {
                Error::Access(AccessError {
                    errmsg: "Page flip commit failed",
                    dev: self.fd.dev_path(),
                    source,
                })
            });

        if res.is_ok() {
            for plane in planes.iter() {
                if plane.config.is_some() {
                    used_planes.insert(plane.handle);
                } else {
                    used_planes.remove(&plane.handle);
                }
            }
        }

        res
    }

    // this helper function disconnects the plane.
    // this is mostly used to remove the contents quickly, e.g. on tty switch,
    // as other compositors might not make use of other planes,
    // leaving our e.g. cursor or overlays as a relict of a better time on the screen.
    pub fn clear_plane(&self, plane: plane::Handle) -> Result<(), Error> {
        if !self.active.load(Ordering::SeqCst) {
            return Err(Error::DeviceInactive);
        }

        let mapping = self.prop_mapping.read().unwrap();
        let mut req = AtomicRequest::new(&mapping);
        req.reset_plane(plane)?;
        let req = req.build()?;

        let result = self
            .fd
            .atomic_commit(AtomicCommitFlags::empty(), req)
            .map_err(|source| {
                Error::Access(AccessError {
                    errmsg: "Failed to commit on clear_plane",
                    dev: self.fd.dev_path(),
                    source,
                })
            });

        if result.is_ok() {
            self.used_planes.lock().unwrap().remove(&plane);
        }

        result
    }

    #[profiling::function]
    fn clear_state(&self) -> Result<(), Error> {
        if !self.active.load(Ordering::SeqCst) {
            return Err(Error::DeviceInactive);
        }

        let _guard = self.span.enter();
        let prop_mapping = self.prop_mapping.read().unwrap();
        let mut req = AtomicRequest::new(&prop_mapping);
        // reset all planes we used
        for plane in self.used_planes.lock().unwrap().iter() {
            req.reset_plane(*plane)?;
        }

        // disable connectors again
        let current = self.state.read().unwrap();
        for conn in current.connectors.iter() {
            req.reset_connector(*conn)?;
        }

        // disable crtc
        req.reset_crtc(self.crtc)?;
        std::mem::drop(current);

        let res = self
            .fd
            .atomic_commit(AtomicCommitFlags::ALLOW_MODESET, req.build()?)
            .map_err(|source| {
                Error::Access(AccessError {
                    errmsg: "Failed to commit on clear_state",
                    dev: self.fd.dev_path(),
                    source,
                })
            });

        if res.is_ok() {
            self.used_planes.lock().unwrap().clear();
            self.state.write().unwrap().clear();
        }

        res
    }

    pub(crate) fn reset_state<B: DevPath + ControlDevice + 'static>(
        &self,
        fd: Option<&B>,
    ) -> Result<(), Error> {
        *self.state.write().unwrap() = if let Some(fd) = fd {
            State::current_state(fd, self.crtc, &mut self.prop_mapping.write().unwrap())?
        } else {
            State::current_state(&*self.fd, self.crtc, &mut self.prop_mapping.write().unwrap())?
        };

        // Re-initialize the mode blob which might got lost after suspend/resume
        let mut pending = self.pending.write().unwrap();
        let blob = self.fd.create_property_blob(&pending.mode).map_err(|source| {
            Error::Access(AccessError {
                errmsg: "Failed to create Property Blob for mode",
                dev: self.fd.dev_path(),
                source,
            })
        })?;

        let old_blob = std::mem::replace(&mut pending.blob, blob);
        let _ = self.fd.destroy_property_blob(old_blob.into());

        Ok(())
    }

    pub(crate) fn device_fd(&self) -> &DrmDeviceFd {
        self.fd.device_fd()
    }

    pub fn clear(&self) -> Result<(), Error> {
        self.clear_state()
    }
}

struct TestBuffer {
    fd: Arc<DrmDeviceInternal>,
    db: DumbBuffer,
    fb: framebuffer::Handle,
}

impl AsRef<framebuffer::Handle> for TestBuffer {
    fn as_ref(&self) -> &framebuffer::Handle {
        &self.fb
    }
}

impl Drop for TestBuffer {
    fn drop(&mut self) {
        let _ = self.fd.destroy_framebuffer(self.fb);
        let _ = self.fd.destroy_dumb_buffer(self.db);
    }
}

impl Drop for AtomicDrmSurface {
    fn drop(&mut self) {
        if !self.active.load(Ordering::SeqCst) {
            // the device is gone or we are on another tty
            // old state has been restored, we shouldn't touch it.
            // if we are on another tty the connectors will get disabled
            // by the device, when switching back
            return;
        }

        let _guard = self.span.enter();
        if let Err(err) = self.clear_state() {
            warn!("Unable to clear state: {}", err);
        }
    }
}

#[inline]
fn to_fixed<N: Coordinate>(n: N) -> u32 {
    f64::round(n.to_f64() * (1 << 16) as f64) as u32
}

bitflags::bitflags! {
    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
    struct DrmRotation: u8 {
        const ROTATE_0      =   0b00000001;
        const ROTATE_90     =   0b00000010;
        const ROTATE_180    =   0b00000100;
        const ROTATE_270    =   0b00001000;
        const REFLECT_X     =   0b00010000;
        const REFLECT_Y     =   0b00100000;
    }
}

impl From<Transform> for DrmRotation {
    #[inline]
    fn from(transform: Transform) -> Self {
        match transform {
            Transform::Normal => DrmRotation::ROTATE_0,
            Transform::_90 => DrmRotation::ROTATE_90,
            Transform::_180 => DrmRotation::ROTATE_180,
            Transform::_270 => DrmRotation::ROTATE_270,
            Transform::Flipped => DrmRotation::REFLECT_Y,
            Transform::Flipped90 => DrmRotation::REFLECT_Y | DrmRotation::ROTATE_90,
            Transform::Flipped180 => DrmRotation::REFLECT_Y | DrmRotation::ROTATE_180,
            Transform::Flipped270 => DrmRotation::REFLECT_Y | DrmRotation::ROTATE_270,
        }
    }
}

#[cfg(test)]
mod test {
    use crate::{
        backend::drm::surface::atomic::to_fixed,
        utils::{Physical, Rectangle},
    };

    use super::AtomicDrmSurface;

    fn is_send<S: Send>() {}

    #[test]
    fn surface_is_send() {
        is_send::<AtomicDrmSurface>();
    }

    #[test]
    fn test_fixed_point() {
        let geometry: Rectangle<f64, Physical> = Rectangle::from_size((1920.0, 1080.0).into());
        let fixed = to_fixed(geometry.size.w) as u64;
        assert_eq!(125829120, fixed);
    }

    #[test]
    fn test_fractional_fixed_point() {
        let geometry: Rectangle<f64, Physical> = Rectangle::from_size((1920.1, 1080.0).into());
        let fixed = to_fixed(geometry.size.w) as u64;
        assert_eq!(125835674, fixed);
    }
}

#[cfg(debug_assertions)]
struct AtomicRequest<'a> {
    mapping: &'a PropMapping,
    crtc_props: HashMap<crtc::Handle, HashMap<&'static str, property::Value<'a>>>,
    connector_props: HashMap<connector::Handle, HashMap<&'static str, property::Value<'a>>>,
    plane_props: HashMap<plane::Handle, HashMap<&'static str, property::Value<'a>>>,
}

#[cfg(not(debug_assertions))]
#[cfg_attr(not(debug_assertions), derive(Debug))]
struct AtomicRequest<'a> {
    mapping: &'a PropMapping,
    request: AtomicModeReq,
}

#[cfg(debug_assertions)]
impl fmt::Debug for AtomicRequest<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("AtomicRequest")
            .field("crtcs", &self.crtc_props)
            .field("connectors", &self.connector_props)
            .field("plane", &self.plane_props)
            .finish()
    }
}

#[cfg(debug_assertions)]
impl<'a> AtomicRequest<'a> {
    fn new(mapping: &'a PropMapping) -> AtomicRequest<'a> {
        AtomicRequest {
            mapping,
            crtc_props: HashMap::new(),
            connector_props: HashMap::new(),
            plane_props: HashMap::new(),
        }
    }

    fn set_connector(&mut self, conn: connector::Handle, crtc: crtc::Handle) -> Result<(), Error> {
        let connector_props = self.connector_props.entry(conn).or_default();
        connector_props.insert("CRTC_ID", property::Value::CRTC(Some(crtc)));
        Ok(())
    }

    fn reset_connector(&mut self, conn: connector::Handle) -> Result<(), Error> {
        let connector_props = self.connector_props.entry(conn).or_default();
        connector_props.insert("CRTC_ID", property::Value::CRTC(None));
        Ok(())
    }

    fn set_crtc(
        &mut self,
        crtc: crtc::Handle,
        mode: Option<property::Value<'static>>,
        vrr: bool,
    ) -> Result<(), Error> {
        let crtc_props = self.crtc_props.entry(crtc).or_default();

        crtc_props.insert("ACTIVE", property::Value::Boolean(true));
        if let Some(blob) = mode {
            crtc_props.insert("MODE_ID", blob);
        }
        if self.mapping.crtc_prop_handle(crtc, "VRR_ENABLED").is_ok() {
            crtc_props.insert("VRR_ENABLED", property::Value::Boolean(vrr));
        } else if vrr {
            return Err(Error::UnknownProperty {
                handle: crtc.into(),
                name: "VRR_ENABLED",
            });
        }

        Ok(())
    }

    fn reset_crtc(&mut self, crtc: crtc::Handle) -> Result<(), Error> {
        let crtc_props = self.crtc_props.entry(crtc).or_default();

        crtc_props.insert("ACTIVE", property::Value::Boolean(false));
        crtc_props.insert("MODE_ID", property::Value::Blob(0));
        if self.mapping.crtc_prop_handle(crtc, "VRR_ENABLED").is_ok() {
            crtc_props.insert("VRR_ENABLED", property::Value::Boolean(false));
        }
        Ok(())
    }

    fn set_plane(&mut self, crtc: crtc::Handle, plane_state: &PlaneState<'a>) -> Result<(), Error> {
        let handle = plane_state.handle;
        let plane_props = self.plane_props.entry(handle).or_default();

        if let Some(config) = plane_state.config.as_ref() {
            plane_props.insert("CRTC_ID", property::Value::CRTC(Some(crtc)));
            plane_props.insert("FB_ID", property::Value::Framebuffer(Some(config.fb)));
            // these are 16.16. fixed point
            plane_props.insert(
                "SRC_X",
                property::Value::UnsignedRange(to_fixed(config.src.loc.x) as u64),
            );
            plane_props.insert(
                "SRC_Y",
                property::Value::UnsignedRange(to_fixed(config.src.loc.y) as u64),
            );
            plane_props.insert(
                "SRC_W",
                property::Value::UnsignedRange(to_fixed(config.src.size.w) as u64),
            );
            plane_props.insert(
                "SRC_H",
                property::Value::UnsignedRange(to_fixed(config.src.size.h) as u64),
            );

            plane_props.insert("CRTC_X", property::Value::SignedRange(config.dst.loc.x as i64));
            plane_props.insert("CRTC_Y", property::Value::SignedRange(config.dst.loc.y as i64));
            plane_props.insert("CRTC_W", property::Value::UnsignedRange(config.dst.size.w as u64));
            plane_props.insert("CRTC_H", property::Value::UnsignedRange(config.dst.size.h as u64));

            if self.mapping.plane_prop_handle(handle, "rotation").is_ok() {
                plane_props.insert(
                    "rotation",
                    property::Value::Bitmask(DrmRotation::from(config.transform).bits() as u64),
                );
            } else if config.transform != Transform::Normal {
                // if we are missing the rotation property we can no rely on
                // the driver to report a non working configuration and can
                // only guarantee that Transform::Normal (no rotation) will
                // work
                return Err(Error::UnknownProperty {
                    handle: handle.into(),
                    name: "rotation",
                });
            }
            if self.mapping.plane_prop_handle(handle, "alpha").is_ok() {
                plane_props.insert(
                    "alpha",
                    property::Value::UnsignedRange((config.alpha * u16::MAX as f32).round() as u64),
                );
            } else if config.alpha != 1.0 {
                // if we are missing the alpha property we can not display any transparent alpha values
                return Err(Error::UnknownProperty {
                    handle: handle.into(),
                    name: "alpha",
                });
            }
            if self.mapping.plane_prop_handle(handle, "FB_DAMAGE_CLIPS").is_ok() {
                if let Some(damage) = config.damage_clips.as_ref() {
                    plane_props.insert("FB_DAMAGE_CLIPS", *damage);
                } else {
                    plane_props.insert("FB_DAMAGE_CLIPS", property::Value::Blob(0));
                }
            }
            if self.mapping.plane_prop_handle(handle, "IN_FENCE_FD").is_ok() {
                if let Some(fence) = config.fence.as_ref().map(|f| f.as_raw_fd()) {
                    plane_props.insert("IN_FENCE_FD", property::Value::SignedRange(fence as i64));
                } else {
                    plane_props.insert("IN_FENCE_FD", property::Value::SignedRange(-1));
                }
            } else if config.fence.is_some() {
                return Err(Error::UnknownProperty {
                    handle: handle.into(),
                    name: "IN_FENCE_FD",
                });
            }
        } else {
            self.reset_plane(handle)?;
        }

        Ok(())
    }

    fn reset_plane(&mut self, plane: plane::Handle) -> Result<(), Error> {
        let plane_props = self.plane_props.entry(plane).or_default();

        plane_props.insert("CRTC_ID", property::Value::CRTC(None));
        plane_props.insert("FB_ID", property::Value::Framebuffer(None));
        // these are 16.16. fixed point
        plane_props.insert("SRC_X", property::Value::UnsignedRange(0u64));
        plane_props.insert("SRC_Y", property::Value::UnsignedRange(0u64));
        plane_props.insert("SRC_W", property::Value::UnsignedRange(0u64));
        plane_props.insert("SRC_H", property::Value::UnsignedRange(0u64));

        plane_props.insert("CRTC_X", property::Value::SignedRange(0i64));
        plane_props.insert("CRTC_Y", property::Value::SignedRange(0i64));
        plane_props.insert("CRTC_W", property::Value::UnsignedRange(0u64));
        plane_props.insert("CRTC_H", property::Value::UnsignedRange(0u64));

        if self.mapping.plane_prop_handle(plane, "rotation").is_ok() {
            plane_props.insert(
                "rotation",
                property::Value::Bitmask(DrmRotation::from(Transform::Normal).bits() as u64),
            );
        }
        if self.mapping.plane_prop_handle(plane, "alpha").is_ok() {
            plane_props.insert("alpha", property::Value::UnsignedRange(0xffff));
        }
        if self.mapping.plane_prop_handle(plane, "FB_DAMAGE_CLIPS").is_ok() {
            plane_props.insert("FB_DAMAGE_CLIPS", property::Value::Blob(0));
        }
        if self.mapping.plane_prop_handle(plane, "IN_FENCE_FD").is_ok() {
            plane_props.insert("IN_FENCE_FD", property::Value::SignedRange(-1));
        }
        Ok(())
    }

    fn build(&self) -> Result<AtomicModeReq, Error> {
        let mut req = AtomicModeReq::new();

        for (crtc, props) in &self.crtc_props {
            for (name, value) in props {
                req.add_property(*crtc, self.mapping.crtc_prop_handle(*crtc, name)?, *value);
            }
        }
        for (conn, props) in &self.connector_props {
            for (name, value) in props {
                req.add_property(*conn, self.mapping.conn_prop_handle(*conn, name)?, *value);
            }
        }
        for (plane, props) in &self.plane_props {
            for (name, value) in props {
                req.add_property(*plane, self.mapping.plane_prop_handle(*plane, name)?, *value);
            }
        }

        Ok(req)
    }
}

#[cfg(not(debug_assertions))]
impl<'a> AtomicRequest<'a> {
    fn new(mapping: &'a PropMapping) -> AtomicRequest<'a> {
        AtomicRequest {
            mapping,
            request: AtomicModeReq::new(),
        }
    }

    fn set_connector(&mut self, conn: connector::Handle, crtc: crtc::Handle) -> Result<(), Error> {
        self.request.add_property(
            conn,
            self.mapping.conn_prop_handle(conn, "CRTC_ID")?,
            property::Value::CRTC(Some(crtc)),
        );
        Ok(())
    }

    fn reset_connector(&mut self, conn: connector::Handle) -> Result<(), Error> {
        self.request.add_property(
            conn,
            self.mapping.conn_prop_handle(conn, "CRTC_ID")?,
            property::Value::CRTC(None),
        );
        Ok(())
    }

    fn set_crtc(
        &mut self,
        crtc: crtc::Handle,
        mode: Option<property::Value<'static>>,
        vrr: bool,
    ) -> Result<(), Error> {
        if let Some(blob) = mode {
            self.request
                .add_property(crtc, self.mapping.crtc_prop_handle(crtc, "MODE_ID")?, blob);
        }

        self.request.add_property(
            crtc,
            self.mapping.crtc_prop_handle(crtc, "ACTIVE")?,
            property::Value::Boolean(true),
        );

        if let Ok(vrr_prop) = self.mapping.crtc_prop_handle(crtc, "VRR_ENABLED") {
            self.request
                .add_property(crtc, vrr_prop, property::Value::Boolean(vrr));
        } else if vrr {
            return Err(Error::UnknownProperty {
                handle: crtc.into(),
                name: "VRR_ENABLED",
            });
        }

        Ok(())
    }

    fn reset_crtc(&mut self, crtc: crtc::Handle) -> Result<(), Error> {
        self.request.add_property(
            crtc,
            self.mapping.crtc_prop_handle(crtc, "ACTIVE")?,
            property::Value::Boolean(false),
        );
        self.request.add_property(
            crtc,
            self.mapping.crtc_prop_handle(crtc, "MODE_ID")?,
            property::Value::Blob(0),
        );
        if let Ok(prop) = self.mapping.crtc_prop_handle(crtc, "VRR_ENABLED") {
            self.request
                .add_property(crtc, prop, property::Value::Boolean(false));
        }
        Ok(())
    }

    fn set_plane(&mut self, crtc: crtc::Handle, plane_state: &PlaneState<'_>) -> Result<(), Error> {
        let handle = plane_state.handle;
        if let Some(config) = plane_state.config.as_ref() {
            // connect the plane to the CRTC
            self.request.add_property(
                handle,
                self.mapping.plane_prop_handle(handle, "CRTC_ID")?,
                property::Value::CRTC(Some(crtc)),
            );

            // Set the fb for the plane
            self.request.add_property(
                handle,
                self.mapping.plane_prop_handle(handle, "FB_ID")?,
                property::Value::Framebuffer(Some(config.fb)),
            );

            self.request.add_property(
                handle,
                self.mapping.plane_prop_handle(handle, "SRC_X")?,
                // these are 16.16. fixed point
                property::Value::UnsignedRange(to_fixed(config.src.loc.x) as u64),
            );
            self.request.add_property(
                handle,
                self.mapping.plane_prop_handle(handle, "SRC_Y")?,
                // these are 16.16. fixed point
                property::Value::UnsignedRange(to_fixed(config.src.loc.y) as u64),
            );
            self.request.add_property(
                handle,
                self.mapping.plane_prop_handle(handle, "SRC_W")?,
                // these are 16.16. fixed point
                property::Value::UnsignedRange(to_fixed(config.src.size.w) as u64),
            );
            self.request.add_property(
                handle,
                self.mapping.plane_prop_handle(handle, "SRC_H")?,
                // these are 16.16. fixed point
                property::Value::UnsignedRange(to_fixed(config.src.size.h) as u64),
            );

            self.request.add_property(
                handle,
                self.mapping.plane_prop_handle(handle, "CRTC_X")?,
                property::Value::SignedRange(config.dst.loc.x as i64),
            );
            self.request.add_property(
                handle,
                self.mapping.plane_prop_handle(handle, "CRTC_Y")?,
                property::Value::SignedRange(config.dst.loc.y as i64),
            );
            self.request.add_property(
                handle,
                self.mapping.plane_prop_handle(handle, "CRTC_W")?,
                property::Value::UnsignedRange(config.dst.size.w as u64),
            );
            self.request.add_property(
                handle,
                self.mapping.plane_prop_handle(handle, "CRTC_H")?,
                property::Value::UnsignedRange(config.dst.size.h as u64),
            );
            if let Ok(prop) = self.mapping.plane_prop_handle(handle, "rotation") {
                self.request.add_property(
                    handle,
                    prop,
                    property::Value::Bitmask(DrmRotation::from(config.transform).bits() as u64),
                );
            } else if config.transform != Transform::Normal {
                // if we are missing the rotation property we can no rely on
                // the driver to report a non working configuration and can
                // only guarantee that Transform::Normal (no rotation) will
                // work
                return Err(Error::UnknownProperty {
                    handle: handle.into(),
                    name: "rotation",
                });
            }
            if let Ok(prop) = self.mapping.plane_prop_handle(handle, "alpha") {
                self.request.add_property(
                    handle,
                    prop,
                    property::Value::UnsignedRange((config.alpha * u16::MAX as f32).round() as u64),
                );
            } else if config.alpha != 1.0 {
                // if we are missing the alpha property we can not display any transparent alpha values
                return Err(Error::UnknownProperty {
                    handle: handle.into(),
                    name: "alpha",
                });
            }
            if let Ok(prop) = self.mapping.plane_prop_handle(handle, "FB_DAMAGE_CLIPS") {
                if let Some(damage) = config.damage_clips.as_ref() {
                    self.request.add_property(handle, prop, *damage);
                } else {
                    self.request.add_property(handle, prop, property::Value::Blob(0));
                }
            }
            if let Ok(prop) = self.mapping.plane_prop_handle(handle, "IN_FENCE_FD") {
                if let Some(fence) = config.fence.as_ref().map(|f| f.as_raw_fd()) {
                    self.request
                        .add_property(handle, prop, property::Value::SignedRange(fence as i64));
                } else {
                    self.request
                        .add_property(handle, prop, property::Value::SignedRange(-1));
                }
            } else if config.fence.is_some() {
                return Err(Error::UnknownProperty {
                    handle: handle.into(),
                    name: "IN_FENCE_FD",
                });
            }
        } else {
            self.reset_plane(handle)?;
        }

        Ok(())
    }

    fn reset_plane(&mut self, plane: plane::Handle) -> Result<(), Error> {
        self.request.add_property(
            plane,
            self.mapping.plane_prop_handle(plane, "CRTC_ID")?,
            property::Value::CRTC(None),
        );

        self.request.add_property(
            plane,
            self.mapping.plane_prop_handle(plane, "FB_ID")?,
            property::Value::Framebuffer(None),
        );

        // reset the plane properties
        self.request.add_property(
            plane,
            self.mapping.plane_prop_handle(plane, "SRC_X")?,
            // these are 16.16. fixed point
            property::Value::UnsignedRange(0u64),
        );
        self.request.add_property(
            plane,
            self.mapping.plane_prop_handle(plane, "SRC_Y")?,
            // these are 16.16. fixed point
            property::Value::UnsignedRange(0u64),
        );
        self.request.add_property(
            plane,
            self.mapping.plane_prop_handle(plane, "SRC_W")?,
            // these are 16.16. fixed point
            property::Value::UnsignedRange(0u64),
        );
        self.request.add_property(
            plane,
            self.mapping.plane_prop_handle(plane, "SRC_H")?,
            // these are 16.16. fixed point
            property::Value::UnsignedRange(0u64),
        );

        self.request.add_property(
            plane,
            self.mapping.plane_prop_handle(plane, "CRTC_X")?,
            property::Value::SignedRange(0i64),
        );
        self.request.add_property(
            plane,
            self.mapping.plane_prop_handle(plane, "CRTC_Y")?,
            property::Value::SignedRange(0i64),
        );
        self.request.add_property(
            plane,
            self.mapping.plane_prop_handle(plane, "CRTC_W")?,
            property::Value::UnsignedRange(0u64),
        );
        self.request.add_property(
            plane,
            self.mapping.plane_prop_handle(plane, "CRTC_H")?,
            property::Value::UnsignedRange(0u64),
        );
        if let Ok(prop) = self.mapping.plane_prop_handle(plane, "rotation") {
            self.request.add_property(
                plane,
                prop,
                property::Value::Bitmask(DrmRotation::from(Transform::Normal).bits() as u64),
            );
        }
        if let Ok(prop) = self.mapping.plane_prop_handle(plane, "alpha") {
            self.request
                .add_property(plane, prop, property::Value::UnsignedRange(0xffff));
        }
        if let Ok(prop) = self.mapping.plane_prop_handle(plane, "FB_DAMAGE_CLIPS") {
            self.request.add_property(plane, prop, property::Value::Blob(0));
        }
        if let Ok(prop) = self.mapping.plane_prop_handle(plane, "IN_FENCE_FD") {
            self.request
                .add_property(plane, prop, property::Value::SignedRange(-1));
        }
        Ok(())
    }

    fn build(&self) -> Result<AtomicModeReq, Error> {
        Ok(self.request.clone())
    }
}

impl<'a> AtomicRequest<'a> {
    fn build_request(
        mapping: &'a PropMapping,
        crtc: crtc::Handle,
        blob: Option<property::Value<'static>>,
        vrr: bool,
        connectors: impl IntoIterator<Item = &'a connector::Handle>,
        removed_connectors: impl IntoIterator<Item = &'a connector::Handle>,
        planes: impl IntoIterator<Item = &'a PlaneState<'a>>,
    ) -> Result<AtomicRequest<'a>, Error> {
        let mut req = AtomicRequest::new(mapping);

        // requests consist out of a set of properties and their new values
        // for different drm objects (crtc, plane, connector, ...).

        // for every connector that is new, we need to set our crtc_id
        for conn in connectors {
            req.set_connector(*conn, crtc)?;
        }

        // for every connector that got removed, we need to set no crtc_id.
        // (this is a bit problematic, because this means we need to remove, commit, add, commit
        // in the right order to move a connector to another surface. otherwise we disable the
        // the connector here again...)
        for conn in removed_connectors {
            req.reset_connector(*conn)?;
        }

        // Set the crtc properties (active, mode_id, vrr_enabled).
        req.set_crtc(crtc, blob, vrr)?;

        for plane_state in planes.into_iter() {
            req.set_plane(crtc, plane_state)?;
        }

        Ok(req)
    }
}
