use palette::convert::FromColorUnclamped;
use palette::{LinSrgb, Oklch, Srgb};
use serde::{Deserialize, Serialize};

#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum ColorError {
    #[error("color channel is not finite")]
    ChannelNotFinite,
    #[error("color channel outside [0, 1]")]
    ChannelOutOfRange,
    #[error("color violates premultiplied invariant: rgb must be <= a")]
    NotPremultiplied,
}

#[derive(Copy, Clone, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "ColorWire", into = "ColorWire")]
pub struct Color([f32; 4]);

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename = "Color", deny_unknown_fields)]
struct ColorWire {
    r: f32,
    g: f32,
    b: f32,
    a: f32,
}

impl From<Color> for ColorWire {
    fn from(c: Color) -> Self {
        Self {
            r: c.0[0],
            g: c.0[1],
            b: c.0[2],
            a: c.0[3],
        }
    }
}

const PREMUL_TOLERANCE: f32 = 1.0e-5;

fn validate_premul(rgba: [f32; 4]) -> Result<(), ColorError> {
    if !rgba.iter().all(|c| c.is_finite()) {
        return Err(ColorError::ChannelNotFinite);
    }
    if !rgba.iter().all(|c| (0.0..=1.0).contains(c)) {
        return Err(ColorError::ChannelOutOfRange);
    }
    let alpha = rgba[3];
    if rgba[..3].iter().any(|c| *c > alpha + PREMUL_TOLERANCE) {
        return Err(ColorError::NotPremultiplied);
    }
    Ok(())
}

impl TryFrom<ColorWire> for Color {
    type Error = ColorError;

    fn try_from(w: ColorWire) -> Result<Self, ColorError> {
        let rgba = [w.r, w.g, w.b, w.a];
        validate_premul(rgba)?;
        Ok(Self(rgba))
    }
}

impl Color {
    pub const TRANSPARENT: Self = Self([0.0, 0.0, 0.0, 0.0]);

    #[must_use]
    pub(in crate::theme) fn from_linear_rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
        Self([r * a, g * a, b * a, a])
    }

    #[must_use]
    pub(in crate::theme) fn from_srgb_u8(r: u8, g: u8, b: u8) -> Self {
        #[allow(
            clippy::disallowed_methods,
            reason = "the theme color constructor is the chosen entry point for raw palette construction"
        )]
        let lin: LinSrgb<f32> = Srgb::new(
            f32::from(r) / 255.0,
            f32::from(g) / 255.0,
            f32::from(b) / 255.0,
        )
        .into_linear();
        Self::from_linear_rgba(lin.red, lin.green, lin.blue, 1.0)
    }

    #[must_use]
    pub(in crate::theme) fn from_oklch(l: f32, c: f32, h_degrees: f32) -> Self {
        let rgb = oklch_to_in_gamut_linsrgb(l, c, h_degrees);
        Self::from_linear_rgba(rgb[0], rgb[1], rgb[2], 1.0)
    }

    #[must_use]
    pub fn linear_rgba_premul(self) -> [f32; 4] {
        self.0
    }

    #[must_use]
    pub fn alpha(self) -> f32 {
        self.0[3]
    }

    #[must_use]
    pub fn with_alpha(self, alpha: f32) -> Self {
        let alpha = alpha.clamp(0.0, 1.0);
        let current = self.0[3];
        if current <= 0.0 {
            return Self([0.0, 0.0, 0.0, alpha]);
        }
        let scale = alpha / current;
        Self([
            (self.0[0] * scale).min(alpha),
            (self.0[1] * scale).min(alpha),
            (self.0[2] * scale).min(alpha),
            alpha,
        ])
    }

    #[must_use]
    pub fn blend(self, other: Self, t: f32) -> Self {
        let t = t.clamp(0.0, 1.0);
        let s = 1.0 - t;
        Self([
            self.0[0] * s + other.0[0] * t,
            self.0[1] * s + other.0[1] * t,
            self.0[2] * s + other.0[2] * t,
            self.0[3] * s + other.0[3] * t,
        ])
    }

    #[must_use]
    pub fn relative_luminance(self) -> f32 {
        let alpha = self.0[3];
        if alpha <= 0.0 {
            return 0.0;
        }
        let inv = 1.0 / alpha;
        let r = self.0[0] * inv;
        let g = self.0[1] * inv;
        let b = self.0[2] * inv;
        0.2126 * r + 0.7152 * g + 0.0722 * b
    }
}

const CONTRAST_LUMINANCE_THRESHOLD: f32 = 0.179;

impl core::fmt::Debug for Color {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(
            f,
            "Color(r={}, g={}, b={}, a={})",
            self.0[0], self.0[1], self.0[2], self.0[3]
        )
    }
}

impl core::fmt::Display for Color {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(
            f,
            "rgba_lin_premul({}, {}, {}, {})",
            self.0[0], self.0[1], self.0[2], self.0[3]
        )
    }
}

const SRGB_GAMUT_EPS: f32 = 1.0e-4;
const GAMUT_BISECT_STEPS: u32 = 24;

fn oklch_to_linsrgb_raw(l: f32, c: f32, h_degrees: f32) -> [f32; 3] {
    #[allow(
        clippy::disallowed_methods,
        reason = "the theme color constructor is the chosen entry point for raw palette construction"
    )]
    let lin = LinSrgb::<f32>::from_color_unclamped(Oklch::<f32>::new(l, c, h_degrees));
    [lin.red, lin.green, lin.blue]
}

fn channel_in_gamut(channel: f32) -> bool {
    (-SRGB_GAMUT_EPS..=1.0 + SRGB_GAMUT_EPS).contains(&channel)
}

fn in_gamut(rgb: [f32; 3]) -> bool {
    rgb.iter().copied().all(channel_in_gamut)
}

fn oklch_to_in_gamut_linsrgb(l: f32, c: f32, h_degrees: f32) -> [f32; 3] {
    let direct = oklch_to_linsrgb_raw(l, c, h_degrees);
    if in_gamut(direct) {
        return clamp_unit_rgb(direct);
    }
    let (chroma_in, _) = (0..GAMUT_BISECT_STEPS).fold((0.0_f32, c), |(lo, hi), _| {
        let mid = 0.5 * (lo + hi);
        if in_gamut(oklch_to_linsrgb_raw(l, mid, h_degrees)) {
            (mid, hi)
        } else {
            (lo, mid)
        }
    });
    clamp_unit_rgb(oklch_to_linsrgb_raw(l, chroma_in, h_degrees))
}

fn clamp_unit_rgb(rgb: [f32; 3]) -> [f32; 3] {
    rgb.map(|c| c.clamp(0.0, 1.0))
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Step12(u8);

impl Step12 {
    pub const APP_BG: Self = Self(1);
    pub const SUBTLE_BG: Self = Self(2);
    pub const ELEMENT_BG: Self = Self(3);
    pub const HOVER_BG: Self = Self(4);
    pub const SELECTED_BG: Self = Self(5);
    pub const SUBTLE_BORDER: Self = Self(6);
    pub const BORDER: Self = Self(7);
    pub const HOVER_BORDER: Self = Self(8);
    pub const SOLID: Self = Self(9);
    pub const HOVER_SOLID: Self = Self(10);
    pub const TEXT_MUTED: Self = Self(11);
    pub const TEXT_HIGH: Self = Self(12);

    #[must_use]
    pub const fn try_new(step: u8) -> Option<Self> {
        if matches!(step, 1..=12) {
            Some(Self(step))
        } else {
            None
        }
    }

    const fn index(self) -> usize {
        (self.0 - 1) as usize
    }
}

impl core::fmt::Display for Step12 {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "step{}", self.0)
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Scale12([Color; 12]);

impl Scale12 {
    #[must_use]
    pub fn step(self, step: Step12) -> Color {
        self.0[step.index()]
    }

    #[must_use]
    pub fn steps(self) -> [Color; 12] {
        self.0
    }

    pub(in crate::theme) fn from_oklch_curve(
        hue: f32,
        chromas: &[f32; 12],
        lightness: &[f32; 12],
    ) -> Self {
        let steps = core::array::from_fn::<Color, 12, _>(|i| {
            Color::from_oklch(lightness[i], chromas[i], hue)
        });
        Self(steps)
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum SurfaceLevel {
    L0,
    L1,
    L2,
    L3,
}

impl SurfaceLevel {
    #[must_use]
    pub const fn step12(self) -> Step12 {
        match self {
            Self::L0 => Step12::APP_BG,
            Self::L1 => Step12::SUBTLE_BG,
            Self::L2 => Step12::ELEMENT_BG,
            Self::L3 => Step12::HOVER_BG,
        }
    }
}

impl core::fmt::Display for SurfaceLevel {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            Self::L0 => f.write_str("L0"),
            Self::L1 => f.write_str("L1"),
            Self::L2 => f.write_str("L2"),
            Self::L3 => f.write_str("L3"),
        }
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Colors {
    pub neutral: Scale12,
    pub accent: Scale12,
    pub info: Scale12,
    pub success: Scale12,
    pub warning: Scale12,
    pub danger: Scale12,
}

impl Colors {
    #[must_use]
    pub fn surface(&self, level: SurfaceLevel) -> Color {
        self.neutral.step(level.step12())
    }

    #[must_use]
    pub fn text_primary(&self) -> Color {
        self.neutral.step(Step12::TEXT_HIGH)
    }

    #[must_use]
    pub fn text_secondary(&self) -> Color {
        self.neutral.step(Step12::TEXT_MUTED)
    }

    #[must_use]
    pub fn text_disabled(&self) -> Color {
        self.neutral.step(Step12::TEXT_MUTED)
    }

    #[must_use]
    pub fn accent_solid(&self) -> Color {
        self.accent.step(Step12::SOLID)
    }

    #[must_use]
    pub fn info_solid(&self) -> Color {
        self.info.step(Step12::SOLID)
    }

    #[must_use]
    pub fn success_solid(&self) -> Color {
        self.success.step(Step12::SOLID)
    }

    #[must_use]
    pub fn warning_solid(&self) -> Color {
        self.warning.step(Step12::SOLID)
    }

    #[must_use]
    pub fn danger_solid(&self) -> Color {
        self.danger.step(Step12::SOLID)
    }

    #[must_use]
    pub fn focus_ring(&self) -> Color {
        self.accent_solid()
    }

    #[must_use]
    pub fn contrast_text(&self, surface: Color) -> Color {
        if surface.relative_luminance() > CONTRAST_LUMINANCE_THRESHOLD {
            Color::from_linear_rgba(0.04, 0.04, 0.04, 1.0)
        } else {
            Color::from_linear_rgba(1.0, 1.0, 1.0, 1.0)
        }
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CadColors {
    pub sketch_under_defined: Color,
    pub sketch_fully_defined: Color,
    pub sketch_over_defined: Color,
    pub sketch_invalid: Color,
    pub sketch_dangling: Color,
    pub sketch_driven: Color,
    pub selection_primary: Color,
    pub selection_hover: Color,
    pub property_pane_active_box: Color,
    pub relation_glyph: Color,
    pub reference_geometry: Color,
}

impl CadColors {
    pub(in crate::theme) fn classic() -> Self {
        Self {
            sketch_under_defined: Color::from_srgb_u8(0x1F, 0x4F, 0xCC),
            sketch_fully_defined: Color::from_srgb_u8(0x10, 0x10, 0x10),
            sketch_over_defined: Color::from_srgb_u8(0xC8, 0x00, 0x1E),
            sketch_invalid: Color::from_srgb_u8(0x7A, 0x00, 0x14),
            sketch_dangling: Color::from_srgb_u8(0x80, 0x80, 0x00),
            sketch_driven: Color::from_srgb_u8(0x7E, 0x7E, 0x7E),
            selection_primary: Color::from_srgb_u8(0x2C, 0xCD, 0x33),
            selection_hover: Color::from_srgb_u8(0xFF, 0x8C, 0x00),
            property_pane_active_box: Color::from_srgb_u8(0xFF, 0xE4, 0xD6),
            relation_glyph: Color::from_srgb_u8(0x00, 0xAA, 0x30),
            reference_geometry: Color::from_srgb_u8(0x9B, 0x9B, 0x9B),
        }
    }
}

const NEUTRAL_HUE: f32 = 250.0;
const ACCENT_HUE: f32 = 230.0;
const INFO_HUE: f32 = 215.0;
const SUCCESS_HUE: f32 = 145.0;
const WARNING_HUE: f32 = 70.0;
const DANGER_HUE: f32 = 25.0;

const NEUTRAL_CHROMAS: [f32; 12] = [
    0.001, 0.001, 0.001, 0.001, 0.0015, 0.0015, 0.002, 0.0025, 0.003, 0.003, 0.003, 0.002,
];
const ACCENT_CHROMAS: [f32; 12] = [
    0.012, 0.020, 0.035, 0.055, 0.075, 0.095, 0.115, 0.135, 0.170, 0.170, 0.140, 0.080,
];
const SEMANTIC_CHROMAS: [f32; 12] = [
    0.012, 0.022, 0.040, 0.060, 0.080, 0.100, 0.120, 0.140, 0.180, 0.180, 0.150, 0.090,
];

const RADIX_LIGHT_L: [f32; 12] = [
    0.99, 0.98, 0.96, 0.94, 0.91, 0.88, 0.83, 0.76, 0.65, 0.60, 0.50, 0.30,
];
const RADIX_DARK_L: [f32; 12] = [
    0.18, 0.21, 0.25, 0.29, 0.33, 0.39, 0.46, 0.55, 0.65, 0.70, 0.78, 0.95,
];

pub(in crate::theme) fn light_colors() -> Colors {
    build_colors(&RADIX_LIGHT_L)
}

pub(in crate::theme) fn dark_colors() -> Colors {
    build_colors(&RADIX_DARK_L)
}

fn build_colors(lightness: &[f32; 12]) -> Colors {
    Colors {
        neutral: Scale12::from_oklch_curve(NEUTRAL_HUE, &NEUTRAL_CHROMAS, lightness),
        accent: Scale12::from_oklch_curve(ACCENT_HUE, &ACCENT_CHROMAS, lightness),
        info: Scale12::from_oklch_curve(INFO_HUE, &SEMANTIC_CHROMAS, lightness),
        success: Scale12::from_oklch_curve(SUCCESS_HUE, &SEMANTIC_CHROMAS, lightness),
        warning: Scale12::from_oklch_curve(WARNING_HUE, &SEMANTIC_CHROMAS, lightness),
        danger: Scale12::from_oklch_curve(DANGER_HUE, &SEMANTIC_CHROMAS, lightness),
    }
}

#[cfg(test)]
mod tests {
    use super::{
        CadColors, Color, ColorWire, PREMUL_TOLERANCE, Scale12, Step12, SurfaceLevel, dark_colors,
        in_gamut, light_colors,
    };

    #[test]
    fn blend_midpoint_is_average() {
        let a = Color::from_linear_rgba(0.0, 0.0, 0.0, 1.0);
        let b = Color::from_linear_rgba(1.0, 1.0, 1.0, 1.0);
        let mid = a.blend(b, 0.5);
        assert!((mid.linear_rgba_premul()[0] - 0.5).abs() < 1e-6);
        assert!((mid.linear_rgba_premul()[1] - 0.5).abs() < 1e-6);
        assert!((mid.linear_rgba_premul()[2] - 0.5).abs() < 1e-6);
    }

    #[test]
    fn contrast_text_picks_dark_text_over_light_surface() {
        let bg = Color::from_srgb_u8(0xFF, 0xFF, 0xFF);
        let text = light_colors().contrast_text(bg);
        assert!(text.relative_luminance() < 0.1);
    }

    #[test]
    fn contrast_text_picks_light_text_over_dark_surface() {
        let bg = Color::from_srgb_u8(0x10, 0x10, 0x10);
        let text = dark_colors().contrast_text(bg);
        assert!(text.relative_luminance() > 0.9);
    }

    #[test]
    fn with_alpha_preserves_chromaticity() {
        let opaque = Color::from_srgb_u8(0x80, 0x40, 0x20);
        let half = opaque.with_alpha(0.5);
        assert!((half.alpha() - 0.5).abs() < 1e-6);
        let opaque_lin = opaque.linear_rgba_premul();
        let half_lin = half.linear_rgba_premul();
        assert!((half_lin[0] / 0.5 - opaque_lin[0]).abs() < 1e-6);
        assert!((half_lin[1] / 0.5 - opaque_lin[1]).abs() < 1e-6);
        assert!((half_lin[2] / 0.5 - opaque_lin[2]).abs() < 1e-6);
    }

    #[test]
    fn step12_try_new_rejects_zero_and_above_twelve() {
        assert!(Step12::try_new(0).is_none());
        assert!(Step12::try_new(13).is_none());
        assert!(Step12::try_new(255).is_none());
        assert_eq!(Step12::try_new(1), Some(Step12::APP_BG));
        assert_eq!(Step12::try_new(12), Some(Step12::TEXT_HIGH));
    }

    #[test]
    fn scale12_step_indexes_one_based() {
        let scale = light_colors().neutral;
        let app_bg = scale.step(Step12::APP_BG);
        let text_high = scale.step(Step12::TEXT_HIGH);
        assert!(app_bg.relative_luminance() > text_high.relative_luminance());
    }

    #[test]
    fn surface_levels_map_to_app_subtle_element_hover() {
        let colors = light_colors();
        let pairs = [
            (SurfaceLevel::L0, Step12::APP_BG),
            (SurfaceLevel::L1, Step12::SUBTLE_BG),
            (SurfaceLevel::L2, Step12::ELEMENT_BG),
            (SurfaceLevel::L3, Step12::HOVER_BG),
        ];
        pairs.iter().fold((), |(), &(level, step)| {
            assert_eq!(colors.surface(level), colors.neutral.step(step));
        });
    }

    #[test]
    fn dark_inverts_neutral_lightness_across_curve() {
        let light = light_colors().neutral.steps();
        let dark = dark_colors().neutral.steps();
        let light_lum: [f32; 12] = light.map(Color::relative_luminance);
        let dark_lum: [f32; 12] = dark.map(Color::relative_luminance);

        light_lum.windows(2).fold((), |(), w| {
            assert!(
                w[0] >= w[1] - 1e-4,
                "light neutral luminance must be non-increasing at every step"
            );
        });
        dark_lum.windows(2).fold((), |(), w| {
            assert!(
                w[0] <= w[1] + 1e-4,
                "dark neutral luminance must be non-decreasing at every step"
            );
        });

        assert!(light_lum[0] > dark_lum[0]);
        assert!(dark_lum[11] > light_lum[11]);
    }

    #[test]
    fn focus_ring_tracks_accent_solid() {
        let colors = light_colors();
        assert_eq!(colors.focus_ring(), colors.accent_solid());
        let [r, g, b, a] = colors.focus_ring().linear_rgba_premul();
        assert!((0.0..=1.0).contains(&r));
        assert!((0.0..=1.0).contains(&g));
        assert!((0.0..=1.0).contains(&b));
        assert!((a - 1.0).abs() < 1e-6);
    }

    #[test]
    fn every_ramp_step_is_in_gamut() {
        let check = |scale: Scale12| {
            scale.steps().iter().fold((), |(), color| {
                let rgba = color.linear_rgba_premul();
                assert!(
                    in_gamut([rgba[0], rgba[1], rgba[2]]),
                    "ramp step out of sRGB gamut: {color:?}"
                );
            });
        };
        let palettes = [light_colors(), dark_colors()];
        palettes.iter().fold((), |(), colors| {
            check(colors.neutral);
            check(colors.accent);
            check(colors.info);
            check(colors.success);
            check(colors.warning);
            check(colors.danger);
        });
    }

    #[test]
    fn cad_colors_are_all_opaque_non_transparent() {
        let cad = CadColors::classic();
        let fields = [
            cad.sketch_under_defined,
            cad.sketch_fully_defined,
            cad.sketch_over_defined,
            cad.sketch_dangling,
            cad.sketch_driven,
            cad.selection_primary,
            cad.selection_hover,
            cad.property_pane_active_box,
            cad.relation_glyph,
            cad.reference_geometry,
        ];
        fields.iter().fold((), |(), color| {
            assert!(
                (color.alpha() - 1.0).abs() < 1e-6,
                "CadColors literal must be fully opaque"
            );
            assert!(
                *color != Color::TRANSPARENT,
                "CadColors literal must not be TRANSPARENT, the hex parse silently failed"
            );
        });
    }

    #[test]
    fn scale12_curve_produces_distinct_steps() {
        let scale = Scale12::from_oklch_curve(
            230.0,
            &[0.05; 12],
            &[
                0.10, 0.18, 0.26, 0.34, 0.42, 0.50, 0.58, 0.66, 0.74, 0.82, 0.90, 0.98,
            ],
        );
        let steps = scale.steps();
        let lums: Vec<f32> = steps.iter().map(|c| c.relative_luminance()).collect();
        let monotone = lums.windows(2).all(|w| w[0] <= w[1]);
        assert!(
            monotone,
            "lightness curve should produce non-decreasing luminance"
        );
    }

    #[test]
    fn deserialize_rejects_non_finite_channel() {
        let bad = "Color(r:NaN, g:0, b:0, a:1)";
        let result: Result<Color, _> = ron::de::from_str(bad);
        assert!(result.is_err(), "NaN must be rejected on deserialize");
    }

    #[test]
    fn deserialize_rejects_non_premultiplied() {
        let bad = "Color(r:1.0, g:0.0, b:0.0, a:0.5)";
        let result: Result<Color, _> = ron::de::from_str(bad);
        assert!(
            result.is_err(),
            "non-premultiplied (rgb > a) must be rejected on deserialize"
        );
    }

    #[test]
    fn deserialize_rejects_out_of_range() {
        let bad = "Color(r:0.0, g:0.0, b:0.0, a:1.5)";
        let result: Result<Color, _> = ron::de::from_str(bad);
        assert!(result.is_err(), "alpha > 1 must be rejected on deserialize");
    }

    #[test]
    fn deserialize_accepts_premul_at_boundary() {
        let good_inside = ColorWire {
            r: 0.5,
            g: 0.25,
            b: 0.0,
            a: 0.5,
        };
        let result: Result<Color, _> = good_inside.try_into();
        assert!(result.is_ok());

        let good_at_boundary = ColorWire {
            r: 0.5 + PREMUL_TOLERANCE * 0.5,
            g: 0.0,
            b: 0.0,
            a: 0.5,
        };
        let result: Result<Color, _> = good_at_boundary.try_into();
        assert!(result.is_ok());
    }

    #[test]
    fn with_alpha_amplification_clamps_to_premul() {
        let wire = ColorWire {
            r: 0.001 + PREMUL_TOLERANCE * 0.5,
            g: 0.0,
            b: 0.0,
            a: 0.001,
        };
        let Ok(edge): Result<Color, _> = wire.try_into() else {
            panic!("tolerance-edge color must deserialize");
        };
        let amped = edge.with_alpha(1.0);
        let [r, g, b, a] = amped.linear_rgba_premul();
        assert!((a - 1.0).abs() < 1e-6);
        assert!(r <= a, "red {r} must stay <= alpha {a} after amplification");
        assert!(g <= a);
        assert!(b <= a);
        assert!(r <= 1.0);
    }
}
