//! UI theming: a set of selectable colour palettes.
//!
//! The UI uses 13 semantic colour roles (background tiers + accents). Each
//! [`AppTheme`] maps to a [`Palette`] of those roles via the pure
//! [`AppTheme::palette`] fn — that's the testable seam: palettes are plain data
//! and the legibility guarantees below are unit-tested. `view()` reads the
//! active palette instead of hardcoded literals, so every widget (incl. the
//! canvas widgets that already take colours as data) re-themes automatically.
//!
//! `AppTheme` also maps to an `iced::Theme` ([`AppTheme::base_theme`]) so iced's
//! built-in widget chrome (scrollbars, text-input selection, dropdown menus)
//! matches the chosen palette.

use iced::Color;
use serde::{Deserialize, Serialize};

/// 13 semantic colour roles the UI draws with. Names follow the Catppuccin
/// vocabulary (the original palette), but each theme supplies its own values.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Palette {
    /// Darkest background (window backdrop behind everything).
    pub crust: Color,
    /// Slightly raised background.
    pub mantle: Color,
    /// Main content background.
    pub base: Color,
    /// Raised panel / card surface.
    pub surface: Color,
    /// Muted surface — placeholders, disabled, faint dividers.
    pub overlay: Color,
    /// Primary foreground text.
    pub text: Color,
    /// Secondary / hint text.
    pub subtext: Color,
    /// Primary accent (titles, primary buttons, selection borders).
    pub blue: Color,
    /// Secondary accent.
    pub lavender: Color,
    /// Danger / record / error.
    pub red: Color,
    /// Red variant (a warmer alternative to `red`).
    pub maroon: Color,
    /// Success / "transmitting" / speaking.
    pub green: Color,
    /// Warning / "reconnecting".
    pub yellow: Color,
}

/// A selectable UI theme. The default is `Mocha` (the project's original look).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum AppTheme {
    #[default]
    Mocha,
    Macchiato,
    Frappe,
    Latte,
    Dracula,
    Nord,
    TokyoNight,
    GruvboxDark,
    SolarizedLight,
    GruvboxLight,
    AyuDark,
    AyuMirage,
    AyuLight,
}

/// Build a `Color` from a packed `0xRRGGBB` literal — keeps the palette tables
/// compact and greppable against each theme's canonical hex values.
fn hex(c: u32) -> Color {
    Color::from_rgb8((c >> 16) as u8, (c >> 8) as u8, c as u8)
}

impl AppTheme {
    /// Every theme, in picker order.
    pub const ALL: [AppTheme; 13] = [
        AppTheme::Mocha,
        AppTheme::Macchiato,
        AppTheme::Frappe,
        AppTheme::Latte,
        AppTheme::Dracula,
        AppTheme::Nord,
        AppTheme::TokyoNight,
        AppTheme::GruvboxDark,
        AppTheme::SolarizedLight,
        AppTheme::GruvboxLight,
        AppTheme::AyuDark,
        AppTheme::AyuMirage,
        AppTheme::AyuLight,
    ];

    /// Human-readable name for the picker.
    pub fn label(self) -> &'static str {
        match self {
            AppTheme::Mocha => "Mocha",
            AppTheme::Macchiato => "Macchiato",
            AppTheme::Frappe => "Frappé",
            AppTheme::Latte => "Latte",
            AppTheme::Dracula => "Dracula",
            AppTheme::Nord => "Nord",
            AppTheme::TokyoNight => "Tokyo Night",
            AppTheme::GruvboxDark => "Gruvbox Dark",
            AppTheme::SolarizedLight => "Solarized Light",
            AppTheme::GruvboxLight => "Gruvbox Light",
            AppTheme::AyuDark => "Ayu Dark",
            AppTheme::AyuMirage => "Ayu Mirage",
            AppTheme::AyuLight => "Ayu Light",
        }
    }

    /// True for dark themes (dark background, light text).
    pub fn is_dark(self) -> bool {
        !matches!(
            self,
            AppTheme::Latte
                | AppTheme::SolarizedLight
                | AppTheme::GruvboxLight
                | AppTheme::AyuLight
        )
    }

    /// The matching iced built-in theme, for built-in widget chrome.
    pub fn base_theme(self) -> iced::Theme {
        match self {
            AppTheme::Mocha => iced::Theme::CatppuccinMocha,
            AppTheme::Macchiato => iced::Theme::CatppuccinMacchiato,
            AppTheme::Frappe => iced::Theme::CatppuccinFrappe,
            AppTheme::Latte => iced::Theme::CatppuccinLatte,
            AppTheme::Dracula => iced::Theme::Dracula,
            AppTheme::Nord => iced::Theme::Nord,
            AppTheme::TokyoNight => iced::Theme::TokyoNight,
            AppTheme::GruvboxDark => iced::Theme::GruvboxDark,
            AppTheme::SolarizedLight => iced::Theme::SolarizedLight,
            AppTheme::GruvboxLight => iced::Theme::GruvboxLight,
            AppTheme::AyuDark | AppTheme::AyuMirage => iced::Theme::TokyoNight,
            AppTheme::AyuLight => iced::Theme::Light,
        }
    }

    /// The 13-role colour palette for this theme. Pure data — colours are each
    /// theme's canonical hex values mapped onto our semantic roles.
    pub fn palette(self) -> Palette {
        match self {
            // Catppuccin Mocha — the project's original palette.
            AppTheme::Mocha => Palette {
                crust: hex(0x11111b),
                mantle: hex(0x181825),
                base: hex(0x1e1e2e),
                surface: hex(0x313244),
                overlay: hex(0x6c7086),
                text: hex(0xcdd6f4),
                subtext: hex(0xa6adc8),
                blue: hex(0x89b4fa),
                lavender: hex(0xb4befe),
                red: hex(0xf38ba8),
                maroon: hex(0xeba0ac),
                green: hex(0xa6e3a1),
                yellow: hex(0xf9e2af),
            },
            AppTheme::Macchiato => Palette {
                crust: hex(0x181926),
                mantle: hex(0x1e2030),
                base: hex(0x24273a),
                surface: hex(0x363a4f),
                overlay: hex(0x6e738d),
                text: hex(0xcad3f5),
                subtext: hex(0xa5adcb),
                blue: hex(0x8aadf4),
                lavender: hex(0xb7bdf8),
                red: hex(0xed8796),
                maroon: hex(0xee99a0),
                green: hex(0xa6da95),
                yellow: hex(0xeed49f),
            },
            AppTheme::Frappe => Palette {
                crust: hex(0x232634),
                mantle: hex(0x292c3c),
                base: hex(0x303446),
                surface: hex(0x414559),
                overlay: hex(0x737994),
                text: hex(0xc6d0f5),
                subtext: hex(0xa5adce),
                blue: hex(0x8caaee),
                lavender: hex(0xbabbf1),
                red: hex(0xe78284),
                maroon: hex(0xea999c),
                green: hex(0xa6d189),
                yellow: hex(0xe5c890),
            },
            // Catppuccin Latte — light.
            AppTheme::Latte => Palette {
                crust: hex(0xdce0e8),
                mantle: hex(0xe6e9ef),
                base: hex(0xeff1f5),
                surface: hex(0xccd0da),
                overlay: hex(0x9ca0b0),
                text: hex(0x4c4f69),
                subtext: hex(0x6c6f85),
                blue: hex(0x1e66f5),
                lavender: hex(0x7287fd),
                red: hex(0xd20f39),
                maroon: hex(0xe64553),
                green: hex(0x40a02b),
                yellow: hex(0xdf8e1d),
            },
            AppTheme::Dracula => Palette {
                crust: hex(0x191a21),
                mantle: hex(0x21222c),
                base: hex(0x282a36),
                surface: hex(0x44475a),
                overlay: hex(0x6272a4),
                text: hex(0xf8f8f2),
                subtext: hex(0xbdc0d4),
                blue: hex(0xbd93f9), // Dracula's signature purple as the primary accent
                lavender: hex(0x8be9fd), // cyan
                red: hex(0xff5555),
                maroon: hex(0xff79c6), // pink
                green: hex(0x50fa7b),
                yellow: hex(0xf1fa8c),
            },
            AppTheme::Nord => Palette {
                crust: hex(0x272c36),
                mantle: hex(0x2b313c),
                base: hex(0x2e3440),
                surface: hex(0x3b4252),
                overlay: hex(0x4c566a),
                text: hex(0xeceff4),
                subtext: hex(0xd8dee9),
                blue: hex(0x88c0d0),
                lavender: hex(0xb48ead),
                red: hex(0xbf616a),
                maroon: hex(0xd08770),
                green: hex(0xa3be8c),
                yellow: hex(0xebcb8b),
            },
            AppTheme::TokyoNight => Palette {
                crust: hex(0x16161e),
                mantle: hex(0x1b1c29),
                base: hex(0x1a1b26),
                surface: hex(0x24283b),
                overlay: hex(0x565f89),
                text: hex(0xc0caf5),
                subtext: hex(0x9aa5ce),
                blue: hex(0x7aa2f7),
                lavender: hex(0xbb9af7),
                red: hex(0xf7768e),
                maroon: hex(0xff9e64),
                green: hex(0x9ece6a),
                yellow: hex(0xe0af68),
            },
            AppTheme::GruvboxDark => Palette {
                crust: hex(0x1d2021),
                mantle: hex(0x242424),
                base: hex(0x282828),
                surface: hex(0x3c3836),
                overlay: hex(0x665c54),
                text: hex(0xebdbb2),
                subtext: hex(0xbdae93),
                blue: hex(0x83a598),
                lavender: hex(0xd3869b),
                red: hex(0xfb4934),
                maroon: hex(0xfe8019),
                green: hex(0xb8bb26),
                yellow: hex(0xfabd2f),
            },
            // Solarized Light — light.
            AppTheme::SolarizedLight => Palette {
                crust: hex(0xe6dfc8),
                mantle: hex(0xf3ecd6),
                base: hex(0xfdf6e3),
                surface: hex(0xeee8d5),
                overlay: hex(0x93a1a1),
                text: hex(0x586e75),
                subtext: hex(0x657b83),
                blue: hex(0x268bd2),
                lavender: hex(0x6c71c4),
                red: hex(0xdc322f),
                maroon: hex(0xcb4b16),
                green: hex(0x859900),
                yellow: hex(0xb58900),
            },
            // Gruvbox Light — light.
            AppTheme::GruvboxLight => Palette {
                crust: hex(0xf2e5bc),
                mantle: hex(0xf9f5d7),
                base: hex(0xfbf1c7),
                surface: hex(0xebdbb2),
                overlay: hex(0xbdae93),
                text: hex(0x3c3836),
                subtext: hex(0x504945),
                blue: hex(0x076678),
                lavender: hex(0x8f3f71),
                red: hex(0x9d0006),
                maroon: hex(0xaf3a03),
                green: hex(0x79740e),
                yellow: hex(0xb57614),
            },
            AppTheme::AyuDark => Palette {
                crust: hex(0x06080a),
                mantle: hex(0x0b0e14),
                base: hex(0x0d1017),
                surface: hex(0x1c222b),
                overlay: hex(0x565b66),
                text: hex(0xbfbdb6),
                subtext: hex(0x9da1a6),
                blue: hex(0xe6b450),
                lavender: hex(0x59c2ff),
                red: hex(0xf07178),
                maroon: hex(0xff8f40),
                green: hex(0xaad94c),
                yellow: hex(0xffb454),
            },
            AppTheme::AyuMirage => Palette {
                crust: hex(0x171b24),
                mantle: hex(0x1a1f29),
                base: hex(0x1f2430),
                surface: hex(0x232834),
                overlay: hex(0x707a8c),
                text: hex(0xcccac2),
                subtext: hex(0xa6abb4),
                blue: hex(0xffcc66),
                lavender: hex(0x73d0ff),
                red: hex(0xf28779),
                maroon: hex(0xffa759),
                green: hex(0xd5ff80),
                yellow: hex(0xffd173),
            },
            // Ayu Light's canonical orange is deepened for legibility on white.
            AppTheme::AyuLight => Palette {
                crust: hex(0xe6e9ec),
                mantle: hex(0xf3f4f5),
                base: hex(0xfcfcfc),
                surface: hex(0xe8eaed),
                overlay: hex(0x8a9199),
                text: hex(0x5c6166),
                subtext: hex(0x737980),
                blue: hex(0xc7500e),
                lavender: hex(0x399ee6),
                red: hex(0xf07171),
                maroon: hex(0xfa8d3e),
                green: hex(0x86b300),
                yellow: hex(0xff9940),
            },
        }
    }
}

/// WCAG relative luminance of a colour (sRGB → linear, 0.0..=1.0).
pub fn relative_luminance(c: Color) -> f32 {
    fn lin(ch: f32) -> f32 {
        if ch <= 0.03928 {
            ch / 12.92
        } else {
            ((ch + 0.055) / 1.055).powf(2.4)
        }
    }
    0.2126 * lin(c.r) + 0.7152 * lin(c.g) + 0.0722 * lin(c.b)
}

/// WCAG contrast ratio between two colours (1.0..=21.0). Order-independent.
pub fn contrast_ratio(a: Color, b: Color) -> f32 {
    let (la, lb) = (relative_luminance(a), relative_luminance(b));
    let (hi, lo) = if la >= lb { (la, lb) } else { (lb, la) };
    (hi + 0.05) / (lo + 0.05)
}

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

    #[test]
    fn contrast_ratio_known_values() {
        // Black on white is the maximum 21:1; a colour against itself is 1:1.
        assert!((contrast_ratio(Color::BLACK, Color::WHITE) - 21.0).abs() < 0.1);
        assert!((contrast_ratio(Color::WHITE, Color::WHITE) - 1.0).abs() < 0.001);
        // Symmetric.
        let a = hex(0x89b4fa);
        let b = hex(0x1e1e2e);
        assert!((contrast_ratio(a, b) - contrast_ratio(b, a)).abs() < 0.001);
    }

    #[test]
    fn every_theme_has_legible_body_text() {
        // Primary text on the main background must clear WCAG AA (4.5:1).
        for theme in AppTheme::ALL {
            let p = theme.palette();
            let ratio = contrast_ratio(p.text, p.base);
            assert!(
                ratio >= 4.5,
                "{}: text-on-base contrast {ratio:.2} < 4.5 (AA)",
                theme.label()
            );
        }
    }

    #[test]
    fn every_theme_has_readable_subtext_and_accents() {
        // Secondary text clears AA-large (3:1); the title/primary accent must be
        // readable on the background too (it's used for headings + button text).
        for theme in AppTheme::ALL {
            let p = theme.palette();
            let sub = contrast_ratio(p.subtext, p.base);
            assert!(
                sub >= 3.0,
                "{}: subtext contrast {sub:.2} < 3.0",
                theme.label()
            );
            let accent = contrast_ratio(p.blue, p.base);
            assert!(
                accent >= 3.0,
                "{}: primary-accent contrast {accent:.2} < 3.0",
                theme.label()
            );
        }
    }

    #[test]
    fn is_dark_matches_luminance_direction() {
        // Dark themes: text brighter than base. Light themes: text darker.
        for theme in AppTheme::ALL {
            let p = theme.palette();
            let text_brighter = relative_luminance(p.text) > relative_luminance(p.base);
            assert_eq!(
                text_brighter,
                theme.is_dark(),
                "{}: is_dark={} but text_brighter={}",
                theme.label(),
                theme.is_dark(),
                text_brighter
            );
        }
    }

    #[test]
    fn all_themes_distinct_and_labeled() {
        // ALL covers exactly the variants once, each with a unique non-empty label
        // and a distinct base colour (so swatches don't look identical).
        assert_eq!(AppTheme::ALL.len(), 13);
        let mut labels: Vec<&str> = AppTheme::ALL.iter().map(|t| t.label()).collect();
        labels.sort_unstable();
        labels.dedup();
        assert_eq!(labels.len(), 13, "labels must be unique + non-empty");
        assert!(labels.iter().all(|l| !l.is_empty()));
    }

    #[test]
    fn serde_round_trips_every_variant() {
        for theme in AppTheme::ALL {
            let json = serde_json::to_string(&theme).unwrap();
            let back: AppTheme = serde_json::from_str(&json).unwrap();
            assert_eq!(theme, back);
        }
    }

    #[test]
    fn default_is_mocha() {
        assert_eq!(AppTheme::default(), AppTheme::Mocha);
        // Default palette matches the project's original literals.
        let p = AppTheme::default().palette();
        assert_eq!(p.base, Color::from_rgb8(30, 30, 46));
        assert_eq!(p.text, Color::from_rgb8(205, 214, 244));
        assert_eq!(p.blue, Color::from_rgb8(137, 180, 250));
    }
}
