use std::collections::BTreeMap;
use crate::cue::Msf;
use crate::sector::SectorMode;
const LEAD_IN_FRAMES: u32 = 150;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CcdMode {
Audio,
Mode1,
Mode2,
Other(u32),
}
impl CcdMode {
#[must_use]
pub fn from_value(v: u32) -> Self {
match v {
0 => Self::Audio,
1 => Self::Mode1,
2 => Self::Mode2,
other => Self::Other(other),
}
}
#[must_use]
pub fn sector_mode(self) -> Option<SectorMode> {
match self {
Self::Mode1 => Some(SectorMode::Raw2352),
Self::Mode2 => Some(SectorMode::Raw2352Mode2),
Self::Audio | Self::Other(_) => None,
}
}
#[must_use]
pub fn is_data(self) -> bool {
self.sector_mode().is_some()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CcdTrack {
pub number: u8,
pub mode: CcdMode,
pub start_lba: u32,
pub isrc: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CcdToc {
pub catalog: Option<String>,
pub first_track: u8,
pub last_track: u8,
pub leadout_lba: u32,
pub tracks: Vec<CcdTrack>,
pub cdtext: Vec<u8>,
}
impl CcdToc {
#[must_use]
pub fn track_count(&self) -> usize {
self.tracks.len()
}
#[must_use]
pub fn data_track(&self) -> Option<&CcdTrack> {
self.tracks.iter().find(|t| t.mode.is_data())
}
}
#[derive(Default)]
struct Entry {
point: Option<u32>,
pmin: u32,
psec: u32,
pframe: u32,
plba: Option<u32>,
}
enum Section {
Disc,
Entry(Entry),
Track(u8),
CdText,
Other,
}
#[must_use]
pub fn parse(text: &str) -> CcdToc {
let mut toc = CcdToc::default();
let mut starts: BTreeMap<u8, u32> = BTreeMap::new();
let mut track_meta: BTreeMap<u8, (CcdMode, Option<String>)> = BTreeMap::new();
let mut cdtext: BTreeMap<u32, Vec<u8>> = BTreeMap::new();
let mut section = Section::Other;
for raw in text.lines() {
let line = raw.trim();
if let Some(name) = line.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
finish_entry(&mut section, &mut toc, &mut starts);
section = classify_section(name);
continue;
}
let Some((key, value)) = split_kv(line) else { continue };
match &mut section {
Section::Disc => {
if key.eq_ignore_ascii_case("CATALOG") {
toc.catalog = Some(value.to_string());
}
}
Section::Entry(entry) => apply_entry_field(entry, key, value),
Section::Track(num) => apply_track_field(*num, key, value, &mut track_meta),
Section::CdText => apply_cdtext_field(key, value, &mut cdtext),
Section::Other => {}
}
}
finish_entry(&mut section, &mut toc, &mut starts);
for bytes in cdtext.into_values() {
toc.cdtext.extend_from_slice(&bytes);
}
for (&number, &start_lba) in &starts {
let (mode, isrc) =
track_meta.get(&number).cloned().unwrap_or((CcdMode::Other(u32::MAX), None));
toc.tracks.push(CcdTrack { number, mode, start_lba, isrc });
}
toc
}
fn classify_section(name: &str) -> Section {
if name.eq_ignore_ascii_case("Disc") {
Section::Disc
} else if name.eq_ignore_ascii_case("Entry") || starts_with_word(name, "Entry") {
Section::Entry(Entry::default())
} else if let Some(n) = track_number(name) {
Section::Track(n)
} else if name.eq_ignore_ascii_case("CDText") {
Section::CdText
} else {
Section::Other
}
}
fn apply_cdtext_field(key: &str, value: &str, packs: &mut BTreeMap<u32, Vec<u8>>) {
let Some(rest) = key.strip_prefix("Entry").or_else(|| key.strip_prefix("entry")) else {
return; };
let Ok(number) = rest.trim().parse::<u32>() else { return };
let bytes: Vec<u8> =
value.split_whitespace().filter_map(|t| u8::from_str_radix(t, 16).ok()).collect();
if !bytes.is_empty() {
packs.insert(number, bytes);
}
}
fn finish_entry(section: &mut Section, toc: &mut CcdToc, starts: &mut BTreeMap<u8, u32>) {
let Section::Entry(entry) = section else { return };
let Some(point) = entry.point else { return };
let lba = entry.plba.unwrap_or_else(|| msf_to_lba(entry.pmin, entry.psec, entry.pframe));
match point {
0xA0 => toc.first_track = entry.pmin as u8,
0xA1 => toc.last_track = entry.pmin as u8,
0xA2 => toc.leadout_lba = lba,
1..=99 => {
starts.insert(point as u8, lba);
}
_ => {}
}
*section = Section::Other;
}
fn apply_entry_field(entry: &mut Entry, key: &str, value: &str) {
match key.to_ascii_uppercase().as_str() {
"POINT" => entry.point = parse_int(value),
"PMIN" => entry.pmin = parse_int(value).unwrap_or(0),
"PSEC" => entry.psec = parse_int(value).unwrap_or(0),
"PFRAME" => entry.pframe = parse_int(value).unwrap_or(0),
"PLBA" => entry.plba = parse_int(value),
_ => {}
}
}
fn apply_track_field(
num: u8,
key: &str,
value: &str,
meta: &mut BTreeMap<u8, (CcdMode, Option<String>)>,
) {
let slot = meta.entry(num).or_insert((CcdMode::Other(u32::MAX), None));
if key.eq_ignore_ascii_case("MODE") {
if let Some(v) = parse_int(value) {
slot.0 = CcdMode::from_value(v);
}
} else if key.eq_ignore_ascii_case("ISRC") {
slot.1 = Some(value.to_string());
}
}
fn split_kv(line: &str) -> Option<(&str, &str)> {
let (k, v) = line.split_once('=')?;
Some((k.trim(), v.trim()))
}
fn starts_with_word(name: &str, word: &str) -> bool {
name.len() > word.len()
&& name[..word.len()].eq_ignore_ascii_case(word)
&& name.as_bytes()[word.len()] == b' '
}
fn track_number(name: &str) -> Option<u8> {
let rest = name.strip_prefix("TRACK").or_else(|| name.strip_prefix("track"))?;
rest.trim().parse().ok()
}
fn parse_int(value: &str) -> Option<u32> {
let v = value.trim();
if let Some(hex) = v.strip_prefix("0x").or_else(|| v.strip_prefix("0X")) {
u32::from_str_radix(hex, 16).ok()
} else {
v.parse().ok()
}
}
fn msf_to_lba(min: u32, sec: u32, frame: u32) -> u32 {
let abs = Msf { minutes: min as u8, seconds: sec as u8, frames: frame as u8 }.to_lba();
abs.saturating_sub(LEAD_IN_FRAMES)
}