use serde::{Deserialize, Serialize};
use std::{
    fmt::Display,
    io,
    process::{Command, Stdio},
};
use thiserror::Error;

/// Node in sway's tree of containers/windows.
#[derive(Serialize, Deserialize, Debug)]
struct Node {
    nodes: Vec<Node>,
    floating_nodes: Vec<Node>,
    focused: bool,
    rect: Rect,
}

impl Node {
    fn take_focused(self) -> Option<Node> {
        if self.focused {
            return Some(self);
        }

        self.nodes
            .into_iter()
            .chain(self.floating_nodes)
            .find_map(Node::take_focused)
    }
}

#[derive(Serialize, Deserialize, Debug)]
struct OutputNode {
    focused: Option<bool>,
    rect: Rect,
}

fn find_output_rect(outputs: &[OutputNode]) -> Option<&Rect> {
    outputs.iter().find_map(|n| {
        if n.focused == Some(true) {
            Some(&n.rect)
        } else {
            None
        }
    })
}

/// Geometry of a node.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub(crate) struct Rect {
    // u16 fits up to 65k, which is more than reasonable for regular desktop window sizes.
    pub(crate) x: u16,
    pub(crate) y: u16,
    pub(crate) width: u16,
    pub(crate) height: u16,
}

impl Display for Rect {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{},{} {}x{}", self.x, self.y, self.width, self.height)
    }
}

#[derive(Error, Debug)]
pub(crate) enum Error {
    #[error("no focused output found")]
    NoFocusedOutput,

    #[error("no focused window found")]
    NoFocusedWindow,

    #[error("running swaymsg failed")]
    SwayMsgFailed(#[from] io::Error),

    #[error("swaymsg returned invalid JSON output")]
    SwayMsgBogusOutput(#[from] serde_json::Error),
}

/// Returns the currently focused window.
pub(crate) fn get_active_window_geom() -> Result<Rect, Error> {
    let output = Command::new("swaymsg")
        .args(["-t", "get_tree"])
        .stdout(Stdio::piped())
        .output()?;

    let tree: Node = serde_json::from_slice(&output.stdout)?;
    let focused = tree.take_focused().ok_or(Error::NoFocusedWindow)?;

    Ok(focused.rect)
}

/// Returns the currently focused output rect.
pub(crate) fn get_focused_output_rect() -> Result<Rect, Error> {
    let output = Command::new("swaymsg")
        .args(["-t", "get_outputs"])
        .stdout(Stdio::piped())
        .output()?;

    let tree: Vec<OutputNode> = serde_json::from_slice(&output.stdout)?;
    let output_rect = find_output_rect(&tree).ok_or(Error::NoFocusedOutput)?;

    Ok(output_rect.clone())
}
