//! Deterministic **integer inter-frame video** decode — the temporal extension of
//! "conformance is a hash".
//!
//! A video decode that propagates a float reference feature from frame to frame
//! lets a ~1e-5 drift compound down the GOP, so its reconstruction is reproducible
//! only on the same build. In this decoder each frame is reconstructed by **integer** block motion compensation
//! from the previous *reconstructed* frame plus an **integer-quantized** residual
//! (rANS-coded), with no float anywhere — so every frame is byte-identical on
//! every machine. That is *frame-state-equivalence*: a per-frame portable hash, a
//! whole clip you can put in a receipt.
//!
//! The decode loop, all i64: read the residual symbols for the frame from the rANS
//! stream (raster `y, x, c`); the prediction is a constant grey for the I-frame and
//! `prev[clamp(y+dy), clamp(x+dx)]` per block for a P-frame; the frame is
//! `clamp(prediction + r·q, 0, 255)`. The payload is `WIV1`
//! (`extensions/integer-payloads.md` §3), parsed by `int_payload::wire` and decoded
//! here through [`decode_typed`] / [`decode_pframes_typed`]; the JSON the
//! `tools/wai_int_video_*encode.py` scripts write is a converter input, read by
//! [`VideoJson`]. Pure i64, no `ort`, no float, wasm-ready.

use serde_json::Value;

/// FNV-1a 64-bit — the per-frame / per-clip checksum the encoder and this decoder
/// share to prove byte-for-byte agreement across machines.
pub fn fnv1a(bytes: &[u8]) -> u64 {
    let mut hash: u64 = 14695981039346656037;
    for &b in bytes {
        hash = (hash ^ b as u64).wrapping_mul(1099511628211);
    }
    hash
}

/// Minimal standard-alphabet base64 decode (the bitstream carries the rANS bytes
/// base64'd so the committed vector stays compact). Dependency-free.
fn b64_decode(s: &str) -> Result<Vec<u8>, &'static str> {
    fn val(c: u8) -> Result<u8, &'static str> {
        match c {
            b'A'..=b'Z' => Ok(c - b'A'),
            b'a'..=b'z' => Ok(c - b'a' + 26),
            b'0'..=b'9' => Ok(c - b'0' + 52),
            b'+' => Ok(62),
            b'/' => Ok(63),
            _ => Err("bad base64"),
        }
    }
    let mut out = Vec::with_capacity(s.len() / 4 * 3);
    let mut chunk = [0u8; 4];
    let mut n = 0usize;
    let mut pad = 0usize;
    for &c in s.as_bytes() {
        if c == b'\n' || c == b'\r' {
            continue;
        }
        if c == b'=' {
            chunk[n] = 0;
            pad += 1;
        } else {
            chunk[n] = val(c)?;
        }
        n += 1;
        if n == 4 {
            out.push((chunk[0] << 2) | (chunk[1] >> 4));
            if pad < 2 {
                out.push((chunk[1] << 4) | (chunk[2] >> 2));
            }
            if pad < 1 {
                out.push((chunk[2] << 6) | chunk[3]);
            }
            n = 0;
            pad = 0;
        }
    }
    Ok(out)
}

/// A decoded clip: `frames[f]` is RGB row-major **HWC** `u8` (`h·w·3` bytes).
pub struct DecodedClip {
    pub frames: Vec<Vec<u8>>,
    pub h: usize,
    pub w: usize,
}

/// A JSON member as a `usize`, refused when it is absent, not an integer, or does not
/// fit the target's `usize` (so a 32-bit sink refuses what it cannot index rather than
/// truncating it).
fn u(v: &Value, k: &'static str) -> Result<usize, &'static str> {
    v[k].as_u64().and_then(|n| usize::try_from(n).ok()).ok_or(k)
}

/// The most elements one decoded frame may hold (`h·w·3`), the same on every target.
///
/// cbindgen:ignore
pub const MAX_FRAME: usize = 1 << 26;
/// The most bytes a whole decoded clip may hold (`n_frames·h·w·3`).
///
/// cbindgen:ignore
pub const MAX_CLIP: usize = 1 << 30;
/// The most frames a clip may hold.
///
/// cbindgen:ignore
pub const MAX_FRAMES: usize = 1 << 20;
/// The most elements the motion-vector field may hold (`2·nb·(n_frames − 1)`).
///
/// cbindgen:ignore
pub const MAX_MV: usize = 1 << 26;

/// The sizes a clip's header declares, checked in the order the extension's §3 caps
/// them, with checked arithmetic: the frame `h·w·3`, the clip `n_frames·h·w·3`, the
/// frame count, and the motion-vector field `2·nb·(n_frames − 1)`. Returns the frame's
/// element count and `nb`.
fn caps(n_frames: usize, h: usize, w: usize, block: usize) -> Result<(usize, usize), &'static str> {
    let frame = h.checked_mul(w).and_then(|v| v.checked_mul(3)).filter(|&n| n <= MAX_FRAME)
        .ok_or("a frame exceeds 2^26 elements")?;
    frame.checked_mul(n_frames).filter(|&n| n <= MAX_CLIP).ok_or("the clip exceeds 2^30 bytes")?;
    if n_frames > MAX_FRAMES {
        return Err("the clip exceeds 2^20 frames");
    }
    // h·w ≤ 2^26, so nb fits.
    let nb = h.div_ceil(block) * w.div_ceil(block);
    nb.checked_mul(n_frames.saturating_sub(1)).and_then(|v| v.checked_mul(2)).filter(|&n| n <= MAX_MV)
        .ok_or("the motion vectors exceed 2^26 elements")?;
    Ok((frame, nb))
}
/// The refusal [`decode_typed`] and [`decode_pframes_typed`] give when the residual
/// stream runs out before the clip's last symbol.
///
/// cbindgen:ignore
pub const STREAM_EXHAUSTED: &str = "the residual stream ended before its last symbol";
/// The refusal [`decode_typed`] and [`decode_pframes_typed`] give when the residual
/// stream does not end canonically after the clip's last symbol: a word left unread,
/// or a state other than `2^31` (`extensions/integer-payloads.md` §2).
///
/// cbindgen:ignore
pub const STREAM_NONCANONICAL: &str = "the residual stream does not end canonically after its last symbol";

/// A clip's coded form, typed: what the `WIV1` payload carries (and what the tool's
/// JSON form carries, read by [`VideoJson`]). Both decode entry points take it.
///
/// `mvs` holds frames `1..n_frames` in order, each its block grid row-major
/// (`⌈h/block⌉·⌈w/block⌉` entries), as `(dy, dx)` in units of `1/mv_precision` pel.
#[derive(Debug, Clone, Copy)]
pub struct VideoStream<'a> {
    pub n_frames: usize,
    pub h: usize,
    pub w: usize,
    pub block: usize,
    pub q: i64,
    pub offset: i32,
    /// `1` whole-pel, `2` half-pel.
    pub mv_precision: u8,
    pub cdf: &'a [i32],
    pub mvs: &'a [(i16, i16)],
    pub rans: &'a [u8],
}

/// The tool's JSON bitstream (`tools/wai_int_video_*encode.py`), read into the typed
/// form. The JSON form is a converter input and a debugging aid; the payload a
/// capability carries is `WIV1` (`extensions/integer-payloads.md`). What the old JSON
/// decoder left implicit is made explicit here, so a JSON bitstream and the `WIV1`
/// converted from it decode alike: an absent `mv_precision` is 1 (only 1 and 2 are
/// read); `mvs` has one list per frame and the first is empty; a frame's list shorter
/// than its block grid is padded with `(0, 0)`, the vector the old decoder read for a
/// missing block; a vector outside `i16`, or a list longer than the grid, is refused.
/// `rmax` is not read.
#[derive(Debug, Clone)]
pub struct VideoJson {
    pub n_frames: usize,
    pub h: usize,
    pub w: usize,
    pub block: usize,
    pub q: i64,
    pub offset: i32,
    pub mv_precision: u8,
    pub cdf: Vec<i32>,
    pub mvs: Vec<(i16, i16)>,
    pub rans: Vec<u8>,
}

impl VideoJson {
    /// Read the tool's JSON bitstream.
    pub fn from_value(bs: &Value) -> Result<Self, &'static str> {
        Self::read(bs, None)
    }

    /// [`Self::from_value`] at the geometry `(h, w)` instead of the bitstream's own
    /// (the P-frames of a clip whose I-frame came from elsewhere).
    fn read(bs: &Value, geometry: Option<(usize, usize)>) -> Result<Self, &'static str> {
        let (block, nf) = (u(bs, "block")?, u(bs, "n_frames")?);
        let (h, w) = match geometry {
            Some(g) => g,
            None => (u(bs, "h")?, u(bs, "w")?),
        };
        if nf == 0 || h == 0 || w == 0 || block == 0 {
            return Err("n_frames, h, w or block is zero");
        }
        // Every size is capped before anything is sized from it: a short list of
        // motion vectors is padded below, so the field's size is the header's, not the
        // JSON's.
        let (_, nb) = caps(nf, h, w, block)?;
        let q = bs["q"].as_i64().ok_or("q")?;
        let offset = bs["offset"].as_i64().and_then(|o| i32::try_from(o).ok()).ok_or("offset")?;
        let cdf = bs["cdf"].as_array().ok_or("cdf")?.iter()
            .map(|n| n.as_i64().and_then(|v| i32::try_from(v).ok()))
            .collect::<Option<Vec<i32>>>().ok_or("cdf holds a value outside i32")?;
        let mv_precision = match bs.get("mv_precision") {
            None => 1,
            Some(p) => match p.as_u64() {
                Some(1) => 1,
                Some(2) => 2,
                _ => return Err("mv_precision is neither 1 nor 2"),
            },
        };
        let lists = bs["mvs"].as_array().ok_or("mvs")?;
        if lists.len() != nf {
            return Err("mvs has not one list per frame");
        }
        if lists.first().and_then(Value::as_array).is_some_and(|l| !l.is_empty()) {
            return Err("the first frame carries motion vectors");
        }
        let mut mvs = Vec::with_capacity(nb * (nf - 1));
        for fr in lists.iter().skip(1) {
            let list = fr.as_array().ok_or("a frame's motion vectors are not a list")?;
            if list.len() > nb {
                return Err("a frame carries more motion vectors than blocks");
            }
            for m in list {
                let comp = |i: usize| m.get(i).and_then(Value::as_i64).and_then(|v| i16::try_from(v).ok());
                mvs.push((comp(0).ok_or("a motion vector is outside i16")?, comp(1).ok_or("a motion vector is outside i16")?));
            }
            mvs.resize(mvs.len() + nb - list.len(), (0, 0));
        }
        let rans = b64_decode(bs["rans_b64"].as_str().ok_or("rans_b64")?)?;
        Ok(VideoJson { n_frames: nf, h, w, block, q, offset, mv_precision, cdf, mvs, rans })
    }

    /// The typed form, borrowing this one's tables.
    pub fn stream(&self) -> VideoStream<'_> {
        VideoStream {
            n_frames: self.n_frames, h: self.h, w: self.w, block: self.block, q: self.q, offset: self.offset,
            mv_precision: self.mv_precision, cdf: &self.cdf, mvs: &self.mvs, rans: &self.rans,
        }
    }
}

/// E10 `mc_predict` (`extensions/integer-operators.md` §5E) on an RGB8 frame `rf`
/// (HWC, `h × w`): the prediction of channel `c` of pixel `(y, x)` from the reference
/// with the vector `(dy, dx)` in `prec`-th-pixel units (1 = whole-pel, 2 = half-pel), as
/// the decode forms it. The encoder predicts with it. 0 for a pixel outside the frame
/// or a precision other than 1 or 2, which no caller forms.
#[inline]
#[allow(clippy::too_many_arguments)]
pub fn predict(rf: &[u8], y: usize, x: usize, dy: i64, dx: i64, c: usize, prec: i64, h: usize, w: usize) -> i64 {
    let prec = u32::try_from(prec).unwrap_or(0);
    wai_ops::stage::mc_predict_at(|yy, xx| rf[(yy * w + xx) * 3 + c] as i64, h as i64, w as i64, y as i64, x as i64, dy, dx, prec).unwrap_or(0)
}

/// The checks both decode entry points make before allocating a frame: every field
/// in range, the CDF a table the rANS decoder is total over, the frame, clip, frame
/// count and motion-vector field under their caps ([`caps`]), and the motion-vector
/// count the block grid's, all formed with checked arithmetic. Returns the frame's
/// element count and `nb`.
pub(crate) fn check(s: &VideoStream<'_>) -> Result<(usize, usize), &'static str> {
    if s.n_frames == 0 || s.h == 0 || s.w == 0 || s.block == 0 {
        return Err("n_frames, h, w or block is zero");
    }
    if !(1..=65_535).contains(&s.q) {
        return Err("q is outside 1..=65535");
    }
    if !matches!(s.mv_precision, 1 | 2) {
        return Err("mv_precision is neither 1 nor 2");
    }
    let c = s.cdf;
    if c.len() < 3 || c.len() > 4097 || c[0] != 0 || c[c.len() - 1] != 1 << 16 || c.windows(2).any(|p| p[0] >= p[1]) {
        return Err("the CDF is not a table from 0 to 65536, strictly increasing, of 3..=4097 entries");
    }
    let (frame, nb) = caps(s.n_frames, s.h, s.w, s.block)?;
    if (s.n_frames - 1).checked_mul(nb) != Some(s.mvs.len()) {
        return Err("the motion vectors are not one per block for frames 1..n_frames");
    }
    Ok((frame, nb))
}

/// The most bytes [`decode_typed`] holds at once, beyond the payload and its parsed
/// tables, on a stream [`check`] admits: the clip (`n_frames·h·w·3`) and the frame list
/// (one `Vec` header of 24 bytes per frame on a 64-bit target).
pub fn working_bytes(s: &VideoStream<'_>) -> u64 {
    let frame = (s.h * s.w * 3) as u64;
    frame * s.n_frames as u64 + std::mem::size_of::<Vec<u8>>() as u64 * s.n_frames as u64
}

/// Decode a typed clip whose I-frame is predicted from grey (`WIV1` keyframe 0,
/// `wai.video.int_motion`) → frames, byte-exact on every machine, through the integer
/// operator set's stage operators ([`ops::decode_typed`]: E1, E10 and E9). Refuses,
/// rather than panicking on or misreading, a stream that breaks [`check`]'s rules or
/// runs out before its last symbol ([`STREAM_EXHAUSTED`], at the first symbol that runs
/// it out). Pure integer.
pub fn decode_typed(s: &VideoStream<'_>) -> Result<DecodedClip, &'static str> {
    ops::decode_typed(s)
}

/// Decode the **P-frames** of a typed clip on top of an already-decoded I-frame
/// (`i_frame`, RGB HWC `u8`, `h·w·3` bytes at the stream's `h × w`) — `WIV1`
/// keyframe 1 (`wai.video.int_hyper`), whose I-frame is the nested `WIH1`'s decode.
/// The residual stream holds frames `1..n_frames` only. Through the stage operators
/// ([`ops::decode_pframes_typed`]). Pure integer.
pub fn decode_pframes_typed(i_frame: &[u8], s: &VideoStream<'_>) -> Result<DecodedClip, &'static str> {
    ops::decode_pframes_typed(i_frame, s)
}

/// The direct decode, as it stood before the clip decoded through the integer operator
/// set ([`ops`]): the crate's rANS decoder, its own predictor, and the step written
/// inline.
/// Test-only (`legacy_int`): the differential and the parity tests hold the decode
/// through the operator set to it.
#[cfg(any(test, feature = "legacy_int"))]
pub mod legacy {
    use super::{check, DecodedClip, VideoStream, STREAM_EXHAUSTED, STREAM_NONCANONICAL};
    use crate::rans::RansDecoder;

    /// The direct decode's predictor, as it stood: one channel of one pixel from the
    /// reference with a motion vector
    /// `(dy, dx)` in `prec`-th-pixel units (1 = whole-pel, 2 = half-pel). Half-pel
    /// positions are bilinearly interpolated with integer rounding — the byte-exact
    /// integer analog of the float sub-pixel filters real video codecs use, so
    /// sub-pixel motion (real footage) predicts well while the decode stays exact.
    #[inline]
    #[allow(clippy::too_many_arguments)]
    pub fn predict(rf: &[u8], y: usize, x: usize, dy: i64, dx: i64, c: usize, prec: i64, h: usize, w: usize) -> i64 {
        let p = |yy: i64, xx: i64| -> i64 {
            let cy = yy.clamp(0, h as i64 - 1) as usize;
            let cx = xx.clamp(0, w as i64 - 1) as usize;
            rf[(cy * w + cx) * 3 + c] as i64
        };
        if prec == 1 {
            return p(y as i64 + dy, x as i64 + dx);
        }
        // prec == 2: half-pel bilinear (integer rounding)
        let (iy, fy) = (y as i64 + (dy >> 1), dy & 1);
        let (ix, fx) = (x as i64 + (dx >> 1), dx & 1);
        match (fy, fx) {
            (0, 0) => p(iy, ix),
            (0, _) => (p(iy, ix) + p(iy, ix + 1) + 1) >> 1,
            (_, 0) => (p(iy, ix) + p(iy + 1, ix) + 1) >> 1,
            _ => (p(iy, ix) + p(iy, ix + 1) + p(iy + 1, ix) + p(iy + 1, ix + 1) + 2) >> 2,
        }
    }

    /// One frame (from a grey `prev` of `None`, the first frame of keyframe 0), appended
    /// to `out` sample by sample as its residual symbols are read (raster `y, x, c`) and
    /// added to the prediction. The frame is never allocated ahead of the symbols that
    /// fill it, and the first symbol during whose decode the stream runs out refuses the
    /// clip ([`STREAM_EXHAUSTED`]); its value is not used. `r·q` is under `2^47` in
    /// magnitude (the decoder saturates a symbol to `i32`, `q ≤ 65535`), so the sum fits
    /// `i64`.
    fn frame(s: &VideoStream<'_>, dec: &mut RansDecoder<'_>, prev: Option<&[u8]>, mvs: &[(i16, i16)], out: &mut Vec<u8>) -> Result<(), &'static str> {
        let (h, w, block) = (s.h, s.w, s.block);
        let n = h * w * 3;
        let nbx = w.div_ceil(block);
        let prec = s.mv_precision as i64;
        for y in 0..h {
            for x in 0..w {
                let (dy, dx) = mvs.get((y / block) * nbx + (x / block)).map_or((0, 0), |&(a, b)| (a as i64, b as i64));
                for c in 0..3 {
                    let pred = match prev {
                        None => 128,
                        Some(rf) => predict(rf, y, x, dy, dx, c, prec, h, w),
                    };
                    let r = dec.decode(s.cdf, s.cdf.len(), s.offset) as i64; // residual symbol
                    if dec.is_exhausted() {
                        return Err(STREAM_EXHAUSTED);
                    }
                    crate::int_hyper_synth::push_capped(out, (pred + r * s.q).clamp(0, 255) as u8, n);
                }
            }
        }
        Ok(())
    }

    /// Frames `first..n_frames` of a clip, each predicted from the last frame of `frames`
    /// (which holds the frames before `first`), appended to `frames`. Each frame grows a
    /// sample at a time to its `h·w·3` bytes and no further, so the clip holds its
    /// declared bytes and the frame list ([`working_bytes`](super::working_bytes)).
    fn decode_frames(s: &VideoStream<'_>, nb: usize, first: usize, frames: &mut Vec<Vec<u8>>) -> Result<(), &'static str> {
        let mut dec = RansDecoder::new(s.rans)?;
        for f in first..s.n_frames {
            let mut cur = Vec::new();
            match f {
                0 => frame(s, &mut dec, None, &[], &mut cur)?,
                _ => frame(s, &mut dec, frames.last().map(Vec::as_slice), &s.mvs[(f - 1) * nb..f * nb], &mut cur)?,
            }
            frames.push(cur);
        }
        if !dec.is_canonical_end() {
            return Err(STREAM_NONCANONICAL);
        }
        Ok(())
    }

    /// Decode a typed clip whose I-frame is predicted from grey (`WIV1` keyframe 0,
    /// `wai.video.int_motion`) → frames, byte-exact on every machine. Refuses, rather
    /// than panicking on or misreading, a stream that breaks [`check`](super::check)'s
    /// rules or runs out before its last symbol ([`STREAM_EXHAUSTED`], at the first symbol
    /// that runs it out). Pure i64.
    pub fn decode_typed(s: &VideoStream<'_>) -> Result<DecodedClip, &'static str> {
        let (_, nb) = check(s)?;
        let mut frames = Vec::with_capacity(s.n_frames);
        decode_frames(s, nb, 0, &mut frames)?;
        Ok(DecodedClip { frames, h: s.h, w: s.w })
    }

    /// Decode the **P-frames** of a typed clip on top of an already-decoded I-frame
    /// (`i_frame`, RGB HWC `u8`, `h·w·3` bytes at the stream's `h × w`) — `WIV1`
    /// keyframe 1 (`wai.video.int_hyper`), whose I-frame is the nested `WIH1`'s decode.
    /// The residual stream holds frames `1..n_frames` only. Pure i64.
    pub fn decode_pframes_typed(i_frame: &[u8], s: &VideoStream<'_>) -> Result<DecodedClip, &'static str> {
        let (n, nb) = check(s)?;
        if i_frame.len() != n {
            return Err("i_frame size disagrees with h·w·3");
        }
        let mut frames = Vec::with_capacity(s.n_frames);
        frames.push(i_frame.to_vec());
        decode_frames(s, nb, 1, &mut frames)?;
        Ok(DecodedClip { frames, h: s.h, w: s.w })
    }
}

/// Decode the tool's JSON bitstream (`int_video_bitstream.json`) → frames, byte-exact
/// on every machine: [`VideoJson`] then [`decode_typed`]. A debugging and converter
/// path; the payload is `WIV1`. Pure i64.
pub fn decode(bs: &Value) -> Result<DecodedClip, &'static str> {
    decode_typed(&VideoJson::from_value(bs)?.stream())
}

/// Decode the **P-frames** of a JSON bitstream on top of an already-decoded I-frame
/// (`i_frame`, RGB HWC `u8`) at `h × w` — the integer block-MC + quantized residual
/// loop, seeded with frame 0 from any source. The NEURAL-KEYFRAME video composes this
/// with the learned `int_hyper` I-frame decode (`int_hyper_synth::decode_conditional_checked`
/// → frame 0): a learned, byte-exact keyframe + integer inter-frames, every frame
/// reproducible by hash. Pure i64. `bs`'s `n_frames` counts the I-frame; `mvs[0]`
/// is empty; the rANS stream holds only frames `1..n_frames`'s residuals. A debugging
/// and converter path; the payload is `WIV1` with keyframe 1.
pub fn decode_pframes(i_frame: &[u8], h: usize, w: usize, bs: &Value) -> Result<DecodedClip, &'static str> {
    decode_pframes_typed(i_frame, &VideoJson::read(bs, Some((h, w)))?.stream())
}

/// `WIV1` through the integer operator set `wai-ops/1`
/// (`extensions/integer-operators.md` §13.5): the stage operators its driver runs, with
/// no graph. A clip runs no network, so it has no lowering and no bounds certificate;
/// what it has is the operands each stage operator takes, its **stage form**
/// ([`stages`]):
///
/// - E1 `rans64_decode` with the modes [`MODES`] (`t4097`, `lenient`, `canonical`),
///   one stream for the whole clip, against the clip's one table at its `offset`;
/// - E10 `mc_predict` at the clip's `mv_precision`, from the previous decoded frame;
/// - E9 `residual_step` with the clip's `q` on [`RANGE`], `[0, 255]`, whose 32-bit form
///   holds there (§5E E9, *Width*).
///
/// Every stage operator works at the width §7.4 gives it: E1 in a 64-bit state, E10 in
/// sums of at most `4·255 + 2`, E9 in `i64`, or in `i32` by its identity.
pub mod ops {
    use wai_ops::stage::rans64::{self, End, Escape, Modes, Rans, TableRule};
    use wai_ops::stage::{mc_predict_at, residual_step};

    /// E9 as a driver calls it: `(pred, r, q, lo, hi)` to the sample, `None` outside its
    /// domain. [`residual_step`], or its 32-bit form.
    type Step = fn(i64, i64, i64, i64, i64) -> Option<i64>;

    use super::{DecodedClip, VideoStream, STREAM_EXHAUSTED, STREAM_NONCANONICAL};

    /// E1's modes for the residual stream (integer-payloads §2 and §3).
    ///
    /// cbindgen:ignore
    pub const MODES: Modes = Modes { table: TableRule::T4097, escape: Escape::Lenient, end: End::Canonical };
    /// E9's range: an RGB8 sample.
    ///
    /// cbindgen:ignore
    pub const RANGE: (i64, i64) = (0, 255);
    /// The prediction of every sample of frame 0 under keyframe 0 (integer-payloads §3).
    ///
    /// cbindgen:ignore
    pub const GREY: i64 = 128;

    /// A clip's stage form: what its decode hands each stage operator.
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    pub struct Stages<'a> {
        /// E1's table and its offset.
        pub cdf: &'a [i32],
        pub offset: i64,
        /// E10's `mv_precision`.
        pub precision: u32,
        /// E9's step; its range is [`RANGE`].
        pub q: i64,
        /// The frame's samples, `h·w·3`, and the block grid's size `nb`.
        pub frame: usize,
        pub nb: usize,
    }

    /// The stage form of a clip, after the payload's checks ([`super::check`]), whose
    /// refusal it gives first. Then each operand must be in its operator's domain: E1's
    /// table rule `t4097` and an offset in `i32`, E10's precision 1 or 2, and E9's step
    /// in `1..=65535`. The payload's checks make each of these, so a stage form that
    /// refuses after them names a disagreement between the payload text and the
    /// operator set.
    pub fn stages<'a>(s: &VideoStream<'a>) -> Result<Stages<'a>, &'static str> {
        let (frame, nb) = super::check(s)?;
        if !rans64::table_ok(s.cdf, MODES.table) {
            return Err("the residual table is not one E1's t4097 rule admits");
        }
        if !matches!(s.mv_precision, 1 | 2) {
            return Err("mv_precision is not one E10 takes");
        }
        if !(1..=65_535).contains(&s.q) {
            return Err("q is outside E9's domain");
        }
        Ok(Stages { cdf: s.cdf, offset: s.offset as i64, precision: s.mv_precision as u32, q: s.q, frame, nb })
    }

    /// The refusal of an operand that a stage operator does not take although the
    /// clip's checks admitted it: a disagreement between the payload text and the
    /// operator set, which no clip [`stages`] admits reaches.
    ///
    /// cbindgen:ignore
    pub const OUTSIDE: &str = "a stage operator refused an operand the clip's checks admit";

    /// E1's decoder over the clip's residual stream, refused as the payload's stream rule
    /// refuses it: a length that is not a multiple of 4, then one under 8.
    fn seed(rans: &[u8]) -> Result<Rans<'_>, &'static str> {
        if !rans.len().is_multiple_of(4) {
            return Err("rANS bitstream length not /4");
        }
        Rans::new(rans).ok_or("rANS bitstream too short to seed 64-bit state")
    }

    /// One frame, appended to `out` sample by sample as its residual symbols are read
    /// (raster `y, x, c`): its prediction is [`GREY`] where `prev` is `None` (frame 0 of
    /// keyframe 0), and otherwise E10 from `prev` with the pixel's block's vector; the
    /// symbol is E1's; the sample is E9's step from the prediction. The frame is never
    /// allocated ahead of the symbols that fill it, and the first symbol during whose
    /// decode the stream runs out refuses the clip ([`STREAM_EXHAUSTED`], E1's
    /// `canonical` end rule); its value is not used.
    fn frame(s: &VideoStream<'_>, f: &Stages<'_>, step: Step, dec: &mut Rans<'_>, prev: Option<&[u8]>, mvs: &[(i16, i16)], out: &mut Vec<u8>) -> Result<(), &'static str> {
        let (h, w, block) = (s.h, s.w, s.block);
        let nbx = w.div_ceil(block);
        for y in 0..h {
            for x in 0..w {
                let (dy, dx) = mvs.get((y / block) * nbx + (x / block)).map_or((0, 0), |&(a, b)| (a as i64, b as i64));
                for c in 0..3 {
                    let pred = match prev {
                        None => GREY,
                        Some(rf) => mc_predict_at(|yy, xx| rf[(yy * w + xx) * 3 + c] as i64, h as i64, w as i64, y as i64, x as i64, dy, dx, f.precision)
                            .ok_or(OUTSIDE)?,
                    };
                    let r = dec.symbol_in(f.cdf, f.offset, MODES.escape).map_err(|_| OUTSIDE)?;
                    if dec.ran_out() {
                        return Err(STREAM_EXHAUSTED);
                    }
                    let v = step(pred, r, f.q, RANGE.0, RANGE.1).ok_or(OUTSIDE)?;
                    crate::int_hyper_synth::push_capped(out, v as u8, f.frame);
                }
            }
        }
        Ok(())
    }

    /// Frames `first..n_frames` of a clip, each predicted from the last frame of `frames`
    /// (which holds the frames before `first`), appended to `frames`, from one E1 decode
    /// of the whole residual stream, which must end canonically after the last symbol
    /// ([`STREAM_NONCANONICAL`]). Each frame grows a sample at a time to its `h·w·3`
    /// bytes and no further, so the clip holds its declared bytes and the frame list
    /// ([`working_bytes`](super::working_bytes)).
    fn decode_frames(s: &VideoStream<'_>, f: &Stages<'_>, step: Step, first: usize, frames: &mut Vec<Vec<u8>>) -> Result<(), &'static str> {
        let mut dec = seed(s.rans)?;
        for k in first..s.n_frames {
            let mut cur = Vec::new();
            match k {
                0 => frame(s, f, step, &mut dec, None, &[], &mut cur)?,
                _ => frame(s, f, step, &mut dec, frames.last().map(Vec::as_slice), &s.mvs[(k - 1) * f.nb..k * f.nb], &mut cur)?,
            }
            frames.push(cur);
        }
        if !dec.ends_canonically() {
            return Err(STREAM_NONCANONICAL);
        }
        Ok(())
    }

    /// [`super::decode_typed`] through the stage operators: a clip whose I-frame is
    /// predicted from grey (`WIV1` keyframe 0, `wai.video.int_motion`), refused with the
    /// payload's checks first ([`stages`]), then at the first symbol that runs its
    /// stream out, then at an end that is not canonical.
    pub fn decode_typed(s: &VideoStream<'_>) -> Result<DecodedClip, &'static str> {
        decode_with(s, residual_step)
    }

    fn decode_with(s: &VideoStream<'_>, step: Step) -> Result<DecodedClip, &'static str> {
        let f = stages(s)?;
        let mut frames = Vec::with_capacity(s.n_frames);
        decode_frames(s, &f, step, 0, &mut frames)?;
        Ok(DecodedClip { frames, h: s.h, w: s.w })
    }

    /// [`super::decode_pframes_typed`] through the stage operators: the P-frames of a
    /// clip on top of an already-decoded I-frame (`WIV1` keyframe 1,
    /// `wai.video.int_hyper`), `h·w·3` bytes at the clip's `h × w`.
    pub fn decode_pframes_typed(i_frame: &[u8], s: &VideoStream<'_>) -> Result<DecodedClip, &'static str> {
        decode_pframes_with(i_frame, s, residual_step)
    }

    fn decode_pframes_with(i_frame: &[u8], s: &VideoStream<'_>, step: Step) -> Result<DecodedClip, &'static str> {
        let f = stages(s)?;
        if i_frame.len() != f.frame {
            return Err("i_frame size disagrees with h·w·3");
        }
        let mut frames = Vec::with_capacity(s.n_frames);
        frames.push(i_frame.to_vec());
        decode_frames(s, &f, step, 1, &mut frames)?;
        Ok(DecodedClip { frames, h: s.h, w: s.w })
    }

    /// [`decode_typed`] with E9 in its 32-bit form (§5E E9, *Width*): every sample's step
    /// in `i32`, on `[0, 255]`, where the form holds. The arithmetic a 32-bit fabric
    /// runs; the parity tests hold it to [`decode_typed`].
    #[cfg(feature = "i32_forms")]
    pub fn decode_typed_i32(s: &VideoStream<'_>) -> Result<DecodedClip, &'static str> {
        decode_with(s, wai_ops::stage::residual_step_by_i32)
    }

    /// [`decode_pframes_typed`] with E9 in its 32-bit form.
    #[cfg(feature = "i32_forms")]
    pub fn decode_pframes_typed_i32(i_frame: &[u8], s: &VideoStream<'_>) -> Result<DecodedClip, &'static str> {
        decode_pframes_with(i_frame, s, wai_ops::stage::residual_step_by_i32)
    }
}
