//! The MPEG-H 3D Audio probe (`wai.audio.mpegh`; format-probes §7): the MHAS packet
//! stream, its synchronisation and configuration packets, and the start of
//! `mpegh3daConfig( )`. ISO/IEC 23008-3's text is not openly readable; the packet syntax
//! and the configuration's first fields are the ones two independent open decoders agree
//! on, and the profile-level codes of the low-complexity profile are confirmed by ATSC
//! A/342-3:2024-04 §5.2.2.1 (format-probes §7.1).

use super::bits::{BitReader, BitWriter};
use super::Refusal;
use serde_json::{json, Value};

pub const PACTYP_FILLDATA: u64 = 0;
pub const PACTYP_MPEGH3DACFG: u64 = 1;
pub const PACTYP_MPEGH3DAFRAME: u64 = 2;
pub const PACTYP_SYNC: u64 = 6;
pub const SYNC_BYTE: u8 = 0xa5;

/// `escapedValue( nBits1, nBits2, nBits3 )`.
pub fn escaped(r: &mut BitReader, n1: u32, n2: u32, n3: u32) -> Result<u64, Refusal> {
    let mut v = r.u(n1)?;
    if v == (1 << n1) - 1 {
        let add = r.u(n2)?;
        v += add;
        if add == (1 << n2) - 1 {
            v += r.u(n3)?;
        }
    }
    Ok(v)
}

/// The writer's inverse of [`escaped`] (the corpus generator's).
pub fn write_escaped(w: &mut BitWriter, v: u64, n1: u32, n2: u32, n3: u32) {
    let m1 = (1u64 << n1) - 1;
    let m2 = (1u64 << n2) - 1;
    if v < m1 {
        w.u(n1, v);
    } else if v - m1 < m2 {
        w.u(n1, m1);
        w.u(n2, v - m1);
    } else {
        w.u(n1, m1);
        w.u(n2, m2);
        w.u(n3, v - m1 - m2);
    }
}

/// The sampling rates of `usacSamplingFrequencyIndex` 0 to 27; `None` where reserved.
pub const SAMPLING_RATES: [Option<u32>; 31] = [
    Some(96000), Some(88200), Some(64000), Some(48000), Some(44100), Some(32000), Some(24000),
    Some(22050), Some(16000), Some(12000), Some(11025), Some(8000), Some(7350), None, None,
    Some(57600), Some(51200), Some(40000), Some(38400), Some(34150), Some(28800), Some(25600),
    Some(20000), Some(19200), Some(17075), Some(14400), Some(12800), Some(9600), None, None, None,
];

/// One packet: its type, label and payload.
pub struct Packet<'a> {
    pub kind: u64,
    pub label: u64,
    pub payload: &'a [u8],
}

/// Splits an MHAS stream into packets. Every packet header is a whole number of bytes.
pub fn packets(b: &[u8]) -> Result<Vec<Packet<'_>>, Refusal> {
    let mut out = Vec::new();
    let mut at = 0usize;
    while at < b.len() {
        let mut r = BitReader::new(&b[at..]);
        let kind = escaped(&mut r, 3, 8, 8)?;
        let label = escaped(&mut r, 2, 8, 32)?;
        let len = escaped(&mut r, 11, 24, 24)? as usize;
        debug_assert!(r.byte_aligned());
        let start = at + r.position() / 8;
        let payload = b.get(start..start.checked_add(len).ok_or(Refusal::Truncated)?).ok_or(Refusal::Truncated)?;
        out.push(Packet { kind, label, payload });
        at = start + len;
    }
    Ok(out)
}

/// The profile-level indication's profile and level, for the codes this probe names.
fn profile_level(pl: u64) -> (Option<&'static str>, Option<u64>) {
    match pl {
        0x0b..=0x0f => (Some("low-complexity"), Some(pl - 0x0a)),
        0x10..=0x14 => (Some("baseline"), Some(pl - 0x0f)),
        _ => (None, None),
    }
}

pub fn probe(b: &[u8]) -> Result<Value, Refusal> {
    // The signature: the first packet is a synchronisation or a configuration packet.
    let first = escaped(&mut BitReader::new(b), 3, 8, 8).map_err(|_| Refusal::NoSignature)?;
    if first != PACTYP_SYNC && first != PACTYP_MPEGH3DACFG {
        return Err(Refusal::NoSignature);
    }
    let ps = packets(b)?;
    let mut cfg: Option<Value> = None;
    let (mut frames, mut syncs, mut configs) = (0u64, 0u64, 0u64);
    for p in &ps {
        match p.kind {
            PACTYP_SYNC => {
                if p.label != 0 || p.payload != [SYNC_BYTE] {
                    return Err(Refusal::ReservedValue);
                }
                syncs += 1;
            }
            PACTYP_MPEGH3DACFG => {
                configs += 1;
                if cfg.is_none() {
                    cfg = Some(config(p.payload)?);
                }
            }
            PACTYP_MPEGH3DAFRAME => {
                if cfg.is_none() {
                    return Err(Refusal::OutOfOrder);
                }
                frames += 1;
            }
            _ => {}
        }
    }
    let Some(mut out) = cfg else { return Err(Refusal::MissingStructure) };
    out["packets"] = json!(ps.len());
    out["frames"] = json!(frames);
    out["syncs"] = json!(syncs);
    out["configs"] = json!(configs);
    Ok(out)
}

/// The start of `mpegh3daConfig( )`: the profile-level indication, the sampling rate, the
/// core frame length index, and the reference layout of `SpeakerConfig3d( )`.
pub fn config(b: &[u8]) -> Result<Value, Refusal> {
    let mut r = BitReader::new(b);
    let pl = r.u(8)?;
    let sfi = r.u(5)?;
    let rate = if sfi == 0x1f {
        r.u(24)?
    } else {
        u64::from(SAMPLING_RATES[sfi as usize].ok_or(Refusal::ReservedValue)?)
    };
    if rate == 0 {
        return Err(Refusal::ReservedValue);
    }
    let core = r.u(3)?;
    let _reserved = r.u(1)?;
    let _receiver_delay = r.u(1)?;
    let layout_type = r.u(2)?;
    let (mut cicp, mut speakers) = (None, None);
    if layout_type == 0 {
        cicp = Some(r.u(6)?);
    } else if layout_type < 3 {
        speakers = Some(escaped(&mut r, 5, 8, 16)? + 1);
    }
    let (profile, level) = profile_level(pl);
    Ok(json!({
        "profile_level": pl,
        "profile": profile,
        "level": level,
        "sampling_rate": rate,
        "core_frame_length_index": core,
        "speaker_layout_type": layout_type,
        "cicp_layout": cicp,
        "speakers": speakers,
    }))
}
