//! The living hybrid cyber-organic sigil.
//!
//! A fixed *cyber skeleton* (spine + mirrored branch walks + rings + glyph
//! nodes) gives a stable occult identity. Over it crawls *organic overgrowth*:
//! tendrils that grow along the music, each bound to one log-spectrum band —
//! they extend and branch while their band is loud, wither and retract when it
//! goes quiet, and the whole population is periodically restructured by
//! broadband transients. Everything is rendered as noise-warped Catmull-Rom
//! curves, so the figure breathes and morphs rather than rigidly transforming.

use crate::audio::{Bands, SPEC_N};
use nannou::prelude::*;

use crate::viz::curve::{Rng, catmull_rom, flow, smoothstep};
use crate::viz::palette::Palette;

const FIELD: f32 = 960.0; // design-space extent (matches old W/H)
const R_MAX: f32 = FIELD * 0.475;
const SOFT_CAP: usize = 88; // tendril population the field settles toward
const MAX_NODES: usize = 30;

const TURNS: [f32; 7] = [
    -PI / 3.0,
    -PI / 6.0,
    -PI / 12.0,
    0.0,
    PI / 12.0,
    PI / 6.0,
    PI / 3.0,
];

/// A skeleton stroke (control points, smoothed at draw time).
struct Bone {
    ctrl: Vec<Vec2>,
    weight: f32,
    glyph: bool,
}

/// One organic overgrowth strand bound to a spectrum band.
struct Tendril {
    nodes: Vec<Vec2>,
    band: usize,
    hue_off: f32,
    curl: f32,
    width: f32,
    vigor: f32,  // 0..1 health; drives growth, decays when band quiet
    budget: f32, // accumulated growth credit
    quiet: f32,  // seconds the band has been quiet (-> retract)
    depth: u8,
}

/// Expanding shockwave ring spawned by a big transient.
struct Ring {
    r: f32,
    speed: f32,
    life: f32,
    hue_off: f32,
}

pub struct Sigil {
    pub seed: u64,
    bones: Vec<Bone>,
    anchors: Vec<Vec2>, // seed points for new tendrils (skeleton extremities)
    tendrils: Vec<Tendril>,
    rings: Vec<Ring>,
    rng: Rng,
    rot: f32,
    breathe: f32,
    restruct_cd: f32,
    prev_flux: f32,
}

impl Sigil {
    pub fn new(seed: u64) -> Self {
        let mut s = Sigil {
            seed,
            bones: Vec::new(),
            anchors: Vec::new(),
            tendrils: Vec::new(),
            rings: Vec::new(),
            rng: Rng::new(seed),
            rot: 0.0,
            breathe: 0.0,
            restruct_cd: 0.0,
            prev_flux: 0.0,
        };
        s.build_skeleton();
        s
    }

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

    pub fn tendril_count(&self) -> usize {
        self.tendrils.len()
    }

    // --- generation --------------------------------------------------------

    fn build_skeleton(&mut self) {
        let mut rng = Rng::new(self.seed ^ 0xB1FF_5EED);
        let spine_h = FIELD * 0.40;
        let segs = 5 + rng.idx(4);
        let mut spine = Vec::with_capacity(segs + 1);
        let dy = (2.0 * spine_h) / segs as f32;
        let mut y = -spine_h;
        for _ in 0..=segs {
            spine.push(vec2(rng.range(-22.0, 22.0), y));
            y += dy;
        }
        self.bones.push(Bone {
            ctrl: spine.clone(),
            weight: 3.0,
            glyph: true,
        });

        let walks = 4 + rng.idx(4);
        for _ in 0..walks {
            let anchor = spine[1 + rng.idx(spine.len() - 2)];
            let mut p = anchor;
            let mut ang = rng.range(-PI / 2.5, PI / 2.5);
            let steps = 3 + rng.idx(6);
            let mut walk = vec![p];
            for _ in 0..steps {
                ang += TURNS[rng.idx(TURNS.len())];
                let len = rng.range(30.0, 95.0);
                let mut np = p + vec2(ang.cos(), ang.sin()) * len;
                np.x = np.x.clamp(2.0, FIELD * 0.46);
                np.y = np.y.clamp(-FIELD * 0.46, FIELD * 0.46);
                walk.push(np);
                p = np;
            }
            let tip = *walk.last().unwrap();
            self.anchors.push(tip);
            self.anchors.push(vec2(-tip.x, tip.y));
            let w = rng.range(1.2, 2.0);
            let mirror: Vec<Vec2> = walk.iter().map(|v| vec2(-v.x, v.y)).collect();
            self.bones.push(Bone {
                ctrl: walk,
                weight: w,
                glyph: false,
            });
            self.bones.push(Bone {
                ctrl: mirror,
                weight: w,
                glyph: false,
            });
        }

        // Closed ring arcs — full circles read as smooth curves.
        let rings = 2 + rng.idx(2);
        for _ in 0..rings {
            let r = rng.range(FIELD * 0.30, R_MAX);
            let mut arc = Vec::with_capacity(33);
            for i in 0..=32 {
                let th = TAU * i as f32 / 32.0;
                arc.push(vec2(r * th.cos(), r * th.sin()));
            }
            self.bones.push(Bone {
                ctrl: arc,
                weight: rng.range(0.9, 1.6),
                glyph: false,
            });
        }

        // A few anchors on the spine itself so growth also erupts from the core.
        for &p in spine.iter().skip(1).step_by(2) {
            self.anchors.push(p);
        }
        // Seed an initial sparse population.
        for _ in 0..18 {
            self.spawn_from_anchor(0.4);
        }
    }

    fn spawn_from_anchor(&mut self, vigor: f32) {
        if self.anchors.is_empty() {
            return;
        }
        let a = self.anchors[self.rng.idx(self.anchors.len())];
        let band = self.rng.idx(SPEC_N);
        let out = a.normalize_or_zero();
        let dir = if out.length() < 0.01 {
            let t = self.rng.range(0.0, TAU);
            vec2(t.cos(), t.sin())
        } else {
            out
        };
        self.tendrils.push(Tendril {
            nodes: vec![a, a + dir * 6.0],
            band,
            hue_off: self.rng.range(-0.6, 0.6),
            curl: self.rng.range(-0.5, 0.5),
            width: self.rng.range(0.9, 2.1),
            vigor,
            budget: 0.0,
            quiet: 0.0,
            depth: 0,
        });
    }

    // --- per-frame growth --------------------------------------------------

    pub fn update(&mut self, b: &Bands, dt: f32) {
        let dt = dt.clamp(0.0, 0.05);
        self.rot += b.mid * 0.55 * dt + 0.04 * dt;
        self.breathe += dt * (0.3 + b.mid * 1.4 + b.low * 0.6);

        // Map each band to its onset group for branching decisions.
        let group_on = |band: usize| -> f32 {
            let f = band as f32 / SPEC_N as f32;
            if f < 0.33 {
                b.low_on
            } else if f < 0.66 {
                b.mid_on
            } else {
                b.high_on
            }
        };

        let mut spawn_children: Vec<(usize, usize)> = Vec::new();
        for (ti, t) in self.tendrils.iter_mut().enumerate() {
            let drive = b.spec[t.band];
            let target = smoothstep(0.04, 0.55, drive) * (0.5 + 0.5 * b.loud);
            let k = if target > t.vigor { 0.16 } else { 0.05 };
            t.vigor += (target - t.vigor) * k;

            if drive < 0.06 {
                t.quiet += dt;
            } else {
                t.quiet = (t.quiet - dt * 2.0).max(0.0);
            }

            // Grow: spend an energy budget into new curved nodes.
            t.budget += (drive * t.vigor) * 26.0 * dt;
            while t.budget >= 1.0 && t.nodes.len() < MAX_NODES {
                t.budget -= 1.0;
                let n = t.nodes.len();
                let prev = t.nodes[n - 2];
                let last = t.nodes[n - 1];
                let mut d = (last - prev).normalize_or_zero();
                if d.length() < 0.01 {
                    d = vec2(1.0, 0.0);
                }
                let ang = d.y.atan2(d.x)
                    + t.curl * 0.35
                    + flow(last, self.breathe, self.seed as u32) .x * 0.06;
                let step = 9.0 + 26.0 * drive + 4.0 * t.vigor;
                let mut np = last + vec2(ang.cos(), ang.sin()) * step;
                let rl = np.length();
                if rl > R_MAX {
                    np *= R_MAX / rl; // curl back along the boundary
                    t.curl = -t.curl;
                }
                t.nodes.push(np);
            }

            // Wither: sustained quiet retracts the strand tip-first.
            if t.quiet > 0.5 && t.nodes.len() > 2 && self.rng.chance(0.20) {
                t.nodes.pop();
            }

            // Branch on a strong onset in this band's group.
            if t.depth < 2
                && t.nodes.len() > 6
                && group_on(t.band) > 0.6
                && self.rng.chance(0.05 + 0.10 * t.vigor)
            {
                spawn_children.push((ti, 4 + self.rng.idx(t.nodes.len() - 5)));
            }
        }

        // Spawn queued children from a mid-node of the parent.
        for (pi, ni) in spawn_children {
            let (origin, band, hue, depth, width) = {
                let p = &self.tendrils[pi];
                (
                    p.nodes[ni.min(p.nodes.len() - 1)],
                    (p.band + 1).min(SPEC_N - 1),
                    p.hue_off,
                    p.depth + 1,
                    p.width * 0.7,
                )
            };
            let a = self.rng.range(0.0, TAU);
            self.tendrils.push(Tendril {
                nodes: vec![origin, origin + vec2(a.cos(), a.sin()) * 6.0],
                band,
                hue_off: hue + self.rng.range(-0.4, 0.4),
                curl: self.rng.range(-0.8, 0.8),
                width,
                vigor: 0.6,
                budget: 0.0,
                quiet: 0.0,
                depth,
            });
        }

        // Cull dead strands (fully withered).
        self.tendrils
            .retain(|t| !(t.vigor < 0.05 && t.nodes.len() <= 2));

        // Restructure: a broadband transient erupts new growth + a shockwave,
        // and prunes the weakest if the field is overgrown.
        self.restruct_cd = (self.restruct_cd - dt).max(0.0);
        let rising = b.flux > 0.62 && self.prev_flux <= 0.62;
        if rising && self.restruct_cd <= 0.0 {
            self.restruct_cd = 0.45;
            let burst = 5 + self.rng.idx(7);
            for _ in 0..burst {
                self.spawn_from_anchor(0.7 + 0.3 * b.loud);
            }
            self.rings.push(Ring {
                r: FIELD * 0.06,
                speed: 220.0 + 360.0 * b.loud,
                life: 1.0,
                hue_off: self.rng.range(0.0, 1.0),
            });
            if self.tendrils.len() > SOFT_CAP {
                self.tendrils
                    .sort_by(|a, c| a.vigor.partial_cmp(&c.vigor).unwrap());
                let drop = self.tendrils.len() - SOFT_CAP;
                self.tendrils.drain(0..drop);
            }
        }
        self.prev_flux = b.flux;

        // Keep a living minimum so quiet passages still shimmer faintly.
        while self.tendrils.len() < 14 {
            self.spawn_from_anchor(0.3);
        }

        for r in &mut self.rings {
            r.r += r.speed * dt;
            r.life -= dt * 0.9;
        }
        self.rings.retain(|r| r.life > 0.0 && r.r < FIELD);
    }

    // --- rendering ---------------------------------------------------------

    fn xf(&self, p: Vec2, scale: f32, fit: f32, warp: f32) -> Vec2 {
        let s = p * scale;
        let (sn, cs) = self.rot.sin_cos();
        let r = vec2(s.x * cs - s.y * sn, s.x * sn + s.y * cs);
        let w = flow(r, self.breathe, self.seed as u32 ^ 0x51A6) * warp;
        (r + w) * fit
    }

    /// Draw the whole sigil into `draw` (design space is `±FIELD/2`, scaled by
    /// `fit` to the target). `scale` is the audio breathing scale, `warp` the
    /// organic displacement amplitude in design px, `glow` toggles haloing,
    /// `tint` is a per-channel RGB multiplier (used for the chromatic-
    /// aberration channel passes; pass `[1.0; 3]` normally).
    #[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],
    ) {
        // Skeleton — bright, structural, slight breathing only.
        for bone in &self.bones {
            let sm = catmull_rom(&bone.ctrl, seg);
            let pts: Vec<Vec2> = sm
                .iter()
                .map(|&p| self.xf(p, scale, fit, warp * 0.45))
                .collect();
            colored_path(draw, &pts, bone.weight, glow, tint, |t| pal.bone(t));
            if bone.glyph {
                for (i, &c) in bone.ctrl.iter().enumerate().skip(1) {
                    if i % 2 == 0 {
                        continue;
                    }
                    self.glyph(draw, c, scale, fit, warp, pal, tint);
                }
            }
        }

        // Expanding shockwave rings.
        for ring in &self.rings {
            let mut pts = Vec::with_capacity(49);
            for i in 0..=48 {
                let th = TAU * i as f32 / 48.0;
                pts.push(self.xf(
                    vec2(ring.r * th.cos(), ring.r * th.sin()),
                    1.0,
                    fit,
                    warp * 0.3,
                ));
            }
            let mut c = pal.bone(ring.hue_off.fract());
            c[3] = ring.life * ring.life * 0.5;
            stroke(draw, &pts, 2.0 * ring.life + 0.5, c, glow, tint);
        }

        // Organic overgrowth — colour travels root->tip, alpha by vigor.
        for t in &self.tendrils {
            if t.nodes.len() < 2 {
                continue;
            }
            let sm = catmull_rom(&t.nodes, seg);
            let pts: Vec<Vec2> = sm
                .iter()
                .map(|&p| self.xf(p, scale, fit, warp))
                .collect();
            let w = t.width * (0.4 + 0.6 * t.vigor);
            let v = t.vigor;
            let ho = t.hue_off;
            colored_path(draw, &pts, w, glow && v > 0.4, tint, |tt| {
                pal.stroke(tt, v, ho)
            });
            // Tip spark on lively strands.
            if v > 0.55 {
                if let Some(&tip) = pts.last() {
                    let mut c = pal.stroke(1.0, v, ho);
                    c[3] = (v - 0.55) * 1.6;
                    draw.ellipse()
                        .xy(tip)
                        .radius(1.5 + 2.5 * v)
                        .color(srgba(c[0] * tint[0], c[1] * tint[1], c[2] * tint[2], c[3]));
                }
            }
        }
    }

    #[allow(clippy::too_many_arguments)]
    fn glyph(
        &self,
        draw: &Draw,
        c: Vec2,
        scale: f32,
        fit: f32,
        warp: f32,
        pal: &Palette,
        tint: [f32; 3],
    ) {
        let s = 5.0;
        let dia = [
            c + vec2(0.0, s),
            c + vec2(s, 0.0),
            c + vec2(0.0, -s),
            c + vec2(-s, 0.0),
            c + vec2(0.0, s),
        ];
        let pts: Vec<Vec2> = dia
            .iter()
            .map(|&p| self.xf(p, scale, fit, warp * 0.4))
            .collect();
        stroke(draw, &pts, 1.0, pal.bone(0.5), false, tint);
    }
}

// --- low-level stroke helpers ---------------------------------------------

#[inline]
fn tint4(c: [f32; 4], t: [f32; 3]) -> [f32; 4] {
    [c[0] * t[0], c[1] * t[1], c[2] * t[2], c[3]]
}
#[inline]
fn col4(c: [f32; 4]) -> nannou::color::Srgba<f32> {
    srgba(c[0], c[1], c[2], c[3])
}
#[inline]
fn tinted(c: [f32; 4], t: [f32; 3]) -> nannou::color::Srgba<f32> {
    col4(tint4(c, t))
}

/// One polyline with optional faux-glow halo (wide low-alpha passes).
fn stroke(draw: &Draw, pts: &[Vec2], w: f32, c: [f32; 4], glow: bool, tn: [f32; 3]) {
    if pts.len() < 2 {
        return;
    }
    if glow {
        draw.polyline()
            .weight(w * 3.4)
            .points(pts.iter().cloned())
            .color(tinted([c[0], c[1], c[2], c[3] * 0.05], tn));
        draw.polyline()
            .weight(w * 1.9)
            .points(pts.iter().cloned())
            .color(tinted([c[0], c[1], c[2], c[3] * 0.10], tn));
    }
    draw.polyline()
        .weight(w)
        .points(pts.iter().cloned())
        .color(tinted(c, tn));
}

/// Polyline whose colour varies along its length (per-segment), tapering the
/// weight root->tip. `col(t)` returns gamma-sRGB rgba for arc-fraction `t`.
fn colored_path(
    draw: &Draw,
    pts: &[Vec2],
    w: f32,
    glow: bool,
    tn: [f32; 3],
    col: impl Fn(f32) -> [f32; 4],
) {
    let n = pts.len();
    if n < 2 {
        return;
    }
    if glow {
        // Cheap halo: a couple of wide low-alpha passes at mid colour.
        let c = col(0.5);
        draw.polyline()
            .weight(w * 3.2)
            .points(pts.iter().cloned())
            .color(tinted([c[0], c[1], c[2], c[3] * 0.045], tn));
        draw.polyline()
            .weight(w * 1.8)
            .points(pts.iter().cloned())
            .color(tinted([c[0], c[1], c[2], c[3] * 0.09], tn));
    }
    for i in 0..n - 1 {
        let t = i as f32 / (n - 1) as f32;
        let c = col(t);
        let ww = (w * (1.0 - 0.55 * t)).max(0.4);
        draw.polyline()
            .weight(ww)
            .points([pts[i], pts[i + 1]])
            .color(tinted(c, tn));
    }
}
