//! Device and DRM-surface construction for the udev backend: probing a card,
//! adding/removing an `OutputDevice`, live reconfiguration of its heads, and
//! building the DRM compositor and scanout surfaces. Associated methods on `Udev`.

use super::*;

impl Udev {
    pub(super) fn has_connected_display(drm: &DrmDevice) -> bool {
        let Ok(resources) = drm.resource_handles() else {
            return false;
        };

        resources.connectors().iter().any(|conn| {
            drm.get_connector(*conn, true)
                .map(|c| c.state() == ConnectorState::Connected)
                .unwrap_or(false)
        })
    }

    pub(super) fn add_device(
        &mut self,
        display: &DisplayHandle,
        windows: &mut Windows,
        path: &Path,
        retries_left: u32,
    ) -> Result<bool, Box<dyn Error>> {
        if self.output_device.is_some() {
            debug!("Output device already configured, skipping {:?}", path);
            return Ok(false);
        }

        let open_flags = OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY;
        let device_fd = match self.session.open(path, open_flags) {
            Ok(fd) => DrmDeviceFd::new(DeviceFd::from(fd)),
            Err(e) => {
                if !self.session.is_active() {
                    return Err(e.into());
                }
                if retries_left > 0 {
                    info!(
                        "Device {:?} busy, scheduling retry ({} left)...",
                        path,
                        retries_left - 1
                    );
                    let timer = Timer::from_duration(DRM_RETRY_DELAY);
                    let retry_path = path.to_path_buf();
                    let remaining = retries_left - 1;
                    self.event_loop
                        .insert_source(timer, move |_, _, forge| {
                            retry_add_device(forge, &retry_path, remaining);
                            TimeoutAction::Drop
                        })
                        .expect("drm retry");
                } else {
                    error!(
                        "Failed to open device {:?} after all retries: {}. \
                         Ensure no other compositor is running and the session has seat access.",
                        path, e
                    );
                }
                return Err(e.into());
            },
        };

        let (mut drm, drm_notifier) = DrmDevice::new(device_fd.clone(), true)?;

        if !drm.is_active() {
            // Opened before libseat dispatched the session resume; retry once it has.
            if retries_left > 0 {
                info!(
                    "Device {:?} opened without DRM master, scheduling retry ({} left)...",
                    path,
                    retries_left - 1
                );
                let timer = Timer::from_duration(DRM_RETRY_DELAY);
                let retry_path = path.to_path_buf();
                let remaining = retries_left - 1;
                self.event_loop
                    .insert_source(timer, move |_, _, forge| {
                        retry_add_device(forge, &retry_path, remaining);
                        TimeoutAction::Drop
                    })
                    .expect("drm master retry");
            } else {
                error!("Device {:?} never acquired DRM master after retries", path);
            }
            return Ok(false);
        }

        if !Self::has_connected_display(&drm) {
            debug!("Device {:?} has no connected display, skipping", path);
            return Ok(false);
        }

        info!("Initializing display device: {:?}", path);

        let gbm = GbmDevice::new(device_fd)?;
        let egl_display = unsafe { EGLDisplay::new(gbm.clone())? };
        let context = EGLContext::new(&egl_display)?;
        let gles = unsafe { GlesRenderer::new(context).expect("create renderer") };

        let (presenter, heads, primary_connector, committers, has_external, output_infos) = self
            .create_drm_compositor(display, windows, &gles, &mut drm, &gbm)
            .ok_or("failed to create drm compositor - no valid connector/crtc")?;

        let device_id = drm.device_id();

        // The vblank notifier and every output's commit half move to the KMS
        // thread. From here the main thread never page-flips or reads vblanks; it
        // dispatches `Flip` commands and reacts to `Presented`/`FlipFailed`.
        self.kms.send(KmsCmd::SetDevice {
            notifier: drm_notifier,
            outputs: committers,
        });
        let kms = self.kms.sender();

        let graphics = Graphics::new();
        let last_render_states = RenderElementStates { states: HashMap::new() };

        self.output_device = Some(OutputDevice {
            last_render_states,
            presenter,
            heads,
            primary_connector,
            graphics,
            gles,
            kms,
            gbm,
            drm,
            id: device_id,
            screencopy: None,
            export_dmabuf: None,
            image_copy: None,
            primary_flipped_last: true,
            has_external,
            output_infos,
            primary_pending: false,
        });

        Ok(true)
    }

    pub(super) fn remove_device(&mut self, device_id: DeviceId) {
        if let Some(dev) = self.output_device.take().filter(|d| d.id == device_id) {
            // Drop the device's notifier and committers on the KMS thread before
            // the `DrmDevice` itself goes away with `dev`.
            self.kms.send(KmsCmd::ClearDevice);
            drop(dev);
            info!("Removed display device (id: {})", device_id);
        }
    }

    pub(super) fn change_device(
        &mut self,
        display: &DisplayHandle,
        windows: &mut Windows,
        device_id: DeviceId,
    ) -> Result<(), Box<dyn Error>> {
        let Some(dev) = self.output_device.as_mut().filter(|d| d.id == device_id) else {
            return Ok(());
        };

        let resources = dev.drm.resource_handles()?;
        let mut connected: HashMap<ConnectorHandle, ConnectorInfo> = HashMap::new();
        for handle in resources.connectors() {
            if let Ok(info) = dev.drm.get_connector(*handle, true) {
                if info.state() == ConnectorState::Connected {
                    connected.insert(*handle, info);
                }
            }
        }
        dev.has_external = connected.values().any(Self::is_external_connector);

        let mut config = crate::config::ForgeConfig::load();
        Self::learn_identities(&mut config, &dev.drm, connected.values());
        let extend = matches!(config.display.mode, crate::config::DisplayMode::Extend)
            && !crate::output::mirror_forced();
        dev.output_infos = Self::collect_output_infos(
            &dev.drm,
            connected.values(),
            dev.primary_connector,
            &config,
            extend,
        );
        if extend {
            crate::output::set_mirror_scale(None);
        } else {
            // Plugging or unplugging a panel of a different density moves the
            // shared factor, so relayout against the new one.
            Self::refresh_mirror_scale(connected.values());
            windows.apply_output_scale();
        }

        let dropped_heads: Vec<(crtc::Handle, String)> = dev
            .heads
            .iter()
            .filter(|m| !connected.contains_key(&m.connector))
            .map(|m| (m.presenter.crtc(), m.name.clone()))
            .collect();
        let before = dev.heads.len();
        dev.heads.retain(|m| connected.contains_key(&m.connector));
        let dropped = before - dev.heads.len();
        if dropped > 0 {
            info!("Hotplug: dropped {dropped} head(s) for disconnected connector(s)");
        }
        for (crtc, name) in dropped_heads {
            crate::kms::send_on(&dev.kms, KmsCmd::RemoveOutput { crtc });
            windows.remove_extra_output(&name);
        }

        let formats = match Bind::<Dmabuf>::supported_formats(&dev.gles) {
            Some(f) => f,
            None => return Ok(()),
        };

        let already: std::collections::HashSet<ConnectorHandle> =
            dev.heads.iter().map(|m| m.connector).collect();

        for (handle, info) in connected.iter() {
            if *handle == dev.primary_connector { continue; }
            if already.contains(handle) { continue; }

            let name = Self::connector_name(info);
            let identity = Self::connector_identity(&dev.drm, info);
            let head_cfg = config.display.output_for(&name, identity.as_deref());
            let cfg_mode = if extend { head_cfg.and_then(|o| o.mode.as_deref()) } else { None };
            let Some(mode) = Self::pick_mode(info, cfg_mode) else { continue };

            let mut used_crtcs: Vec<crtc::Handle> = vec![dev.presenter.crtc()];
            for m in &dev.heads {
                used_crtcs.push(m.presenter.crtc());
            }

            let Some(surface) =
                Self::create_surface(&mut dev.drm, &resources, info, mode, &used_crtcs)
            else { continue };

            let (mw, mh) = mode.size();
            let size = Size::<i32, Physical>::from((mw as i32, mh as i32));
            let code = Self::pick_format(&surface, &formats);
            let modifiers = Self::pick_modifiers(&surface, &formats, code);
            let allocator = GbmAllocator::new(
                dev.gbm.clone(),
                GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT,
            );

            let (kind, head_scale) = if extend {
                let logical_mode = Mode {
                    size: (mw as i32, mh as i32).into(),
                    refresh: mode.vrefresh() as i32 * 1000,
                };
                if head_cfg.is_some_and(|o| !o.enabled) {
                    info!("Hotplug: extend output {name} disabled by config, skipping");
                    continue;
                }
                let (cw, ch) = info.size().unwrap_or((0, 0));
                let (make, model, serial) = Self::connector_edid(&dev.drm, info)
                    .map(|e| (e.make, e.model, e.serial))
                    .unwrap_or_else(|| ("Forge".into(), "Generic DRM".into(), "Unknown".into()));
                // Auto placement: to the right of everything already there.
                let next_x = windows
                    .output_rects()
                    .iter()
                    .map(|r| r.loc.x + r.size.w)
                    .max()
                    .unwrap_or(0);
                let pinned = head_cfg.and_then(|o| o.position);
                let location: Point<i32, Logical> = pinned
                    .map(Into::into)
                    .unwrap_or_else(|| (next_x, 0).into());
                let mut out = Output::new_positioned(
                    display,
                    name.clone(),
                    logical_mode,
                    PhysicalProperties {
                        size: (cw as i32, ch as i32).into(),
                        subpixel: Subpixel::Unknown,
                        serial_number: serial,
                        model,
                        make,
                    },
                    head_cfg.and_then(|o| o.scale),
                    location,
                    pinned.is_some(),
                );
                out.set_identity(identity.clone());
                let scale = out.scale();
                info!("Hotplug: extend output {name} at {location:?}, scale {scale}");
                windows.add_extra_output(out);
                (HeadKind::Extend, scale)
            } else {
                info!("Hotplug: added mirror presenter for {name}");
                (HeadKind::Mirror, 1.0)
            };

            let (presenter, committer) = Presenter::new(
                surface,
                allocator,
                GbmFramebufferExporter::new(dev.gbm.clone(), NodeFilter::All),
                code,
                modifiers,
                size,
                head_scale,
                None,
            );
            let crtc = presenter.crtc();
            dev.heads.push(SecondaryHead {
                presenter,
                pending_frame: false,
                connector: *handle,
                kind,
                name,
            });
            crate::kms::send_on(&dev.kms, KmsCmd::AddOutput { crtc, committer });
        }

        if extend {
            // Undo the shared minimum the primary picked up while mirroring —
            // after the heads exist, so the relayout anchors every window to
            // its own screen instead of pulling it into the primary.
            windows.apply_output_scale();
        }
        windows.refresh_output_overlaps();

        Ok(())
    }

    pub(super) fn create_drm_compositor(
        &mut self,
        display: &DisplayHandle,
        windows: &mut Windows,
        gles: &GlesRenderer,
        drm: &mut DrmDevice,
        gbm: &GbmDevice<DrmDeviceFd>,
    ) -> Option<(
        Presenter,
        Vec<SecondaryHead>,
        ConnectorHandle,
        Vec<(crtc::Handle, Committer)>,
        bool,
        Vec<OutputInfo>,
    )> {
        let formats = Bind::<Dmabuf>::supported_formats(gles)?;
        let resources = drm.resource_handles().ok()?;

        // Find all connected connectors
        let connected: Vec<ConnectorInfo> = resources
            .connectors()
            .iter()
            .filter_map(|conn| {
                drm.get_connector(*conn, true)
                    .ok()
                    .filter(|c| c.state() == ConnectorState::Connected)
            })
            .collect();
        let has_external = connected.iter().any(Self::is_external_connector);

        let mut config = crate::config::ForgeConfig::load();
        Self::learn_identities(&mut config, drm, connected.iter());
        let extend = matches!(config.display.mode, crate::config::DisplayMode::Extend)
            && !crate::output::mirror_forced();
        if extend {
            // The min-across-connectors factor exists for the shared mirror
            // framebuffer; with per-screen scenes it must not influence anyone.
            crate::output::set_mirror_scale(None);
        } else {
            Self::refresh_mirror_scale(connected.iter());
        }

        // Extend elects the internal panel (or the configured pick) as the
        // primary that carries chrome and layers; mirror keeps enumeration
        // order, where the choice only names the shared mode.
        let primary = if extend {
            connected
                .iter()
                .find(|c| {
                    config
                        .display
                        .output_for(
                            &Self::connector_name(c),
                            Self::connector_identity(drm, c).as_deref(),
                        )
                        .is_some_and(|o| o.primary)
                })
                .or_else(|| connected.iter().find(|c| !Self::is_external_connector(c)))
                .unwrap_or(connected.first()?)
        } else {
            connected.first()?
        };

        windows.clear_extra_outputs();

        for conn in &connected {
            info!("Connected output: {:?} ({:?})", conn.interface(), conn.state());
        }

        // The primary's configured mode (extend only), else its preferred.
        let primary_name = Self::connector_name(primary);
        let primary_identity = Self::connector_identity(drm, primary);
        let primary_cfg_mode = if extend {
            config
                .display
                .output_for(&primary_name, primary_identity.as_deref())
                .and_then(|o| o.mode.clone())
        } else {
            None
        };
        let primary_mode = Self::pick_mode(primary, primary_cfg_mode.as_deref())?;

        let (w, h) = primary_mode.size();
        info!("Primary display mode: {}x{}@{}Hz", w, h, primary_mode.vrefresh());

        let mode = Mode {
            size: (w as i32, h as i32).into(),
            refresh: primary_mode.vrefresh() as i32 * 1000,
        };

        let (pw, ph) = primary.size().unwrap_or((0, 0));
        let output_name = primary_name.clone();
        let (make, model, serial) = Self::connector_edid(drm, primary)
            .map(|e| (e.make, e.model, e.serial))
            .unwrap_or_else(|| ("Forge".into(), "Generic DRM".into(), "Unknown".into()));
        let properties = PhysicalProperties {
            size: (pw as i32, ph as i32).into(),
            subpixel: Subpixel::Unknown,
            serial_number: serial,
            model,
            make,
        };

        if extend {
            // The primary anchors the global space at (0, 0); its scale is the
            // per-connector pick or its own panel DPI, never the mirror minimum.
            let pick = config
                .display
                .output_for(&output_name, primary_identity.as_deref())
                .and_then(|o| o.scale)
                .or_else(crate::output::user_scale_override);
            let mut out = Output::new_positioned(
                display,
                output_name,
                mode,
                properties,
                pick,
                (0, 0).into(),
                true,
            );
            out.set_identity(primary_identity.clone());
            windows.set_output(out);
        } else {
            let mut out = Output::new(display, output_name, mode, properties);
            out.set_identity(primary_identity.clone());
            windows.set_output(out);
        }

        let gbm_flags = GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT;

        // Prefer a hardware clone: one CRTC driving every connected output from a
        // single framebuffer (one flip, one vblank). Driving a second CRTC's
        // commit every frame starves the event loop, so cloning removes it. On
        // any failure, including hardware that can't clone these connectors, fall
        // back to per-output mirror compositors; the clone path never returns
        // early, so a rejected clone can't leave the screen without a compositor.
        let size = Size::<i32, Physical>::from((w as i32, h as i32));
        let scale = windows.canvas().scale();

        // Primary output presenter. The hardware clone is not used here: i915
        // cannot clone eDP+DP from one CRTC, and the per-output mirror path
        // handles additional connectors below.
        let cloned = false;
        let surface = Self::create_surface(drm, &resources, primary, primary_mode, &[])?;
        let code = Self::pick_format(&surface, &formats);
        let modifiers = Self::pick_modifiers(&surface, &formats, code);
        let (primary_compositor, primary_committer) = Presenter::new(
            surface,
            GbmAllocator::new(gbm.clone(), gbm_flags),
            GbmFramebufferExporter::new(gbm.clone(), NodeFilter::All),
            code,
            modifiers,
            size,
            scale,
            Some((
                GbmAllocator::new(
                    gbm.clone(),
                    GbmBufferFlags::CURSOR | GbmBufferFlags::WRITE,
                ),
                GbmFramebufferExporter::new(gbm.clone(), NodeFilter::All),
                drm.cursor_size(),
            )),
        );
        let primary_crtc = primary_compositor.crtc();
        let mut committers = vec![(primary_crtc, primary_committer)];

        // Each additional connector gets its own per-output compositor driven
        // by its own vblank, decoupled from the primary (skipped when a
        // hardware clone already drives every output from the primary CRTC).
        // Mirror heads blit the primary's buffer at scale 1.0; extend heads
        // compose their own region at their own scale.
        let mut heads = Vec::new();
        let mut used_crtcs = vec![primary_crtc];
        let mut next_x = windows.canvas().size().w;

        for conn in connected.iter().filter(|c| c.handle() != primary.handle()) {
            if cloned { break; }

            let name = Self::connector_name(conn);
            let identity = Self::connector_identity(drm, conn);
            let head_cfg = config.display.output_for(&name, identity.as_deref());
            if extend && head_cfg.is_some_and(|o| !o.enabled) {
                info!("Extend output {name}: disabled by config, skipping");
                continue;
            }

            let cfg_mode = if extend { head_cfg.and_then(|o| o.mode.as_deref()) } else { None };
            let conn_mode = match Self::pick_mode(conn, cfg_mode) {
                Some(m) => m,
                None => {
                    info!("No modes for connector {name}, skipping");
                    continue;
                }
            };

            let (mw, mh) = conn_mode.size();

            let surface = match Self::create_surface(drm, &resources, conn, conn_mode, &used_crtcs)
            {
                Some(s) => s,
                None => {
                    info!("Could not create surface for {name}");
                    continue;
                }
            };

            let head_crtc = surface.crtc();
            used_crtcs.push(head_crtc);

            let allocator = GbmAllocator::new(
                gbm.clone(),
                GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT,
            );
            let head_size = Size::<i32, Physical>::from((mw as i32, mh as i32));
            let head_modifiers = Self::pick_modifiers(&surface, &formats, code);

            let (kind, head_scale) = if extend {
                let logical_mode = Mode {
                    size: (mw as i32, mh as i32).into(),
                    refresh: conn_mode.vrefresh() as i32 * 1000,
                };
                let (cw, ch) = conn.size().unwrap_or((0, 0));
                let (make, model, serial) = Self::connector_edid(drm, conn)
                    .map(|e| (e.make, e.model, e.serial))
                    .unwrap_or_else(|| ("Forge".into(), "Generic DRM".into(), "Unknown".into()));
                let pinned = head_cfg.and_then(|o| o.position);
                let location: Point<i32, Logical> = pinned
                    .map(Into::into)
                    .unwrap_or_else(|| (next_x, 0).into());
                let mut out = Output::new_positioned(
                    display,
                    name.clone(),
                    logical_mode,
                    PhysicalProperties {
                        size: (cw as i32, ch as i32).into(),
                        subpixel: Subpixel::Unknown,
                        serial_number: serial,
                        model,
                        make,
                    },
                    head_cfg.and_then(|o| o.scale),
                    location,
                    pinned.is_some(),
                );
                out.set_identity(identity.clone());
                let scale = out.scale();
                next_x = next_x.max(location.x + out.size().w);
                info!(
                    "Extend output {name}: {mw}x{mh}@{}Hz at {location:?}, scale {scale}",
                    conn_mode.vrefresh()
                );
                windows.add_extra_output(out);
                (HeadKind::Extend, scale)
            } else {
                info!("Mirror output {name}: {mw}x{mh}@{}Hz", conn_mode.vrefresh());
                (HeadKind::Mirror, 1.0)
            };

            // Composited cursor on every secondary head for now — see the
            // cursor-plane note in `OutputDevice::render`.
            let (presenter, committer) = Presenter::new(
                surface,
                allocator,
                GbmFramebufferExporter::new(gbm.clone(), NodeFilter::All),
                code,
                head_modifiers,
                head_size,
                head_scale,
                None,
            );
            heads.push(SecondaryHead {
                presenter,
                pending_frame: false,
                connector: conn.handle(),
                kind,
                name,
            });
            committers.push((head_crtc, committer));
        }

        if !heads.is_empty() {
            info!(
                "Multi-output: 1 primary + {} {} head(s)",
                heads.len(),
                if extend { "extend" } else { "mirror" }
            );
        }

        let infos =
            Self::collect_output_infos(drm, connected.iter(), primary.handle(), &config, extend);

        Some((primary_compositor, heads, primary.handle(), committers, has_external, infos))
    }

    /// Scanout format for `surface`'s primary plane: the first of a preference
    /// order that both the plane and the renderer support. `Abgr8888` stays
    /// first so GPUs that expose it are unaffected; a simple display controller
    /// that offers only `Xrgb8888` (e.g. the i.MX8 mxsfb primary plane) falls
    /// back to it instead of a format the plane rejects at atomic check.
    pub(super) fn pick_format(surface: &DrmSurface, renderer_formats: &FormatSet) -> Fourcc {
        use std::collections::HashSet;
        let plane: HashSet<Fourcc> =
            surface.plane_info().formats.iter().map(|f| f.code).collect();
        let rend: HashSet<Fourcc> = renderer_formats.iter().map(|f| f.code).collect();
        const PREFERENCE: [Fourcc; 4] =
            [Fourcc::Abgr8888, Fourcc::Xrgb8888, Fourcc::Argb8888, Fourcc::Xbgr8888];
        PREFERENCE
            .into_iter()
            .find(|c| plane.contains(c) && rend.contains(c))
            .unwrap_or(Fourcc::Abgr8888)
    }

    /// Modifiers usable for `code` on both the primary plane and the renderer,
    /// mirroring smithay's `DrmCompositor` format selection. Falls back to the
    /// implicit modifier when the intersection is empty.
    pub(super) fn pick_modifiers(surface: &DrmSurface, renderer_formats: &FormatSet, code: Fourcc) -> Vec<DrmModifier> {
        use std::collections::HashSet;
        let plane: HashSet<DrmModifier> = surface
            .plane_info()
            .formats
            .iter()
            .filter(|f| f.code == code)
            .map(|f| f.modifier)
            .collect();
        let rend: HashSet<DrmModifier> = renderer_formats
            .iter()
            .filter(|f| f.code == code)
            .map(|f| f.modifier)
            .collect();
        let inter: Vec<DrmModifier> = plane.intersection(&rend).copied().collect();
        if inter.is_empty() {
            vec![DrmModifier::Invalid]
        } else {
            inter
        }
    }

    /// Create a DRM surface for a single connector, avoiding CRTCs in `excluded`.
    pub(super) fn create_surface(
        drm: &mut DrmDevice,
        resources: &ResourceHandles,
        connector: &ConnectorInfo,
        mode: DrmMode,
        excluded: &[crtc::Handle],
    ) -> Option<DrmSurface> {
        for encoder in connector.encoders() {
            let Ok(encoder) = drm.get_encoder(*encoder) else { continue };

            let mut crtcs = resources.filter_crtcs(encoder.possible_crtcs());
            crtcs.sort_by_cached_key(|crtc| {
                drm.planes(crtc).map_or(0, |p| -(p.overlay.len() as isize))
            });

            for crtc in crtcs {
                if excluded.contains(&crtc) { continue; }
                if let Ok(surface) = drm.create_surface(crtc, mode, &[connector.handle()]) {
                    info!("Created DRM surface with CRTC {:?}", crtc);
                    return Some(surface);
                }
            }
        }

        error!("Could not create DRM surface - no valid encoder/crtc combination");
        None
    }
}
