//! HLS playlists and DASH manifests (media-identify §5.18, §5.19): what the first bytes of the text
//! state, line by line and by substring. No URI is followed, and neither is parsed as more than that.

use super::{clean, Candidate, Strength};
use serde_json::json;

/// The most `codecs` values reported.
const MAX_CODECS: usize = 16;

fn strip_bom(b: &[u8]) -> &[u8] {
    b.strip_prefix(b"\xef\xbb\xbf").unwrap_or(b)
}

/// The value of `name="..."` in `b`, at or after its first occurrence.
fn attr<'a>(b: &'a [u8], name: &[u8]) -> Option<&'a [u8]> {
    let at = find(b, name)?;
    let rest = &b[at + name.len()..];
    let end = rest.iter().position(|&c| c == b'"')?;
    Some(&rest[..end])
}

fn find(b: &[u8], needle: &[u8]) -> Option<usize> {
    b.windows(needle.len()).position(|w| w == needle)
}

fn count(b: &[u8], needle: &[u8]) -> u64 {
    let (mut n, mut i) = (0, 0);
    while let Some(at) = find(&b[i..], needle) {
        n += 1;
        i += at + needle.len();
    }
    n
}

pub fn hls(head: &[u8]) -> Option<Candidate> {
    let b = strip_bom(head);
    let offset = (head.len() - b.len()) as u64;
    let first = b.strip_prefix(b"#EXTM3U")?;
    if !matches!(first.first(), Some(b'\r' | b'\n') | None) {
        return None;
    }
    let mut c = Candidate::new("hls", "playlist");
    c.see(offset, 7, "extm3u");
    let (mut variants, mut segments, mut tags, mut maps) = (0u64, 0u64, 0u64, 0u64);
    let mut key = None;
    let mut version = None;
    let mut codecs = None;
    let mut ended = false;
    for line in b.split(|&x| x == b'\n') {
        let line = line.strip_suffix(b"\r").unwrap_or(line);
        if line.starts_with(b"#EXT-X-") {
            tags += 1;
        }
        if line.starts_with(b"#EXT-X-STREAM-INF:") {
            variants += 1;
            if codecs.is_none() {
                codecs = attr(line, b"CODECS=\"").map(clean);
            }
        } else if line.starts_with(b"#EXTINF:") {
            segments += 1;
        } else if line.starts_with(b"#EXT-X-MAP:") {
            maps += 1;
        } else if line.starts_with(b"#EXT-X-ENDLIST") {
            ended = true;
        } else if let Some(v) = line.strip_prefix(b"#EXT-X-VERSION:") {
            version = std::str::from_utf8(v).ok().and_then(|s| s.parse::<u64>().ok());
        } else if line.starts_with(b"#EXT-X-KEY:") || line.starts_with(b"#EXT-X-SESSION-KEY:") {
            let method = line.windows(7).position(|w| w == b"METHOD=").map(|i| {
                let rest = &line[i + 7..];
                let end = rest.iter().position(|&x| x == b',').unwrap_or(rest.len());
                &rest[..end]
            });
            if let Some(m) = method.filter(|m| *m != b"NONE") {
                key.get_or_insert_with(|| clean(m));
            }
        }
    }
    c.fact("playlist", if variants > 0 { "multivariant" } else if segments > 0 { "media" } else { "other" });
    c.fact("variants", variants);
    c.fact("segments", segments);
    c.fact("maps", maps);
    c.fact("ended", ended);
    c.fact("version", version);
    c.fact("key_method", key);
    c.fact("codecs", codecs);
    if tags > 0 {
        c.strength = Strength::Structure;
    }
    Some(c)
}

pub fn dash(head: &[u8]) -> Option<Candidate> {
    let b = strip_bom(head);
    let mut i = 0usize;
    let skip = |i: &mut usize| {
        while b.get(*i).is_some_and(u8::is_ascii_whitespace) {
            *i += 1;
        }
    };
    skip(&mut i);
    loop {
        if b[i..].starts_with(b"<?") {
            i += find(&b[i..], b"?>")? + 2;
        } else if b[i..].starts_with(b"<!--") {
            i += find(&b[i..], b"-->")? + 3;
        } else {
            break;
        }
        skip(&mut i);
    }
    let at = i;
    let rest = b[i..].strip_prefix(b"<MPD")?;
    if !rest.first().is_some_and(|&c| c.is_ascii_whitespace() || c == b'>') {
        return None;
    }
    let mut c = Candidate::new("dash", "playlist");
    c.see((head.len() - b.len() + at) as u64, 4, "mpd");
    if let Some(n) = find(b, b"urn:mpeg:dash:schema:mpd:2011") {
        c.see((head.len() - b.len() + n) as u64, 29, "namespace");
        c.strength = Strength::Structure;
    }
    let root = &rest[..rest.iter().position(|&x| x == b'>').unwrap_or(rest.len())];
    c.fact("type", attr(root, b" type=\"").map(clean));
    c.fact("periods", count(b, b"<Period"));
    c.fact("adaptation_sets", count(b, b"<AdaptationSet"));
    c.fact("representations", count(b, b"<Representation"));
    c.fact("content_protection", count(b, b"<ContentProtection"));
    let mut codecs: Vec<String> = Vec::new();
    let mut j = 0;
    while let Some(at) = find(&b[j..], b"codecs=\"") {
        let s = j + at + 8;
        let Some(end) = b[s..].iter().position(|&x| x == b'"') else { break };
        let v = clean(&b[s..s + end]);
        if !codecs.contains(&v) && codecs.len() < MAX_CODECS {
            codecs.push(v);
        }
        j = s + end;
    }
    c.fact("codecs", json!(codecs));
    Some(c)
}
