//! Settings file — user preferences loaded from `~/.config/marten/settings.toml`.
//!
//! Settings are distinct from the keymap (which lives in `keymap.toml`):
//! settings are scalar values (intervals, sizes, toggles) that affect runtime
//! behavior, while the keymap is purely about which key triggers which action.
//!
//! Loading order:
//! 1. Built-in defaults (hardcoded in `Settings::defaults()`).
//! 2. `$XDG_CONFIG_HOME/marten/settings.toml` if it exists.
//! 3. Unknown keys in the user's toml emit a warning but do not crash.
//!
//! Schema:
//! ```toml
//! slideshow_interval_secs = 3.0
//! thumbnail_size = 72
//! thumbnail_cache_window = 15
//! default_zoom_mode = "FitToWindow"   # or "ActualSize"
//! smooth_scroll_thumbnails = true
//! show_fullscreen_hint = true
//! ```

use std::path::PathBuf;

use serde::{Deserialize, Serialize};

/// User-configurable runtime settings.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Settings {
    /// Seconds between auto-advances in slideshow mode.
    #[serde(default = "default_slideshow_interval")]
    pub slideshow_interval_secs: f32,

    /// Thumbnail edge length in pixels (square thumbnails).
    #[serde(default = "default_thumbnail_size")]
    pub thumbnail_size: u32,

    /// How many thumbnails ahead/behind the current image to cache.
    #[serde(default = "default_thumbnail_cache_window")]
    pub thumbnail_cache_window: usize,

    /// Zoom mode applied when a new image is loaded.
    /// "FitToWindow" or "ActualSize".
    #[serde(default = "default_zoom_mode")]
    pub default_zoom_mode: String,

    /// Whether the thumbnail strip auto-scrolls to follow the current image.
    #[serde(default = "default_smooth_scroll_thumbnails")]
    pub smooth_scroll_thumbnails: bool,

    /// Whether the "Press F11 to exit fullscreen" hint is shown.
    #[serde(default = "default_show_fullscreen_hint")]
    pub show_fullscreen_hint: bool,
}

fn default_slideshow_interval() -> f32 { 3.0 }
fn default_thumbnail_size() -> u32 { 72 }
fn default_thumbnail_cache_window() -> usize { 15 }
fn default_zoom_mode() -> String { "FitToWindow".to_string() }
fn default_smooth_scroll_thumbnails() -> bool { true }
fn default_show_fullscreen_hint() -> bool { true }

impl Default for Settings {
    fn default() -> Self {
        Self {
            slideshow_interval_secs: default_slideshow_interval(),
            thumbnail_size: default_thumbnail_size(),
            thumbnail_cache_window: default_thumbnail_cache_window(),
            default_zoom_mode: default_zoom_mode(),
            smooth_scroll_thumbnails: default_smooth_scroll_thumbnails(),
            show_fullscreen_hint: default_show_fullscreen_hint(),
        }
    }
}

impl Settings {
    /// The hardcoded default settings. Matches the schema documented above.
    pub fn defaults() -> Self {
        Self::default()
    }

    /// The path where user settings are loaded from:
    /// `$XDG_CONFIG_HOME/marten/settings.toml`, falling back to
    /// `~/.config/marten/settings.toml` if XDG is not set.
    pub fn user_settings_path() -> Option<PathBuf> {
        let base = dirs::config_dir()?;
        Some(base.join("marten").join("settings.toml"))
    }

    /// Load settings from the user's `settings.toml`, merging over defaults.
    /// Missing file → defaults. Malformed file → defaults + warning.
    /// Partial file → defaults with overrides for present fields.
    pub fn load() -> (Self, Vec<String>) {
        let mut warnings = Vec::new();
        let mut settings = Self::defaults();

        let path = match Self::user_settings_path() {
            Some(p) => p,
            None => return (settings, warnings),
        };

        if !path.exists() {
            return (settings, 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 (settings, warnings);
            }
        };

        let user_settings: Self = match toml::from_str(&contents) {
            Ok(s) => s,
            Err(e) => {
                warnings.push(format!("Could not parse {}: {e}", path.display()));
                return (settings, warnings);
            }
        };

        // Override: serde already applied defaults for missing fields,
        // so we can just replace the whole struct.
        settings = user_settings;

        // Validate.
        if settings.slideshow_interval_secs <= 0.0 {
            warnings.push(format!(
                "slideshow_interval_secs must be > 0 (got {}); using default",
                settings.slideshow_interval_secs
            ));
            settings.slideshow_interval_secs = default_slideshow_interval();
        }
        if settings.thumbnail_size < 16 || settings.thumbnail_size > 256 {
            warnings.push(format!(
                "thumbnail_size must be 16..=256 (got {}); using default",
                settings.thumbnail_size
            ));
            settings.thumbnail_size = default_thumbnail_size();
        }
        if settings.thumbnail_cache_window == 0 {
            warnings.push("thumbnail_cache_window must be > 0; using default".into());
            settings.thumbnail_cache_window = default_thumbnail_cache_window();
        }
        if settings.default_zoom_mode != "FitToWindow"
            && settings.default_zoom_mode != "ActualSize"
        {
            warnings.push(format!(
                "default_zoom_mode must be \"FitToWindow\" or \"ActualSize\" (got {:?}); using default",
                settings.default_zoom_mode
            ));
            settings.default_zoom_mode = default_zoom_mode();
        }

        (settings, warnings)
    }
}

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

    #[test]
    fn defaults_are_sane() {
        let s = Settings::defaults();
        assert_eq!(s.slideshow_interval_secs, 3.0);
        assert_eq!(s.thumbnail_size, 72);
        assert_eq!(s.thumbnail_cache_window, 15);
        assert_eq!(s.default_zoom_mode, "FitToWindow");
        assert!(s.smooth_scroll_thumbnails);
        assert!(s.show_fullscreen_hint);
    }

    #[test]
    fn parses_full_toml() {
        let toml = r#"
slideshow_interval_secs = 5.0
thumbnail_size = 96
thumbnail_cache_window = 20
default_zoom_mode = "ActualSize"
smooth_scroll_thumbnails = false
show_fullscreen_hint = false
"#;
        let s: Settings = toml::from_str(toml).unwrap();
        assert_eq!(s.slideshow_interval_secs, 5.0);
        assert_eq!(s.thumbnail_size, 96);
        assert_eq!(s.thumbnail_cache_window, 20);
        assert_eq!(s.default_zoom_mode, "ActualSize");
        assert!(!s.smooth_scroll_thumbnails);
        assert!(!s.show_fullscreen_hint);
    }

    #[test]
    fn parses_partial_toml_uses_defaults_for_missing() {
        let toml = r#"
slideshow_interval_secs = 7.0
"#;
        let s: Settings = toml::from_str(toml).unwrap();
        assert_eq!(s.slideshow_interval_secs, 7.0);
        // Missing fields get serde defaults via the `default =` attributes.
        assert_eq!(s.thumbnail_size, 72);
        assert_eq!(s.default_zoom_mode, "FitToWindow");
    }

    #[test]
    fn rejects_garbage() {
        let toml = "this is not valid toml = = =";
        assert!(toml::from_str::<Settings>(toml).is_err());
    }
}
