//! Theme and colour palette.
//!
//! Two themes are provided — one for light terminal backgrounds
//! and one for dark — per NFR-7. The palette is intentionally
//! small for the walking skeleton; it grows as more views are
//! added. Contrast is chosen against the target background so
//! that foreground text meets WCAG-AA (NFR-5) on both variants.
//!
//! Per-token-class colours (the `tok_*` fields) drive ambient
//! typing assistance (ADR-0022 §3). Each token class has a
//! distinct colour so the user can tell keywords from
//! identifiers from string literals at a glance, with punct and
//! identifier intentionally close to `fg` to keep the surface
//! quiet for the dominant content. The `tok_error` colour
//! reuses the existing error palette so lex-error tokens and
//! parse-error overlays read consistently with `[error]`
//! lines elsewhere.

use ratatui::style::Color;

use crate::dsl::grammar::HighlightClass;

/// Foreground of the demonstration-mode overlays (ADR-0047 D4).
///
/// Deliberately a fixed, theme-independent high-contrast pair — black
/// on yellow — so the badge / caption boxes are hard to overlook in a
/// screencast on any background.
pub const DEMO_OVERLAY_FG: Color = Color::Black;
/// Background of the demonstration-mode overlays (ADR-0047 D4); see
/// [`DEMO_OVERLAY_FG`].
pub const DEMO_OVERLAY_BG: Color = Color::Rgb(0xFF, 0xD7, 0x00);

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Background {
    Light,
    Dark,
}

#[derive(Debug, Clone)]
pub struct Theme {
    pub background: Background,
    pub bg: Color,
    pub fg: Color,
    pub muted: Color,
    pub border: Color,
    pub border_advanced: Color,
    pub mode_simple: Color,
    pub mode_advanced: Color,
    pub system: Color,
    pub error: Color,
    /// Validity-indicator WARNING colour (ADR-0027 §4) — an
    /// amber distinct from `error`'s red. Drives the `[WRN]`
    /// label; `[ERR]` reuses `error`. Also the query-plan
    /// "expensive step" colour (ADR-0028 §6).
    pub warning: Color,
    /// Query-plan "efficient step" colour (ADR-0028 §6) — a
    /// green deliberately distinct from `system` so green never
    /// reads as two things. Indexed lookups carry it; expensive
    /// steps reuse `warning`.
    pub plan_efficient: Color,
    // ---- Per-token-class colours (ADR-0022 §3) -------------------
    pub tok_keyword: Color,
    pub tok_identifier: Color,
    /// Column data-type keyword colour (ADR-0022 Amendment 4) —
    /// a dedicated tone distinct from both `tok_keyword` and
    /// `tok_identifier` so a learner can tell a type from a
    /// clause keyword or an invented name at a glance.
    pub tok_type: Color,
    pub tok_number: Color,
    pub tok_string: Color,
    pub tok_punct: Color,
    pub tok_flag: Color,
    pub tok_error: Color,
    /// SQL function-name candidate colour (ADR-0022 Amendment 6,
    /// issue #15) — a dedicated tone distinct from `tok_keyword`,
    /// `tok_identifier`, and `tok_type` so a learner can tell a
    /// callable (`sum`, `upper`) apart from a clause keyword, a
    /// column reference, and a column type. Drives the
    /// `CandidateKind::Function` colour in the hint panel.
    pub tok_function: Color,
}

impl Theme {
    #[must_use]
    pub const fn dark() -> Self {
        Self {
            background: Background::Dark,
            bg: Color::Rgb(0x18, 0x1B, 0x22),
            fg: Color::Rgb(0xE6, 0xE6, 0xE6),
            muted: Color::Rgb(0x8B, 0x90, 0x9A),
            border: Color::Rgb(0x4A, 0x52, 0x65),
            border_advanced: Color::Rgb(0xE0, 0x60, 0x60),
            mode_simple: Color::Rgb(0x6E, 0xC4, 0xFF),
            mode_advanced: Color::Rgb(0xFF, 0x9E, 0x6B),
            system: Color::Rgb(0x9F, 0xD8, 0x91),
            error: Color::Rgb(0xFF, 0x6B, 0x6B),
            warning: Color::Rgb(0xF5, 0xA9, 0x4B), // amber
            plan_efficient: Color::Rgb(0x4D, 0xD0, 0xA8), // teal-green

            // Token classes — distinct enough to tell apart at a
            // glance, quiet enough that 80-char lines don't read
            // like a Christmas tree. Identifier and punct sit
            // close to `fg`/`muted` so the dominant content
            // remains restful; literals and flags get warm
            // accent tones; keyword takes a cool accent tone
            // distinct from the mode-banner blue.
            tok_keyword: Color::Rgb(0xC7, 0x92, 0xEA), // muted purple
            tok_identifier: Color::Rgb(0x56, 0xB6, 0xC2), // cyan-teal — identifiers are the user's content, deserve a vivid distinct colour
            tok_type: Color::Rgb(0xF5, 0x8A, 0xAE), // rose-pink — types sit in the red-purple range, clearly apart from the lavender keyword and teal identifier (ADR-0057: widened ΔE from keyword 14.5→18.4)
            tok_number: Color::Rgb(0xF7, 0x8C, 0x6C), // warm orange
            tok_string: Color::Rgb(0xC3, 0xE8, 0x8D), // soft green
            tok_punct: Color::Rgb(0x8B, 0x90, 0x9A), // == muted
            tok_flag: Color::Rgb(0xFF, 0xCB, 0x6B), // amber
            tok_error: Color::Rgb(0xFF, 0x6B, 0x6B), // == error
            tok_function: Color::Rgb(0x6C, 0xB2, 0xFF), // sky blue — cool like keyword but bluer, clearly apart from purple keyword + teal identifier + rose type (ADR-0057: widened ΔE from identifier 14.3→19.5)
        }
    }

    #[must_use]
    pub const fn light() -> Self {
        Self {
            background: Background::Light,
            bg: Color::Rgb(0xFA, 0xFA, 0xF7),
            fg: Color::Rgb(0x1A, 0x1F, 0x2C),
            muted: Color::Rgb(0x60, 0x66, 0x73),
            border: Color::Rgb(0xB6, 0xBC, 0xC8),
            border_advanced: Color::Rgb(0xC2, 0x3A, 0x3A),
            mode_simple: Color::Rgb(0x21, 0x69, 0xC7),
            mode_advanced: Color::Rgb(0xB0, 0x4A, 0x12),
            system: Color::Rgb(0x2E, 0x7C, 0x3C),
            error: Color::Rgb(0xC0, 0x39, 0x2B),
            warning: Color::Rgb(0xA6, 0x5A, 0x00), // burnt amber
            plan_efficient: Color::Rgb(0x0B, 0x80, 0x6A), // deep teal-green

            // Light-theme token palette: same intent as dark —
            // identifier/punct close to fg/muted; warm tones for
            // literals + flags; cool accent for keyword.
            tok_keyword: Color::Rgb(0x6F, 0x42, 0xC1), // royal purple
            tok_identifier: Color::Rgb(0x0F, 0x6B, 0x76), // deep teal — same role as dark variant: identifiers stand out
            tok_type: Color::Rgb(0xA8, 0x2D, 0x73), // deep magenta — red-purple, distinct from royal-purple keyword + teal identifier
            tok_number: Color::Rgb(0xBC, 0x4F, 0x1F), // burnt orange
            tok_string: Color::Rgb(0x1B, 0x7A, 0x33), // forest green (ADR-0057: darkened for WCAG-AA, 4.42→5.18:1)
            tok_punct: Color::Rgb(0x60, 0x66, 0x73),  // == muted
            tok_flag: Color::Rgb(0x7A, 0x5C, 0x00), // dark mustard/olive (ADR-0057: darkened for WCAG-AA, 3.15→5.98:1)
            tok_error: Color::Rgb(0xC0, 0x39, 0x2B), // == error
            tok_function: Color::Rgb(0x1A, 0x5F, 0xB0), // strong blue — cool like keyword but bluer, apart from royal-purple keyword + teal identifier + magenta type
        }
    }

    /// Map a walker `HighlightClass` to its display colour
    /// (ADR-0024 §architecture, Phase F). This is the walker-side
    /// equivalent of `token_color` — the renderer consumes
    /// `walker::highlight_runs` output, which produces
    /// `HighlightClass` per byte range, and looks up colours
    /// through this method.
    #[must_use]
    pub const fn highlight_class_color(&self, class: HighlightClass) -> Color {
        match class {
            HighlightClass::Keyword => self.tok_keyword,
            HighlightClass::Identifier => self.tok_identifier,
            HighlightClass::Type => self.tok_type,
            HighlightClass::Number => self.tok_number,
            HighlightClass::String => self.tok_string,
            HighlightClass::Punct => self.tok_punct,
            HighlightClass::Flag => self.tok_flag,
            HighlightClass::Function => self.tok_function,
            HighlightClass::Error => self.tok_error,
        }
    }
}

impl Default for Theme {
    fn default() -> Self {
        Self::dark()
    }
}

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

    #[test]
    fn dark_theme_token_colours_differ_from_background() {
        let t = Theme::dark();
        for (name, c) in [
            ("tok_keyword", t.tok_keyword),
            ("tok_type", t.tok_type),
            ("tok_number", t.tok_number),
            ("tok_string", t.tok_string),
            ("tok_flag", t.tok_flag),
            ("tok_error", t.tok_error),
            ("tok_function", t.tok_function),
            ("warning", t.warning),
        ] {
            assert_ne!(c, t.bg, "{name} must contrast against bg in dark theme",);
        }
    }

    #[test]
    fn light_theme_token_colours_differ_from_background() {
        let t = Theme::light();
        for (name, c) in [
            ("tok_keyword", t.tok_keyword),
            ("tok_type", t.tok_type),
            ("tok_number", t.tok_number),
            ("tok_string", t.tok_string),
            ("tok_flag", t.tok_flag),
            ("tok_error", t.tok_error),
            ("tok_function", t.tok_function),
            ("warning", t.warning),
        ] {
            assert_ne!(c, t.bg, "{name} must contrast against bg in light theme",);
        }
    }

    #[test]
    fn highlight_class_color_maps_each_variant() {
        let t = Theme::dark();
        assert_eq!(
            t.highlight_class_color(HighlightClass::Keyword),
            t.tok_keyword
        );
        assert_eq!(
            t.highlight_class_color(HighlightClass::Identifier),
            t.tok_identifier
        );
        assert_eq!(t.highlight_class_color(HighlightClass::Type), t.tok_type);
        assert_eq!(
            t.highlight_class_color(HighlightClass::Number),
            t.tok_number
        );
        assert_eq!(
            t.highlight_class_color(HighlightClass::String),
            t.tok_string
        );
        assert_eq!(t.highlight_class_color(HighlightClass::Punct), t.tok_punct);
        assert_eq!(t.highlight_class_color(HighlightClass::Flag), t.tok_flag);
        assert_eq!(
            t.highlight_class_color(HighlightClass::Function),
            t.tok_function
        );
        assert_eq!(t.highlight_class_color(HighlightClass::Error), t.tok_error);
    }

    #[test]
    fn type_colour_is_distinct_from_keyword_and_identifier() {
        // ADR-0022 Amendment 4 / issue #8: the whole point of a
        // dedicated type class is that types do NOT share a colour
        // with clause keywords or invented identifiers.
        for t in [Theme::dark(), Theme::light()] {
            assert_ne!(t.tok_type, t.tok_keyword);
            assert_ne!(t.tok_type, t.tok_identifier);
        }
    }

    #[test]
    fn function_colour_is_distinct_from_keyword_identifier_and_type() {
        // ADR-0022 Amendment 6 / issue #15: function-name candidates
        // get their own tone so a callable reads apart from a clause
        // keyword, a column reference, and a column type.
        for t in [Theme::dark(), Theme::light()] {
            assert_ne!(t.tok_function, t.tok_keyword);
            assert_ne!(t.tok_function, t.tok_identifier);
            assert_ne!(t.tok_function, t.tok_type);
        }
    }

    // ---- NFR-5 / NFR-7 (ADR-0057): WCAG-AA contrast gate ----
    //
    // Every text-bearing foreground must clear WCAG-AA 4.5:1 against
    // the panel background, in BOTH themes, so the colour scheme stays
    // legible on light and dark terminals (no dark-on-dark / light-on-
    // light failures). Borders are non-text structural chrome and are
    // handled by `advanced_mode_border_meets_ui_contrast` below; the
    // plain `border` is intentionally exempt (decorative — see ADR-0057).
    //
    // These checks rely on every `Theme` colour being `Color::Rgb`
    // (true-colour). `rgb_channels` panics on any other variant, which
    // also guards against a future regression to a named palette colour
    // (whose real contrast can't be known).

    fn rgb_channels(c: Color) -> (f64, f64, f64) {
        match c {
            Color::Rgb(r, g, b) => (f64::from(r), f64::from(g), f64::from(b)),
            other => panic!("theme colours must be Color::Rgb for contrast checks; got {other:?}"),
        }
    }

    /// WCAG 2.x relative luminance of an sRGB colour.
    // The luminance/Lab/CIEDE2000 maths below are published standards; we
    // keep them in canonical `a*x + b*y` form (verified against reference
    // vectors) rather than refactoring into `mul_add`, which would obscure
    // the formula for a negligible test-only gain.
    #[allow(clippy::suboptimal_flops)]
    fn relative_luminance(c: Color) -> f64 {
        let chan = |v: f64| {
            let v = v / 255.0;
            if v <= 0.03928 {
                v / 12.92
            } else {
                ((v + 0.055) / 1.055).powf(2.4)
            }
        };
        let (r, g, b) = rgb_channels(c);
        0.2126 * chan(r) + 0.7152 * chan(g) + 0.0722 * chan(b)
    }

    /// WCAG 2.x contrast ratio between two colours (1.0 ..= 21.0).
    fn contrast_ratio(a: Color, b: Color) -> f64 {
        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)
    }

    fn theme_label(t: &Theme) -> &'static str {
        match t.background {
            Background::Dark => "dark",
            Background::Light => "light",
        }
    }

    /// Every text foreground (incl. dimmed `muted`/`tok_punct` and the
    /// syntax-token classes) on `bg`, both themes.
    fn text_foregrounds(t: &Theme) -> [(&'static str, Color); 17] {
        [
            ("fg", t.fg),
            ("muted", t.muted),
            ("mode_simple", t.mode_simple),
            ("mode_advanced", t.mode_advanced),
            ("system", t.system),
            ("error", t.error),
            ("warning", t.warning),
            ("plan_efficient", t.plan_efficient),
            ("tok_keyword", t.tok_keyword),
            ("tok_identifier", t.tok_identifier),
            ("tok_type", t.tok_type),
            ("tok_number", t.tok_number),
            ("tok_string", t.tok_string),
            ("tok_punct", t.tok_punct),
            ("tok_flag", t.tok_flag),
            ("tok_error", t.tok_error),
            ("tok_function", t.tok_function),
        ]
    }

    #[test]
    fn all_text_colours_meet_wcag_aa_contrast() {
        for t in [Theme::dark(), Theme::light()] {
            let label = theme_label(&t);
            for (name, c) in text_foregrounds(&t) {
                let ratio = contrast_ratio(c, t.bg);
                assert!(
                    ratio >= 4.5,
                    "{label} theme: {name} contrast {ratio:.2}:1 on bg is below WCAG-AA 4.5:1",
                );
            }
        }
    }

    #[test]
    fn advanced_mode_border_meets_ui_contrast() {
        // The advanced-mode border carries a mode-warning signal, so it
        // meets WCAG's 3:1 non-text / UI-component threshold in both
        // themes. The plain `border` is decorative structural chrome and
        // is intentionally exempt (recorded in ADR-0057).
        for t in [Theme::dark(), Theme::light()] {
            let label = theme_label(&t);
            let ratio = contrast_ratio(t.border_advanced, t.bg);
            assert!(
                ratio >= 3.0,
                "{label} theme: border_advanced contrast {ratio:.2}:1 below the 3:1 UI bar",
            );
        }
    }

    // ---- NFR-5 perceptual distinctness (ADR-0057) ----
    //
    // Contrast-against-background is necessary but not sufficient: two
    // token colours can each clear 4.5:1 yet be hard to tell APART from
    // each other (a real pain reported on high-quality screens). We lock
    // that in with a CIEDE2000 (ΔE2000) floor between every pair of
    // syntax-token classes that can share a line. The metric itself is
    // validated against the Sharma et al. reference vectors.

    /// sRGB `Color` → CIELAB (D65), for perceptual-difference checks.
    #[allow(clippy::suboptimal_flops)]
    fn rgb_to_lab(c: Color) -> (f64, f64, f64) {
        let lin = |v: f64| {
            let v = v / 255.0;
            if v <= 0.03928 {
                v / 12.92
            } else {
                ((v + 0.055) / 1.055).powf(2.4)
            }
        };
        let (r0, g0, b0) = rgb_channels(c);
        let (r, g, b) = (lin(r0), lin(g0), lin(b0));
        let x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.950_47;
        let y = r * 0.2126 + g * 0.7152 + b * 0.0722;
        let z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.088_83;
        let f = |t: f64| {
            if t > 0.008_856 {
                t.cbrt()
            } else {
                7.787 * t + 16.0 / 116.0
            }
        };
        let (fx, fy, fz) = (f(x), f(y), f(z));
        (116.0 * fy - 16.0, 500.0 * (fx - fy), 200.0 * (fy - fz))
    }

    /// CIEDE2000 colour difference between two CIELAB values.
    #[allow(clippy::suboptimal_flops)]
    fn delta_e_2000(lab1: (f64, f64, f64), lab2: (f64, f64, f64)) -> f64 {
        let (l1, a1, b1) = lab1;
        let (l2, a2, b2) = lab2;
        let pow7 = |v: f64| v.powi(7);
        let k_l = pow7(25.0);

        let avg_lp = (l1 + l2) / 2.0;
        let c1 = a1.hypot(b1);
        let c2 = a2.hypot(b2);
        let avg_c = (c1 + c2) / 2.0;
        let g = if avg_c > 0.0 {
            0.5 * (1.0 - (pow7(avg_c) / (pow7(avg_c) + k_l)).sqrt())
        } else {
            0.0
        };
        let a1p = (1.0 + g) * a1;
        let a2p = (1.0 + g) * a2;
        let c1p = a1p.hypot(b1);
        let c2p = a2p.hypot(b2);
        let avg_cp = (c1p + c2p) / 2.0;

        let hp = |ap: f64, b: f64| -> f64 {
            if ap == 0.0 && b == 0.0 {
                return 0.0;
            }
            let ang = b.atan2(ap).to_degrees();
            if ang < 0.0 { ang + 360.0 } else { ang }
        };
        let h1p = hp(a1p, b1);
        let h2p = hp(a2p, b2);

        let dlp = l2 - l1;
        let dcp = c2p - c1p;
        let dhp = if c1p * c2p == 0.0 {
            0.0
        } else if (h2p - h1p).abs() <= 180.0 {
            h2p - h1p
        } else if h2p - h1p > 180.0 {
            h2p - h1p - 360.0
        } else {
            h2p - h1p + 360.0
        };
        let d_big_hp = 2.0 * (c1p * c2p).sqrt() * (dhp.to_radians() / 2.0).sin();

        let avg_hp = if c1p * c2p == 0.0 {
            h1p + h2p
        } else if (h1p - h2p).abs() <= 180.0 {
            (h1p + h2p) / 2.0
        } else if h1p + h2p < 360.0 {
            (h1p + h2p + 360.0) / 2.0
        } else {
            (h1p + h2p - 360.0) / 2.0
        };
        let t = 1.0 - 0.17 * (avg_hp - 30.0).to_radians().cos()
            + 0.24 * (2.0 * avg_hp).to_radians().cos()
            + 0.32 * (3.0 * avg_hp + 6.0).to_radians().cos()
            - 0.20 * (4.0 * avg_hp - 63.0).to_radians().cos();
        let d_ro = 30.0 * (-((avg_hp - 275.0) / 25.0).powi(2)).exp();
        let rc = if avg_cp > 0.0 {
            2.0 * (pow7(avg_cp) / (pow7(avg_cp) + k_l)).sqrt()
        } else {
            0.0
        };
        let sl = 1.0 + (0.015 * (avg_lp - 50.0).powi(2)) / (20.0 + (avg_lp - 50.0).powi(2)).sqrt();
        let sc = 1.0 + 0.045 * avg_cp;
        let sh = 1.0 + 0.015 * avg_cp * t;
        let rt = -(2.0 * d_ro.to_radians()).sin() * rc;

        ((dlp / sl).powi(2)
            + (dcp / sc).powi(2)
            + (d_big_hp / sh).powi(2)
            + rt * (dcp / sc) * (d_big_hp / sh))
            .sqrt()
    }

    #[test]
    fn delta_e_2000_matches_reference_vectors() {
        // Sharma, Wu & Dalal (2005) CIEDE2000 test data — validates the
        // metric so the distinctness gate below rests on a correct base.
        let cases = [
            ((50.0, 2.6772, -79.7751), (50.0, 0.0, -82.7485), 2.0425),
            ((50.0, 3.1571, -77.2803), (50.0, 0.0, -82.7485), 2.8615),
            ((50.0, 2.4900, -0.0010), (50.0, -2.4900, 0.0009), 7.1792),
            ((50.0, -1.0, 2.0), (50.0, 0.0, 0.0), 2.3669),
        ];
        for (l1, l2, exp) in cases {
            let got = delta_e_2000(l1, l2);
            assert!(
                (got - exp).abs() < 0.01,
                "ΔE2000 {got:.4} != reference {exp:.4}",
            );
        }
    }

    #[test]
    fn syntax_token_colours_are_perceptually_distinct() {
        // Every pair of syntax-token classes that can appear together on
        // one line must be perceptually separable, not merely unequal in
        // hex. Floor at ΔE2000 >= 15 (post-ADR-0057 minimum is ~18).
        // Excludes tok_punct (== muted) and tok_error (== error), which
        // deliberately alias other roles.
        const MIN_DE: f64 = 15.0;
        for t in [Theme::dark(), Theme::light()] {
            let label = theme_label(&t);
            let toks = [
                ("tok_keyword", t.tok_keyword),
                ("tok_identifier", t.tok_identifier),
                ("tok_type", t.tok_type),
                ("tok_number", t.tok_number),
                ("tok_string", t.tok_string),
                ("tok_flag", t.tok_flag),
                ("tok_function", t.tok_function),
            ];
            for (i, (name_a, ca)) in toks.iter().enumerate() {
                for (name_b, cb) in toks.iter().skip(i + 1) {
                    let de = delta_e_2000(rgb_to_lab(*ca), rgb_to_lab(*cb));
                    assert!(
                        de >= MIN_DE,
                        "{label} theme: {name_a} vs {name_b} ΔE2000 {de:.1} below {MIN_DE} — too similar",
                    );
                }
            }
        }
    }
}
