use num::integer::div_floor;
use num::Rational64;
use regex::Match;
use std::convert::TryFrom;
use std::fmt::Debug;
use crate::consts::{
FEET_AND_FRAMES_REGEX, SECONDS_PER_HOUR_I64, SECONDS_PER_MINUTE_I64, TIMECODE_REGEX,
};
use crate::{
timecode_parse, FeetFramesStr, FilmFormat, Framerate, Ntsc, TimecodeParseError,
TimecodeSections,
};
pub type FramesSourceResult = Result<i64, TimecodeParseError>;
pub trait FramesSource: Debug {
fn to_frames(&self, rate: Framerate) -> FramesSourceResult;
}
impl<T> FramesSource for &T
where
T: FramesSource,
{
fn to_frames(&self, rate: Framerate) -> FramesSourceResult {
(*self).to_frames(rate)
}
}
impl FramesSource for &dyn FramesSource {
fn to_frames(&self, rate: Framerate) -> FramesSourceResult {
(*self).to_frames(rate)
}
}
impl FramesSource for i64 {
fn to_frames(&self, _: Framerate) -> FramesSourceResult {
Ok(*self)
}
}
impl FramesSource for isize {
fn to_frames(&self, _: Framerate) -> FramesSourceResult {
let i64_val = match i64::try_from(*self) {
Ok(converted) => converted,
Err(err) => {
return Err(TimecodeParseError::Conversion(format!(
"error converting isize to i64 : {}",
err
)))
}
};
Ok(i64_val)
}
}
impl FramesSource for usize {
fn to_frames(&self, _: Framerate) -> FramesSourceResult {
let i64_val = match i64::try_from(*self) {
Ok(converted) => converted,
Err(err) => {
return Err(TimecodeParseError::Conversion(format!(
"error converting usize to i64 : {}",
err
)))
}
};
Ok(i64_val)
}
}
impl FramesSource for u64 {
fn to_frames(&self, _: Framerate) -> FramesSourceResult {
let i64_val = match i64::try_from(*self) {
Ok(converted) => converted,
Err(err) => {
return Err(TimecodeParseError::Conversion(format!(
"error converting u64 to i64 : {}",
err
)))
}
};
Ok(i64_val)
}
}
impl FramesSource for i32 {
fn to_frames(&self, _: Framerate) -> FramesSourceResult {
Ok(i64::from(*self))
}
}
impl FramesSource for u32 {
fn to_frames(&self, _: Framerate) -> FramesSourceResult {
Ok(i64::from(*self))
}
}
impl FramesSource for i16 {
fn to_frames(&self, _: Framerate) -> FramesSourceResult {
Ok(i64::from(*self))
}
}
impl FramesSource for u16 {
fn to_frames(&self, _: Framerate) -> FramesSourceResult {
Ok(i64::from(*self))
}
}
impl FramesSource for i8 {
fn to_frames(&self, _: Framerate) -> FramesSourceResult {
Ok(i64::from(*self))
}
}
impl FramesSource for u8 {
fn to_frames(&self, _: Framerate) -> FramesSourceResult {
Ok(i64::from(*self))
}
}
impl FramesSource for &str {
fn to_frames(&self, rate: Framerate) -> FramesSourceResult {
if let Some(matched) = TIMECODE_REGEX.captures(self) {
return parse_timecode_string(matched, rate);
}
if let Some(matched) = FEET_AND_FRAMES_REGEX.captures(self) {
return parse_feet_and_frames_str(matched, None);
}
Err(TimecodeParseError::UnknownStrFormat(format!(
"{} is not a known frame-count timecode format",
self
)))
}
}
impl<'a> FramesSource for FeetFramesStr<'a> {
fn to_frames(&self, _rate: Framerate) -> FramesSourceResult {
if let Some(matched) = FEET_AND_FRAMES_REGEX.captures(self.input) {
parse_feet_and_frames_str(matched, Some(self.format))
} else {
Err(TimecodeParseError::UnknownStrFormat(format!(
"{} is not a known frame-count timecode format",
self.input
)))
}
}
}
impl FramesSource for String {
fn to_frames(&self, rate: Framerate) -> FramesSourceResult {
self.as_str().to_frames(rate)
}
}
fn parse_timecode_string(matched: regex::Captures, rate: Framerate) -> FramesSourceResult {
let frames =
timecode_parse::convert_tc_int(matched.name("frames").unwrap().as_str(), "frames")?;
let mut sections: Vec<Match> = Vec::new();
if let Some(section) = matched.name("section1") {
sections.push(section);
};
if let Some(section) = matched.name("section2") {
sections.push(section);
};
if let Some(section) = matched.name("section3") {
sections.push(section);
};
let is_negative = matched.name("negative").is_some();
let seconds: i64 = match sections.pop() {
None => 0,
Some(section) => timecode_parse::convert_tc_int(section.as_str(), "seconds")?,
};
let minutes: i64 = match sections.pop() {
None => 0,
Some(section) => timecode_parse::convert_tc_int(section.as_str(), "minutes")?,
};
let hours: i64 = match sections.pop() {
None => 0,
Some(section) => timecode_parse::convert_tc_int(section.as_str(), "frames")?,
};
let drop_adjustment = if rate.ntsc() == Ntsc::DropFrame {
drop_frame_tc_adjustment(
TimecodeSections {
negative: is_negative,
hours,
minutes,
seconds,
frames,
},
rate,
)?
} else {
0
};
let seconds = seconds + minutes * SECONDS_PER_MINUTE_I64 + hours * SECONDS_PER_HOUR_I64;
let frames_rat =
Rational64::from_integer(seconds) * rate.timebase() + Rational64::from_integer(frames);
let mut frames = frames_rat.round().to_integer();
frames += drop_adjustment;
if is_negative {
frames *= -1
}
Ok(frames)
}
fn drop_frame_tc_adjustment(sections: TimecodeSections, rate: Framerate) -> FramesSourceResult {
let drop_frames = rate.drop_frames_per_minute().unwrap();
let has_bad_frames = sections.frames < drop_frames;
let is_tenth_minute = sections.minutes % 10 == 0;
let is_minute_boundary = sections.seconds == 0;
if has_bad_frames && is_minute_boundary && !is_tenth_minute {
return Err(TimecodeParseError::DropFrameValue(format!(
"drop-frame tc cannot have a frames value of less than {} on minutes not divisible by 10, found '{}'",
drop_frames,
sections.frames,
)));
};
let total_minutes = 60 * sections.hours + sections.minutes;
let adjustment = drop_frames * (total_minutes - total_minutes / 10);
Ok(-adjustment)
}
fn parse_feet_and_frames_str(
matched: regex::Captures,
given_format: Option<FilmFormat>,
) -> FramesSourceResult {
let feet = timecode_parse::convert_tc_int(matched.name("feet").unwrap().as_str(), "feet")?;
let frames =
timecode_parse::convert_tc_int(matched.name("frames").unwrap().as_str(), "frames")?;
let perfs_n = matched
.name("perf")
.and_then(|perfs_n| perfs_n.as_str().parse::<i64>().ok());
let is_negative = matched.name("negative").is_some();
let final_format : Result<FilmFormat, TimecodeParseError> = match (given_format, perfs_n) {
(Some(film_format) , Some(_)) if !film_format.allows_perf_field() => Err(TimecodeParseError::UnknownStrFormat(
format!("Perf field was present in string \"{}\", which is not allowed for given film format {:?}.",
matched.get(0).unwrap().as_str(), film_format)
)
),
(Some(film_format), _) => Ok(film_format),
(None, Some(_)) => Ok(FilmFormat::FF35mm3perf),
(_, _) => Ok(FilmFormat::FF35mm4perf),
};
if let Ok(final_format) = final_format {
let mut rem_frames = frames;
let footage_moduli = div_floor(feet, final_format.footage_modulus_footage_count());
let mut rem_feet = feet - (footage_moduli * final_format.footage_modulus_footage_count());
rem_frames += footage_moduli * final_format.footage_modulus_frame_count();
while rem_feet > 0 {
rem_frames += final_format.footage_modulus_frame_count()
/ final_format.footage_modulus_footage_count();
rem_feet -= 1;
}
if is_negative {
rem_frames = -rem_frames;
};
Ok(dbg!(rem_frames))
} else {
Err(final_format.err().unwrap())
}
}