//! ISO base media files (media-identify §5.3): the top-level boxes in order, the brands, and each
//! track as `moov` states it. The samples are not read.

use super::{be16, be32, be64, clean, get, Candidate, Source, Strength, Track, MAX_STEPS};
use serde_json::json;
use std::io;

/// A box may begin a file only with one of these types.
const FIRST: &[&[u8; 4]] = &[b"ftyp", b"styp", b"moov", b"mdat", b"moof", b"free", b"skip", b"wide", b"sidx", b"pnot"];

/// The longest `moov` read.
const MAX_MOOV: u64 = 64 << 20;
/// The most brands and box types reported.
const MAX_LISTED: usize = 16;

/// A sample entry's type, as a codec.
pub(super) fn codec(fourcc: &[u8]) -> Option<&'static str> {
    Some(match fourcc {
        b"avc1" | b"avc2" | b"avc3" | b"avc4" | b"dvav" | b"dva1" => "avc",
        b"hvc1" | b"hev1" | b"dvh1" | b"dvhe" => "hevc",
        b"vvc1" | b"vvi1" => "vvc",
        b"av01" | b"dav1" => "av1",
        b"vp08" => "vp8",
        b"vp09" => "vp9",
        b"evc1" => "evc",
        b"lvc1" => "lcevc",
        b"jxsm" => "jpeg-xs",
        b"mjp2" => "jpeg2000",
        b"Opus" => "opus",
        b"fLaC" => "flac",
        b"ac-3" => "ac3",
        b"ec-3" => "eac3",
        b"ac-4" => "ac4",
        b"mhm1" | b"mhm2" | b"mha1" | b"mha2" => "mpegh",
        b"iamf" => "iamf",
        b"wvtt" => "webvtt",
        b"stpp" => "ttml",
        _ => return None,
    })
}

/// The boxes inside `b`, stopping at the first that does not fit.
fn children(mut b: &[u8]) -> Vec<(&[u8], &[u8])> {
    let mut out = Vec::new();
    while let Some(size) = be32(b, 0) {
        let Some(ty) = b.get(4..8) else { break };
        let (hdr, len) = match size {
            0 => (8, b.len() as u64),
            1 => match be64(b, 8) {
                Some(l) if l >= 16 => (16, l),
                _ => break,
            },
            n if n < 8 => break,
            n => (8, n),
        };
        let Some(content) = usize::try_from(len).ok().and_then(|l| b.get(hdr..l)) else { break };
        out.push((ty, content));
        b = &b[len as usize..];
        if out.len() as u64 == MAX_STEPS {
            break;
        }
    }
    out
}

fn child<'a>(b: &'a [u8], ty: &[u8; 4]) -> Option<&'a [u8]> {
    children(b).into_iter().find(|(t, _)| t == ty).map(|(_, c)| c)
}

/// An MPEG-4 descriptor's size at `i`, and the index after it.
fn descr_size(b: &[u8], mut i: usize) -> Option<(usize, usize)> {
    let mut n = 0usize;
    for _ in 0..4 {
        let c = *b.get(i)?;
        i += 1;
        n = (n << 7) | (c & 0x7f) as usize;
        if c & 0x80 == 0 {
            return Some((n, i));
        }
    }
    None
}

/// The codec an `esds` box states: the object type of its decoder configuration.
fn esds_codec(b: &[u8]) -> Option<&'static str> {
    let mut i = 4;
    if *b.get(i)? != 3 {
        return None;
    }
    let (_, mut j) = descr_size(b, i + 1)?;
    let flags = *b.get(j + 2)?;
    j += 3;
    if flags & 0x80 != 0 {
        j += 2;
    }
    if flags & 0x40 != 0 {
        j += 1 + *b.get(j)? as usize;
    }
    if flags & 0x20 != 0 {
        j += 2;
    }
    i = j;
    if *b.get(i)? != 4 {
        return None;
    }
    let (_, k) = descr_size(b, i + 1)?;
    match *b.get(k)? {
        0x40 | 0x66 | 0x67 | 0x68 => Some("aac"),
        0x69 | 0x6b => Some("mp3"),
        _ => None,
    }
}

/// A protected sample entry's scheme and what it protects (`sinf`).
fn protection(entries: &[u8]) -> (Option<serde_json::Value>, Option<Vec<u8>>) {
    let Some(sinf) = child(entries, b"sinf") else {
        return (Some(json!({"scheme": null, "original": null})), None);
    };
    let original = child(sinf, b"frma").and_then(|f| f.get(..4)).map(<[u8]>::to_vec);
    let scheme = child(sinf, b"schm").and_then(|s| s.get(4..8)).map(clean);
    (Some(json!({"scheme": scheme, "original": original.as_deref().map(clean)})), original)
}

fn track(trak: &[u8]) -> Option<Track> {
    let mut t = Track::default();
    if let Some(tkhd) = child(trak, b"tkhd") {
        t.track = match tkhd.first()? {
            0 => be32(tkhd, 12),
            _ => be32(tkhd, 20),
        };
    }
    let mdia = child(trak, b"mdia")?;
    let handler = child(mdia, b"hdlr").and_then(|h| h.get(8..12))?;
    t.media = match handler {
        b"vide" | b"auxv" | b"pict" => "video",
        b"soun" => "audio",
        b"text" | b"subt" | b"sbtl" | b"clcp" => "text",
        _ => "other",
    };
    let stbl = child(child(mdia, b"minf")?, b"stbl")?;
    let stsd = child(stbl, b"stsd")?;
    let entries = stsd.get(8..)?;
    let (ty, entry) = children(entries).into_iter().next()?;
    t.stated = clean(ty);
    let mut named: Vec<u8> = ty.to_vec();
    // The sample entry's own fields come before its child boxes.
    let inner: &[u8] = match t.media {
        "video" => {
            t.width = be16(entry, 24);
            t.height = be16(entry, 26);
            entry.get(78..).unwrap_or(&[])
        }
        "audio" => {
            t.channels = be16(entry, 16);
            t.sample_rate = be32(entry, 24).map(|r| r >> 16);
            if be16(entry, 8) == Some(0) { entry.get(28..).unwrap_or(&[]) } else { &[] }
        }
        _ => &[],
    };
    if matches!(ty, b"encv" | b"enca" | b"enct" | b"encs") {
        let (p, original) = protection(inner);
        t.protected = p;
        if let Some(o) = original {
            named = o;
        }
    }
    t.codec = if named == b"mp4a" { child(inner, b"esds").and_then(esds_codec) } else { codec(&named) };
    Some(t)
}

pub fn walk(src: &dyn Source, head: &[u8]) -> io::Result<Option<Candidate>> {
    let Some(first) = head.get(4..8) else { return Ok(None) };
    if !FIRST.iter().any(|t| t.as_slice() == first) {
        return Ok(None);
    }
    let size = src.size();
    let mut c = Candidate::new("isobmff", "container");
    let (mut pos, mut headers, mut steps, mut fragments) = (0u64, 0u64, 0u64, 0u64);
    let mut boxes: Vec<String> = Vec::new();
    loop {
        if pos == size {
            break;
        }
        if steps == MAX_STEPS {
            c.stop(pos, "limit");
            break;
        }
        let h = get(src, pos, 16)?;
        let Some(s) = be32(&h, 0) else {
            c.stop(pos, "overrun");
            break;
        };
        let Some(ty) = h.get(4..8) else {
            c.stop(pos, "overrun");
            break;
        };
        if !ty.iter().all(|b| (0x20..0x7f).contains(b) || *b == 0xa9) {
            c.stop(pos, "invalid");
            break;
        }
        let (hdr, len) = match s {
            0 => (8u64, size - pos),
            1 => match be64(&h, 8) {
                Some(l) if l >= 16 => (16, l),
                Some(_) => {
                    c.stop(pos, "invalid");
                    break;
                }
                None => {
                    c.stop(pos, "overrun");
                    break;
                }
            },
            n if n < 8 => {
                c.stop(pos, "invalid");
                break;
            }
            n => (8, n),
        };
        headers += 1;
        let name = clean(ty);
        c.see(pos + 4, 4, format!("box:{name}"));
        if !boxes.contains(&name) && boxes.len() < MAX_LISTED {
            boxes.push(name);
        }
        if ty == b"moof" {
            fragments += 1;
        }
        let end = pos.checked_add(len).filter(|&e| e <= size);
        // A box that does not fit in the file is named and not read.
        if end.is_some() {
            if ty == b"ftyp" {
                let b = get(src, pos + hdr, (len - hdr).min(4096) as usize)?;
                if b.len() < 8 {
                    c.stop(pos, "invalid");
                    break;
                }
                c.fact("major_brand", clean(&b[..4]));
                let listed = b[8..].as_chunks::<4>().0;
                let brands: Vec<String> = listed.iter().take(MAX_LISTED).map(|x| clean(x)).collect();
                c.fact("compatible_brands", json!(brands));
                for brand in listed.iter().map(|x| x.as_slice()).chain(std::iter::once(&b[..4])) {
                    if brand == b"avif" || brand == b"avis" {
                        c.cap(crate::codecs::CAP_IMAGE_AVIF);
                    }
                }
            } else if ty == b"moov" {
                if len > MAX_MOOV {
                    c.fact("moov_not_read", true);
                } else {
                    let b = get(src, pos + hdr, (len - hdr) as usize)?;
                    for (t, content) in children(&b) {
                        if t == b"trak"
                            && let Some(tr) = track(content)
                        {
                            c.track(tr);
                        }
                    }
                }
            }
        }
        let Some(end) = end else {
            c.stop(pos, "overrun");
            break;
        };
        pos = end;
        steps += 1;
    }
    c.walked_to = Some(pos);
    c.fact("boxes", json!(boxes));
    if fragments > 0 {
        c.fact("fragments", fragments);
    }
    if headers >= 2 {
        c.strength = Strength::Structure;
    }
    Ok(Some(c))
}
