//! Header probes for the sink-supplied formats the registry names (extensions/format-probes.md):
//! each identifies a payload as the format its capability names and reads the parameters
//! its headers state, or refuses it with one of [`Refusal::ALL`]'s codes. No probe decodes
//! a picture or a sound; the decoders stay sink-supplied.
//!
//! Every probe is pure integer code over bytes, with no dependency beyond `serde_json`, so
//! it builds for wasm32. Its result is a JSON object of the format's parameters, written
//! the same way on every machine (keys in sorted order).

use crate::codecs::*;
use serde_json::Value;

pub mod avs3;
pub mod bits;
pub mod corpus;
pub mod hevc;
pub mod jxs;
pub mod lcevc;
pub mod mhas;
pub mod nic;
pub mod projection;
pub mod vvc;

/// Why a probe refuses a payload (format-probes §12).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Refusal {
    /// The payload does not begin with the format's signature.
    NoSignature,
    /// The bytes end inside a structure the probe reads.
    Truncated,
    /// A structure is where the format does not allow it.
    OutOfOrder,
    /// A field holds a value the format forbids or reserves.
    ReservedValue,
    /// A length a structure declares disagrees with the bytes.
    LengthMismatch,
    /// A structure the format requires is absent.
    MissingStructure,
    /// A `wai.video.mvhevc` payload with no second view.
    NotMultiview,
    /// The framing of a `wai.video.jxs` payload is wrong.
    FramingInvalid,
    /// Bytes follow the end of the payload's last structure.
    TrailingBytes,
    /// A projection descriptor's fields disagree with each other.
    DescriptorInvalid,
    /// A projection descriptor names a layer its rendition does not hold.
    LayerAbsent,
    /// The payload uses a feature whose syntax the probe's sources do not establish.
    OutsideProbe,
    /// No probe is defined for the capability.
    NoProbe,
}

impl Refusal {
    /// Every refusal, in the order of format-probes §12's table.
    pub const ALL: [Refusal; 13] = [
        Refusal::NoSignature, Refusal::Truncated, Refusal::OutOfOrder, Refusal::ReservedValue,
        Refusal::LengthMismatch, Refusal::MissingStructure, Refusal::NotMultiview,
        Refusal::FramingInvalid, Refusal::TrailingBytes, Refusal::DescriptorInvalid,
        Refusal::LayerAbsent, Refusal::OutsideProbe, Refusal::NoProbe,
    ];

    pub fn code(self) -> &'static str {
        match self {
            Refusal::NoSignature => "no_signature",
            Refusal::Truncated => "truncated",
            Refusal::OutOfOrder => "out_of_order",
            Refusal::ReservedValue => "reserved_value",
            Refusal::LengthMismatch => "length_mismatch",
            Refusal::MissingStructure => "missing_structure",
            Refusal::NotMultiview => "not_multiview",
            Refusal::FramingInvalid => "framing_invalid",
            Refusal::TrailingBytes => "trailing_bytes",
            Refusal::DescriptorInvalid => "descriptor_invalid",
            Refusal::LayerAbsent => "layer_absent",
            Refusal::OutsideProbe => "outside_probe",
            Refusal::NoProbe => "no_probe",
        }
    }

    pub fn from_code(code: &str) -> Option<Refusal> {
        Refusal::ALL.iter().copied().find(|r| r.code() == code)
    }
}

impl std::fmt::Display for Refusal {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.code())
    }
}

impl std::error::Error for Refusal {}

/// Where a probe's syntax comes from (format-probes §2.3).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Basis {
    /// The format's own openly readable text.
    Standard,
    /// Open implementations of a format whose text is not openly readable.
    Implementation,
    /// WAI's own format.
    Wai,
}

impl Basis {
    pub fn label(self) -> &'static str {
        match self {
            Basis::Standard => "standard",
            Basis::Implementation => "implementation",
            Basis::Wai => "wai",
        }
    }
}

/// The capabilities with a probe, each with its basis, in format-probes §2.4's order.
pub const PROBES: &[(&str, Basis)] = &[
    (CAP_IMAGE_JXS, Basis::Implementation),
    (CAP_VIDEO_JXS, Basis::Implementation),
    (CAP_VIDEO_HEVC, Basis::Standard),
    (CAP_VIDEO_MVHEVC, Basis::Standard),
    (CAP_VIDEO_VVC, Basis::Standard),
    (CAP_VIDEO_LCEVC, Basis::Implementation),
    (CAP_AUDIO_MPEGH, Basis::Implementation),
    (CAP_VIDEO_AVS3, Basis::Implementation),
    (CAP_AUDIO_AVS3, Basis::Implementation),
    (CAP_IMAGE_IEEE1857_11, Basis::Implementation),
    (CAP_META_PROJECTION, Basis::Wai),
];

/// The basis of `capability`'s probe, or `None` when it has none.
pub fn basis(capability: &str) -> Option<Basis> {
    PROBES.iter().find(|(c, _)| *c == capability).map(|&(_, b)| b)
}

/// Probes `payload` as the payload of `capability`.
pub fn probe(capability: &str, payload: &[u8]) -> Result<Value, Refusal> {
    match capability {
        CAP_IMAGE_JXS => jxs::probe_image(payload),
        CAP_VIDEO_JXS => jxs::probe_video(payload),
        CAP_VIDEO_HEVC => hevc::probe(payload, false),
        CAP_VIDEO_MVHEVC => hevc::probe(payload, true),
        CAP_VIDEO_VVC => vvc::probe(payload),
        CAP_VIDEO_LCEVC => lcevc::probe(payload),
        CAP_AUDIO_MPEGH => mhas::probe(payload),
        CAP_VIDEO_AVS3 => avs3::probe_video(payload),
        CAP_AUDIO_AVS3 => avs3::probe_audio(payload),
        CAP_IMAGE_IEEE1857_11 => nic::probe(payload),
        CAP_META_PROJECTION => projection::probe(payload),
        _ => Err(Refusal::NoProbe),
    }
}

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

    #[test]
    fn every_probed_capability_is_registered_and_dispatches() {
        for &(c, _) in PROBES {
            assert!(is_known_capability(c), "{c}");
            assert_ne!(probe(c, &[]), Err(Refusal::NoProbe), "{c}");
        }
        assert_eq!(probe(CAP_IMAGE_PNG, &[]), Err(Refusal::NoProbe));
    }

    /// A payload is whatever arrives: no probe may panic on it, whatever its bytes.
    #[test]
    fn no_bytes_make_a_probe_panic() {
        let mut x = 0x9e37_79b9_7f4a_7c15u64;
        let mut next = move || {
            x ^= x << 13;
            x ^= x >> 7;
            x ^= x << 17;
            x
        };
        // What the probes' framing is made of, so that noise reaches past their first checks.
        const EDGE: [u8; 14] = [0, 0, 0, 1, 2, 3, 0x40, 0x42, 0x44, 0xb0, 0xb3, 0xff, 0x10, 0x7c];
        const STARTS: [&[u8]; 8] = [
            b"", b"\x00\x00\x01", b"\x00\x00\x01\x00\x00\x01", b"\x00\x00\x01\xb0", b"\x00\x00\x01\x40\x01",
            b"\x00\x00\x01\x7c", b"\xff\x10\xff\x50", b"\xc0\x01\x10",
        ];
        for round in 0..30_000u32 {
            let mut b = STARTS[round as usize % STARTS.len()].to_vec();
            for _ in 0..next() as usize % 128 {
                let r = next();
                b.push(if r & 1 == 0 { EDGE[(r >> 8) as usize % EDGE.len()] } else { (r >> 16) as u8 });
            }
            for &(c, _) in PROBES {
                let _ = probe(c, &b);
            }
        }
    }

    #[test]
    fn adjacent_start_codes_are_refused_and_do_not_panic() {
        for c in [CAP_VIDEO_AVS3, CAP_AUDIO_AVS3, CAP_VIDEO_HEVC, CAP_VIDEO_VVC, CAP_VIDEO_LCEVC] {
            assert!(probe(c, b"\x00\x00\x01\x00\x00\x01\xb0\x00\x00\x01").is_err(), "{c}");
        }
    }

    #[test]
    fn codes_are_distinct_and_read_back() {
        for r in Refusal::ALL {
            assert_eq!(Refusal::from_code(r.code()), Some(r));
        }
        let mut codes: Vec<&str> = Refusal::ALL.iter().map(|r| r.code()).collect();
        codes.sort_unstable();
        codes.dedup();
        assert_eq!(codes.len(), Refusal::ALL.len());
    }
}
