//! Ogg, FLAC, RIFF, MPEG audio and ADTS (media-identify §5.6 to §5.10). No samples are read.

use super::{be32, clean, get, le16, le32, Candidate, Source, Strength, Track, MAX_STEPS};
use crate::codecs::*;
use serde_json::json;
use std::io;

/// The most Ogg pages read: each is read whole, for its checksum.
const OGG_PAGES: u64 = 256;
/// The frames an MPEG audio or ADTS run needs.
const MIN_FRAMES: u64 = 3;

/// Ogg's checksum: the polynomial 0x04C11DB7, from zero, not reflected.
fn crc32_ogg(b: &[u8]) -> u32 {
    let mut c = 0u32;
    for &x in b {
        c ^= (x as u32) << 24;
        for _ in 0..8 {
            c = if c & 0x8000_0000 != 0 { (c << 1) ^ 0x04c1_1db7 } else { c << 1 };
        }
    }
    c
}

/// What the first packet of a logical stream says it holds.
fn ogg_stream(serial: u64, packet: &[u8]) -> Track {
    let mut t = Track { track: Some(serial), media: "other", ..Track::default() };
    if packet.starts_with(b"OpusHead") {
        t.media = "audio";
        t.stated = "OpusHead".into();
        t.codec = Some("opus");
        t.channels = packet.get(9).map(|&c| c as u64);
        t.sample_rate = le32(packet, 12);
    } else if packet.starts_with(b"\x01vorbis") {
        t.media = "audio";
        t.stated = "vorbis".into();
        t.codec = Some("vorbis");
        t.channels = packet.get(11).map(|&c| c as u64);
        t.sample_rate = le32(packet, 12);
    } else if packet.starts_with(b"\x7fFLAC") {
        t.media = "audio";
        t.stated = "FLAC".into();
        t.codec = Some("flac");
    } else if packet.starts_with(b"\x80theora") {
        t.media = "video";
        t.stated = "theora".into();
        t.codec = Some("theora");
    } else if packet.starts_with(b"Speex   ") {
        t.media = "audio";
        t.stated = "Speex".into();
        t.codec = Some("speex");
    } else {
        t.stated = clean(packet.get(..8.min(packet.len())).unwrap_or(&[]));
    }
    t
}

pub fn ogg(src: &dyn Source, head: &[u8]) -> io::Result<Option<Candidate>> {
    if head.get(..5) != Some(b"OggS\x00") {
        return Ok(None);
    }
    let size = src.size();
    let mut c = Candidate::new("ogg", "container");
    let (mut pos, mut pages) = (0u64, 0u64);
    loop {
        if pos == size {
            break;
        }
        if pages == OGG_PAGES {
            c.stop(pos, "limit");
            break;
        }
        let h = get(src, pos, 27)?;
        if h.len() < 27 {
            c.stop(pos, "overrun");
            break;
        }
        if &h[..5] != b"OggS\x00" {
            c.stop(pos, "invalid");
            break;
        }
        let nseg = h[26] as usize;
        let table = get(src, pos + 27, nseg)?;
        if table.len() < nseg {
            c.stop(pos, "overrun");
            break;
        }
        let body_len: usize = table.iter().map(|&s| s as usize).sum();
        let body = get(src, pos + 27 + nseg as u64, body_len)?;
        if body.len() < body_len {
            c.stop(pos, "overrun");
            break;
        }
        let mut page = h.clone();
        page[22..26].fill(0);
        page.extend_from_slice(&table);
        page.extend_from_slice(&body);
        if crc32_ogg(&page) as u64 != le32(&h, 22).unwrap_or(0) {
            c.stop(pos, "crc");
            break;
        }
        c.see(pos, (27 + nseg + body_len) as u64, "page");
        if h[5] & 2 != 0 {
            c.track(ogg_stream(le32(&h, 14).unwrap_or(0), &body));
        }
        pos += (27 + nseg + body_len) as u64;
        pages += 1;
    }
    c.walked_to = Some(pos);
    c.fact("pages_read", pages);
    if pages >= 2 || (pages == 1 && pos == size) {
        c.strength = Strength::Structure;
    }
    Ok(Some(c))
}

/// Where the bytes after an ID3v2 tag begin, or 0.
fn after_id3(head: &[u8]) -> Option<u64> {
    if head.get(..3) != Some(b"ID3") {
        return Some(0);
    }
    let h = head.get(..10)?;
    if h[3] == 0xff || h[4] == 0xff || h[6..10].iter().any(|&b| b & 0x80 != 0) {
        return None;
    }
    let n = h[6..10].iter().fold(0u64, |a, &b| (a << 7) | b as u64);
    Some(10 + n + if h[5] & 0x10 != 0 { 10 } else { 0 })
}

pub fn flac(src: &dyn Source, head: &[u8]) -> io::Result<Option<Candidate>> {
    let Some(start) = after_id3(head) else { return Ok(None) };
    let magic = get(src, start, 4)?;
    if magic != b"fLaC" {
        return Ok(None);
    }
    let size = src.size();
    let mut c = Candidate::new("flac", "audio");
    c.cap(CAP_AUDIO_FLAC);
    c.see(start, 4, "fLaC");
    let mut pos = start + 4;
    let mut blocks = 0u64;
    let mut info = false;
    loop {
        if blocks == MAX_STEPS {
            c.stop(pos, "limit");
            break;
        }
        let h = get(src, pos, 4)?;
        if h.len() < 4 {
            c.stop(pos, "overrun");
            break;
        }
        let (last, ty, len) = (h[0] & 0x80 != 0, h[0] & 0x7f, ((h[1] as u64) << 16) | ((h[2] as u64) << 8) | h[3] as u64);
        if blocks == 0 {
            if ty != 0 || len != 34 {
                c.stop(pos, "invalid");
                break;
            }
            let b = get(src, pos + 4, 34)?;
            if b.len() < 34 {
                c.stop(pos, "overrun");
                break;
            }
            c.see(pos + 4, 34, "streaminfo");
            let rate = ((b[10] as u64) << 12) | ((b[11] as u64) << 4) | ((b[12] as u64) >> 4);
            let channels = (((b[12] >> 1) & 7) as u64) + 1;
            let bits = ((((b[12] & 1) as u64) << 4) | ((b[13] >> 4) as u64)) + 1;
            let samples = (((b[13] & 0x0f) as u64) << 32) | be32(&b, 14).unwrap_or(0);
            c.fact("sample_rate", rate);
            c.fact("channels", channels);
            c.fact("bits_per_sample", bits);
            c.fact("samples", samples);
            info = true;
        }
        if pos + 4 + len > size {
            c.stop(pos, "overrun");
            break;
        }
        pos += 4 + len;
        blocks += 1;
        if last {
            let f = get(src, pos, 2)?;
            if f.len() == 2 && f[0] == 0xff && f[1] & 0xfe == 0xf8 {
                c.see(pos, 2, "frame_sync");
                if info {
                    c.strength = Strength::Structure;
                }
            } else if !f.is_empty() {
                c.stop(pos, "invalid");
            } else {
                c.stop(pos, "overrun");
            }
            break;
        }
    }
    c.walked_to = Some(pos);
    Ok(Some(c))
}

pub fn riff(src: &dyn Source, head: &[u8]) -> io::Result<Option<Candidate>> {
    if head.get(..4) != Some(b"RIFF") {
        return Ok(None);
    }
    let Some(form) = head.get(8..12) else { return Ok(None) };
    let (id, kind) = match form {
        b"WAVE" => ("wave", "audio"),
        b"AVI " => ("avi", "container"),
        b"WEBP" => ("webp", "image"),
        _ => ("riff", "container"),
    };
    let size = src.size();
    let mut c = Candidate::new(id, kind);
    c.see(8, 4, format!("form:{}", clean(form)));
    c.fact("form", clean(form));
    let declared = le32(head, 4).unwrap_or(0).saturating_add(8);
    let end = declared.min(size);
    if declared != size {
        c.fact("declared_size", declared);
    }
    let (mut pos, mut chunks) = (12u64, 0u64);
    let mut names: Vec<String> = Vec::new();
    loop {
        if pos + 8 > end {
            if pos < end {
                c.stop(pos, "overrun");
            }
            break;
        }
        if chunks == MAX_STEPS {
            c.stop(pos, "limit");
            break;
        }
        let h = get(src, pos, 8)?;
        if h.len() < 8 {
            c.stop(pos, "overrun");
            break;
        }
        let ty = &h[..4];
        let len = le32(&h, 4).unwrap_or(0);
        let name = clean(ty);
        c.see(pos, 4, format!("chunk:{name}"));
        if !names.contains(&name) && names.len() < 16 {
            names.push(name);
        }
        match (id, ty) {
            ("wave", b"fmt ") => {
                let b = get(src, pos + 8, len.min(64) as usize)?;
                let mut tag = le16(&b, 0);
                if tag == Some(0xfffe) {
                    tag = le16(&b, 24);
                }
                c.fact("format_tag", tag);
                c.fact("channels", le16(&b, 2));
                c.fact("sample_rate", le32(&b, 4));
                c.fact("bits_per_sample", le16(&b, 14));
            }
            ("wave", b"data") => {
                c.fact("data_bytes", len);
            }
            ("webp", b"VP8 ") => {
                let b = get(src, pos + 8, 10)?;
                if b.get(3..6) == Some(&[0x9d, 0x01, 0x2a]) {
                    c.fact("coding", "lossy");
                    c.fact("width", le16(&b, 6).map(|v| v & 0x3fff));
                    c.fact("height", le16(&b, 8).map(|v| v & 0x3fff));
                }
            }
            ("webp", b"VP8L") => {
                let b = get(src, pos + 8, 5)?;
                if b.first() == Some(&0x2f) && b.len() == 5 {
                    let v = le32(&b, 1).unwrap_or(0);
                    c.fact("coding", "lossless");
                    c.fact("width", (v & 0x3fff) + 1);
                    c.fact("height", ((v >> 14) & 0x3fff) + 1);
                }
            }
            ("webp", b"VP8X") => {
                let b = get(src, pos + 8, 10)?;
                if b.len() == 10 {
                    let w = (b[4] as u64) | ((b[5] as u64) << 8) | ((b[6] as u64) << 16);
                    let h2 = (b[7] as u64) | ((b[8] as u64) << 8) | ((b[9] as u64) << 16);
                    c.fact("coding", "extended");
                    c.fact("width", w + 1);
                    c.fact("height", h2 + 1);
                }
            }
            _ => {}
        }
        let next = pos + 8 + len + (len & 1);
        if pos + 8 + len > end {
            c.stop(pos, "overrun");
            break;
        }
        pos = next;
        chunks += 1;
    }
    c.walked_to = Some(pos.min(end));
    c.fact("chunks", json!(names));
    if chunks >= 1 {
        c.strength = Strength::Structure;
    }
    Ok(Some(c))
}

/// An MPEG audio frame header: its fixed fields, and the frame's length.
pub(super) fn mpeg_frame(b: &[u8]) -> Option<(u8, u8, u64, u64, u8, u64)> {
    let h = b.get(..4)?;
    if h[0] != 0xff || h[1] & 0xe0 != 0xe0 {
        return None;
    }
    let version = (h[1] >> 3) & 3; // 0 = 2.5, 2 = 2, 3 = 1
    let layer = (h[1] >> 1) & 3; // 1 = III, 2 = II, 3 = I
    let br_i = (h[2] >> 4) as usize;
    let sr_i = ((h[2] >> 2) & 3) as usize;
    if version == 1 || layer == 0 || br_i == 0 || br_i == 15 || sr_i == 3 {
        return None;
    }
    const V1: [[u64; 15]; 3] = [
        [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320],
        [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384],
        [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448],
    ];
    const V2: [[u64; 15]; 2] = [
        [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160],
        [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256],
    ];
    let kbps = if version == 3 {
        V1[(layer - 1) as usize][br_i]
    } else if layer == 3 {
        V2[1][br_i]
    } else {
        V2[0][br_i]
    };
    let rate = [44100u64, 48000, 32000][sr_i] >> if version == 3 { 0 } else if version == 2 { 1 } else { 2 };
    let pad = ((h[2] >> 1) & 1) as u64;
    let len = if layer == 3 {
        (12 * kbps * 1000 / rate + pad) * 4
    } else if version != 3 && layer == 1 {
        72 * kbps * 1000 / rate + pad
    } else {
        144 * kbps * 1000 / rate + pad
    };
    Some((version, layer, kbps, rate, h[3] >> 6, len))
}

pub fn mpeg_audio(head: &[u8], size: u64) -> Option<Candidate> {
    let start = after_id3(head)?;
    let first = mpeg_frame(head.get(usize::try_from(start).ok()?..)?)?;
    let mut c = Candidate::new("mpeg-audio", "audio");
    if start > 0 {
        c.see(0, 3, "id3v2");
    }
    c.see(start, 4, "frame_header");
    c.fact("version", match first.0 { 3 => "1", 2 => "2", _ => "2.5" });
    c.fact("layer", 4 - first.1 as u64);
    c.fact("bitrate_kbps", first.2);
    c.fact("sample_rate", first.3);
    c.fact("channels", if first.4 == 3 { 1 } else { 2 });
    let mut pos = start;
    let mut frames = 0u64;
    loop {
        if frames == MAX_STEPS {
            c.stop(pos, "limit");
            break;
        }
        let Some(rest) = usize::try_from(pos).ok().and_then(|p| head.get(p..)) else { break };
        if rest.is_empty() {
            if pos < size {
                c.stop(pos, "limit");
            }
            break;
        }
        // A closing ID3v1 tag is the end of the frames.
        if rest.len() == 128 && rest.starts_with(b"TAG") {
            c.see(pos, 3, "id3v1");
            pos += 128;
            break;
        }
        let Some(f) = mpeg_frame(rest).filter(|f| f.0 == first.0 && f.1 == first.1 && f.3 == first.3) else {
            c.stop(pos, if rest.len() < 4 { "overrun" } else { "invalid" });
            break;
        };
        if pos + f.5 > size {
            c.stop(pos, "overrun");
            break;
        }
        if f.5 as usize > rest.len() {
            c.stop(pos, "limit");
            break;
        }
        pos += f.5;
        frames += 1;
    }
    c.walked_to = Some(pos);
    c.fact("frames_read", frames);
    if frames >= MIN_FRAMES {
        c.strength = Strength::Structure;
    }
    if frames == 0 {
        return None;
    }
    Some(c)
}

/// ADTS's sampling frequencies, by index (§5.10).
pub(super) const RATES: [u64; 13] = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350];

pub fn adts(head: &[u8], size: u64) -> Option<Candidate> {
    let frame = |b: &[u8]| -> Option<(u64, u64, u64, u64)> {
        let h = b.get(..7)?;
        if h[0] != 0xff || h[1] & 0xf6 != 0xf0 {
            return None;
        }
        let profile = (h[2] >> 6) as u64 + 1;
        let sr = ((h[2] >> 2) & 0x0f) as usize;
        let channels = (((h[2] & 1) as u64) << 2) | (h[3] >> 6) as u64;
        let len = (((h[3] & 3) as u64) << 11) | ((h[4] as u64) << 3) | (h[5] >> 5) as u64;
        if sr >= RATES.len() || len < 7 {
            return None;
        }
        Some((profile, RATES[sr], channels, len))
    };
    let first = frame(head)?;
    let mut c = Candidate::new("adts", "audio");
    c.cap(CAP_AUDIO_AAC);
    c.see(0, 7, "frame_header");
    c.fact("audio_object_type", first.0);
    c.fact("sample_rate", first.1);
    c.fact("channel_configuration", first.2);
    let (mut pos, mut frames) = (0u64, 0u64);
    loop {
        if frames == MAX_STEPS {
            c.stop(pos, "limit");
            break;
        }
        let rest = &head[pos as usize..];
        if rest.is_empty() {
            if pos < size {
                c.stop(pos, "limit");
            }
            break;
        }
        let Some(f) = frame(rest).filter(|f| f.0 == first.0 && f.1 == first.1) else {
            c.stop(pos, if rest.len() < 7 { "overrun" } else { "invalid" });
            break;
        };
        if pos + f.3 > size {
            c.stop(pos, "overrun");
            break;
        }
        if f.3 as usize > rest.len() {
            c.stop(pos, "limit");
            break;
        }
        pos += f.3;
        frames += 1;
    }
    c.walked_to = Some(pos);
    c.fact("frames_read", frames);
    if frames >= MIN_FRAMES {
        c.strength = Strength::Structure;
    }
    if frames == 0 {
        return None;
    }
    Some(c)
}
