//! Connector, mode and EDID inspection for the udev backend: scale picks,
//! external-vs-panel classification, connector naming, EDID parsing, mode
//! string/parse/pick, and the `OutputInfo` snapshot. Associated helpers on `Udev`.

use super::*;

impl Udev {
    /// Scale `conn` would ask for on its own, at its preferred mode.
    pub(super) fn connector_scale(conn: &ConnectorInfo) -> Option<f64> {
        let modes = conn.modes();
        let m = modes
            .iter()
            .find(|m| m.mode_type().contains(ModeTypeFlags::PREFERRED))
            .or_else(|| modes.first())?;
        let (w, h) = m.size();
        let (pw, ph) = conn.size().unwrap_or((0, 0));
        Some(Output::compute_scale(
            Mode {
                size: (w as i32, h as i32).into(),
                refresh: m.vrefresh() as i32 * 1000,
            },
            &PhysicalProperties {
                size: (pw as i32, ph as i32).into(),
                subpixel: Subpixel::Unknown,
                serial_number: "Unknown".into(),
                model: "Generic DRM".into(),
                make: "Forge".into(),
            },
        ))
    }

    /// Publish the smallest scale any connected output asks for. While forge
    /// mirrors, one framebuffer feeds every screen, so the densest panel must
    /// not decide the factor for the coarsest one.
    pub(super) fn refresh_mirror_scale<'a>(connected: impl Iterator<Item = &'a ConnectorInfo>) {
        let min = connected
            .filter_map(Self::connector_scale)
            .fold(f64::INFINITY, f64::min);
        crate::output::set_mirror_scale(min.is_finite().then_some(min));
    }

    pub(super) fn is_external_connector(info: &ConnectorInfo) -> bool {
        info.state() == ConnectorState::Connected
            && !matches!(
                info.interface(),
                Interface::EmbeddedDisplayPort | Interface::LVDS | Interface::DSI
            )
    }

    /// Kernel-style connector name ("eDP-1", "DP-2", "HDMI-A-1").
    pub(super) fn connector_name(info: &ConnectorInfo) -> String {
        let prefix = match info.interface() {
            Interface::EmbeddedDisplayPort => "eDP",
            Interface::DisplayPort => "DP",
            Interface::HDMIA => "HDMI-A",
            Interface::HDMIB => "HDMI-B",
            Interface::LVDS => "LVDS",
            Interface::DSI => "DSI",
            Interface::DVII => "DVI-I",
            Interface::DVID => "DVI-D",
            Interface::DVIA => "DVI-A",
            Interface::VGA => "VGA",
            Interface::Virtual => "Virtual",
            other => return format!("{:?}-{}", other, info.interface_id()),
        };
        format!("{prefix}-{}", info.interface_id())
    }

    /// Persist the EDID identity of every connected screen into its config
    /// entry, so a later connector rename still finds the user's settings.
    pub(super) fn learn_identities<'a>(
        config: &mut crate::config::ForgeConfig,
        drm: &DrmDevice,
        connected: impl Iterator<Item = &'a ConnectorInfo>,
    ) {
        let mut changed = false;
        for conn in connected {
            let name = Self::connector_name(conn);
            let identity = Self::connector_identity(drm, conn);
            changed |= config.display.learn_identity(&name, identity.as_deref());
        }
        if changed {
            if let Err(e) = config.save() {
                warn!("could not persist output identities: {e}");
            }
        }
    }

    pub(super) fn connector_identity(drm: &DrmDevice, conn: &ConnectorInfo) -> Option<String> {
        Self::connector_edid(drm, conn).and_then(|e| e.identity())
    }

    pub(super) fn connector_edid(drm: &DrmDevice, conn: &ConnectorInfo) -> Option<crate::edid::EdidInfo> {
        let props = drm.get_properties(conn.handle()).ok()?;
        let (handles, values) = props.as_props_and_values();
        for (prop, value) in handles.iter().zip(values) {
            let Ok(info) = drm.get_property(*prop) else { continue };
            if info.name().to_bytes() == b"EDID" {
                let blob = drm.get_property_blob(*value).ok()?;
                return crate::edid::parse(&blob);
            }
        }
        None
    }

    pub(super) fn mode_string(m: &DrmMode) -> String {
        let (w, h) = m.size();
        format!("{w}x{h}@{}", m.vrefresh())
    }

    pub(super) fn parse_mode(s: &str) -> Option<(i32, i32, i32)> {
        let (res, hz) = s.split_once('@')?;
        let (w, h) = res.split_once('x')?;
        Some((w.parse().ok()?, h.parse().ok()?, hz.parse().ok()?))
    }

    /// The connector's mode to run: the configured one when it exists on
    /// the connector, else preferred, else the first advertised.
    pub(super) fn pick_mode(conn: &ConnectorInfo, want: Option<&str>) -> Option<DrmMode> {
        let modes = conn.modes();
        if let Some((w, h, r)) = want.and_then(Self::parse_mode) {
            if let Some(m) = modes.iter().find(|m| {
                let (mw, mh) = m.size();
                i32::from(mw) == w && i32::from(mh) == h && m.vrefresh() as i32 == r
            }) {
                return Some(*m);
            }
        }
        modes
            .iter()
            .find(|m| m.mode_type().contains(ModeTypeFlags::PREFERRED))
            .or_else(|| modes.first())
            .copied()
    }

    pub(super) fn collect_output_infos<'a>(
        drm: &DrmDevice,
        connected: impl Iterator<Item = &'a ConnectorInfo>,
        primary: ConnectorHandle,
        config: &crate::config::ForgeConfig,
        extend: bool,
    ) -> Vec<OutputInfo> {
        connected
            .map(|conn| {
                let connector = Self::connector_name(conn);
                let edid = Self::connector_edid(drm, conn);
                let identity = edid.as_ref().and_then(|e| e.identity());
                let (make, model, serial) = edid
                    .map(|e| (e.make, e.model, e.serial))
                    .unwrap_or_default();
                // Preferred mode first, the rest deduped in kernel order.
                let mut modes = IndexSet::new();
                if let Some(p) = conn
                    .modes()
                    .iter()
                    .find(|m| m.mode_type().contains(ModeTypeFlags::PREFERRED))
                {
                    modes.insert(Self::mode_string(p));
                }
                for m in conn.modes() {
                    modes.insert(Self::mode_string(m));
                }
                let modes: Vec<String> = modes.into_iter().collect();
                // What the head actually runs: the configured mode when it
                // exists on the connector (extend applies it), else preferred.
                let current_mode = if extend {
                    config
                        .display
                        .output_for(&connector, identity.as_deref())
                        .and_then(|o| o.mode.clone())
                        .filter(|m| modes.contains(m))
                } else {
                    None
                };
                let (pw, ph) = conn.size().unwrap_or((0, 0));
                OutputInfo {
                    connector,
                    identity,
                    make,
                    model,
                    serial,
                    external: Self::is_external_connector(conn),
                    primary: conn.handle() == primary,
                    physical_mm: (pw, ph),
                    auto_scale: Self::connector_scale(conn).unwrap_or(1.0),
                    current_mode: current_mode
                        .unwrap_or_else(|| modes.first().cloned().unwrap_or_default()),
                    modes,
                }
            })
            .collect()
    }
}
