//! The markers of stitched-presentation §3 and the documents they are read from: an HLS
//! Media Playlist's `EXT-X-DATERANGE` tags, an interstitial's asset list, an MPD's events
//! (over the XML subset of §3.5), and the VAST response a pod-input record is checked
//! against (§7.2).
//!
//! Every reader here is a pure function of the bytes it is given: no I/O, no clock and no
//! floating point.

use sha2::{Digest, Sha256};

use super::{carriage, MARKER_DOMAIN};

/// The `CLASS` of an interstitial date range (`draft-pantos-hls-rfc8216bis-22` Appendix D.2,
/// as live-ads §5.1 writes it).
pub const INTERSTITIAL_CLASS: &str = "com.apple.hls.interstitial"; // allow-name: the interstitial date-range class, a protocol identifier

/// The prefix of a date range tag.
const DATERANGE: &str = "#EXT-X-DATERANGE:";

/// The most bytes of a field of a marker (§3.1): a long field's length.
const LONG: usize = u16::MAX as usize;

// ---------------------------------------------------------------------------------------
// §3.1 The marker and its digest
// ---------------------------------------------------------------------------------------

/// A marker: the fields that place one ad in a stream (§3), by carriage.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Marker {
    /// Carriage 1: a date range's `ID` and `START-DATE`.
    DateRange { id: Vec<u8>, start: Vec<u8> },
    /// Carriage 2: an interstitial date range's `ID` and `START-DATE`, and the asset's index
    /// in its asset list.
    Interstitial { id: Vec<u8>, start: Vec<u8>, index: u16 },
    /// Carriage 3: the event's Period `id`, its EventStream's `schemeIdUri` and `value`, and
    /// its own `id`; an absent attribute is empty.
    Event { period: Vec<u8>, scheme: Vec<u8>, value: Vec<u8>, id: Vec<u8> },
}

fn long(o: &mut Vec<u8>, b: &[u8]) {
    o.extend_from_slice(&(b.len() as u16).to_le_bytes());
    o.extend_from_slice(b);
}

impl Marker {
    /// Its carriage (§3).
    pub fn carriage(&self) -> u8 {
        match self {
            Marker::DateRange { .. } => carriage::HLS_DATERANGE,
            Marker::Interstitial { .. } => carriage::HLS_INTERSTITIAL,
            Marker::Event { .. } => carriage::DASH_EVENT,
        }
    }

    /// Its body (§3.1): the fields as long fields, and an interstitial's index as a u16.
    /// `None` when a field is longer than a long field holds.
    pub fn body(&self) -> Option<Vec<u8>> {
        let mut o = Vec::new();
        let fields: Vec<&[u8]> = match self {
            Marker::DateRange { id, start } | Marker::Interstitial { id, start, .. } => vec![id, start],
            Marker::Event { period, scheme, value, id } => vec![period, scheme, value, id],
        };
        for f in fields {
            if f.len() > LONG {
                return None;
            }
            long(&mut o, f);
        }
        if let Marker::Interstitial { index, .. } = self {
            o.extend_from_slice(&index.to_le_bytes());
        }
        Some(o)
    }

    /// Read a body of `carriage` (§3.1): exactly its fields. `None` when it does not read.
    pub fn from_body(carriage: u8, b: &[u8]) -> Option<Marker> {
        let mut p = 0usize;
        let mut field = || -> Option<Vec<u8>> {
            let n = u16::from_le_bytes(b.get(p..p + 2)?.try_into().ok()?) as usize;
            let f = b.get(p + 2..p + 2 + n)?.to_vec();
            p += 2 + n;
            Some(f)
        };
        let m = match carriage {
            carriage::HLS_DATERANGE => Marker::DateRange { id: field()?, start: field()? },
            carriage::HLS_INTERSTITIAL => {
                let (id, start) = (field()?, field()?);
                let index = u16::from_le_bytes(b.get(p..p + 2)?.try_into().ok()?);
                p += 2;
                Marker::Interstitial { id, start, index }
            }
            carriage::DASH_EVENT => Marker::Event { period: field()?, scheme: field()?, value: field()?, id: field()? },
            _ => return None,
        };
        (p == b.len()).then_some(m)
    }

    /// The marker digest (§3.1): SHA-256 over the domain, the carriage and the body.
    pub fn digest(&self) -> [u8; 32] {
        digest(self.carriage(), &self.body().expect("a marker of fields a long field holds"))
    }
}

/// The marker digest of a body of `carriage` (§3.1).
pub fn digest(carriage: u8, body: &[u8]) -> [u8; 32] {
    let mut h = Sha256::new();
    h.update(MARKER_DOMAIN);
    h.update([carriage]);
    h.update(body);
    h.finalize().into()
}

/// An ad a document names (§3.2 to §3.4): its marker and the ad identity it carries.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Found {
    pub marker: Marker,
    pub ad_id: Vec<u8>,
}

// ---------------------------------------------------------------------------------------
// §3.2 HLS: an attribute list and the date ranges of a Media Playlist
// ---------------------------------------------------------------------------------------

/// An attribute's value (§3.2): a quoted string, without its quotes, or the text of any
/// other value.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Value {
    Quoted(String),
    Plain(String),
}

impl Value {
    /// The value as the ad identity reads it: a quoted string's contents, or the text.
    pub fn text(&self) -> &str {
        match self {
            Value::Quoted(s) | Value::Plain(s) => s,
        }
    }
}

/// An attribute list (§3.2): `NAME=VALUE` pairs separated by commas, each name of `A`–`Z`,
/// `0`–`9` and `-` and named once; a quoted value holds no `"`, CR or LF; any other value is
/// not empty and holds no `"`, `,` or white space. `None` when it does not read.
pub fn attribute_list(s: &str) -> Option<Vec<(String, Value)>> {
    let mut out: Vec<(String, Value)> = Vec::new();
    let mut rest = s;
    loop {
        let (name, after) = rest.split_once('=')?;
        if name.is_empty() || !name.bytes().all(|b| b.is_ascii_uppercase() || b.is_ascii_digit() || b == b'-') || out.iter().any(|(n, _)| n == name) {
            return None;
        }
        let (value, after) = if let Some(q) = after.strip_prefix('"') {
            let close = q.find('"')?;
            let v = &q[..close];
            if v.contains(['\r', '\n']) {
                return None;
            }
            (Value::Quoted(v.to_string()), &q[close + 1..])
        } else {
            let end = after.find(',').unwrap_or(after.len());
            let v = &after[..end];
            if v.is_empty() || v.bytes().any(|b| b == b'"' || b.is_ascii_whitespace()) {
                return None;
            }
            (Value::Plain(v.to_string()), &after[end..])
        };
        out.push((name.to_string(), value));
        if after.is_empty() {
            return Some(out);
        }
        rest = after.strip_prefix(',')?;
        if rest.is_empty() {
            return None;
        }
    }
}

fn get<'a>(a: &'a [(String, Value)], name: &str) -> Option<&'a Value> {
    a.iter().find(|(n, _)| n == name).map(|(_, v)| v)
}

fn quoted<'a>(a: &'a [(String, Value)], name: &str) -> Option<&'a str> {
    match get(a, name)? {
        Value::Quoted(s) => Some(s),
        Value::Plain(_) => None,
    }
}

/// The lines of a playlist (§3.2): split at LF, a CR before it dropped. `None` unless it is
/// UTF-8 and begins `#EXTM3U`.
fn lines(b: &[u8]) -> Option<Vec<&str>> {
    let s = std::str::from_utf8(b).ok()?;
    let out: Vec<&str> = s.split('\n').map(|l| l.strip_suffix('\r').unwrap_or(l)).collect();
    (out.first() == Some(&"#EXTM3U")).then_some(out)
}

/// The date range tags of a playlist that read (§3.2): each one's attributes. A tag that
/// does not read, or whose `ID` or `START-DATE` is not a quoted string, names nothing.
fn dateranges(b: &[u8]) -> Option<Vec<Vec<(String, Value)>>> {
    Some(
        lines(b)?
            .into_iter()
            .filter_map(|l| l.strip_prefix(DATERANGE))
            .filter_map(attribute_list)
            .filter(|a| quoted(a, "ID").is_some() && quoted(a, "START-DATE").is_some())
            .collect(),
    )
}

/// Carriage 1 (§3.2): every date range of a Media Playlist that carries `id_attr`, and the
/// ad identity it carries there. `None` when the bytes are not a playlist.
pub fn hls_ads(playlist: &[u8], id_attr: &str) -> Option<Vec<Found>> {
    Some(
        dateranges(playlist)?
            .iter()
            .filter_map(|a| {
                let ad = get(a, id_attr)?.text().as_bytes().to_vec();
                let marker = Marker::DateRange { id: quoted(a, "ID")?.as_bytes().to_vec(), start: quoted(a, "START-DATE")?.as_bytes().to_vec() };
                Some(Found { marker, ad_id: ad })
            })
            .collect(),
    )
}

/// Carriage 2 (§3.3): the interstitial date ranges of a Media Playlist, as (`ID`,
/// `START-DATE`). `None` when the bytes are not a playlist.
pub fn interstitials(playlist: &[u8]) -> Option<Vec<(String, String)>> {
    Some(
        dateranges(playlist)?
            .iter()
            .filter(|a| quoted(a, "CLASS") == Some(INTERSTITIAL_CLASS))
            .filter_map(|a| Some((quoted(a, "ID")?.to_string(), quoted(a, "START-DATE")?.to_string())))
            .collect(),
    )
}

/// Carriage 2 (§3.3): the assets' URIs of an asset list, in order: a JSON object (RFC 8259)
/// whose `ASSETS` is an array of objects each with a string `URI`. `None` when it does not
/// read so.
pub fn asset_uris(asset_list: &[u8]) -> Option<Vec<String>> {
    let v: serde_json::Value = serde_json::from_slice(asset_list).ok()?;
    v.as_object()?.get("ASSETS")?.as_array()?.iter().map(|a| a.as_object()?.get("URI")?.as_str().map(String::from)).collect()
}

// ---------------------------------------------------------------------------------------
// §3.5 The XML subset
// ---------------------------------------------------------------------------------------

/// An element of an XML document (§3.5): its local name, its attributes as written (each
/// value with its references replaced), and its content in order.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Element {
    pub name: String,
    pub attrs: Vec<(String, String)>,
    pub children: Vec<Node>,
}

/// A node of an element's content.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Node {
    Element(Element),
    /// Character data: text with its references replaced, or a CDATA section's contents.
    Text(String),
}

impl Element {
    /// An attribute's value, by its local name.
    pub fn attr(&self, name: &str) -> Option<&str> {
        self.attrs.iter().find(|(n, _)| n == name).map(|(_, v)| v.as_str())
    }

    /// The child elements of a local name, in order.
    pub fn children_named<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a Element> + 'a {
        self.children.iter().filter_map(move |c| match c {
            Node::Element(e) if e.name == name => Some(e),
            _ => None,
        })
    }

    /// Its text (§3.5): its character data children, joined, with XML white space trimmed
    /// from both ends.
    pub fn text(&self) -> String {
        let s: String = self
            .children
            .iter()
            .filter_map(|c| match c {
                Node::Text(t) => Some(t.as_str()),
                Node::Element(_) => None,
            })
            .collect();
        s.trim_matches(|c| matches!(c, ' ' | '\t' | '\n' | '\r')).to_string()
    }
}

/// The most nested elements the subset reads (§3.5), the root counted as 1.
const MAX_DEPTH: usize = 64;

struct X<'a> {
    s: &'a str,
    p: usize,
}

fn local(qname: &str) -> String {
    qname.rsplit(':').next().unwrap_or(qname).to_string()
}

fn name_ok(n: &str) -> bool {
    let mut cs = n.chars();
    matches!(cs.next(), Some(c) if c.is_alphabetic() || c == '_' || c == ':') && cs.all(|c| c.is_alphanumeric() || matches!(c, '_' | '-' | '.' | ':'))
}

/// Replace the five predefined entity references and character references (§3.5).
fn unescape(t: &str) -> Option<String> {
    let mut out = String::with_capacity(t.len());
    let mut rest = t;
    while let Some(i) = rest.find('&') {
        out.push_str(&rest[..i]);
        let after = &rest[i + 1..];
        let end = after.find(';')?;
        let r = &after[..end];
        let c = match r {
            "lt" => '<',
            "gt" => '>',
            "amp" => '&',
            "quot" => '"',
            "apos" => '\'',
            _ if r.starts_with("#x") => char::from_u32(u32::from_str_radix(&r[2..], 16).ok()?)?,
            _ if r.starts_with('#') && r.len() > 1 && r[1..].bytes().all(|b| b.is_ascii_digit()) => char::from_u32(r[1..].parse().ok()?)?,
            _ => return None,
        };
        out.push(c);
        rest = &after[end + 1..];
    }
    out.push_str(rest);
    Some(out)
}

impl<'a> X<'a> {
    fn rest(&self) -> &'a str {
        &self.s[self.p..]
    }
    fn skip_ws(&mut self) {
        while let Some(c) = self.rest().chars().next().filter(|c| matches!(c, ' ' | '\t' | '\n' | '\r')) {
            self.p += c.len_utf8();
        }
    }
    fn eat(&mut self, t: &str) -> bool {
        if self.rest().starts_with(t) {
            self.p += t.len();
            true
        } else {
            false
        }
    }
    fn until(&mut self, t: &str) -> Option<&'a str> {
        let i = self.rest().find(t)?;
        let out = &self.rest()[..i];
        self.p += i + t.len();
        Some(out)
    }
    /// Comments and processing instructions, and white space between them.
    fn misc(&mut self) -> Option<()> {
        loop {
            self.skip_ws();
            if self.eat("<!--") {
                self.until("-->")?;
            } else if self.rest().starts_with("<!") {
                return None; // a document type declaration, or anything else the subset leaves out
            } else if self.eat("<?") {
                self.until("?>")?;
            } else {
                return Some(());
            }
        }
    }
    fn name(&mut self) -> Option<&'a str> {
        let r = self.rest();
        let end = r.find([' ', '\t', '\n', '\r', '/', '>', '=']).unwrap_or(r.len());
        let n = &r[..end];
        if !name_ok(n) {
            return None;
        }
        self.p += end;
        Some(n)
    }
    fn element(&mut self, depth: usize) -> Option<Element> {
        if depth > MAX_DEPTH || !self.eat("<") {
            return None;
        }
        let qname = self.name()?;
        let mut attrs: Vec<(String, String)> = Vec::new();
        loop {
            let before = self.p;
            self.skip_ws();
            if self.eat("/>") {
                return Some(Element { name: local(qname), attrs, children: Vec::new() });
            }
            if self.eat(">") {
                break;
            }
            if self.p == before {
                return None; // an attribute must follow white space
            }
            let an = self.name()?;
            self.skip_ws();
            if !self.eat("=") {
                return None;
            }
            self.skip_ws();
            let q = if self.eat("\"") {
                "\""
            } else if self.eat("'") {
                "'"
            } else {
                return None;
            };
            let raw = self.until(q)?;
            if raw.contains('<') {
                return None;
            }
            // Taken as written, its references replaced: no white space is normalised (§3.5).
            let v = unescape(raw)?;
            let ln = local(an);
            if attrs.iter().any(|(n, _)| *n == ln) {
                return None;
            }
            attrs.push((ln, v));
        }
        let mut children = Vec::new();
        loop {
            if self.eat("</") {
                let close = self.name()?;
                self.skip_ws();
                if close != qname || !self.eat(">") {
                    return None;
                }
                return Some(Element { name: local(qname), attrs, children });
            }
            if self.eat("<![CDATA[") {
                children.push(Node::Text(self.until("]]>")?.to_string()));
            } else if self.eat("<!--") {
                self.until("-->")?;
            } else if self.rest().starts_with("<!") {
                return None;
            } else if self.eat("<?") {
                self.until("?>")?;
            } else if self.rest().starts_with('<') {
                children.push(Node::Element(self.element(depth + 1)?));
            } else {
                let r = self.rest();
                let end = r.find('<')?;
                children.push(Node::Text(unescape(&r[..end])?));
                self.p += end;
            }
        }
    }
}

/// Read an XML document over the subset of §3.5: UTF-8 with no byte order mark; an XML
/// declaration, comments, processing instructions and white space before and after one
/// root element; no document type declaration; elements, attributes in double or single
/// quotes, character data, CDATA sections and comments; the five predefined entity
/// references and character references. Names are taken by their local part. `None` when
/// the document is not in the subset.
pub fn read_xml(b: &[u8]) -> Option<Element> {
    let s = std::str::from_utf8(b).ok()?;
    let mut x = X { s, p: 0 };
    x.misc()?;
    let root = x.element(1)?;
    x.misc()?;
    (x.p == s.len()).then_some(root)
}

// ---------------------------------------------------------------------------------------
// §3.4 DASH: the events of an MPD
// ---------------------------------------------------------------------------------------

/// Carriage 3 (§3.4): every `Event` of an `EventStream` of a `Period` of an MPD that has an
/// `id` and carries `id_attr`, and the ad identity it carries there. `None` when the bytes
/// are not an MPD in the subset.
pub fn mpd_ads(mpd: &[u8], id_attr: &str) -> Option<Vec<Found>> {
    let root = read_xml(mpd)?;
    if root.name != "MPD" {
        return None;
    }
    let mut out = Vec::new();
    for period in root.children_named("Period") {
        for es in period.children_named("EventStream") {
            let Some(scheme) = es.attr("schemeIdUri") else { continue };
            for ev in es.children_named("Event") {
                let (Some(id), Some(ad)) = (ev.attr("id"), ev.attr(id_attr)) else { continue };
                let marker = Marker::Event {
                    period: period.attr("id").unwrap_or("").as_bytes().to_vec(),
                    scheme: scheme.as_bytes().to_vec(),
                    value: es.attr("value").unwrap_or("").as_bytes().to_vec(),
                    id: id.as_bytes().to_vec(),
                };
                out.push(Found { marker, ad_id: ad.as_bytes().to_vec() });
            }
        }
    }
    Some(out)
}

// ---------------------------------------------------------------------------------------
// §7.2 The VAST response
// ---------------------------------------------------------------------------------------

/// A decimal number of VAST's `Pricing` (§7.2): digits, optionally a point and digits.
/// Times 10^`scale`, when that is an integer of at most `u64`.
pub fn decimal_scaled(s: &str, scale: u32) -> Option<u64> {
    let (int, frac) = s.split_once('.').unwrap_or((s, ""));
    if int.is_empty() || !int.bytes().all(|b| b.is_ascii_digit()) || (s.contains('.') && frac.is_empty()) || !frac.bytes().all(|b| b.is_ascii_digit()) {
        return None;
    }
    let scale = scale as usize;
    let (kept, dropped) = if frac.len() > scale { frac.split_at(scale) } else { (frac, "") };
    if dropped.bytes().any(|b| b != b'0') {
        return None;
    }
    let digits = format!("{int}{kept}{}", "0".repeat(scale - kept.len()));
    let trimmed = digits.trim_start_matches('0');
    if trimmed.is_empty() {
        return Some(0);
    }
    if trimmed.len() > 20 {
        return None;
    }
    u64::try_from(trimmed.parse::<u128>().ok()?).ok()
}
