//! `wai.mpd` (http-delivery §11.1): a separate static MPD (ISO/IEC 23009-1) with one
//! `AdaptationSet` per WAI stream, `mimeType="application/wai"`, `@codecs` the union, an
//! `EssentialProperty` a client that does not implement the extension ignores the set
//! by, and a `SupplementalProperty` carrying the floor. Its timeline is the classical
//! one (§11.2: the same `Period@id` and `@start`, and a `SegmentTimeline` with the same
//! timescale, `t` and `d`), read from the classical MPD ([`DashTimeline::from_mpd`]), or,
//! beside a presentation with no MPD, from the classical HLS media playlist it shares a
//! timeline with ([`DashTimeline::from_hls`]). [`super::dash_read::check_dash_adjacent`]
//! checks the result against the classical MPD.
//!
//! [`render_mpd`] writes any static MPD of one period this binding's packagers need —
//! the classical one beside it too — and [`wai_mpd`] is `wai.mpd` on a classical
//! timeline. A DASH playlist is not authenticated in this revision: the playlist
//! receipt (§8.6) signs HLS renderings only.

use super::codecs::{self, Element};
use super::dash_read::{read_mpd, xml_char};
use super::hls::{MediaPlaylist, PlaylistError};

/// The `EssentialProperty` scheme a client that does not implement the extension
/// ignores a WAI `AdaptationSet` by.
pub const SCHEME_DELIVERY: &str = "tag:transaction.science,2026:wai/http-delivery";
/// The `SupplementalProperty` scheme whose `value` is the floor.
pub const SCHEME_FLOOR: &str = "tag:transaction.science,2026:wai/floor";

/// The classical timeline a WAI `AdaptationSet` is aligned with: one period's `@id`
/// (none when the classical period has none) and `@start` in milliseconds, and its
/// segments' `t` and `d` at `timescale`.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DashTimeline {
    pub period_id: Option<String>,
    pub period_start_ms: u64,
    pub timescale: u64,
    pub segments: Vec<(u64, u64)>,
}

/// Segment times at a timescale: each `(start, duration)` in some unit, `t` counted from
/// `origin`, both multiplied by `per_unit` ticks. Refused (`playlist_malformed`) when a
/// start precedes `origin` or a product passes 2^64 − 1.
pub fn scaled_timeline(spans: &[(u64, u64)], origin: u64, per_unit: u64) -> Result<Vec<(u64, u64)>, PlaylistError> {
    spans.iter().map(|&(start, d)| {
        let t = start.checked_sub(origin).ok_or_else(|| PlaylistError::Malformed("a segment before the timeline's start".into()))?;
        let past = || PlaylistError::Malformed("a segment time past 2^64 - 1".into());
        Ok((t.checked_mul(per_unit).ok_or_else(past)?, d.checked_mul(per_unit).ok_or_else(past)?))
    }).collect()
}

impl DashTimeline {
    /// The timeline of a classical HLS media playlist at timescale 1000, for a
    /// presentation with no classical MPD: `t` from 0, each `d` a segment's duration in
    /// milliseconds. Refused (`playlist_malformed`) when a time passes 2^64 − 1.
    pub fn from_hls(p: &MediaPlaylist, period_id: &str, period_start_ms: u64) -> Result<DashTimeline, PlaylistError> {
        let mut t = 0u64;
        let mut segments = Vec::with_capacity(p.segments.len());
        for s in &p.segments {
            segments.push((t, s.duration_ms));
            t = t.checked_add(s.duration_ms).ok_or_else(|| PlaylistError::Malformed("a segment time past 2^64 - 1".into()))?;
        }
        Ok(DashTimeline { period_id: Some(period_id.to_string()), period_start_ms, timescale: 1000, segments })
    }

    /// The timeline of a classical MPD (§11.1): the period with `@id` `period_id` (the
    /// first by default), its adaptation set with `@id` `set_id` (by default the first
    /// that has a timeline), and that set's first timeline (Appendix I), expanded to one
    /// entry per segment. Refused (`playlist_malformed`) when the MPD does not read,
    /// holds no such timeline, or lists more than `max_segments` segments there (the
    /// caller's own count, so that no `@r` makes it hold more than it lists).
    pub fn from_mpd(text: &str, period_id: Option<&str>, set_id: Option<&str>, max_segments: usize) -> Result<DashTimeline, PlaylistError> {
        let periods = read_mpd(text)?;
        let p = match period_id {
            Some(id) => periods.iter().find(|p| p.id.as_deref() == Some(id)),
            None => periods.first(),
        }.ok_or_else(|| PlaylistError::Malformed("no such Period in the classical MPD".into()))?;
        let set = match set_id {
            Some(id) => p.sets.iter().find(|s| s.id.as_deref() == Some(id)),
            None => p.sets.iter().find(|s| !s.timelines.is_empty()),
        }.ok_or_else(|| PlaylistError::Malformed("no such AdaptationSet in the classical MPD".into()))?;
        let tl = set.timelines.first().ok_or_else(|| PlaylistError::Malformed("the classical AdaptationSet has no SegmentTimeline".into()))?;
        let segments = tl.segments(max_segments)
            .ok_or_else(|| PlaylistError::Malformed(format!("the classical timeline lists {} segments, more than {max_segments}", tl.segment_count())))?;
        Ok(DashTimeline { period_id: p.id.clone(), period_start_ms: p.start_ms, timescale: tl.timescale, segments })
    }

    /// The timeline's length in milliseconds, and its longest segment in milliseconds.
    fn lengths_ms(&self) -> (u64, u64) {
        let to_ms = |ticks: u64| u64::try_from(u128::from(ticks) * 1000 / u128::from(self.timescale.max(1))).unwrap_or(u64::MAX);
        (self.segments.iter().map(|&(_, d)| to_ms(d)).fold(0, u64::saturating_add), self.segments.iter().map(|&(_, d)| to_ms(d)).max().unwrap_or(0))
    }
}

/// One `Representation` of the WAI `AdaptationSet`: its `@id`, its `@bandwidth` (the
/// peak of `hls::bandwidths`) and, per segment of the timeline, its object's URI with its
/// fragment, or `None` for a gap, which the representation leaves out (§11.1).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WaiRepresentation {
    pub id: String,
    pub bandwidth: u64,
    pub media: Vec<Option<String>>,
}

/// A descriptor (`EssentialProperty`, `SupplementalProperty`): its scheme and value.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Descriptor {
    pub scheme: String,
    pub value: Option<String>,
}

/// A `Representation` with a `SegmentList`: its `@id`, `@dependencyId`, `@bandwidth`, and
/// `@codecs`, `@width` and `@height` where the representations of its set differ in
/// them, the list's timescale, an `Initialization`, the `S` elements (`t`, `d`) and one
/// `SegmentURL` per segment.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Representation {
    pub id: String,
    pub dependency_id: Option<String>,
    pub bandwidth: u64,
    pub codecs: Option<String>,
    pub size: Option<(u32, u32)>,
    pub timescale: u64,
    pub initialization: Option<String>,
    pub timeline: Vec<(u64, u64)>,
    pub media: Vec<String>,
}

/// An `AdaptationSet`: `@codecs` where every representation of it shares it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AdaptationSet {
    pub id: u32,
    pub content_type: Option<String>,
    pub mime_type: String,
    pub codecs: Option<String>,
    pub size: Option<(u32, u32)>,
    pub segment_alignment: bool,
    pub start_with_sap: Option<u8>,
    pub essential: Vec<Descriptor>,
    pub supplemental: Vec<Descriptor>,
    pub representations: Vec<Representation>,
}

/// A static MPD of one period.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Mpd {
    /// `mediaPresentationDuration`, in milliseconds.
    pub duration_ms: u64,
    /// `minBufferTime`, in whole seconds.
    pub min_buffer_s: u64,
    /// `Period@id`, none when the classical period has none.
    pub period_id: Option<String>,
    /// `Period@start`, in milliseconds.
    pub period_start_ms: u64,
    pub adaptation_sets: Vec<AdaptationSet>,
}

/// An XML attribute value, escaped: `&`, `<`, `>` and `"` as entities, and tab, LF and
/// CR as character references, which XML 1.0 §3.3.3 does not normalise to a space.
/// Refused (`playlist_malformed`) for a character that is not an XML `Char`.
fn esc(s: &str) -> Result<String, PlaylistError> {
    let mut out = String::with_capacity(s.len());
    for c in s.chars() {
        match c {
            '&' => out.push_str("&amp;"),
            '<' => out.push_str("&lt;"),
            '>' => out.push_str("&gt;"),
            '"' => out.push_str("&quot;"),
            '\t' => out.push_str("&#9;"),
            '\n' => out.push_str("&#10;"),
            '\r' => out.push_str("&#13;"),
            c if xml_char(c) => out.push(c),
            c => return Err(PlaylistError::Malformed(format!("U+{:04X} is not an XML character", c as u32))),
        }
    }
    Ok(out)
}

/// A duration of whole milliseconds as `xs:duration`: whole seconds without a fraction,
/// otherwise with three decimals.
fn xs_seconds(ms: u64) -> String {
    if ms % 1000 == 0 { format!("PT{}S", ms / 1000) } else { format!("PT{}S", crate::hls_render::seconds(ms)) }
}

fn id_ok(s: &str) -> bool {
    !s.is_empty() && !s.chars().any(char::is_whitespace)
}

/// An MPD's text: `<?xml …?>`, `<MPD>` (the DASH namespace, the full profile, `static`,
/// `mediaPresentationDuration` in seconds with three decimals, `minBufferTime` in whole
/// seconds), one `<Period>` (`@id` when it has one, `@start` in seconds, with three
/// decimals when not whole), each `AdaptationSet`
/// (`@id`, `@contentType`, `@mimeType`, `@codecs`, `@width`, `@height`,
/// `@segmentAlignment`, `@startWithSAP`, those given, in that order) with its essential
/// then supplemental descriptors and its representations (`@id`, `@dependencyId`,
/// `@bandwidth`, `@codecs`, `@width`, `@height`, those given, in that order), each a
/// `SegmentList` (`@timescale`) of an `Initialization`, a
/// `SegmentTimeline` of one `S` per segment with its `t` and `d`, and one `SegmentURL`
/// per segment. Attribute values are escaped, elements are indented by two spaces a
/// level, and lines end in LF. Refused (`playlist_malformed`) for no adaptation set or
/// representation, an `@id` that is empty or holds white space, a timescale of 0, a
/// representation with no segment or whose timeline and segment URLs differ in number,
/// or a value holding a character that is not an XML `Char`.
pub fn render_mpd(m: &Mpd) -> Result<String, PlaylistError> {
    let bad = |s: &str| PlaylistError::Malformed(s.to_string());
    if m.adaptation_sets.is_empty() || m.adaptation_sets.iter().any(|a| a.representations.is_empty()) || m.period_id.as_deref().is_some_and(|i| !id_ok(i)) {
        return Err(bad("an MPD needs an adaptation set and a representation, and a period id without white space"));
    }
    let mut out = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
    out.push_str(&format!(
        "<MPD xmlns=\"urn:mpeg:dash:schema:mpd:2011\" profiles=\"urn:mpeg:dash:profile:full:2011\" type=\"static\" mediaPresentationDuration=\"PT{}S\" minBufferTime=\"PT{}S\">\n",
        crate::hls_render::seconds(m.duration_ms), m.min_buffer_s
    ));
    match &m.period_id {
        Some(id) => out.push_str(&format!("  <Period id=\"{}\" start=\"{}\">\n", esc(id)?, xs_seconds(m.period_start_ms))),
        None => out.push_str(&format!("  <Period start=\"{}\">\n", xs_seconds(m.period_start_ms))),
    }
    for a in &m.adaptation_sets {
        out.push_str(&format!("    <AdaptationSet id=\"{}\"", a.id));
        if let Some(c) = &a.content_type {
            out.push_str(&format!(" contentType=\"{}\"", esc(c)?));
        }
        out.push_str(&format!(" mimeType=\"{}\"", esc(&a.mime_type)?));
        if let Some(c) = &a.codecs {
            out.push_str(&format!(" codecs=\"{}\"", esc(c)?));
        }
        if let Some((w, h)) = a.size {
            out.push_str(&format!(" width=\"{w}\" height=\"{h}\""));
        }
        if a.segment_alignment {
            out.push_str(" segmentAlignment=\"true\"");
        }
        if let Some(sap) = a.start_with_sap {
            out.push_str(&format!(" startWithSAP=\"{sap}\""));
        }
        out.push_str(">\n");
        for (tag, ds) in [("EssentialProperty", &a.essential), ("SupplementalProperty", &a.supplemental)] {
            for d in ds {
                out.push_str(&format!("      <{tag} schemeIdUri=\"{}\"", esc(&d.scheme)?));
                if let Some(v) = &d.value {
                    out.push_str(&format!(" value=\"{}\"", esc(v)?));
                }
                out.push_str("/>\n");
            }
        }
        for r in &a.representations {
            if !id_ok(&r.id) || r.dependency_id.as_deref().is_some_and(|d| !id_ok(d)) || r.timescale == 0 || r.timeline.is_empty() || r.timeline.len() != r.media.len() {
                return Err(bad("a representation's id, dependency id, timescale or segment count"));
            }
            out.push_str(&format!("      <Representation id=\"{}\"", esc(&r.id)?));
            if let Some(d) = &r.dependency_id {
                out.push_str(&format!(" dependencyId=\"{}\"", esc(d)?));
            }
            out.push_str(&format!(" bandwidth=\"{}\"", r.bandwidth));
            if let Some(c) = &r.codecs {
                out.push_str(&format!(" codecs=\"{}\"", esc(c)?));
            }
            if let Some((w, h)) = r.size {
                out.push_str(&format!(" width=\"{w}\" height=\"{h}\""));
            }
            out.push_str(&format!(">\n        <SegmentList timescale=\"{}\">\n", r.timescale));
            if let Some(i) = &r.initialization {
                out.push_str(&format!("          <Initialization sourceURL=\"{}\"/>\n", esc(i)?));
            }
            out.push_str("          <SegmentTimeline>\n");
            for &(t, d) in &r.timeline {
                out.push_str(&format!("            <S t=\"{t}\" d=\"{d}\"/>\n"));
            }
            out.push_str("          </SegmentTimeline>\n");
            for u in &r.media {
                out.push_str(&format!("          <SegmentURL media=\"{}\"/>\n", esc(u)?));
            }
            out.push_str("        </SegmentList>\n      </Representation>\n");
        }
        out.push_str("    </AdaptationSet>\n");
    }
    out.push_str("  </Period>\n</MPD>\n");
    Ok(out)
}

/// `wai.mpd` (§11.1) as [`render_mpd`] writes it: one period on timeline `t`, its
/// length the timeline's and `minBufferTime` its longest segment rounded up to whole
/// seconds, and one WAI `AdaptationSet` (`@id` `set_id`, `application/wai`, the union as
/// `@codecs`, segment-aligned, the delivery `EssentialProperty` and the floor
/// `SupplementalProperty`), each representation listing the segments of the timeline it
/// offers and leaving out its gaps. Refused (`playlist_malformed`) as [`render_mpd`]
/// refuses, for no segment, a representation whose segments are not the timeline's in
/// number, or a floor whose container is not the union's.
pub fn wai_mpd(t: &DashTimeline, set_id: u32, union: &Element, floor: &Element, reps: &[WaiRepresentation]) -> Result<String, PlaylistError> {
    if t.segments.is_empty() || t.timescale == 0 {
        return Err(PlaylistError::Malformed("an MPD needs a segment and a timescale".into()));
    }
    if floor.prefix != union.prefix {
        return Err(PlaylistError::Malformed("the floor is written with the union's prefix".into()));
    }
    let (total, longest) = t.lengths_ms();
    render_mpd(&Mpd {
        duration_ms: total,
        min_buffer_s: longest.div_ceil(1000),
        period_id: t.period_id.clone(),
        period_start_ms: t.period_start_ms,
        adaptation_sets: vec![AdaptationSet {
            id: set_id,
            content_type: None,
            mime_type: super::MEDIA_TYPE.into(),
            codecs: Some(codecs::render(union)),
            size: None,
            segment_alignment: true,
            start_with_sap: None,
            essential: vec![Descriptor { scheme: SCHEME_DELIVERY.into(), value: None }],
            supplemental: vec![Descriptor { scheme: SCHEME_FLOOR.into(), value: Some(codecs::render(floor)) }],
            representations: reps.iter().map(|r| {
                if r.media.len() != t.segments.len() {
                    return Err(PlaylistError::Malformed(format!("representation {}: {} segments on a timeline of {}", r.id, r.media.len(), t.segments.len())));
                }
                let offered: Vec<(&(u64, u64), &String)> = t.segments.iter().zip(&r.media).filter_map(|(s, m)| m.as_ref().map(|m| (s, m))).collect();
                Ok(Representation {
                    id: r.id.clone(), dependency_id: None, bandwidth: r.bandwidth, codecs: None, size: None, timescale: t.timescale, initialization: None,
                    timeline: offered.iter().map(|(s, _)| **s).collect(), media: offered.iter().map(|(_, m)| (*m).clone()).collect(),
                })
            }).collect::<Result<_, _>>()?,
        }],
    })
}
