//! Config loading — keymap.toml and (future) theme overrides.
//!
//! Loading order:
//! 1. Built-in defaults (hardcoded in this file).
//! 2. `$XDG_CONFIG_HOME/marten/keymap.toml` if it exists.
//! 3. Unknown keys in the user's toml emit a warning but don't crash.
//!
//! The toml schema is intentionally simple — a flat map of action_name → key string.
//! Example:
//!
//! ```toml
//! next_photo = "Right"        # or "l"
//! prev_photo = "Left"         # or "h"
//! first_photo = "g"
//! last_photo = "G"
//! zoom_in = "Ctrl+Equal"      # Ctrl + +
//! zoom_out = "Ctrl+Minus"
//! fit_to_window = "0"
//! actual_size = "1"
//! toggle_fullscreen = "F11"
//! toggle_chrome = "Shift+F"
//! open_folder = "o"
//! quit = "q"
//! rotate_cw = "r"
//! rotate_ccw = "Shift+R"
//! properties = "i"
//! ```

use std::path::PathBuf;

use serde::{Deserialize, Serialize};

/// A parsed key binding. Mods are sorted canonically (Ctrl, Shift, Alt, Logo)
/// so equality comparisons work regardless of user-written order.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct KeyBinding {
    pub mods: Modifiers,
    pub key: String,
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct Modifiers {
    pub ctrl: bool,
    pub shift: bool,
    pub alt: bool,
    pub logo: bool,
}

impl Modifiers {
    pub fn none() -> Self {
        Self::default()
    }
}

/// Parse a key string like "Ctrl+Shift+L" into a KeyBinding.
/// Modifiers (case-insensitive: ctrl, shift, alt, logo/super/meta) are split
/// off by '+'. The remaining segment is the key name, case-preserved (because
/// 'g' and 'G' are different bindings — vim convention).
pub fn parse_binding(s: &str) -> Option<KeyBinding> {
    let parts: Vec<&str> = s.split('+').collect();
    let mut mods = Modifiers::none();
    let mut key = String::new();

    for (i, part) in parts.iter().enumerate() {
        let trimmed = part.trim();
        if i + 1 < parts.len() {
            // It's a modifier.
            match trimmed.to_ascii_lowercase().as_str() {
                "ctrl" | "control" => mods.ctrl = true,
                "shift" => mods.shift = true,
                "alt" | "option" => mods.alt = true,
                "logo" | "super" | "meta" | "win" | "cmd" => mods.logo = true,
                _ => return None, // unknown modifier
            }
        } else {
            key = trimmed.to_string();
        }
    }

    if key.is_empty() {
        return None;
    }

    Some(KeyBinding { mods, key })
}

/// All keymap actions. Each variant maps to a single KeyBinding (or multiple —
/// we keep a Vec so users can bind both `Right` and `l` to next_photo).
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct Keymap {
    #[serde(default)]
    pub next_photo: Vec<String>,
    #[serde(default)]
    pub prev_photo: Vec<String>,
    #[serde(default)]
    pub first_photo: Vec<String>,
    #[serde(default)]
    pub last_photo: Vec<String>,
    #[serde(default)]
    pub zoom_in: Vec<String>,
    #[serde(default)]
    pub zoom_out: Vec<String>,
    #[serde(default)]
    pub fit_to_window: Vec<String>,
    #[serde(default)]
    pub actual_size: Vec<String>,
    #[serde(default)]
    pub toggle_fullscreen: Vec<String>,
    #[serde(default)]
    pub toggle_chrome: Vec<String>,
    #[serde(default)]
    pub open_folder: Vec<String>,
    #[serde(default)]
    pub quit: Vec<String>,
    #[serde(default)]
    pub rotate_cw: Vec<String>,
    #[serde(default)]
    pub rotate_ccw: Vec<String>,
    #[serde(default)]
    pub properties: Vec<String>,
    #[serde(default)]
    pub copy_to_pictures: Vec<String>,
    #[serde(default)]
    pub delete_permanently: Vec<String>,
    #[serde(default)]
    pub about: Vec<String>,
    #[serde(default)]
    pub toggle_sidebar: Vec<String>,
    #[serde(default)]
    pub toggle_slideshow: Vec<String>,
    #[serde(default)]
    pub random_same_folder: Vec<String>,
    #[serde(default)]
    pub random_folder_tree: Vec<String>,
}

impl Keymap {
    /// The hardcoded default keymap. Matches `DECISION.md` D005.
    pub fn defaults() -> Self {
        Self {
            next_photo: vec!["Right".into(), "l".into()],
            prev_photo: vec!["Left".into(), "h".into()],
            first_photo: vec!["g".into()],
            last_photo: vec!["G".into()],
            zoom_in: vec!["+".into(), "Ctrl+Up".into()],
            zoom_out: vec!["-".into(), "Ctrl+Down".into()],
            fit_to_window: vec!["0".into()],
            actual_size: vec!["1".into()],
            toggle_fullscreen: vec!["F11".into()],
            toggle_chrome: vec!["Shift+F".into()],
            open_folder: vec!["o".into()],
            quit: vec!["q".into(), "Ctrl+Q".into()],
            rotate_cw: vec!["r".into()],
            rotate_ccw: vec!["Shift+R".into()],
            properties: vec!["i".into()],
            copy_to_pictures: vec!["Shift+Home".into()],
            delete_permanently: vec!["Shift+Delete".into()],
            about: vec!["a".into()],
            toggle_sidebar: vec!["Tab".into()],
            toggle_slideshow: vec!["s".into()],
            random_same_folder: vec!["z".into()],
            random_folder_tree: vec!["Shift+Z".into()],
        }
    }

    /// Look up all bindings for an action as parsed KeyBindings.
    pub fn bindings_for(&self, action: KeymapAction) -> &[String] {
        match action {
            KeymapAction::NextPhoto => &self.next_photo,
            KeymapAction::PrevPhoto => &self.prev_photo,
            KeymapAction::FirstPhoto => &self.first_photo,
            KeymapAction::LastPhoto => &self.last_photo,
            KeymapAction::ZoomIn => &self.zoom_in,
            KeymapAction::ZoomOut => &self.zoom_out,
            KeymapAction::FitToWindow => &self.fit_to_window,
            KeymapAction::ActualSize => &self.actual_size,
            KeymapAction::ToggleFullscreen => &self.toggle_fullscreen,
            KeymapAction::ToggleChrome => &self.toggle_chrome,
            KeymapAction::OpenFolder => &self.open_folder,
            KeymapAction::Quit => &self.quit,
            KeymapAction::RotateCw => &self.rotate_cw,
            KeymapAction::RotateCcw => &self.rotate_ccw,
            KeymapAction::Properties => &self.properties,
            KeymapAction::CopyToPictures => &self.copy_to_pictures,
            KeymapAction::DeletePermanently => &self.delete_permanently,
            KeymapAction::About => &self.about,
            KeymapAction::ToggleSidebar => &self.toggle_sidebar,
            KeymapAction::ToggleSlideshow => &self.toggle_slideshow,
            KeymapAction::RandomSameFolder => &self.random_same_folder,
            KeymapAction::RandomFolderTree => &self.random_folder_tree,
        }
    }
}

/// Enum of all keymap actions, for match-style dispatch.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KeymapAction {
    NextPhoto,
    PrevPhoto,
    FirstPhoto,
    LastPhoto,
    ZoomIn,
    ZoomOut,
    FitToWindow,
    ActualSize,
    ToggleFullscreen,
    ToggleChrome,
    OpenFolder,
    Quit,
    RotateCw,
    RotateCcw,
    Properties,
    CopyToPictures,
    DeletePermanently,
    About,
    ToggleSidebar,
    ToggleSlideshow,
    RandomSameFolder,
    RandomFolderTree,
}

/// Where to look for the user's keymap.toml.
/// `$XDG_CONFIG_HOME/marten/keymap.toml`, falling back to
/// `~/.config/marten/keymap.toml` if XDG isn't set.
pub fn user_keymap_path() -> Option<PathBuf> {
    let base = dirs::config_dir()?;
    Some(base.join("marten").join("keymap.toml"))
}

/// Load the user's keymap.toml on top of the defaults.
/// Missing file → defaults. Malformed file → defaults + warning.
/// Partial file (only some actions overridden) → defaults with overrides.
pub fn load_keymap() -> (Keymap, Vec<String>) {
    let mut warnings = Vec::new();
    let mut keymap = Keymap::defaults();

    let path = match user_keymap_path() {
        Some(p) => p,
        None => return (keymap, warnings),
    };

    if !path.exists() {
        return (keymap, warnings);
    }

    let contents = match std::fs::read_to_string(&path) {
        Ok(s) => s,
        Err(e) => {
            warnings.push(format!("Could not read {}: {e}", path.display()));
            return (keymap, warnings);
        }
    };

    let user_keymap: Keymap = match toml::from_str(&contents) {
        Ok(k) => k,
        Err(e) => {
            warnings.push(format!("Could not parse {}: {e}", path.display()));
            return (keymap, warnings);
        }
    };

    // Override: for each action, if the user specified at least one binding,
    // replace the default; otherwise keep the default.
    macro_rules! override_if_nonempty {
        ($field:ident) => {
            if !user_keymap.$field.is_empty() {
                // Validate each binding parses; skip invalid ones with a warning.
                let mut valid = Vec::new();
                for b in &user_keymap.$field {
                    if parse_binding(b).is_some() {
                        valid.push(b.clone());
                    } else {
                        warnings.push(format!(
                            "Invalid binding {:?} for {} in {}",
                            b,
                            stringify!($field),
                            path.display()
                        ));
                    }
                }
                keymap.$field = valid;
            }
        };
    }

    override_if_nonempty!(next_photo);
    override_if_nonempty!(prev_photo);
    override_if_nonempty!(first_photo);
    override_if_nonempty!(last_photo);
    override_if_nonempty!(zoom_in);
    override_if_nonempty!(zoom_out);
    override_if_nonempty!(fit_to_window);
    override_if_nonempty!(actual_size);
    override_if_nonempty!(toggle_fullscreen);
    override_if_nonempty!(toggle_chrome);
    override_if_nonempty!(open_folder);
    override_if_nonempty!(quit);
    override_if_nonempty!(rotate_cw);
    override_if_nonempty!(rotate_ccw);
    override_if_nonempty!(properties);
    override_if_nonempty!(copy_to_pictures);
    override_if_nonempty!(delete_permanently);
    override_if_nonempty!(about);
    override_if_nonempty!(toggle_sidebar);
    override_if_nonempty!(toggle_slideshow);
    override_if_nonempty!(random_same_folder);
    override_if_nonempty!(random_folder_tree);

    (keymap, warnings)
}

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

    #[test]
    fn parses_simple_key() {
        let b = parse_binding("Right").unwrap();
        assert_eq!(b.key, "Right");
        assert!(!b.mods.ctrl);
    }

    #[test]
    fn parses_modified_key() {
        let b = parse_binding("Ctrl+Shift+R").unwrap();
        assert_eq!(b.key, "R");
        assert!(b.mods.ctrl);
        assert!(b.mods.shift);
    }

    #[test]
    fn case_insensitive_modifiers() {
        let b = parse_binding("ctrl+SHIFT+alt+x").unwrap();
        assert!(b.mods.ctrl);
        assert!(b.mods.shift);
        assert!(b.mods.alt);
        assert_eq!(b.key, "x");
    }

    #[test]
    fn case_sensitive_key() {
        // 'g' and 'G' are different bindings (vim convention).
        let lower = parse_binding("g").unwrap();
        let upper = parse_binding("G").unwrap();
        assert_ne!(lower.key, upper.key);
    }

    #[test]
    fn rejects_unknown_modifier() {
        assert!(parse_binding("Foo+Bar").is_none());
    }

    #[test]
    fn rejects_empty() {
        assert!(parse_binding("").is_none());
        assert!(parse_binding("Ctrl+").is_none());
    }

    #[test]
    fn defaults_have_both_arrows_and_vim() {
        let k = Keymap::defaults();
        assert!(k.next_photo.contains(&"Right".to_string()));
        assert!(k.next_photo.contains(&"l".to_string()));
        assert!(k.prev_photo.contains(&"Left".to_string()));
        assert!(k.prev_photo.contains(&"h".to_string()));
    }
}
