//! CTA-608 on field 1, data channel 1 (CC1): pop-on and roll-up (extensions/timed-text.md §8).
//!
//! The encoder turns cues into byte pairs on the field-1 slot clock: one pair per slot,
//! odd parity on every byte, every two-byte code sent twice in consecutive slots, and a
//! standard character before each extended one, which the extended code backspaces over.
//! The decoder reads those pairs back, for round trips: it emulates the two caption
//! memories, the cursor and the attributes, and reports what is displayed.

use super::{check_document, lines, sorted, Cue, Reason};
use std::collections::BTreeMap;

/// How captions are presented (§8.6, §8.7).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Mode {
    PopOn,
    /// Roll-up with a window of 2, 3 or 4 rows.
    RollUp(u8),
}

impl Mode {
    pub fn name(self) -> String {
        match self {
            Mode::PopOn => "pop-on".into(),
            Mode::RollUp(d) => format!("roll-up-{d}"),
        }
    }

    pub fn from_name(s: &str) -> Option<Mode> {
        match s {
            "pop-on" => Some(Mode::PopOn),
            "roll-up-2" => Some(Mode::RollUp(2)),
            "roll-up-3" => Some(Mode::RollUp(3)),
            "roll-up-4" => Some(Mode::RollUp(4)),
            _ => None,
        }
    }

    /// The most rows one cue may have.
    fn max_rows(self) -> usize {
        match self {
            Mode::PopOn => 4,
            Mode::RollUp(d) => d as usize,
        }
    }
}

/// The rate of the field-1 slot clock, `num / den` slots a second (§8.2).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Rate {
    pub num: u64,
    pub den: u64,
}

/// The slot clock of line 21 and of 608 data carried with video at 30000/1001 frames
/// a second.
pub const NTSC: Rate = Rate { num: 30000, den: 1001 };

impl Rate {
    /// The first slot at or after media time `ms` (§8.2).
    pub fn slot(self, ms: u64) -> u64 {
        let n = ms as u128 * self.num as u128;
        let d = 1000u128 * self.den as u128;
        n.div_ceil(d) as u64
    }

    fn ok(self) -> bool {
        self.num > 0 && self.den > 0 && self.num <= 1_000_000 && self.den <= 1_000_000
    }
}

/// The characters of 0x11 0x30 to 0x11 0x3F (§8.4): 0x39 is the transparent space,
/// written U+00A0.
pub const SPECIAL: [char; 16] =
    ['®', '°', '½', '¿', '™', '¢', '£', '♪', 'à', '\u{a0}', 'è', 'â', 'ê', 'î', 'ô', 'û'];

/// The characters of 0x12 0x20 to 0x12 0x3F (§8.4); 0x12 0x29 is not assigned here.
pub const EXT12: [Option<char>; 32] = [
    Some('Á'), Some('É'), Some('Ó'), Some('Ú'), Some('Ü'), Some('ü'), Some('‘'), Some('¡'),
    Some('*'), None, Some('—'), Some('©'), Some('℠'), Some('•'), Some('“'), Some('”'),
    Some('À'), Some('Â'), Some('Ç'), Some('È'), Some('Ê'), Some('Ë'), Some('ë'), Some('Î'),
    Some('Ï'), Some('ï'), Some('Ô'), Some('Ù'), Some('ù'), Some('Û'), Some('«'), Some('»'),
];

/// The characters of 0x13 0x20 to 0x13 0x3F (§8.4).
pub const EXT13: [Option<char>; 32] = [
    Some('Ã'), Some('ã'), Some('Í'), Some('Ì'), Some('ì'), Some('Ò'), Some('ò'), Some('Õ'),
    Some('õ'), Some('{'), Some('}'), Some('\\'), Some('^'), Some('_'), Some('|'), Some('~'),
    Some('Ä'), Some('ä'), Some('Ö'), Some('ö'), Some('ß'), Some('¥'), Some('¤'), Some('¦'),
    Some('Å'), Some('å'), Some('Ø'), Some('ø'), Some('┌'), Some('┐'), Some('└'), Some('┘'),
];

/// The characters this mapping writes as another (§8.4).
pub const SUBSTITUTIONS: [(char, char); 4] = [('\u{2019}', '\''), ('\u{2013}', '-'), ('\u{2010}', '-'), ('\u{2011}', '-')];

/// The standard character of the basic set at `b` (§8.4).
pub fn basic_char(b: u8) -> Option<char> {
    Some(match b {
        0x2A => 'á',
        0x5C => 'é',
        0x5E => 'í',
        0x5F => 'ó',
        0x60 => 'ú',
        0x7B => 'ç',
        0x7C => '÷',
        0x7D => 'Ñ',
        0x7E => 'ñ',
        0x20..=0x7A => b as char,
        _ => return None,
    })
}

/// The byte of `c` in the basic set (§8.4).
pub fn basic_byte(c: char) -> Option<u8> {
    (0x20u8..=0x7E).find(|&b| basic_char(b) == Some(c))
}

/// The standard character sent before an extended one (§8.4).
pub fn fallback(c: char) -> u8 {
    match c {
        'Á' | 'À' | 'Â' | 'Ã' | 'Ä' | 'Å' => b'A',
        'É' | 'È' | 'Ê' | 'Ë' => b'E',
        'Í' | 'Ì' | 'Î' | 'Ï' => b'I',
        'Ó' | 'Ò' | 'Ô' | 'Õ' | 'Ö' | 'Ø' => b'O',
        'Ú' | 'Ù' | 'Û' | 'Ü' => b'U',
        'Ç' => b'C',
        'ã' | 'ä' | 'å' => b'a',
        'ë' => b'e',
        'ì' | 'ï' => b'i',
        'ò' | 'õ' | 'ö' | 'ø' => b'o',
        'ù' | 'ü' => b'u',
        'ß' => b's',
        '“' | '”' | '«' | '»' => b'"',
        '‘' => b'\'',
        '¡' => b'!',
        _ => b' ',
    }
}

/// How one character is sent (§8.4).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Char {
    Basic(u8),
    Special(u8),
    Extended(u8, u8, u8),
}

/// The character `c` is written as, after substitution, and how it is sent.
fn map_char(c: char) -> Option<(char, Char)> {
    let c = SUBSTITUTIONS.iter().find(|(f, _)| *f == c).map_or(c, |(_, t)| *t);
    if let Some(b) = basic_byte(c) {
        return Some((c, Char::Basic(b)));
    }
    if let Some(i) = SPECIAL.iter().position(|&s| s == c) {
        return Some((c, Char::Special(0x30 + i as u8)));
    }
    for (first, table) in [(0x12u8, &EXT12), (0x13, &EXT13)] {
        if let Some(i) = table.iter().position(|&s| s == Some(c)) {
            return Some((c, Char::Extended(first, 0x20 + i as u8, fallback(c))));
        }
    }
    None
}

/// The text `c` is decoded as when the encoder writes `c` (§8.4).
pub fn written_as(c: char) -> Option<char> {
    map_char(c).map(|(w, _)| w)
}

/// The first byte of a preamble address code for rows 1 to 15 (§8.5).
const ROW_FIRST: [u8; 15] = [0x11, 0x11, 0x12, 0x12, 0x15, 0x15, 0x16, 0x16, 0x17, 0x17, 0x10, 0x13, 0x13, 0x14, 0x14];

fn row_base(row: u8) -> u8 {
    if matches!(row, 2 | 4 | 6 | 8 | 10 | 13 | 15) { 0x60 } else { 0x40 }
}

/// The preamble address code of `row` at indent `4 * k`, white.
pub fn pac_indent(row: u8, k: u8) -> [u8; 2] {
    [ROW_FIRST[row as usize - 1], row_base(row) + 0x10 + 2 * k]
}

/// The preamble address code of `row`, white italics, at column 0.
pub fn pac_italic(row: u8) -> [u8; 2] {
    [ROW_FIRST[row as usize - 1], row_base(row) + 0x0E]
}

pub const RCL: [u8; 2] = [0x14, 0x20];
pub const EDM: [u8; 2] = [0x14, 0x2C];
pub const CR: [u8; 2] = [0x14, 0x2D];
pub const ENM: [u8; 2] = [0x14, 0x2E];
pub const EOC: [u8; 2] = [0x14, 0x2F];
pub const MRC_WHITE: [u8; 2] = [0x11, 0x20];
pub const MRC_ITALIC: [u8; 2] = [0x11, 0x2E];

pub fn ru(d: u8) -> [u8; 2] {
    [0x14, 0x23 + d]
}

pub fn to(k: u8) -> [u8; 2] {
    [0x17, 0x20 + k]
}

/// A byte with odd parity in bit 7 (§8.3).
pub fn parity(b: u8) -> u8 {
    let b = b & 0x7F;
    if b.count_ones().is_multiple_of(2) { b | 0x80 } else { b }
}

/// One row a cue occupies (§8.5), and one row a decoder displays.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Row {
    /// 1 to 15.
    pub row: u8,
    /// The column of its first character, 0 to 31.
    pub col: u8,
    /// The text, after substitution.
    pub text: String,
    /// Italic ranges over `text`, in code points.
    pub italic: Vec<(u32, u32)>,
}

/// The rows of a cue in a mode (§8.5), checked in order: rows, characters, width, style.
pub fn layout(mode: Mode, c: &Cue) -> Result<Vec<Row>, Reason> {
    let ls = lines(c);
    if ls.len() > mode.max_rows() {
        return Err(Reason::TooManyRows);
    }
    let mut rows = Vec::new();
    let n = ls.len() as u8;
    for (k, line) in ls.iter().enumerate() {
        let mut text = String::new();
        let mut flags = Vec::new();
        for (it, s) in line {
            for ch in s.chars() {
                text.push(written_as(ch).ok_or(Reason::CharNotIn608)?);
                flags.push(*it);
            }
        }
        rows.push((k as u8, text, flags));
    }
    let mut out = Vec::new();
    for (k, text, flags) in rows {
        let width = flags.len();
        if width > 32 {
            return Err(Reason::RowTooLong);
        }
        let cps: Vec<char> = text.chars().collect();
        let mut italic: Vec<(u32, u32)> = Vec::new();
        for (i, &f) in flags.iter().enumerate() {
            if !f {
                continue;
            }
            match italic.last_mut() {
                Some((s, l)) if *s + *l == i as u32 => *l += 1,
                _ => italic.push((i as u32, 1)),
            }
        }
        for &(s, l) in &italic {
            let (s, e) = (s as usize, (s + l) as usize);
            if (s > 0 && cps[s - 1] != ' ') || (e < width && cps[e] != ' ') {
                return Err(Reason::StyleNotExpressible);
            }
        }
        let row = match (mode, c.anchor.top()) {
            (Mode::PopOn, true) => 1 + k,
            (Mode::PopOn, false) => 16 - n + k,
            (Mode::RollUp(d), true) => d,
            (Mode::RollUp(_), false) => 15,
        };
        let col = match c.anchor.align() {
            "left" => 0,
            "right" => 32 - width,
            _ => (32 - width) / 2,
        } as u8;
        out.push(Row { row, col, text, italic });
    }
    Ok(out)
}

/// What the encoder sends: a byte of the basic set, or a two-byte code.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Code {
    Byte(u8),
    Two([u8; 2]),
}

fn position(out: &mut Vec<Code>, row: u8, col: u8) {
    out.push(Code::Two(pac_indent(row, col / 4)));
    if !col.is_multiple_of(4) {
        out.push(Code::Two(to(col % 4)));
    }
}

/// The codes that write one row (§8.5).
fn row_codes(r: &Row) -> Vec<Code> {
    let cps: Vec<char> = r.text.chars().collect();
    let mut flags = vec![false; cps.len()];
    for &(s, l) in &r.italic {
        for f in flags.iter_mut().skip(s as usize).take(l as usize) {
            *f = true;
        }
    }
    let mut out = Vec::new();
    match (flags.first() == Some(&true), r.col) {
        (true, 0) => out.push(Code::Two(pac_italic(r.row))),
        (true, c) => {
            position(&mut out, r.row, c - 1);
            out.push(Code::Two(MRC_ITALIC));
        }
        (false, c) => position(&mut out, r.row, c),
    }
    for (i, &ch) in cps.iter().enumerate() {
        if ch == ' ' && !flags[i] && flags.get(i + 1) == Some(&true) {
            out.push(Code::Two(MRC_ITALIC));
        } else if ch == ' ' && !flags[i] && i > 0 && flags[i - 1] {
            out.push(Code::Two(MRC_WHITE));
        } else {
            match map_char(ch).expect("laid out").1 {
                Char::Basic(b) => out.push(Code::Byte(b)),
                Char::Special(b) => out.push(Code::Two([0x11, b])),
                Char::Extended(f, b, fb) => {
                    out.push(Code::Byte(fb));
                    out.push(Code::Two([f, b]));
                }
            }
        }
    }
    out
}

/// A unit of the schedule: one pair, or a two-byte code sent in two consecutive slots.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Unit {
    Pair([u8; 2]),
    Doubled([u8; 2]),
}

impl Unit {
    fn slots(self) -> u64 {
        match self {
            Unit::Pair(_) => 1,
            Unit::Doubled(_) => 2,
        }
    }
}

/// Pack codes into units (§8.3): standard bytes two to a pair in order, a lone one
/// before a two-byte code or at the end padded with 0x00.
fn pack(codes: &[Code]) -> Vec<Unit> {
    let mut out = Vec::new();
    let mut pending: Option<u8> = None;
    for &c in codes {
        match c {
            Code::Byte(b) => match pending.take() {
                Some(p) => out.push(Unit::Pair([p, b])),
                None => pending = Some(b),
            },
            Code::Two(x) => {
                if let Some(p) = pending.take() {
                    out.push(Unit::Pair([p, 0]));
                }
                out.push(Unit::Doubled(x));
            }
        }
    }
    if let Some(p) = pending {
        out.push(Unit::Pair([p, 0]));
    }
    out
}

fn put(slots: &mut BTreeMap<u64, [u8; 2]>, at: u64, u: Unit) {
    let x = match u {
        Unit::Pair(x) | Unit::Doubled(x) => [parity(x[0]), parity(x[1])],
    };
    for k in 0..u.slots() {
        slots.insert(at + k, x);
    }
}

/// Encode cues in `lang` for `mode` on the slot clock `rate` (§8): the slots that carry
/// a pair, with the pair, parity applied; every other slot carries 0x80 0x80.
pub fn encode(mode: Mode, rate: Rate, lang: &str, cues: &[Cue]) -> Result<Vec<(u64, [u8; 2])>, Reason> {
    check_document(lang, cues)?;
    if !rate.ok() || !matches!(mode, Mode::PopOn | Mode::RollUp(2..=4)) {
        return Err(Reason::ParameterMalformed);
    }
    let cues = sorted(cues);
    let mut laid = Vec::new();
    for c in &cues {
        laid.push(layout(mode, c)?);
    }
    if cues.windows(2).any(|w| w[1].start < w[0].end) {
        return Err(Reason::CuesOverlap);
    }
    if matches!(mode, Mode::RollUp(_)) && cues.windows(2).any(|w| w[0].anchor.top() != w[1].anchor.top()) {
        return Err(Reason::StyleNotExpressible);
    }
    let s: Vec<u64> = cues.iter().map(|c| rate.slot(c.start)).collect();
    let e: Vec<u64> = cues.iter().map(|c| rate.slot(c.end)).collect();
    let n = cues.len();
    let edm: Vec<bool> = (0..n).map(|i| i + 1 == n || s[i + 1] >= e[i] + 2).collect();
    let mut slots: BTreeMap<u64, [u8; 2]> = BTreeMap::new();
    match mode {
        Mode::PopOn => {
            for i in 0..n {
                if e[i] < s[i] + 2 {
                    return Err(Reason::TooDense);
                }
                put(&mut slots, s[i], Unit::Doubled(EOC));
                if edm[i] {
                    put(&mut slots, e[i], Unit::Doubled(EDM));
                }
            }
            for i in 0..n {
                let mut codes = vec![Code::Two(RCL), Code::Two(ENM)];
                for r in &laid[i] {
                    codes.extend(row_codes(r));
                }
                let lb = if i == 0 { 0 } else { s[i - 1] + 2 };
                let mut cursor = s[i];
                for u in pack(&codes).into_iter().rev() {
                    let need = u.slots();
                    let mut q = cursor.checked_sub(need).ok_or(Reason::TooDense)?;
                    loop {
                        if q < lb {
                            return Err(Reason::TooDense);
                        }
                        if (q..q + need).all(|k| !slots.contains_key(&k)) {
                            break;
                        }
                        q = q.checked_sub(1).ok_or(Reason::TooDense)?;
                    }
                    put(&mut slots, q, u);
                    cursor = q;
                }
            }
        }
        Mode::RollUp(d) => {
            let mut prev_end = 0u64;
            let mut base_has_text = false;
            for i in 0..n {
                if s[i] < prev_end {
                    return Err(Reason::TooDense);
                }
                let mut codes = Vec::new();
                if i == 0 {
                    codes.push(Code::Two(ru(d)));
                }
                for r in &laid[i] {
                    if base_has_text {
                        codes.push(Code::Two(CR));
                    }
                    codes.extend(row_codes(r));
                    base_has_text = true;
                }
                let mut at = s[i];
                for u in pack(&codes) {
                    put(&mut slots, at, u);
                    at += u.slots();
                }
                let limit = if edm[i] { e[i] } else { s[i + 1] };
                if at > limit {
                    return Err(Reason::TooDense);
                }
                if edm[i] {
                    put(&mut slots, e[i], Unit::Doubled(EDM));
                    base_has_text = false;
                    prev_end = e[i] + 2;
                } else {
                    prev_end = at;
                }
            }
        }
    }
    Ok(slots.into_iter().collect())
}

/// The hexadecimal form of encoded pairs (§8.3): one line per slot that carries a pair,
/// its slot in decimal, a space, and the pair as four lowercase hexadecimal digits.
pub fn to_hex(pairs: &[(u64, [u8; 2])]) -> String {
    pairs.iter().map(|(s, p)| format!("{s} {:02x}{:02x}\n", p[0], p[1])).collect()
}

/// Read the hexadecimal form (§8.3), slots strictly ascending.
pub fn from_hex(s: &str) -> Result<Vec<(u64, [u8; 2])>, Reason> {
    let m = Reason::CcMalformed;
    let body = s.strip_suffix('\n').ok_or(m)?;
    let mut out: Vec<(u64, [u8; 2])> = Vec::new();
    for line in body.split('\n') {
        let (slot, pair) = line.split_once(' ').ok_or(m)?;
        if slot.is_empty() || !slot.bytes().all(|c| c.is_ascii_digit()) || (slot.len() > 1 && slot.starts_with('0')) {
            return Err(m);
        }
        if pair.len() != 4 || !pair.bytes().all(|c| c.is_ascii_digit() || (b'a'..=b'f').contains(&c)) {
            return Err(m);
        }
        let slot: u64 = slot.parse().map_err(|_| m)?;
        let b = [u8::from_str_radix(&pair[..2], 16).map_err(|_| m)?, u8::from_str_radix(&pair[2..], 16).map_err(|_| m)?];
        if out.last().is_some_and(|(p, _)| *p >= slot) {
            return Err(m);
        }
        out.push((slot, b));
    }
    Ok(out)
}

/// What a decoder reports (§8.8).
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event {
    /// An End of Caption: what is displayed from `slot`.
    Show { slot: u64, rows: Vec<Row> },
    /// A roll-up line, from the slot of its preamble address code, committed by a
    /// carriage return, an erase or the end of the stream.
    Line { slot: u64, row: Row },
    /// An Erase Displayed Memory.
    Erase { slot: u64 },
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Cell {
    ch: char,
    italic: bool,
    mrc: bool,
}

type Memory = [[Option<Cell>; 32]; 15];

fn read_row(row: u8, cells: &[Option<Cell>; 32]) -> Option<Row> {
    let real = |k: usize| cells[k].is_some_and(|c| !c.mrc);
    let i = (0..32).find(|&k| real(k))?;
    let j = (0..32).rev().find(|&k| real(k))?;
    let mut text = String::new();
    let mut flags = Vec::new();
    let mut space = Vec::new();
    for c in &cells[i..=j] {
        let ch = match c {
            Some(c) if !c.mrc => c.ch,
            _ => ' ',
        };
        text.push(ch);
        flags.push(c.is_some_and(|c| !c.mrc && c.italic && c.ch != ' '));
        space.push(ch == ' ');
    }
    let len = flags.len();
    let inside = |k: usize| flags[k] || (space[k] && k > 0 && k + 1 < len && flags[k - 1] && flags[k + 1]);
    let mut italic: Vec<(u32, u32)> = Vec::new();
    for k in 0..len {
        if inside(k) {
            match italic.last_mut() {
                Some((s, l)) if *s + *l == k as u32 => *l += 1,
                _ => italic.push((k as u32, 1)),
            }
        }
    }
    Some(Row { row, col: i as u8, text, italic })
}

fn snapshot(mem: &Memory) -> Vec<Row> {
    (1..=15u8).filter_map(|r| read_row(r, &mem[r as usize - 1])).collect()
}

/// Decode pairs (§8.8): the events a decoder of channel CC1 reports, for round trips of
/// what [`encode`] writes. A pair this encoder does not write is `cc_malformed`.
pub fn decode(pairs: &[(u64, [u8; 2])]) -> Result<Vec<Event>, Reason> {
    let m = Reason::CcMalformed;
    #[derive(PartialEq)]
    enum St {
        Unset,
        Pop,
        Roll(u8),
    }
    let mut disp: Memory = [[None; 32]; 15];
    let mut nond: Memory = [[None; 32]; 15];
    let mut st = St::Unset;
    let (mut row, mut col, mut italic, mut base) = (15u8, 0usize, false, 15u8);
    let mut line_slot: Option<u64> = None;
    let mut last: Option<([u8; 2], u64)> = None;
    let mut prev: Option<u64> = None;
    let mut out = Vec::new();
    let commit = |disp: &Memory, base: u8, line_slot: &mut Option<u64>, out: &mut Vec<Event>| {
        if let (Some(r), Some(s)) = (read_row(base, &disp[base as usize - 1]), line_slot.take()) {
            out.push(Event::Line { slot: s, row: r });
        }
    };
    for &(slot, pair) in pairs {
        if prev.is_some_and(|p| slot <= p) {
            return Err(m);
        }
        prev = Some(slot);
        if pair.iter().any(|b| b.count_ones() % 2 == 0) {
            return Err(m);
        }
        let (b1, b2) = (pair[0] & 0x7F, pair[1] & 0x7F);
        let write = |ch: char, mrc: bool, italic: bool, col: &mut usize, disp: &mut Memory, nond: &mut Memory| -> Result<(), Reason> {
            let (mem, r) = match st {
                St::Unset => return Err(m),
                St::Pop => (nond, row),
                St::Roll(_) => (disp, base),
            };
            mem[r as usize - 1][*col] = Some(Cell { ch, italic, mrc });
            if *col < 31 {
                *col += 1;
            }
            Ok(())
        };
        if (0x10..=0x1F).contains(&b1) {
            if last.is_some_and(|(p, s)| p == [b1, b2] && s + 1 == slot) {
                last = None;
                continue;
            }
            last = Some(([b1, b2], slot));
            match (b1, b2) {
                (_, 0x40..=0x7F) => {
                    let r = (1..=15u8).find(|&r| ROW_FIRST[r as usize - 1] == b1 && row_base(r) == b2 & 0x60).ok_or(m)?;
                    let attr = b2 & 0x1F;
                    let (c, it) = match attr {
                        0x00 => (0, false),
                        0x0E => (0, true),
                        0x10..=0x1E if attr % 2 == 0 => ((attr as usize - 0x10) * 2, false),
                        _ => return Err(m),
                    };
                    row = r;
                    col = c;
                    italic = it;
                    if let St::Roll(_) = st {
                        base = r;
                        if line_slot.is_none() {
                            line_slot = Some(slot);
                        }
                    }
                }
                (0x14, 0x20) => st = St::Pop,
                (0x14, 0x25..=0x27) => {
                    if st == St::Pop {
                        disp = [[None; 32]; 15];
                        nond = [[None; 32]; 15];
                    }
                    st = St::Roll(b2 - 0x23);
                    base = 15;
                    col = 0;
                }
                (0x14, 0x2C) => {
                    if let St::Roll(_) = st {
                        commit(&disp, base, &mut line_slot, &mut out);
                    }
                    line_slot = None;
                    disp = [[None; 32]; 15];
                    out.push(Event::Erase { slot });
                }
                (0x14, 0x2D) => {
                    let St::Roll(d) = st else { return Err(m) };
                    commit(&disp, base, &mut line_slot, &mut out);
                    let top = base as usize - d as usize;
                    for r in top..base as usize - 1 {
                        disp[r] = disp[r + 1];
                    }
                    disp[base as usize - 1] = [None; 32];
                    col = 0;
                }
                (0x14, 0x2E) => nond = [[None; 32]; 15],
                (0x14, 0x2F) => {
                    std::mem::swap(&mut disp, &mut nond);
                    st = St::Pop;
                    out.push(Event::Show { slot, rows: snapshot(&disp) });
                }
                (0x17, 0x21..=0x23) => col = (col + (b2 - 0x20) as usize).min(31),
                (0x11, 0x20) | (0x11, 0x2E) => {
                    italic = b2 == 0x2E;
                    write(' ', true, italic, &mut col, &mut disp, &mut nond)?;
                }
                (0x11, 0x30..=0x3F) => write(SPECIAL[(b2 - 0x30) as usize], false, italic, &mut col, &mut disp, &mut nond)?,
                (0x12 | 0x13, 0x20..=0x3F) => {
                    let table = if b1 == 0x12 { &EXT12 } else { &EXT13 };
                    let ch = table[(b2 - 0x20) as usize].ok_or(m)?;
                    col = col.saturating_sub(1);
                    write(ch, false, italic, &mut col, &mut disp, &mut nond)?;
                }
                _ => return Err(m),
            }
        } else {
            last = None;
            for b in [b1, b2] {
                if b == 0 {
                    continue;
                }
                let ch = basic_char(b).ok_or(m)?;
                write(ch, false, italic, &mut col, &mut disp, &mut nond)?;
            }
        }
    }
    if let St::Roll(_) = st {
        commit(&disp, base, &mut line_slot, &mut out);
    }
    Ok(out)
}

#[cfg(test)]
mod tests {
    use super::super::{origin, Anchor};
    use super::*;

    fn cue(start: u64, end: u64, text: &str, anchor: Anchor, italic: Vec<(u32, u32)>) -> Cue {
        Cue { start, end, lang: "en".into(), origin: origin::HUMAN, model: None, anchor, text: text.into(), italic }
    }

    #[test]
    fn parity_and_the_well_known_codes() {
        let hex = |x: [u8; 2]| format!("{:02x}{:02x}", parity(x[0]), parity(x[1]));
        assert_eq!(hex(RCL), "9420");
        assert_eq!(hex(EOC), "942f");
        assert_eq!(hex(EDM), "942c");
        assert_eq!(hex(ENM), "94ae");
        assert_eq!(hex(CR), "94ad");
        assert_eq!(hex(ru(2)), "9425");
        assert_eq!(hex(pac_indent(15, 0)), "9470");
        assert_eq!(hex(pac_indent(14, 0)), "94d0");
        assert_eq!(hex(pac_indent(1, 0)), "91d0");
        assert_eq!(hex(pac_italic(14)), "94ce");
        assert_eq!(hex(pac_italic(15)), "946e");
    }

    #[test]
    fn the_character_tables_are_one_to_one() {
        let mut seen = std::collections::BTreeSet::new();
        for b in 0x20..=0x7E {
            if let Some(c) = basic_char(b) {
                assert!(seen.insert(c), "{c:?}");
            }
        }
        for c in SPECIAL.iter().chain(EXT12.iter().flatten()).chain(EXT13.iter().flatten()) {
            assert!(seen.insert(*c), "{c:?} twice");
        }
        assert_eq!(basic_byte('*'), None, "'*' is not 0x2A, which is á");
        assert_eq!(map_char('*').map(|x| x.1), Some(Char::Extended(0x12, 0x28, b' ')));
        assert_eq!(written_as('\u{2019}'), Some('\''));
        assert_eq!(written_as('€'), None);
    }

    #[test]
    fn pop_on_round_trips() {
        let cues = vec![
            cue(1000, 3000, "Hello big world\nsecond", Anchor::BottomCenter, vec![(6, 9)]),
            cue(3000, 4000, "Right ♪", Anchor::TopRight, vec![]),
            cue(5000, 7000, "Él dijo «sí»", Anchor::BottomLeft, vec![(0, 2)]),
        ];
        let pairs = encode(Mode::PopOn, NTSC, "en", &cues).unwrap();
        let ev = decode(&pairs).unwrap();
        let shows: Vec<&Event> = ev.iter().filter(|e| matches!(e, Event::Show { .. })).collect();
        assert_eq!(shows.len(), 3);
        if let Event::Show { slot, rows } = shows[0] {
            assert_eq!(*slot, NTSC.slot(1000));
            assert_eq!(rows, &layout(Mode::PopOn, &cues[0]).unwrap());
        }
        if let Event::Show { rows, .. } = shows[2] {
            assert_eq!(rows, &layout(Mode::PopOn, &cues[2]).unwrap());
        }
        assert_eq!(ev.iter().filter(|e| matches!(e, Event::Erase { .. })).count(), 2, "no erase between back-to-back cues");
        assert_eq!(from_hex(&to_hex(&pairs)).unwrap(), pairs);
    }

    #[test]
    fn roll_up_round_trips() {
        let cues = vec![
            cue(1000, 4000, "one\ntwo", Anchor::BottomLeft, vec![]),
            cue(4000, 6000, "three is longer", Anchor::BottomCenter, vec![(0, 5)]),
        ];
        for d in 2..=4 {
            let pairs = encode(Mode::RollUp(d), NTSC, "en", &cues).unwrap();
            let lines: Vec<String> = decode(&pairs)
                .unwrap()
                .into_iter()
                .filter_map(|e| match e {
                    Event::Line { row, .. } => Some(row.text),
                    _ => None,
                })
                .collect();
            assert_eq!(lines, ["one", "two", "three is longer"]);
        }
    }

    #[test]
    fn what_608_cannot_hold_is_refused() {
        let one = |t: &str, it: Vec<(u32, u32)>| vec![cue(1000, 3000, t, Anchor::BottomCenter, it)];
        assert_eq!(encode(Mode::PopOn, NTSC, "en", &one("a\nb\nc\nd\ne", vec![])), Err(Reason::TooManyRows));
        assert_eq!(encode(Mode::PopOn, NTSC, "en", &one(&"x".repeat(33), vec![])), Err(Reason::RowTooLong));
        assert_eq!(encode(Mode::PopOn, NTSC, "en", &one("5 €", vec![])), Err(Reason::CharNotIn608));
        assert_eq!(encode(Mode::PopOn, NTSC, "en", &one("unbelievable", vec![(2, 4)])), Err(Reason::StyleNotExpressible));
        let two = vec![cue(0, 2000, "a", Anchor::BottomCenter, vec![]), cue(1000, 3000, "b", Anchor::BottomCenter, vec![])];
        assert_eq!(encode(Mode::PopOn, NTSC, "en", &two), Err(Reason::CuesOverlap));
        let dense = vec![cue(1000, 1100, &"x".repeat(32), Anchor::BottomCenter, vec![]), cue(1100, 1300, &"y".repeat(32), Anchor::BottomCenter, vec![])];
        assert_eq!(encode(Mode::PopOn, NTSC, "en", &dense), Err(Reason::TooDense));
        let mut bad = encode(Mode::PopOn, NTSC, "en", &one("ok", vec![])).unwrap();
        bad[0].1[1] ^= 0x01;
        assert_eq!(decode(&bad), Err(Reason::CcMalformed));
    }
}
