use anyhow::{Context, Result, bail, ensure};
use x11rb::{
    connect,
    connection::Connection,
    protocol::{
        randr::ConnectionExt as RandrConnectionExt, xproto::ConnectionExt as XprotoConnectionExt,
    },
};

use crate::pipeline::CaptureArea;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Monitor {
    pub index: usize,
    pub name: String,
    pub x: i32,
    pub y: i32,
    pub width: i32,
    pub height: i32,
    pub primary: bool,
}

impl Monitor {
    pub fn area(&self) -> Result<CaptureArea> {
        CaptureArea::new(self.x, self.y, self.width, self.height)
    }
}

pub fn list_x11_monitors() -> Result<Vec<Monitor>> {
    ensure!(
        std::env::var_os("DISPLAY").is_some(),
        "DISPLAY is not set, so X11 monitors cannot be listed"
    );

    let (conn, screen_num) = connect(None).context("failed to connect to X11 display")?;
    let screen = &conn.setup().roots[screen_num];
    let root = screen.root;

    let monitors_reply = conn
        .randr_get_monitors(root, true)
        .ok()
        .and_then(|cookie| cookie.reply().ok());

    match monitors_reply {
        Some(reply) if !reply.monitors.is_empty() => reply
            .monitors
            .into_iter()
            .enumerate()
            .map(|(index, monitor)| {
                let name = conn
                    .get_atom_name(monitor.name)
                    .ok()
                    .and_then(|cookie| cookie.reply().ok())
                    .and_then(|reply| String::from_utf8(reply.name).ok())
                    .filter(|name| !name.is_empty())
                    .unwrap_or_else(|| format!("Monitor {}", index + 1));
                Ok(Monitor {
                    index: index + 1,
                    name,
                    x: i32::from(monitor.x),
                    y: i32::from(monitor.y),
                    width: i32::from(monitor.width),
                    height: i32::from(monitor.height),
                    primary: monitor.primary,
                })
            })
            .collect(),
        _ => Ok(vec![Monitor {
            index: 1,
            name: "Screen".to_string(),
            x: 0,
            y: 0,
            width: i32::from(screen.width_in_pixels),
            height: i32::from(screen.height_in_pixels),
            primary: true,
        }]),
    }
}

pub fn x11_monitor_area(index: usize) -> Result<CaptureArea> {
    let monitors = list_x11_monitors()?;
    let monitor = monitors
        .iter()
        .find(|monitor| monitor.index == index)
        .with_context(|| format!("monitor {index} was not found; run --list-monitors"))?;

    monitor.area()
}

pub fn validate_x11_capture_area(area: CaptureArea) -> Result<()> {
    let (width, height) = x11_screen_size()?;
    validate_area_within_bounds(area, width, height)
}

pub fn x11_screen_size() -> Result<(i32, i32)> {
    ensure!(
        std::env::var_os("DISPLAY").is_some(),
        "DISPLAY is not set, so X11 screen size cannot be detected"
    );

    let (conn, screen_num) = connect(None).context("failed to connect to X11 display")?;
    let screen = &conn.setup().roots[screen_num];
    let width = i32::from(screen.width_in_pixels);
    let height = i32::from(screen.height_in_pixels);
    ensure!(width > 0, "X11 screen width must be > 0");
    ensure!(height > 0, "X11 screen height must be > 0");

    Ok((width, height))
}

fn validate_area_within_bounds(area: CaptureArea, width: i32, height: i32) -> Result<()> {
    ensure!(width > 0, "X11 screen width must be > 0");
    ensure!(height > 0, "X11 screen height must be > 0");
    ensure!(
        area.right() <= width && area.bottom() <= height,
        "selection area {}x{}+{}+{} extends outside X11 screen {}x{}",
        area.width,
        area.height,
        area.x,
        area.y,
        width,
        height
    );

    Ok(())
}

pub fn format_monitor(monitor: &Monitor) -> String {
    let primary = if monitor.primary { " primary" } else { "" };
    format!(
        "{}: {} {}x{}+{}+{}{}",
        monitor.index, monitor.name, monitor.width, monitor.height, monitor.x, monitor.y, primary
    )
}

pub fn validate_monitor_index(index: usize, monitor_count: usize) -> Result<()> {
    if index == 0 || index > monitor_count {
        bail!("monitor index must be between 1 and {monitor_count}");
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn format_monitor_marks_primary() {
        let monitor = Monitor {
            index: 2,
            name: "HDMI-1".to_string(),
            x: 1920,
            y: 0,
            width: 2560,
            height: 1440,
            primary: true,
        };

        assert_eq!(
            format_monitor(&monitor),
            "2: HDMI-1 2560x1440+1920+0 primary"
        );
    }

    #[test]
    fn validate_monitor_index_rejects_out_of_range() {
        assert!(validate_monitor_index(1, 2).is_ok());
        assert!(validate_monitor_index(0, 2).is_err());
        assert!(validate_monitor_index(3, 2).is_err());
    }

    #[test]
    fn validate_area_within_bounds_rejects_out_of_range_area() {
        assert!(
            validate_area_within_bounds(CaptureArea::new(0, 0, 640, 480).unwrap(), 640, 480)
                .is_ok()
        );
        assert!(
            validate_area_within_bounds(CaptureArea::new(639, 0, 1, 480).unwrap(), 640, 480)
                .is_ok()
        );
        assert!(
            validate_area_within_bounds(CaptureArea::new(640, 0, 1, 1).unwrap(), 640, 480).is_err()
        );
        assert!(
            validate_area_within_bounds(CaptureArea::new(0, 479, 640, 2).unwrap(), 640, 480)
                .is_err()
        );
    }
}
