use crate::TocError;
use dactyl::{
NiceElapsed,
NiceFloat,
traits::NiceInflection,
};
use std::{
fmt,
hash,
iter::Sum,
ops::{
Add,
AddAssign,
Sub,
SubAssign,
Div,
DivAssign,
Mul,
MulAssign,
},
time,
};
const SAMPLES_PER_SECTOR: u64 = 588;
const SECTORS_PER_SECOND: u64 = 75;
#[derive(Debug, Clone, Copy, Default, Ord, PartialOrd)]
pub struct Duration(pub(crate) u64);
impl<T> Add<T> for Duration
where u64: From<T> {
type Output = Self;
#[inline]
fn add(self, other: T) -> Self { Self(self.0 + u64::from(other)) }
}
impl<T> AddAssign<T> for Duration
where u64: From<T> {
#[inline]
fn add_assign(&mut self, other: T) { self.0 += u64::from(other); }
}
impl<T> Div<T> for Duration
where u64: From<T> {
type Output = Self;
#[inline]
fn div(self, other: T) -> Self {
let other = u64::from(other);
if other == 0 { Self(0) }
else { Self(self.0.wrapping_div(other)) }
}
}
impl<T> DivAssign<T> for Duration
where u64: From<T> {
#[inline]
fn div_assign(&mut self, other: T) {
let other = u64::from(other);
if other == 0 { self.0 = 0; }
else { self.0 = self.0.wrapping_div(other); }
}
}
impl Eq for Duration {}
impl fmt::Display for Duration {
#[expect(clippy::many_single_char_names, reason = "Consistency is preferred.")]
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (d, h, m, s, frames) = self.dhmsf();
if d == 0 {
write!(f, "{h:02}:{m:02}:{s:02}+{frames:02}")
}
else {
write!(f, "{d}d {h:02}:{m:02}:{s:02}+{frames:02}")
}
}
}
impl From<u32> for Duration {
#[inline]
fn from(src: u32) -> Self { Self(src.into()) }
}
impl From<u64> for Duration {
#[inline]
fn from(src: u64) -> Self { Self(src) }
}
impl From<usize> for Duration {
#[inline]
fn from(src: usize) -> Self { Self(src as u64) }
}
impl From<Duration> for u64 {
#[inline]
fn from(src: Duration) -> Self { src.0 }
}
impl hash::Hash for Duration {
#[inline]
fn hash<H: hash::Hasher>(&self, state: &mut H) { state.write_u64(self.0); }
}
impl PartialEq for Duration {
#[inline]
fn eq(&self, other: &Self) -> bool { self.0 == other.0 }
}
impl<T> Mul<T> for Duration
where u64: From<T> {
type Output = Self;
#[inline]
fn mul(self, other: T) -> Self { Self(self.0 * u64::from(other)) }
}
impl<T> MulAssign<T> for Duration
where u64: From<T> {
#[inline]
fn mul_assign(&mut self, other: T) { self.0 *= u64::from(other); }
}
impl<T> Sub<T> for Duration
where u64: From<T> {
type Output = Self;
#[inline]
fn sub(self, other: T) -> Self { Self(self.0.saturating_sub(u64::from(other))) }
}
impl<T> SubAssign<T> for Duration
where u64: From<T> {
#[inline]
fn sub_assign(&mut self, other: T) { self.0 = self.0.saturating_sub(u64::from(other)); }
}
impl Sum for Duration {
#[inline]
fn sum<I>(iter: I) -> Self
where I: Iterator<Item = Self> { iter.fold(Self::default(), |a, b| a + b) }
}
impl Duration {
pub const fn from_cdda_samples(total_samples: u64) -> Result<Self, TocError> {
let out = total_samples.wrapping_div(SAMPLES_PER_SECTOR);
if total_samples.is_multiple_of(SAMPLES_PER_SECTOR) { Ok(Self(out)) }
else { Err(TocError::CDDASampleCount) }
}
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "False positive.",
)]
#[must_use]
pub fn from_samples(sample_rate: u32, total_samples: u64) -> Self {
if sample_rate == 0 || total_samples == 0 { Self::default() }
else {
let sample_rate = u64::from(sample_rate);
let (s, rem) = (total_samples.wrapping_div(sample_rate), total_samples % sample_rate);
if rem == 0 { Self(s * SECTORS_PER_SECOND) }
else {
let f = NiceFloat::div_u64(rem * 75, sample_rate)
.map_or(0, |f| f.trunc() as u64);
Self(s * SECTORS_PER_SECOND + f)
}
}
}
}
impl Duration {
#[expect(clippy::cast_possible_truncation, reason = "False positive.")]
#[expect(clippy::many_single_char_names, reason = "Consistency is preferred.")]
#[must_use]
pub const fn dhmsf(self) -> (u64, u8, u8, u8, u8) {
let (s, f) = self.seconds_frames();
if s <= 4_294_967_295 {
let (d, h, m, s) = NiceElapsed::dhms(s as u32);
(d as u64, h, m, s, f)
}
else {
let d = s.wrapping_div(86_400);
let [h, m, s] = NiceElapsed::hms((s - d * 86_400) as u32);
(d, h, m, s, f)
}
}
#[must_use]
pub const fn samples(self) -> u64 { self.0 * SAMPLES_PER_SECTOR }
#[must_use]
pub const fn seconds_frames(self) -> (u64, u8) {
(self.0.wrapping_div(SECTORS_PER_SECOND), (self.0 % SECTORS_PER_SECOND) as u8)
}
#[must_use]
pub const fn sectors(self) -> u64 { self.0 }
#[expect(clippy::cast_precision_loss, reason = "False positive.")]
#[must_use]
pub const fn to_f64_lossy(self) -> f64 {
if self.0 <= 4_294_967_295 { self.0 as f64 / 75.0 }
else {
let (s, f) = self.seconds_frames();
s as f64 + ((f as f64) / 75.0)
}
}
#[must_use]
pub const fn to_std_duration_lossy(self) -> time::Duration {
if let Some(n) = self.0.checked_mul(40_000_000) {
time::Duration::from_nanos(n.wrapping_div(3))
}
else {
let (s, f) = self.seconds_frames();
time::Duration::from_secs(s).saturating_add(
time::Duration::from_nanos((f as u64 * 40_000_000).wrapping_div(3))
)
}
}
#[expect(clippy::many_single_char_names, reason = "Consistency is preferred.")]
#[must_use]
pub fn to_string_pretty(self) -> String {
let (d, h, m, s, f) = self.dhmsf();
let mut parts: Vec<String> = Vec::new();
if d != 0 { parts.push(d.nice_inflect("day", "days").to_string()); }
for (num, single, plural) in [
(h, "hour", "hours"),
(m, "minute", "minutes"),
(s, "second", "seconds"),
(f, "frame", "frames"),
] {
if num != 0 { parts.push(num.nice_inflect(single, plural).to_string()); }
}
match parts.len() {
0 => "0 seconds".to_owned(),
1 => parts.remove(0),
2 => parts.join(" and "),
n => {
let last = parts.remove(n - 1);
let mut out = parts.join(", ");
out.push_str(", and ");
out.push_str(&last);
out
},
}
}
}