//! Audio-driven colour. Perceptual OKLCH so hue sweeps stay even in
//! brightness/chroma; converted to gamma sRGB for nannou's `srgba`.
//!
//! centroid -> base hue (brightness of the mix picks the colour)
//! dominant chroma pitch class -> accent hue rotation (harmony tints the edges)
//! loudness -> lightness · broadband flux -> a chroma/lightness flash

use crate::audio::Bands;

fn srgb_encode(c: f32) -> f32 {
    let c = c.clamp(0.0, 1.0);
    if c <= 0.0031308 {
        12.92 * c
    } else {
        1.055 * c.powf(1.0 / 2.4) - 0.055
    }
}

/// OKLCH (L 0..1, C ~0..0.4, H radians) -> gamma sRGB `[r,g,b]`.
pub fn oklch(l: f32, c: f32, h: f32) -> [f32; 3] {
    let a = c * h.cos();
    let b = c * h.sin();
    let l_ = l + 0.3963377774 * a + 0.2158037573 * b;
    let m_ = l - 0.1055613458 * a - 0.0638541728 * b;
    let s_ = l - 0.0894841775 * a - 1.2914855480 * b;
    let (l3, m3, s3) = (l_ * l_ * l_, m_ * m_ * m_, s_ * s_ * s_);
    let r = 4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3;
    let g = -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3;
    let bl = -0.0041960863 * l3 - 0.7034186147 * m3 + 1.7076147010 * s3;
    [srgb_encode(r), srgb_encode(g), srgb_encode(bl)]
}

/// A momentary colour state derived from one analysis frame.
#[derive(Clone, Copy)]
pub struct Palette {
    base_h: f32,   // radians
    accent_h: f32, // radians
    light: f32,
    chroma: f32,
    pub flash: f32, // 0..1 broadband onset, used by post too
}

const TAU: f32 = std::f32::consts::TAU;

impl Palette {
    pub fn from_audio(b: &Bands) -> Self {
        // Centroid sweeps a deep-violet -> cyan -> warm-gold arc (~250°..30°).
        let base_h = (4.4 - b.centroid * 3.1) % TAU;
        // Dominant pitch class rotates an accent around the wheel.
        let (mut dom, mut dv) = (0usize, 0.0f32);
        for (i, &c) in b.chroma.iter().enumerate() {
            if c > dv {
                dv = c;
                dom = i;
            }
        }
        let accent_h = base_h + 1.7 + dom as f32 / 12.0 * TAU * 0.5;
        let light = 0.42 + b.loud * 0.34;
        let chroma = 0.10 + (b.loud * 0.5 + b.mid * 0.4).min(1.0) * 0.13;
        Palette {
            base_h,
            accent_h,
            light,
            chroma,
            flash: b.flux,
        }
    }

    /// Colour along a strand. `t` 0..1 root->tip blends base->accent hue and
    /// fades lightness toward the tip; `vigor` 0..1 scales presence.
    /// Returns gamma-sRGB `[r,g,b,a]`.
    pub fn stroke(&self, t: f32, vigor: f32, hue_off: f32) -> [f32; 4] {
        let h = self.base_h + (self.accent_h - self.base_h) * t + hue_off;
        let l = (self.light + 0.18 * (1.0 - t) + self.flash * 0.25).min(0.97);
        let c = self.chroma * (0.55 + 0.45 * vigor) + self.flash * 0.04;
        let [r, g, bl] = oklch(l, c, h);
        let a = (0.30 + 0.70 * vigor) * (0.55 + 0.45 * (1.0 - t * 0.6));
        [r, g, bl, a.clamp(0.0, 1.0)]
    }

    /// Bright structural colour for the cyber skeleton (less hue travel,
    /// higher lightness so the bones stay legible under the overgrowth).
    pub fn bone(&self, t: f32) -> [f32; 4] {
        let h = self.base_h + 0.25 * t;
        let l = (self.light + 0.30 + self.flash * 0.2).min(0.99);
        let [r, g, bl] = oklch(l, self.chroma * 0.7, h);
        [r, g, bl, 0.92]
    }

    /// Dim background field tint (very low lightness, base hue).
    pub fn bg(&self) -> [f32; 3] {
        let [r, g, bl] = oklch(0.06 + self.flash * 0.02, 0.03, self.base_h);
        [r, g, bl]
    }
}
