//! Oscilloscope *art* — vector-display structures in the spirit of
//! oscilloscope-music visuals (Jerobeam Fenderson / sakr / OsciStudio).
//!
//! A phosphor beam traces a deterministic 3D wireframe **figure** — a torus
//! knot, a Gielis supershape, a 3D Lissajous, a harmonograph, a rose-helix —
//! whose parameters are seeded so every track/seed yields a distinct object.
//! The figure is not chaotic frame-to-frame: it holds, and *morphs* into a
//! freshly-seeded figure on a strong broadband transient (cooldown-gated, like
//! the sigil's restructure), the two point-sets lerped so the change reads as
//! the music turning a corner rather than a glitch.
//!
//! Audio drives it continuously: rotation from mid/low, a breathing scale from
//! low/loud, slow figure-parameter drift from spectral brightness, and a gentle
//! beam-noise wobble from the live waveform + flux — so it captures what is
//! playing *now* while staying a coherent shape.
//!
//! Rendering is vector-display: a faint continuous beam, brightened where the
//! beam moves slowly (the real-scope intensity trick), dithered into dots, over
//! a faint CRT grain, near-monochrome (the palette desaturated so the hue still
//! drifts with timbre). The post stack's feedback gives the phosphor decay.
//!
//! Determinism: `Rng` is only advanced in `update` (figure selection); the
//! dither/grain are pure hashes of (index, frame). `update` runs once per
//! frame, so live and `--render` stay bit-identical per seed + timeline.

use crate::audio::{Bands, WAVE_N};
use crate::viz::curve::{Rng, flow};
use crate::viz::palette::Palette;
use nannou::prelude::*;

const FIELD: f32 = 960.0; // design-space extent (matches sigil/post)
const N: usize = 1600; // beam samples per figure
const KINDS: u32 = 5;
const PARAMS: usize = 7;
const MORPH_SECS: f32 = 0.85; // figure cross-fade time

/// Stateless hash -> 0..1 (ordered dither + grain; deterministic per frame).
fn h01(a: u32, b: u32) -> f32 {
    let mut x = a.wrapping_mul(0x9E37_79B1) ^ b.wrapping_mul(0x85EB_CA77) ^ 0xC2B2_AE3D;
    x ^= x >> 15;
    x = x.wrapping_mul(0x2545_F491);
    x ^= x >> 13;
    (x >> 9) as f32 / (1u32 << 23) as f32
}

fn smoothstep(t: f32) -> f32 {
    let t = t.clamp(0.0, 1.0);
    t * t * (3.0 - 2.0 * t)
}

/// One figure: a kind tag + its numeric parameters. Sampled into a Vec3 path.
#[derive(Clone, Copy)]
struct Figure {
    kind: u32,
    p: [f32; PARAMS],
}

impl Figure {
    /// Seed a fresh figure. Ratios/petals are small integers so the curves
    /// close cleanly (the oscilloscope-art look); free exponents add variety.
    fn random(rng: &mut Rng) -> Self {
        let kind = (rng.idx(KINDS as usize)) as u32;
        let mut p = [0.0f32; PARAMS];
        match kind {
            // torus knot (p,q): coprime-ish small ints, tube radius
            0 => {
                p[0] = (2 + rng.idx(6)) as f32;
                p[1] = (1 + rng.idx(7)) as f32;
                p[2] = rng.range(0.25, 0.6);
                p[3] = rng.range(2.0, 4.0); // winds (path loops)
            }
            // 3D supershape (Gielis): two superformulas, spherical product
            1 => {
                p[0] = (rng.idx(12) as f32) + 1.0; // m
                p[1] = rng.range(0.3, 3.0); // n1
                p[2] = rng.range(0.3, 4.0); // n2
                p[3] = rng.range(0.3, 4.0); // n3
                p[4] = (1 + rng.idx(8)) as f32; // surface-spiral turns
            }
            // 3D Lissajous: integer freqs + phase offsets
            2 => {
                p[0] = (1 + rng.idx(7)) as f32;
                p[1] = (1 + rng.idx(7)) as f32;
                p[2] = (1 + rng.idx(7)) as f32;
                p[3] = rng.range(0.0, std::f32::consts::TAU);
                p[4] = rng.range(0.0, std::f32::consts::TAU);
            }
            // harmonograph: damped sum of sinusoids
            3 => {
                for s in p.iter_mut().take(4) {
                    *s = (1 + rng.idx(5)) as f32;
                }
                p[4] = rng.range(0.0, std::f32::consts::TAU);
                p[5] = rng.range(0.0, std::f32::consts::TAU);
                p[6] = rng.range(0.6, 2.4); // decay
            }
            // rose-helix: k-petal rose climbing in z
            _ => {
                p[0] = (2 + rng.idx(9)) as f32; // petals
                p[1] = rng.range(3.0, 9.0); // turns
                p[2] = rng.range(0.4, 1.1); // height
            }
        }
        Figure { kind, p }
    }

    /// Sample at `u` in 0..1, returned roughly within a unit-ish box.
    fn at(&self, u: f32) -> Vec3 {
        let tau = std::f32::consts::TAU;
        let p = &self.p;
        match self.kind {
            0 => {
                let t = tau * p[3].max(1.0) * u;
                let (pn, qn) = (p[0], p[1]);
                let r = 1.0 + p[2] * (qn * t).cos();
                vec3(r * (pn * t).cos(), r * (pn * t).sin(), p[2] * (qn * t).sin()) * 0.85
            }
            1 => {
                let sf = |ang: f32, m: f32, n1: f32, n2: f32, n3: f32| -> f32 {
                    let a = ((m * ang / 4.0).cos().abs()).powf(n2);
                    let b = ((m * ang / 4.0).sin().abs()).powf(n3);
                    (a + b).powf(-1.0 / n1.max(0.05)).min(3.0)
                };
                // wind a spiral over the supershape surface
                let lon = (u * p[4].max(1.0) * tau).rem_euclid(tau) - std::f32::consts::PI;
                let lat = (u - 0.5) * std::f32::consts::PI;
                let r1 = sf(lon, p[0], p[1], p[2], p[3]);
                let r2 = sf(lat, p[0], p[1], p[2], p[3]);
                vec3(
                    r1 * lon.cos() * r2 * lat.cos(),
                    r1 * lon.sin() * r2 * lat.cos(),
                    r2 * lat.sin(),
                ) * 0.7
            }
            2 => {
                let t = tau * u;
                vec3(
                    (p[0] * t + p[3]).sin(),
                    (p[1] * t + p[4]).sin(),
                    (p[2] * t).sin(),
                )
            }
            3 => {
                let t = u * tau * 4.0;
                let d = (-p[6] * u).exp();
                vec3(
                    d * ((p[0] * t).sin() + 0.6 * (p[2] * t + p[4]).sin()),
                    d * ((p[1] * t + p[5]).sin() + 0.6 * (p[3] * t).sin()),
                    d * (0.5 * ((p[0] + p[1]) * 0.5 * t).sin()),
                )
            }
            _ => {
                let th = u * tau * p[1].max(1.0);
                let r = (p[0] * th).cos();
                vec3(r * th.cos(), r * th.sin(), p[2] * (u - 0.5) * 2.0)
            }
        }
    }
}

pub struct Scope {
    pub seed: u64,
    rng: Rng,
    cur: Figure,
    tgt: Figure,
    morph: f32, // 0..1 cur->tgt (1 = settled)
    yaw: f32,
    pitch: f32,
    roll: f32,
    breathe: f32,
    restruct_cd: f32,
    prev_flux: f32,
    idle: f32, // seconds since last change (quiet-track fallback)
    wave: [f32; WAVE_N],
    loud: f32,
    flux: f32,
    centroid: f32,
    t: f32,
}

impl Scope {
    pub fn new(seed: u64) -> Self {
        let mut rng = Rng::new(seed ^ 0x05C0_BE11);
        let cur = Figure::random(&mut rng);
        Scope {
            seed,
            rng,
            cur,
            tgt: cur,
            morph: 1.0,
            yaw: 0.0,
            pitch: 0.0,
            roll: 0.0,
            breathe: 0.0,
            restruct_cd: 0.0,
            prev_flux: 0.0,
            idle: 0.0,
            wave: [0.0; WAVE_N],
            loud: 0.0,
            flux: 0.0,
            centroid: 0.0,
            t: 0.0,
        }
    }

    pub fn reseed(&mut self, seed: u64) {
        *self = Scope::new(seed);
    }

    pub fn point_count(&self) -> usize {
        N
    }

    /// Begin a morph into a freshly-seeded figure.
    fn restructure(&mut self) {
        self.tgt = Figure::random(&mut self.rng);
        self.morph = 0.0;
        self.idle = 0.0;
    }

    pub fn update(&mut self, b: &Bands, dt: f32) {
        let dt = dt.clamp(0.0, 0.05);
        self.t += dt;
        self.wave = b.wave;
        self.loud = b.loud;
        self.flux = b.flux;
        self.centroid = b.centroid;

        // Smooth, music-locked motion (no random snaps).
        self.yaw += (0.14 + 0.85 * b.mid) * dt;
        self.pitch += (0.06 + 0.45 * b.low) * dt + 0.025 * dt;
        self.roll += 0.04 * dt + 0.35 * b.high * dt;
        self.breathe += dt * (0.25 + 1.1 * b.mid + 0.5 * b.low);

        // Advance an in-flight morph; settle onto the target.
        if self.morph < 1.0 {
            self.morph = (self.morph + dt / MORPH_SECS).min(1.0);
            if self.morph >= 1.0 {
                self.cur = self.tgt;
            }
        }

        // Change figure on a rising broadband transient (cooldown-gated), or
        // on a long idle so quiet passages still evolve.
        self.restruct_cd = (self.restruct_cd - dt).max(0.0);
        self.idle += dt;
        let rising = b.flux > 0.6 && self.prev_flux <= 0.6;
        if self.morph >= 1.0
            && self.restruct_cd <= 0.0
            && (rising || self.idle > 12.0)
        {
            self.restruct_cd = 1.2;
            self.restructure();
        }
        self.prev_flux = b.flux;
    }

    /// Near-monochrome phosphor: keep the palette's hue drift but pull most of
    /// the chroma out and lift luminance so it reads as a vector display.
    fn phosphor(c: [f32; 4]) -> [f32; 4] {
        let lum = 0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2];
        let mix = 0.62;
        [
            ((c[0] * (1.0 - mix) + lum * mix) * 1.18).min(1.0),
            ((c[1] * (1.0 - mix) + lum * mix) * 1.18).min(1.0),
            ((c[2] * (1.0 - mix) + lum * mix) * 1.18).min(1.0),
            c[3],
        ]
    }

    #[allow(clippy::too_many_arguments)]
    pub fn draw(
        &self,
        draw: &Draw,
        pal: &Palette,
        fit: f32,
        scale: f32,
        warp: f32,
        glow: bool,
        _seg: usize,
        tint: [f32; 3],
    ) {
        let (sy, cy) = self.yaw.sin_cos();
        let (sp, cp) = self.pitch.sin_cos();
        let (sr, cr) = self.roll.sin_cos();
        let dist = FIELD * 1.7;
        let amp = FIELD * 0.40 * (0.85 + 0.30 * scale.min(1.6) + 0.10 * self.breathe.sin());
        let e = smoothstep(self.morph);
        // Slow figure-character drift from spectral brightness, beam-noise from
        // the live waveform + flux — subtle, so the shape stays coherent.
        let drift = 1.0 + 0.10 * (self.centroid - 0.5);
        let beam_amp = (0.012 + 0.05 * self.flux) * warp.max(0.2);

        let project = |i: usize| -> (Vec2, f32) {
            let u = i as f32 / N as f32;
            let a = self.cur.at(u);
            let mut q = if e < 1.0 {
                let bpt = self.tgt.at(u);
                a + (bpt - a) * e
            } else {
                a
            };
            q *= drift * amp;
            // beam-signal wobble: the actual waveform perturbs the trace
            let wv = self.wave[(i * WAVE_N / N) % WAVE_N];
            let nz = flow(vec2(q.x, q.y), self.t, self.seed as u32);
            q.x += nz.x * amp * beam_amp + wv * amp * beam_amp * 1.5;
            q.y += nz.y * amp * beam_amp;
            // rotate yaw(Y) -> pitch(X) -> roll(Z)
            let (x1, z1) = (q.x * cy - q.z * sy, q.x * sy + q.z * cy);
            let (y2, z2) = (q.y * cp - z1 * sp, q.y * sp + z1 * cp);
            let (x3, y3) = (x1 * cr - y2 * sr, x1 * sr + y2 * cr);
            let f = dist / (dist + z2.max(-dist * 0.9));
            (vec2(x3 * f, y3 * f) * fit, z2)
        };

        // Build the screen path + per-segment beam speed (for brightness).
        let mut scr: Vec<Vec2> = Vec::with_capacity(N);
        for i in 0..N {
            scr.push(project(i).0);
        }

        let roll_h = self.roll.rem_euclid(std::f32::consts::TAU) / std::f32::consts::TAU;
        let base = Self::phosphor(pal.stroke(0.5, (0.5 + 0.5 * self.loud).min(1.0), roll_h));
        let put = |a: Vec2, c: Vec2, w: f32, col: [f32; 4]| {
            draw.polyline()
                .weight(w)
                .points([a, c])
                .color(srgba(
                    col[0] * tint[0],
                    col[1] * tint[1],
                    col[2] * tint[2],
                    col[3],
                ));
        };

        // Faint continuous beam for path continuity (phosphor base + halo).
        if glow {
            draw.polyline()
                .weight(5.0)
                .points(scr.iter().cloned())
                .color(srgba(
                    base[0] * tint[0],
                    base[1] * tint[1],
                    base[2] * tint[2],
                    0.035,
                ));
        }
        draw.polyline()
            .weight(1.0)
            .points(scr.iter().cloned())
            .color(srgba(
                base[0] * tint[0],
                base[1] * tint[1],
                base[2] * tint[2],
                0.10,
            ));

        // Dithered beam: bright where it moves slowly (real-scope intensity),
        // gated by an ordered dither so it reads as grain, not a solid line.
        let fr = (self.t * 60.0) as u32;
        let s32 = self.seed as u32;
        for i in 1..N {
            let (a, c) = (scr[i - 1], scr[i]);
            let len = (c - a).length().max(1e-3);
            // slow beam -> bright; fast beam -> dim (energy spreads over px)
            let inten = (10.0 / (1.0 + 0.05 * len)).min(1.0);
            let dith = h01(s32 ^ i as u32, fr ^ (i as u32 >> 3));
            if inten < dith * 0.85 {
                continue;
            }
            let mut col = Self::phosphor(pal.stroke(i as f32 / N as f32, 0.6 + 0.4 * self.loud, roll_h));
            col[3] = (0.18 + 0.55 * inten) * (0.7 + 0.3 * self.loud);
            put(a, c, 1.0 + 1.4 * inten, col);
        }

        // Faint CRT grain so the field is alive even between strokes.
        let grain = 90 + (self.loud * 140.0) as usize;
        for k in 0..grain {
            let gx = (h01(s32 ^ 0x00A1 ^ k as u32, fr) - 0.5) * FIELD * fit;
            let gy = (h01(s32 ^ 0x005C ^ k as u32, fr.wrapping_add(7)) - 0.5) * FIELD * fit;
            draw.rect().x_y(gx, gy).w_h(1.0, 1.0).color(srgba(
                base[0] * tint[0],
                base[1] * tint[1],
                base[2] * tint[2],
                0.05,
            ));
        }
    }
}
