//! Haptics — `extensions/haptics.md` (Phase 2). Touch as instructions.
//!
//! A `wai.haptic.signal` is a per-channel keyframed envelope of
//! **intensity** and **sharpness** (the pair modern actuator APIs map
//! to). The sink reconstructs a per-sample `u8` buffer in the
//! `wai.det.fixed64` floor and plays it on whatever actuators it has.
//! Conformance is **sample-equivalence**: same signal → identical
//! reconstructed buffer → identical BLAKE3. Trig-free (amplitude +
//! sharpness, not a modulated carrier), so the floor reconstructs it
//! exactly with `+,−,×,÷` alone — the simplest cargo class to make
//! bit-exact.

use crate::world::Fx;

mod receipt;
pub use receipt::{ChannelHash, HapticReceipt};

pub const CAP_HAPTIC_SIGNAL: &str = "wai.haptic.signal";
pub const DOMAIN_SIGNAL: &[u8] = b"wai:haptic-signal\x01";
pub const MAGIC_WHAP: &[u8; 4] = b"WHAP";

const SECTION_CONTRACT: u8 = 0x01;
const SECTION_CHANNELS: u8 = 0x02;

#[derive(Debug)]
pub enum HapticError {
    BadMagic,
    Truncated,
    MissingSection(u8),
    BadContract(String),
}
impl std::fmt::Display for HapticError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::BadMagic => write!(f, "payload is not a WHAP container"),
            Self::Truncated => write!(f, "byte stream truncated"),
            Self::MissingSection(k) => write!(f, "required section 0x{k:02x} missing"),
            Self::BadContract(w) => write!(f, "bad haptic contract: {w}"),
        }
    }
}
impl std::error::Error for HapticError {}

#[derive(Clone, Debug, PartialEq)]
pub struct HapticContract {
    pub sample_rate: u32,
    pub numeric: String,
    pub channels: u32,
    pub duration: u64,
}
impl HapticContract {
    pub fn to_canonical_json(&self) -> Vec<u8> {
        format!(
            "{{\"channels\":{},\"duration\":{},\"numeric\":\"{}\",\"sample_rate\":{}}}",
            self.channels, self.duration, self.numeric, self.sample_rate
        )
        .into_bytes()
    }
    pub fn from_json(b: &[u8]) -> Result<HapticContract, HapticError> {
        let v: serde_json::Value =
            serde_json::from_slice(b).map_err(|e| HapticError::BadContract(e.to_string()))?;
        let o = v.as_object().ok_or_else(|| HapticError::BadContract("not an object".into()))?;
        let u = |k: &str| o.get(k).and_then(|x| x.as_u64()).ok_or_else(|| HapticError::BadContract(format!("missing `{k}`")));
        let s = |k: &str| o.get(k).and_then(|x| x.as_str()).map(str::to_owned).ok_or_else(|| HapticError::BadContract(format!("missing `{k}`")));
        let sr = u("sample_rate")?;
        if sr == 0 || sr > u32::MAX as u64 {
            return Err(HapticError::BadContract("sample_rate out of range".into()));
        }
        let ch = u("channels")?;
        let duration = u("duration")?;
        let samples = duration.saturating_mul(ch.max(1));
        if samples > crate::limits::RENDER_MAX_SAMPLES {
            return Err(HapticError::BadContract(crate::limits::duration_too_long(
                "samples (duration × channels)", samples, crate::limits::RENDER_MAX_SAMPLES)));
        }
        Ok(HapticContract { sample_rate: sr as u32, numeric: s("numeric")?, channels: ch as u32, duration })
    }
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct HapticKey {
    pub pos_sample: u64,
    pub intensity: Fx,
    pub sharpness: Fx,
}

#[derive(Clone, Debug, PartialEq)]
pub struct HapticSignal {
    pub contract: HapticContract,
    pub contract_bytes: Vec<u8>,
    /// One keyframe list per channel.
    pub channels: Vec<Vec<HapticKey>>,
}

#[inline]
fn clamp01(v: Fx) -> Fx {
    if v.to_bits() < 0 { Fx::ZERO } else if v > Fx::ONE { Fx::ONE } else { v }
}

/// `Fx` [0,1] → u8, round half away from zero in integer space.
#[inline]
fn quant_u8(v: Fx) -> u8 {
    let scaled = clamp01(v) * Fx::from_int(255) + (Fx::ONE / Fx::from_int(2));
    (scaled.to_bits() / (1i64 << 32)).clamp(0, 255) as u8
}

fn interp(keys: &[HapticKey], t: u64) -> (Fx, Fx) {
    match keys {
        [] => (Fx::ZERO, Fx::ZERO),
        [k] => (k.intensity, k.sharpness),
        _ => {
            if t <= keys[0].pos_sample {
                return (keys[0].intensity, keys[0].sharpness);
            }
            let last = keys.last().unwrap();
            if t >= last.pos_sample {
                return (last.intensity, last.sharpness);
            }
            let mut a = &keys[0];
            for k in &keys[1..] {
                if t < k.pos_sample {
                    let span = (k.pos_sample - a.pos_sample) as i32;
                    let into = (t - a.pos_sample) as i32;
                    let frac = Fx::from_int(into) / Fx::from_int(span);
                    let l = |p: Fx, q: Fx| p + (q - p) * frac;
                    return (l(a.intensity, k.intensity), l(a.sharpness, k.sharpness));
                }
                a = k;
            }
            (last.intensity, last.sharpness)
        }
    }
}

impl HapticSignal {
    /// Reconstruct channel `c` to interleaved `[intensity, sharpness]` u8.
    pub fn reconstruct_channel(&self, c: usize) -> Vec<u8> {
        let keys = &self.channels[c];
        let n = self.contract.duration as usize;
        let mut out = Vec::with_capacity(n * 2);
        for t in 0..n {
            let (i, s) = interp(keys, t as u64);
            out.push(quant_u8(i));
            out.push(quant_u8(s));
        }
        out
    }

    /// `BLAKE3(domain || per-channel u8 pairs)` — the sample-equivalence
    /// quantity.
    pub fn signal_hash(&self) -> [u8; 32] {
        let mut h = blake3::Hasher::new();
        h.update(DOMAIN_SIGNAL);
        for c in 0..self.channels.len() {
            h.update(&self.reconstruct_channel(c));
        }
        *h.finalize().as_bytes()
    }

    pub fn to_bytes(&self) -> Vec<u8> {
        let mut chans = Vec::new();
        chans.extend_from_slice(&(self.channels.len() as u32).to_le_bytes());
        for ch in &self.channels {
            chans.extend_from_slice(&(ch.len() as u16).to_le_bytes());
            for k in ch {
                chans.extend_from_slice(&k.pos_sample.to_le_bytes());
                chans.extend_from_slice(&k.intensity.to_bits().to_le_bytes());
                chans.extend_from_slice(&k.sharpness.to_bits().to_le_bytes());
            }
        }
        let sections = [(SECTION_CONTRACT, &self.contract_bytes[..]), (SECTION_CHANNELS, &chans[..])];
        let mut out = Vec::new();
        out.extend_from_slice(MAGIC_WHAP);
        out.extend_from_slice(&(sections.len() as u16).to_le_bytes());
        let mut off = 0u32;
        let mut offs = Vec::new();
        for (_, b) in &sections {
            offs.push(off);
            off += b.len().div_ceil(8) as u32 * 8;
        }
        for ((kind, b), o) in sections.iter().zip(&offs) {
            out.push(*kind);
            out.extend_from_slice(&o.to_le_bytes());
            out.extend_from_slice(&(b.len() as u32).to_le_bytes());
        }
        for (_, b) in &sections {
            out.extend_from_slice(b);
            out.extend_from_slice(&[0u8; 8][..b.len().div_ceil(8) * 8 - b.len()]);
        }
        out
    }

    pub fn from_bytes(bytes: &[u8]) -> Result<HapticSignal, HapticError> {
        if bytes.len() < 6 {
            return Err(HapticError::Truncated);
        }
        if &bytes[..4] != MAGIC_WHAP {
            return Err(HapticError::BadMagic);
        }
        let n = u16::from_le_bytes(bytes[4..6].try_into().unwrap()) as usize;
        let table_end = 6 + n * 9;
        if bytes.len() < table_end {
            return Err(HapticError::Truncated);
        }
        let pay = &bytes[table_end..];
        let (mut contract_bytes, mut chbytes) = (None, None);
        for i in 0..n {
            let e = &bytes[6 + i * 9..6 + (i + 1) * 9];
            let off = u32::from_le_bytes(e[1..5].try_into().unwrap()) as usize;
            let len = u32::from_le_bytes(e[5..9].try_into().unwrap()) as usize;
            let end = off.checked_add(len).ok_or(HapticError::Truncated)?;
            if end > pay.len() {
                return Err(HapticError::Truncated);
            }
            match e[0] {
                SECTION_CONTRACT => contract_bytes = Some(pay[off..end].to_vec()),
                SECTION_CHANNELS => chbytes = Some(&pay[off..end]),
                _ => {}
            }
        }
        let contract_bytes = contract_bytes.ok_or(HapticError::MissingSection(SECTION_CONTRACT))?;
        let chbytes = chbytes.ok_or(HapticError::MissingSection(SECTION_CHANNELS))?;
        let contract = HapticContract::from_json(&contract_bytes)?;
        let channels = parse_channels(chbytes)?;
        Ok(HapticSignal { contract, contract_bytes, channels })
    }
}

fn parse_channels(b: &[u8]) -> Result<Vec<Vec<HapticKey>>, HapticError> {
    let rd = |p: usize, n: usize| -> Result<&[u8], HapticError> {
        if p + n > b.len() { Err(HapticError::Truncated) } else { Ok(&b[p..p + n]) }
    };
    let nc = u32::from_le_bytes(rd(0, 4)?.try_into().unwrap()) as usize;
    let mut p = 4usize;
    // Reserve no more entries than the bytes left could hold: the count is the input's,
    // and a reservation sized by it alone let a few bytes ask for gigabytes.
    let mut chans = Vec::with_capacity(nc.min(b.len() / 2));
    for _ in 0..nc {
        let nk = u16::from_le_bytes(rd(p, 2)?.try_into().unwrap()) as usize;
        p += 2;
        let mut keys = Vec::with_capacity(nk);
        for _ in 0..nk {
            let pos = u64::from_le_bytes(rd(p, 8)?.try_into().unwrap());
            let inten = i64::from_le_bytes(rd(p + 8, 8)?.try_into().unwrap());
            let sharp = i64::from_le_bytes(rd(p + 16, 8)?.try_into().unwrap());
            p += 24;
            keys.push(HapticKey { pos_sample: pos, intensity: Fx::from_bits(inten), sharpness: Fx::from_bits(sharp) });
        }
        chans.push(keys);
    }
    Ok(chans)
}

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

    fn sig(keys: Vec<HapticKey>, dur: u64) -> HapticSignal {
        let c = HapticContract { sample_rate: 1000, numeric: crate::world::NUMERIC_FIXED64.into(), channels: 1, duration: dur };
        HapticSignal { contract_bytes: c.to_canonical_json(), contract: c, channels: vec![keys] }
    }
    fn f(n: i32, d: i32) -> Fx {
        Fx::from_int(n) / Fx::from_int(d)
    }

    #[test]
    fn ramp_reconstructs_monotonically_and_hashes_stably() {
        let s = sig(
            vec![
                HapticKey { pos_sample: 0, intensity: Fx::ZERO, sharpness: f(1, 2) },
                HapticKey { pos_sample: 100, intensity: Fx::ONE, sharpness: f(1, 2) },
            ],
            100,
        );
        let buf = s.reconstruct_channel(0);
        assert_eq!(buf.len(), 200); // 100 samples × (intensity,sharpness)
        assert_eq!(buf[0], 0); // intensity starts at 0
        assert!(buf[198] > buf[0], "intensity ramps up");
        assert!(buf.iter().step_by(2).is_sorted(), "intensity is monotonic non-decreasing");
        assert_eq!(s.signal_hash(), s.signal_hash());
    }

    #[test]
    fn full_buzz_is_max_intensity() {
        let s = sig(vec![HapticKey { pos_sample: 0, intensity: Fx::ONE, sharpness: Fx::ONE }], 50);
        let buf = s.reconstruct_channel(0);
        assert!(buf.iter().all(|&b| b == 255), "constant full effect is 255 everywhere");
    }

    #[test]
    fn container_round_trips_and_distinct_signals_differ() {
        let s = sig(
            vec![
                HapticKey { pos_sample: 0, intensity: Fx::ZERO, sharpness: Fx::ZERO },
                HapticKey { pos_sample: 40, intensity: Fx::ONE, sharpness: f(3, 4) },
                HapticKey { pos_sample: 80, intensity: Fx::ZERO, sharpness: Fx::ZERO },
            ],
            80,
        );
        let back = HapticSignal::from_bytes(&s.to_bytes()).unwrap();
        assert_eq!(back.channels, s.channels);
        assert_eq!(back.contract, s.contract);
        assert_eq!(back.signal_hash(), s.signal_hash());
        // a different envelope → different hash
        let s2 = sig(vec![HapticKey { pos_sample: 0, intensity: f(1, 2), sharpness: Fx::ZERO }], 80);
        assert_ne!(s.signal_hash(), s2.signal_hash());
    }
}
