//! Acciones de escritorio y su mapa de teclas por defecto.
//!
//! Una [`DesktopAction`] es una orden de alto nivel del usuario, ya
//! desligada de la tecla concreta: el [`Desktop`](crate::Desktop) las
//! aplica sin saber qué combinación las disparó.
//!
//! Cada acción tiene una **forma textual** estable ([`Display`] /
//! [`FromStr`]) — `"focus-next"`, `"layout:grid"`, `"workspace:3"` — que
//! es el vocabulario del keymap configurable en RON (ver [`crate::keymap`]).

use std::fmt;
use std::str::FromStr;

use serde::{Deserialize, Serialize};

use mirada_layout::{LayoutMode, WindowId};

/// Número de escritorios virtuales que mantiene el `Desktop`.
pub const WORKSPACE_COUNT: usize = 9;

/// Una dirección cardinal en pantalla — para el foco (y, a futuro, el
/// movimiento) espacial entre ventanas teseladas.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Direction {
    Left,
    Right,
    Up,
    Down,
}

impl Direction {
    /// El sufijo textual de la dirección — `"left"`, `"right"`, …
    fn slug(self) -> &'static str {
        match self {
            Direction::Left => "left",
            Direction::Right => "right",
            Direction::Up => "up",
            Direction::Down => "down",
        }
    }

    /// La dirección desde su sufijo, o `None` si no calza.
    fn from_slug(s: &str) -> Option<Direction> {
        Some(match s {
            "left" => Direction::Left,
            "right" => Direction::Right,
            "up" => Direction::Up,
            "down" => Direction::Down,
            _ => return None,
        })
    }
}

/// Una orden de escritorio de alto nivel.
///
/// Es serializable (`postcard`) para viajar por el API de control
/// ([`crate::ctl`]) y tiene una forma textual estable ([`Display`] /
/// [`FromStr`]) para el keymap y `mirada-ctl`.
///
/// No es `Copy`: [`Spawn`](DesktopAction::Spawn) lleva su comando.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DesktopAction {
    /// Mueve el foco a la ventana siguiente del escritorio activo.
    FocusNext,
    /// Mueve el foco a la ventana anterior.
    FocusPrev,
    /// Mueve el foco a la ventana teselada más cercana en una dirección
    /// cardinal (espacial, no cíclico) — el `Super+flechas` clásico.
    FocusDir(Direction),
    /// Enfoca una ventana concreta por su id; si está en otro escritorio,
    /// salta a él. Para clics de taskbar o `mirada-ctl focus-window`.
    FocusWindow(WindowId),
    /// Intercambia la ventana enfocada con su vecina teselada en una
    /// dirección cardinal (mueve la ventana por geometría) — Super+Shift+flechas.
    MoveDir(Direction),
    /// Adelanta la ventana enfocada en el orden de teselado.
    MoveForward,
    /// Atrasa la ventana enfocada en el orden de teselado.
    MoveBackward,
    /// Cierra la ventana enfocada (cierre ordenado).
    CloseFocused,
    /// Alterna entre flotante y teselada la ventana enfocada.
    ToggleFloat,
    /// Alterna el escritorio entero entre teselado y flotante: si queda
    /// alguna teselada, las hace flotar todas (en cascada); si ya están
    /// todas flotando, las devuelve al teselado.
    ToggleTiling,
    /// Alterna pantalla completa en la ventana enfocada.
    ToggleFullscreen,
    /// Guarda la ventana enfocada en el scratchpad (la oculta).
    SendToScratchpad,
    /// Invoca u oculta la ventana del scratchpad — aparece flotando.
    ToggleScratchpad,
    /// Despliega/oculta la terminal dropdown estilo *quake* — un toplevel
    /// real anclado arriba a todo el ancho, con foco de teclado normal. La
    /// crea perezosamente la primera vez (patrón pypr).
    ToggleDropterm,
    /// Pasa al siguiente modo de teselado.
    CycleLayout,
    /// Fija un modo de teselado concreto.
    SetLayout(LayoutMode),
    /// Agranda el área de la ventana maestra (`MasterStack`/`CenteredMaster`).
    GrowMaster,
    /// Encoge el área de la ventana maestra.
    ShrinkMaster,
    /// Mete una ventana más en el área maestra (`nmaster`).
    IncMaster,
    /// Saca una ventana del área maestra.
    DecMaster,
    /// Lleva la ventana enfocada al puesto maestro (la inserta al frente,
    /// desplazando al resto).
    PromoteToMaster,
    /// Intercambia la ventana enfocada con la maestra (sólo esas dos; el
    /// resto del orden queda igual). El foco acompaña a la ventana.
    SwapMaster,
    /// Activa el escritorio virtual `n` (índice 0-based).
    SwitchWorkspace(usize),
    /// Manda la ventana enfocada al escritorio virtual `n` (queda donde está).
    SendToWorkspace(usize),
    /// Manda la ventana enfocada al escritorio `n` y salta con ella allí.
    MoveToWorkspace(usize),
    /// Mueve el foco a la siguiente salida (monitor).
    FocusOutputNext,
    /// Mueve el foco a la salida (monitor) vecina en una dirección cardinal.
    FocusOutputDir(Direction),
    /// Manda la ventana enfocada a la salida vecina en una dirección — pasa
    /// al escritorio que muestra esa salida.
    SendToOutputDir(Direction),
    /// Redimensiona la ventana flotante enfocada hacia una dirección
    /// (derecha/abajo agrandan; izquierda/arriba achican), por `float_step`
    /// px. No hace nada sobre una teselada.
    ResizeFloatDir(Direction),
    /// Lanza un programa — abre una terminal, un navegador, lo que sea.
    /// El comando se pasa a `sh -c` en el Cuerpo.
    Spawn(String),
    /// Apaga el compositor.
    Quit,
}

/// El nombre RON-seguro de un modo de teselado (sin guiones problemáticos
/// para identificadores: aquí van como valor de cadena, no de enum).
pub(crate) fn layout_slug(mode: LayoutMode) -> &'static str {
    match mode {
        LayoutMode::MasterStack => "master-stack",
        LayoutMode::Monocle => "monocle",
        LayoutMode::Grid => "grid",
        LayoutMode::Columns => "columns",
        LayoutMode::Rows => "rows",
        LayoutMode::CenteredMaster => "centered-master",
        LayoutMode::Spiral => "spiral",
    }
}

/// Modo de teselado desde su `slug`.
pub(crate) fn layout_from_slug(slug: &str) -> Option<LayoutMode> {
    Some(match slug {
        "master-stack" => LayoutMode::MasterStack,
        "monocle" => LayoutMode::Monocle,
        "grid" => LayoutMode::Grid,
        "columns" => LayoutMode::Columns,
        "rows" => LayoutMode::Rows,
        "centered-master" => LayoutMode::CenteredMaster,
        "spiral" => LayoutMode::Spiral,
        _ => return None,
    })
}

impl fmt::Display for DesktopAction {
    /// La forma textual estable de la acción — el vocabulario del keymap.
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            DesktopAction::FocusNext => f.write_str("focus-next"),
            DesktopAction::FocusPrev => f.write_str("focus-prev"),
            DesktopAction::FocusDir(d) => write!(f, "focus-{}", d.slug()),
            DesktopAction::MoveDir(d) => write!(f, "move-{}", d.slug()),
            DesktopAction::FocusWindow(id) => write!(f, "focus-window:{id}"),
            DesktopAction::MoveForward => f.write_str("move-forward"),
            DesktopAction::MoveBackward => f.write_str("move-backward"),
            DesktopAction::CloseFocused => f.write_str("close-focused"),
            DesktopAction::ToggleFloat => f.write_str("toggle-float"),
            DesktopAction::ToggleTiling => f.write_str("toggle-tiling"),
            DesktopAction::ToggleFullscreen => f.write_str("toggle-fullscreen"),
            DesktopAction::SendToScratchpad => f.write_str("send-to-scratchpad"),
            DesktopAction::ToggleScratchpad => f.write_str("toggle-scratchpad"),
            DesktopAction::ToggleDropterm => f.write_str("toggle-dropterm"),
            DesktopAction::CycleLayout => f.write_str("cycle-layout"),
            DesktopAction::SetLayout(m) => write!(f, "layout:{}", layout_slug(*m)),
            DesktopAction::GrowMaster => f.write_str("grow-master"),
            DesktopAction::ShrinkMaster => f.write_str("shrink-master"),
            DesktopAction::IncMaster => f.write_str("inc-master"),
            DesktopAction::DecMaster => f.write_str("dec-master"),
            DesktopAction::PromoteToMaster => f.write_str("promote-to-master"),
            DesktopAction::SwapMaster => f.write_str("swap-master"),
            // Los escritorios se numeran 1-based de cara al usuario.
            DesktopAction::SwitchWorkspace(n) => write!(f, "workspace:{}", n + 1),
            DesktopAction::SendToWorkspace(n) => write!(f, "send-to-workspace:{}", n + 1),
            DesktopAction::MoveToWorkspace(n) => write!(f, "move-to-workspace:{}", n + 1),
            DesktopAction::FocusOutputNext => f.write_str("focus-output-next"),
            DesktopAction::FocusOutputDir(d) => write!(f, "focus-output-{}", d.slug()),
            DesktopAction::SendToOutputDir(d) => write!(f, "send-to-output-{}", d.slug()),
            DesktopAction::ResizeFloatDir(d) => write!(f, "resize-float-{}", d.slug()),
            DesktopAction::Spawn(cmd) => write!(f, "spawn:{cmd}"),
            DesktopAction::Quit => f.write_str("quit"),
        }
    }
}

impl FromStr for DesktopAction {
    /// Mensaje de error ya formateado, listo para mostrar al usuario.
    type Err = String;

    fn from_str(s: &str) -> Result<Self, String> {
        let s = s.trim();
        Ok(match s {
            "focus-next" => Self::FocusNext,
            "focus-prev" => Self::FocusPrev,
            "move-forward" => Self::MoveForward,
            "move-backward" => Self::MoveBackward,
            "close-focused" => Self::CloseFocused,
            "toggle-float" => Self::ToggleFloat,
            "toggle-tiling" => Self::ToggleTiling,
            "toggle-fullscreen" => Self::ToggleFullscreen,
            "send-to-scratchpad" => Self::SendToScratchpad,
            "toggle-scratchpad" => Self::ToggleScratchpad,
            "toggle-dropterm" => Self::ToggleDropterm,
            "cycle-layout" => Self::CycleLayout,
            "grow-master" => Self::GrowMaster,
            "shrink-master" => Self::ShrinkMaster,
            "inc-master" => Self::IncMaster,
            "dec-master" => Self::DecMaster,
            "promote-to-master" => Self::PromoteToMaster,
            "swap-master" => Self::SwapMaster,
            "focus-output-next" => Self::FocusOutputNext,
            "quit" => Self::Quit,
            _ => {
                if let Some(slug) = s.strip_prefix("layout:") {
                    Self::SetLayout(
                        layout_from_slug(slug)
                            .ok_or_else(|| format!("modo de teselado desconocido: '{slug}'"))?,
                    )
                } else if let Some(d) = s.strip_prefix("focus-").and_then(Direction::from_slug) {
                    Self::FocusDir(d)
                } else if let Some(d) = s.strip_prefix("focus-output-").and_then(Direction::from_slug) {
                    Self::FocusOutputDir(d)
                } else if let Some(d) = s.strip_prefix("send-to-output-").and_then(Direction::from_slug) {
                    Self::SendToOutputDir(d)
                } else if let Some(d) = s.strip_prefix("resize-float-").and_then(Direction::from_slug) {
                    Self::ResizeFloatDir(d)
                } else if let Some(d) = s.strip_prefix("move-").and_then(Direction::from_slug) {
                    Self::MoveDir(d)
                } else if let Some(id) = s.strip_prefix("focus-window:") {
                    Self::FocusWindow(
                        id.trim()
                            .parse()
                            .map_err(|_| format!("id de ventana inválido: '{id}'"))?,
                    )
                } else if let Some(n) = s.strip_prefix("send-to-workspace:") {
                    Self::SendToWorkspace(parse_workspace(n)?)
                } else if let Some(n) = s.strip_prefix("move-to-workspace:") {
                    Self::MoveToWorkspace(parse_workspace(n)?)
                } else if let Some(n) = s.strip_prefix("workspace:") {
                    Self::SwitchWorkspace(parse_workspace(n)?)
                } else if let Some(cmd) = s.strip_prefix("spawn:") {
                    let cmd = cmd.trim();
                    if cmd.is_empty() {
                        return Err("spawn: necesita un comando".into());
                    }
                    Self::Spawn(cmd.to_string())
                } else {
                    return Err(format!("acción desconocida: '{s}'"));
                }
            }
        })
    }
}

/// Parsea el número de escritorio del keymap (1-based) a índice (0-based),
/// acotado a [`WORKSPACE_COUNT`].
fn parse_workspace(s: &str) -> Result<usize, String> {
    let n: usize = s
        .trim()
        .parse()
        .map_err(|_| format!("número de escritorio inválido: '{s}'"))?;
    if (1..=WORKSPACE_COUNT).contains(&n) {
        Ok(n - 1)
    } else {
        Err(format!("escritorio fuera de rango (1..={WORKSPACE_COUNT}): {n}"))
    }
}

/// Mapa de teclas por defecto, estilo *tiling WM* (modificador `Super`).
///
/// Las cadenas deben coincidir literalmente con las que el Cuerpo emite
/// en [`BodyEvent::Keybind`](mirada_protocol::BodyEvent::Keybind); son
/// también las que se registran con
/// [`BrainCommand::GrabKeys`](mirada_protocol::BrainCommand::GrabKeys).
pub fn default_keymap() -> Vec<(String, DesktopAction)> {
    let mut map = vec![
        ("Super+j".into(), DesktopAction::FocusNext),
        ("Super+k".into(), DesktopAction::FocusPrev),
        // Foco espacial estilo i3/sway — el clásico Super+flechas.
        ("Super+Left".into(), DesktopAction::FocusDir(Direction::Left)),
        ("Super+Right".into(), DesktopAction::FocusDir(Direction::Right)),
        ("Super+Up".into(), DesktopAction::FocusDir(Direction::Up)),
        ("Super+Down".into(), DesktopAction::FocusDir(Direction::Down)),
        // Mover la ventana enfocada por geometría — Super+Shift+flechas.
        ("Super+Shift+Left".into(), DesktopAction::MoveDir(Direction::Left)),
        ("Super+Shift+Right".into(), DesktopAction::MoveDir(Direction::Right)),
        ("Super+Shift+Up".into(), DesktopAction::MoveDir(Direction::Up)),
        ("Super+Shift+Down".into(), DesktopAction::MoveDir(Direction::Down)),
        ("Super+Shift+j".into(), DesktopAction::MoveForward),
        ("Super+Shift+k".into(), DesktopAction::MoveBackward),
        ("Super+q".into(), DesktopAction::CloseFocused),
        ("Super+f".into(), DesktopAction::ToggleFloat),
        ("Super+Shift+f".into(), DesktopAction::ToggleFullscreen),
        // La tecla «quake» clásica baja la terminal dropdown.
        ("Super+`".into(), DesktopAction::ToggleDropterm),
        // Scratchpad genérico: enviar la enfocada / invocar la guardada.
        // (Shift+` produce «~» tras canonizar, así que ése es el combo.)
        ("Super+Shift+s".into(), DesktopAction::SendToScratchpad),
        ("Super+Shift+~".into(), DesktopAction::ToggleScratchpad),
        ("Super+space".into(), DesktopAction::CycleLayout),
        ("Super+Shift+space".into(), DesktopAction::ToggleTiling),
        ("Super+t".into(), DesktopAction::SetLayout(LayoutMode::MasterStack)),
        ("Super+m".into(), DesktopAction::SetLayout(LayoutMode::Monocle)),
        ("Super+g".into(), DesktopAction::SetLayout(LayoutMode::Grid)),
        ("Super+c".into(), DesktopAction::SetLayout(LayoutMode::Columns)),
        ("Super+r".into(), DesktopAction::SetLayout(LayoutMode::Rows)),
        ("Super+d".into(), DesktopAction::SetLayout(LayoutMode::CenteredMaster)),
        ("Super+s".into(), DesktopAction::SetLayout(LayoutMode::Spiral)),
        ("Super+h".into(), DesktopAction::ShrinkMaster),
        ("Super+l".into(), DesktopAction::GrowMaster),
        ("Super+o".into(), DesktopAction::FocusOutputNext),
        ("Super+Return".into(), DesktopAction::PromoteToMaster),
        ("Super+Shift+Return".into(), DesktopAction::Spawn("foot".into())),
        ("Super+p".into(), DesktopAction::Spawn("foot -e mirada-launcher".into())),
        ("Super+,".into(), DesktopAction::IncMaster),
        ("Super+.".into(), DesktopAction::DecMaster),
        ("Super+Shift+e".into(), DesktopAction::Quit),
    ];
    // Un escritorio por dígito: `Super+1`..`Super+9` lo activan,
    // `Super+Shift+1`.. mandan la ventana enfocada allí.
    for n in 0..WORKSPACE_COUNT {
        map.push((format!("Super+{}", n + 1), DesktopAction::SwitchWorkspace(n)));
        map.push((
            format!("Super+Shift+{}", n + 1),
            DesktopAction::SendToWorkspace(n),
        ));
    }
    map
}

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

    #[test]
    fn keymap_has_no_duplicate_bindings() {
        let map = default_keymap();
        let mut keys: Vec<_> = map.iter().map(|(k, _)| k.clone()).collect();
        keys.sort();
        let unique = keys.len();
        keys.dedup();
        assert_eq!(keys.len(), unique, "hay un atajo repetido");
    }

    #[test]
    fn keymap_covers_every_virtual_workspace() {
        let map = default_keymap();
        for n in 0..WORKSPACE_COUNT {
            assert!(map
                .iter()
                .any(|(_, a)| a == &DesktopAction::SwitchWorkspace(n)));
            assert!(map
                .iter()
                .any(|(_, a)| a == &DesktopAction::SendToWorkspace(n)));
        }
    }

    #[test]
    fn every_default_action_round_trips_through_its_text_form() {
        for (_, action) in default_keymap() {
            let text = action.to_string();
            let back: DesktopAction = text.parse().unwrap();
            assert_eq!(action, back, "no redondea: {text}");
        }
    }

    #[test]
    fn every_layout_mode_round_trips() {
        for mode in LayoutMode::ALL {
            let a = DesktopAction::SetLayout(mode);
            assert_eq!(a, a.to_string().parse().unwrap());
        }
    }

    #[test]
    fn workspace_actions_are_one_based_in_text() {
        assert_eq!(DesktopAction::SwitchWorkspace(0).to_string(), "workspace:1");
        assert_eq!(
            "workspace:1".parse::<DesktopAction>().unwrap(),
            DesktopAction::SwitchWorkspace(0)
        );
        assert_eq!(
            "send-to-workspace:9".parse::<DesktopAction>().unwrap(),
            DesktopAction::SendToWorkspace(8)
        );
    }

    #[test]
    fn out_of_range_or_unknown_actions_are_rejected() {
        assert!("workspace:0".parse::<DesktopAction>().is_err());
        assert!("workspace:99".parse::<DesktopAction>().is_err());
        assert!("layout:fractal".parse::<DesktopAction>().is_err());
        assert!("focus-window:abc".parse::<DesktopAction>().is_err());
        assert!("teleport".parse::<DesktopAction>().is_err());
    }

    #[test]
    fn directional_actions_round_trip_in_every_direction() {
        for d in [Direction::Left, Direction::Right, Direction::Up, Direction::Down] {
            for a in [
                DesktopAction::FocusDir(d),
                DesktopAction::MoveDir(d),
                DesktopAction::FocusOutputDir(d),
                DesktopAction::SendToOutputDir(d),
                DesktopAction::ResizeFloatDir(d),
            ] {
                let text = a.to_string();
                assert_eq!(text.parse::<DesktopAction>().unwrap(), a, "no redondea: {text}");
            }
        }
    }

    #[test]
    fn focus_window_round_trips_with_its_id() {
        let a = DesktopAction::FocusWindow(42);
        assert_eq!(a.to_string(), "focus-window:42");
        assert_eq!("focus-window:42".parse::<DesktopAction>().unwrap(), a);
    }

    #[test]
    fn spawn_round_trips_keeping_the_whole_command() {
        let a = DesktopAction::Spawn("foot --title diario".into());
        assert_eq!(a.to_string(), "spawn:foot --title diario");
        assert_eq!(a.to_string().parse::<DesktopAction>().unwrap(), a);
    }

    #[test]
    fn spawn_without_a_command_is_rejected() {
        assert!("spawn:".parse::<DesktopAction>().is_err());
        assert!("spawn:   ".parse::<DesktopAction>().is_err());
    }
}
