//! Music as instructions — `wai.audio.score` (Phase 6).
//!
//! The deepest audio lever. Spatial audio (`wai.audio.scene`) carries
//! recorded sound *objects* and mixes them; a **score** carries no
//! recording at all — just the notes. A few hundred bytes of note events
//! become a piece of music the sink synthesizes; the rendered audio is
//! megabytes, the score is kilobytes. "Ship the composition, not the
//! recording."
//!
//! Like every other class, conformance is over a deterministic
//! reference: a [`Score`] reconstructs to a canonical `i16` PCM mixdown
//! in the `wai.det.fixed64` floor, and conformance is
//! **mixdown-equivalence** (identical BLAKE3 over the PCM, no tolerance).
//! The reference synthesis is trig-free by construction — a phase
//! accumulator drives a piecewise-linear oscillator (saw / square /
//! triangle / pulse) shaped by a linear ADSR envelope, all `+ − × ÷`.
//! A sink with a soundfont or a neural instrument renders the same score
//! richly; that is presentation, exactly like the worlds renderer or the
//! audio-scene HRTF — the exact mix is what's verified.
//!
//! Pitch is carried as a per-sample **phase increment** (`freq /
//! sample_rate`) authored once off-line, so the floor never needs an
//! exponential to turn a MIDI note into a frequency — the same
//! authoring discipline as the avatar quaternion.

use crate::world::Fx;

mod container;
pub use container::{ScoreContract, ScoreError};
mod receipt;
pub use receipt::{ScoreReceipt, SegmentHash, SEGMENT_SAMPLES};

const DOMAIN_MIXDOWN: &[u8] = b"wai:score-mixdown\x01";
const DOMAIN_SCORE: &[u8] = b"wai:score\x01";

/// Reference oscillator timbres — all piecewise-linear, trig-free.
pub mod waveform {
    pub const SAW: u8 = 0;
    pub const SQUARE: u8 = 1;
    pub const TRIANGLE: u8 = 2;
    pub const PULSE25: u8 = 3;
}

/// One note event. `phase_inc = freq / sample_rate` (Q32.32) is authored
/// off-line from the pitch. The ADSR times are in samples; `sustain` is
/// the held level in `[0, 1]`.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Note {
    pub start: u64,
    /// Note-on duration in samples (release follows, lasting `release`).
    pub dur: u64,
    pub phase_inc: Fx,
    pub gain: Fx,
    pub waveform: u8,
    pub attack: u64,
    pub decay: u64,
    pub sustain: Fx,
    pub release: u64,
}

/// A musical score: a contract + a list of note events.
#[derive(Clone, Debug, PartialEq)]
pub struct Score {
    pub contract: ScoreContract,
    /// Exact contract bytes on the wire (the score hash binds them).
    pub contract_bytes: Vec<u8>,
    pub notes: Vec<Note>,
}

fn fxn(n: u64) -> Fx {
    Fx::from_bits((n as i64) << 32)
}

const HALF: Fx = Fx::from_bits(1i64 << 31);
const QUARTER: Fx = Fx::from_bits(1i64 << 30);

/// Piecewise-linear oscillator value at `phase ∈ [0, 1)` → `[-1, 1]`.
fn osc(phase: Fx, kind: u8) -> Fx {
    let two = Fx::from_int(2);
    match kind {
        waveform::SAW => two * phase - Fx::ONE,
        waveform::SQUARE => {
            if phase < HALF {
                Fx::ONE
            } else {
                -Fx::ONE
            }
        }
        waveform::TRIANGLE => {
            let four = Fx::from_int(4);
            if phase < HALF {
                four * phase - Fx::ONE
            } else {
                Fx::from_int(3) - four * phase
            }
        }
        waveform::PULSE25 => {
            if phase < QUARTER {
                Fx::ONE
            } else {
                -Fx::ONE
            }
        }
        _ => Fx::ZERO,
    }
}

/// Linear ADSR envelope at `t` samples into the note (`[0, 1]`).
fn adsr(t: u64, n: &Note) -> Fx {
    if n.attack > 0 && t < n.attack {
        return fxn(t) / fxn(n.attack);
    }
    let ad = n.attack + n.decay;
    if t < ad {
        return if n.decay > 0 {
            Fx::ONE - (Fx::ONE - n.sustain) * fxn(t - n.attack) / fxn(n.decay)
        } else {
            n.sustain
        };
    }
    if t < n.dur {
        return n.sustain;
    }
    if n.release > 0 && t < n.dur + n.release {
        return n.sustain * (Fx::ONE - fxn(t - n.dur) / fxn(n.release));
    }
    Fx::ZERO
}

/// Round a Q32.32 value to the nearest `i16`, half away from zero, in
/// integer space (no float) — the same quantization the audio-scene
/// mixdown uses.
fn quant_i16(v: Fx) -> i16 {
    let one = Fx::ONE.to_bits();
    let mut bits = v.to_bits();
    if bits > one {
        bits = one;
    } else if bits < -one {
        bits = -one;
    }
    let num = bits as i128 * 32767;
    let half = 1i128 << 31;
    let r = if num >= 0 { (num + half) >> 32 } else { -((-num + half) >> 32) };
    r as i16
}

impl Score {
    /// Reconstruct the canonical mono `i16` PCM mixdown — the
    /// mixdown-equivalence quantity. Notes are summed in the `Fx` floor;
    /// polyphony that exceeds full scale clamps at quantization.
    pub fn mixdown(&self) -> Vec<i16> {
        let n = self.contract.duration as usize;
        let mut buf = vec![Fx::ZERO; n];
        for note in &self.notes {
            let mut phase = Fx::ZERO;
            let end = (note.start + note.dur + note.release).min(self.contract.duration);
            for i in note.start..end {
                let t = i - note.start;
                let env = adsr(t, note);
                let s = osc(phase, note.waveform) * env * note.gain;
                buf[i as usize] = buf[i as usize] + s;
                phase = phase + note.phase_inc;
                if phase >= Fx::ONE {
                    phase = phase - Fx::ONE;
                }
            }
        }
        buf.into_iter().map(quant_i16).collect()
    }

    /// `BLAKE3(DOMAIN_MIXDOWN || i16le PCM)` — mixdown-equivalence.
    pub fn mixdown_hash(&self) -> [u8; 32] {
        let mut h = blake3::Hasher::new();
        h.update(DOMAIN_MIXDOWN);
        for s in self.mixdown() {
            h.update(&s.to_le_bytes());
        }
        *h.finalize().as_bytes()
    }

    /// Score identity: `BLAKE3(DOMAIN_SCORE || contract || notes)`,
    /// independent of the rendered audio.
    pub fn score_hash(&self) -> [u8; 32] {
        let mut h = blake3::Hasher::new();
        h.update(DOMAIN_SCORE);
        h.update(&self.contract_bytes);
        h.update(&container::notes_bytes(&self.notes));
        *h.finalize().as_bytes()
    }
}

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

    fn inc(freq: f64, sr: u32) -> Fx {
        Fx::from_bits(((freq / sr as f64) * 4294967296.0).round() as i64)
    }
    fn f(n: i32, d: i32) -> Fx {
        Fx::from_int(n) / Fx::from_int(d)
    }

    fn score(notes: Vec<Note>, dur: u64) -> Score {
        let c = ScoreContract { sample_rate: 22050, numeric: NUMERIC_FIXED64.into(), channels: 1, duration: dur };
        Score { contract_bytes: c.to_canonical_json(), contract: c, notes }
    }

    fn note(start: u64, dur: u64, freq: f64, wf: u8) -> Note {
        Note {
            start,
            dur,
            phase_inc: inc(freq, 22050),
            gain: f(1, 2),
            waveform: wf,
            attack: 200,
            decay: 400,
            sustain: f(7, 10),
            release: 600,
        }
    }

    #[test]
    fn renders_and_hashes_deterministically() {
        let s = score(vec![note(0, 4000, 440.0, waveform::SAW), note(2000, 4000, 660.0, waveform::SQUARE)], 8000);
        let pcm = s.mixdown();
        assert_eq!(pcm.len(), 8000);
        // some audible signal exists, and it's bounded
        assert!(pcm.iter().any(|&x| x.abs() > 1000));
        assert!(pcm.iter().all(|&x| x.abs() <= 32767));
        // deterministic
        assert_eq!(s.mixdown_hash(), score(vec![note(0, 4000, 440.0, waveform::SAW), note(2000, 4000, 660.0, waveform::SQUARE)], 8000).mixdown_hash());
    }

    #[test]
    fn pitch_and_waveform_change_the_mix() {
        let a = score(vec![note(0, 2000, 440.0, waveform::SAW)], 3000);
        let b = score(vec![note(0, 2000, 880.0, waveform::SAW)], 3000); // octave up
        let c = score(vec![note(0, 2000, 440.0, waveform::TRIANGLE)], 3000); // diff timbre
        assert_ne!(a.mixdown_hash(), b.mixdown_hash());
        assert_ne!(a.mixdown_hash(), c.mixdown_hash());
        assert_ne!(a.score_hash(), b.score_hash());
    }

    #[test]
    fn silence_before_and_after_notes() {
        let s = score(vec![note(1000, 1000, 440.0, waveform::SAW)], 4000);
        let pcm = s.mixdown();
        assert_eq!(pcm[0], 0); // before the note
        assert_eq!(pcm[3999], 0); // well after release
    }

    #[test]
    fn container_round_trips() {
        let s = score(vec![note(0, 3000, 523.25, waveform::TRIANGLE), note(1500, 3000, 783.99, waveform::PULSE25)], 6000);
        let bytes = s.to_bytes();
        let back = Score::from_bytes(&bytes).unwrap();
        assert_eq!(back, s);
        assert_eq!(back.mixdown_hash(), s.mixdown_hash());
    }
}
