//! Linear RGBA [`Color`].
//!
//! Colors are stored as `f32` components in **linear** space (the space shaders
//! and lighting math expect). Helpers convert to/from 8-bit sRGB for I/O.

use glam::{Vec3, Vec4};
use serde::{Deserialize, Serialize};

/// An RGBA color with linear `f32` components, nominally in `[0, 1]` but not
/// clamped (values above 1.0 represent HDR / emissive intensity).
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Color {
    /// Red channel (linear).
    pub r: f32,
    /// Green channel (linear).
    pub g: f32,
    /// Blue channel (linear).
    pub b: f32,
    /// Alpha (opacity); `1.0` is fully opaque.
    pub a: f32,
}

impl Color {
    /// Opaque black.
    pub const BLACK: Self = Self::rgb(0.0, 0.0, 0.0);
    /// Opaque white.
    pub const WHITE: Self = Self::rgb(1.0, 1.0, 1.0);
    /// Opaque red.
    pub const RED: Self = Self::rgb(1.0, 0.0, 0.0);
    /// Opaque green.
    pub const GREEN: Self = Self::rgb(0.0, 1.0, 0.0);
    /// Opaque blue.
    pub const BLUE: Self = Self::rgb(0.0, 0.0, 1.0);
    /// Fully transparent (all channels zero).
    pub const TRANSPARENT: Self = Self::rgba(0.0, 0.0, 0.0, 0.0);

    /// Creates a color from linear RGBA components.
    #[inline]
    pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
        Self { r, g, b, a }
    }

    /// Creates an opaque color from linear RGB components.
    #[inline]
    pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
        Self { r, g, b, a: 1.0 }
    }

    /// Creates a linear color from 8-bit **sRGB** components (the usual format
    /// of color pickers and image files), with full opacity.
    #[inline]
    pub fn from_srgb_u8(r: u8, g: u8, b: u8) -> Self {
        Self::rgb(
            srgb_to_linear(r as f32 / 255.0),
            srgb_to_linear(g as f32 / 255.0),
            srgb_to_linear(b as f32 / 255.0),
        )
    }

    /// Creates a linear color from a packed `0xRRGGBB` hex value.
    #[inline]
    pub fn from_hex(hex: u32) -> Self {
        Self::from_srgb_u8(
            ((hex >> 16) & 0xFF) as u8,
            ((hex >> 8) & 0xFF) as u8,
            (hex & 0xFF) as u8,
        )
    }

    /// Converts to 8-bit sRGB `(r, g, b, a)`, clamping to `[0, 1]` first.
    #[inline]
    pub fn to_srgb_u8(&self) -> [u8; 4] {
        [
            (linear_to_srgb(self.r.clamp(0.0, 1.0)) * 255.0).round() as u8,
            (linear_to_srgb(self.g.clamp(0.0, 1.0)) * 255.0).round() as u8,
            (linear_to_srgb(self.b.clamp(0.0, 1.0)) * 255.0).round() as u8,
            (self.a.clamp(0.0, 1.0) * 255.0).round() as u8,
        ]
    }

    /// Returns the color as a `Vec4` (`[r, g, b, a]`).
    #[inline]
    pub fn to_vec4(&self) -> Vec4 {
        Vec4::new(self.r, self.g, self.b, self.a)
    }

    /// Returns the RGB channels as a `Vec3`.
    #[inline]
    pub fn to_vec3(&self) -> Vec3 {
        Vec3::new(self.r, self.g, self.b)
    }

    /// Returns a copy with the alpha replaced.
    #[inline]
    pub fn with_alpha(&self, a: f32) -> Self {
        Self { a, ..*self }
    }

    /// Linearly interpolates between two colors. `t` is clamped to `[0, 1]`.
    #[inline]
    pub fn lerp(&self, other: Color, t: f32) -> Color {
        let t = t.clamp(0.0, 1.0);
        Color {
            r: self.r + (other.r - self.r) * t,
            g: self.g + (other.g - self.g) * t,
            b: self.b + (other.b - self.b) * t,
            a: self.a + (other.a - self.a) * t,
        }
    }
}

/// Converts a single sRGB channel value in `[0, 1]` to linear space.
#[inline]
fn srgb_to_linear(c: f32) -> f32 {
    if c <= 0.04045 {
        c / 12.92
    } else {
        ((c + 0.055) / 1.055).powf(2.4)
    }
}

/// Converts a single linear channel value in `[0, 1]` to sRGB space.
#[inline]
fn linear_to_srgb(c: f32) -> f32 {
    if c <= 0.003_130_8 {
        c * 12.92
    } else {
        1.055 * c.powf(1.0 / 2.4) - 0.055
    }
}

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

    const EPS: f32 = 1e-4;

    #[test]
    fn constants() {
        assert_eq!(Color::WHITE, Color::rgb(1.0, 1.0, 1.0));
        assert_eq!(Color::TRANSPARENT.a, 0.0);
    }

    #[test]
    fn srgb_round_trip() {
        let original = [10u8, 128, 240];
        let c = Color::from_srgb_u8(original[0], original[1], original[2]);
        let back = c.to_srgb_u8();
        assert_eq!([back[0], back[1], back[2]], original);
        assert_eq!(back[3], 255);
    }

    #[test]
    fn srgb_endpoints_are_exact() {
        assert!(srgb_to_linear(0.0).abs() <= EPS);
        assert!((srgb_to_linear(1.0) - 1.0).abs() <= EPS);
        assert!((linear_to_srgb(1.0) - 1.0).abs() <= EPS);
    }

    #[test]
    fn hex_parsing() {
        let c = Color::from_hex(0xFF0000);
        assert_eq!(c.to_srgb_u8()[0], 255);
        assert_eq!(c.to_srgb_u8()[1], 0);
        assert_eq!(c.to_srgb_u8()[2], 0);
    }

    #[test]
    fn lerp_endpoints_and_midpoint() {
        let a = Color::rgba(0.0, 0.0, 0.0, 0.0);
        let b = Color::rgba(1.0, 1.0, 1.0, 1.0);
        assert_eq!(a.lerp(b, 0.0), a);
        assert_eq!(a.lerp(b, 1.0), b);
        assert_eq!(a.lerp(b, 0.5), Color::rgba(0.5, 0.5, 0.5, 0.5));
        // Clamps out-of-range t.
        assert_eq!(a.lerp(b, 2.0), b);
    }

    #[test]
    fn vec_conversions_and_alpha() {
        let c = Color::rgba(0.1, 0.2, 0.3, 0.4);
        assert_eq!(c.to_vec4(), Vec4::new(0.1, 0.2, 0.3, 0.4));
        assert_eq!(c.to_vec3(), Vec3::new(0.1, 0.2, 0.3));
        assert_eq!(c.with_alpha(1.0).a, 1.0);
    }

    #[test]
    fn hdr_values_not_clamped_in_storage() {
        let c = Color::rgb(4.0, 0.0, 0.0);
        assert_eq!(c.r, 4.0);
        // But output is clamped.
        assert_eq!(c.to_srgb_u8()[0], 255);
    }
}
