//! Work with durations of time-sampled data, like digital audio.
//!
//! The `sampled_data_duration` crate provides two stucts:
//! `ConstantRateDuration` and `MixedRateDuration`.
//!
//! A `ConstantRateDuraiton` can be used to represents the duration of
//! any data-set which has been sampled at a constant frequency, a
//! prime example might be an audio file sampled at 44.1kHz.
//!
//! A `MixedRateDuration` can be used to represent the duration of a
//! collection of data-sets which have different sampling
//! frequencies. A typical example might be a playlist of audio files
//! where some have been sampled at 44.1kHz, and others at 48kHz or
//! 96kHz, etc.
//!
//! # Example
//!
//! ```
//! use sampled_data_duration::ConstantRateDuration;
//! use sampled_data_duration::MixedRateDuration;
//!
//! // Consider an audio file which consists of `12345678` samples per
//! // channel recorded at a sampling rate of 44.1kHz.
//! let crd = ConstantRateDuration::new(12345678, 44100);
//!
//! // The default string representation is of the form
//! // `hh:mm:ss;samples`
//! assert_eq!(crd.to_string(), "00:04:39;41778");
//!
//! // Get the duration in various different time-units
//! assert_eq!(crd.as_hours(), 0);
//! assert_eq!(crd.as_mins(), 4);
//! assert_eq!(crd.submin_secs(), 39);
//! assert_eq!(crd.as_secs(), 4 * 60 + 39);
//! assert_eq!(crd.subsec_samples(), 41778);
//! assert_eq!(crd.subsec_secs(), 0.9473469387755102);
//!
//! // Consider and audio playlist which already consists of a file
//! // recorded at 96kHz.
//! let mut mrd = MixedRateDuration::from(ConstantRateDuration::new(87654321, 96000));
//!
//! // The default string representation of the a mixed rate duration
//! // which consits of only one entry is of the form `hh:mm:ss;samples`
//! assert_eq!(mrd.to_string(), "00:15:13;6321");
//!
//! // However if we add-assign `crd` to `mrd`
//! mrd += crd;
//!
//! // Then we have a duration which is made up of different sampling
//! // rates and the default string representation changes to be of the
//! // form `hh:mm:ss.s`
//! assert_eq!(mrd.to_string(), "00:19:53.013190688");
//!```
//!
//! An attempt has been made to follow the naming conventions defined
//! by [`std::time::Duration`].

use std::collections::HashMap;
use std::fmt;
use std::ops::Add;
use std::ops::AddAssign;
use std::time::Duration;

// Various constants for converting between different units of time
const NANOS_PER_SEC: f64 = 1_000_000_000.0;
const SECS_PER_MIN: u64 = 60;
const SECS_PER_HOUR: u64 = 3600;
const SECS_PER_DAY: u64 = 86400;
const SECS_PER_WEEK: u64 = 604800;
const DAYS_PER_WEEK: u64 = 7;
const HOURS_PER_DAY: u64 = 24;
const MINS_PER_HOUR: u64 = 60;

/// Represents the duration of a dataset which has been sampled at a
/// constant rate.
///
/// # Examples
///
/// Consider an audio file which consists of `8394223` samples per
/// channel recorded with a 48kHz sampling frequency. We can see what
/// the duration of this is by using the default implementation of
/// `std::fmt::Display` for a `ConstantRateDuration` which outputs the
/// duration in the form `hh:mm:ss;samples`.
///
/// ```
/// use sampled_data_duration::ConstantRateDuration;
///
/// let crd = ConstantRateDuration::new(8394223, 48000);
/// assert_eq!(crd.to_string(), "00:02:54;42223");
///```
pub struct ConstantRateDuration {
    count: u64,
    rate: u64,
}
impl ConstantRateDuration {
    /// Construct a new `ConstantRateDuration`, where `count`
    /// corresponds to the number of samples and `rate` is the
    /// sampling rate in Hertz.
    pub fn new(count: u64, rate: u64) -> ConstantRateDuration {
        ConstantRateDuration { count, rate }
    }

    /// Returns the number of _whole_ seconds contained by this `ConstantRateDuration`.
    ///
    /// The returned value does not include the fractional part of the
    /// duration which can be obtained with [`subsec_secs`].
    ///
    /// # Example
    ///
    /// ```
    /// use sampled_data_duration::ConstantRateDuration;
    ///
    /// let crd = ConstantRateDuration::new(48000 * 62 + 123, 48000);
    /// assert_eq!(crd.to_string(), "00:01:02;123");
    /// assert_eq!(crd.as_secs(), 62);
    /// ```
    /// [`subsec_secs`]: ConstantRateDuration::subsec_secs
    pub fn as_secs(&self) -> u64 {
        self.count / self.rate
    }

    /// Returns the number of _whole_ minutes contained by this `ConstantRateDuration`.
    ///
    /// The returned value does not include the fractional part of the
    /// duration, and can be a value greater than 59.
    ///
    /// # Example
    ///
    /// ```
    /// use sampled_data_duration::ConstantRateDuration;
    ///
    /// let crd = ConstantRateDuration::new(48000 * 60 * 91, 48000);
    /// assert_eq!(crd.to_string(), "01:31:00;0");
    /// assert_eq!(crd.as_mins(), 91);
    /// ```
    pub fn as_mins(&self) -> u64 {
        self.as_secs() / SECS_PER_MIN
    }

    /// Returns the number of _whole_ hours contained by this `ConstantRateDuration`.
    ///
    /// The returned value does not include the fractional part of the
    /// duration, and can be a value greater than 23.
    ///
    /// # Example
    ///
    /// ```
    /// use sampled_data_duration::ConstantRateDuration;
    ///
    /// let crd = ConstantRateDuration::new(48000 * 60 * 60 * 48 + 12345, 48000);
    /// assert_eq!(crd.to_string(), "48:00:00;12345");
    /// assert_eq!(crd.as_hours(), 48);
    /// ```
    pub fn as_hours(&self) -> u64 {
        self.as_secs() / SECS_PER_HOUR
    }

    /// Returns the number of _whole_ days contained by this `ConstantRateDuration`.
    ///
    /// The returned value does not include the fractional part of the
    /// duration, and can be a value greater than 6.
    ///
    /// # Example
    ///
    /// ```
    /// use sampled_data_duration::ConstantRateDuration;
    ///
    /// let crd = ConstantRateDuration::new(48000 * 60 * 60 * 48 + 12345, 48000);
    /// assert_eq!(crd.to_string(), "48:00:00;12345");
    /// assert_eq!(crd.as_days(), 2);
    /// ```
    pub fn as_days(&self) -> u64 {
        self.as_secs() / SECS_PER_DAY
    }

    /// Returns the number of _whole_ weeks contained by this `ConstantRateDuration`.
    ///
    /// The returned value does not include the fractional part of the
    /// duration.
    ///
    /// # Example
    ///
    /// ```
    /// use sampled_data_duration::ConstantRateDuration;
    ///
    /// let crd = ConstantRateDuration::new(48000 * 60 * 60 * 24 * 21 + 12345, 48000);
    /// assert_eq!(crd.to_string(), "504:00:00;12345");
    /// assert_eq!(crd.as_weeks(), 3);
    /// ```
    pub fn as_weeks(&self) -> u64 {
        self.as_secs() / SECS_PER_WEEK
    }

    /// Returns the number of samples in the sub-second part of this
    /// `ConstantRateDuration`.
    ///
    /// The returned value will always be less than the sampling rate.
    ///
    /// # Example
    ///
    /// ```
    /// use sampled_data_duration::ConstantRateDuration;
    ///
    /// let crd = ConstantRateDuration::new(48000 + 12345, 48000);
    /// assert_eq!(crd.to_string(), "00:00:01;12345");
    /// assert_eq!(crd.subsec_samples(), 12345);
    /// ```
    pub fn subsec_samples(&self) -> u64 {
        self.count % self.rate
    }

    /// Returns the _whole_ number of nanoseconds in the fractional part of this `ConstantRateDuration`.
    ///
    /// The returned value will always be less than one second i.e. >
    /// 1_000_000_000 nanoseconds.
    ///
    /// # Example
    ///
    /// ```
    /// use sampled_data_duration::ConstantRateDuration;
    ///
    /// let crd = ConstantRateDuration::new(48000 + 24000, 48000);
    /// assert_eq!(crd.to_string(), "00:00:01;24000");
    /// assert_eq!(crd.subsec_nanos(), 500_000_000);
    /// ```
    pub fn subsec_nanos(&self) -> u32 {
        let nanos_as_f64 = self.subsec_secs() * NANOS_PER_SEC;

        nanos_as_f64 as u32
    }

    /// Return the sub-second part of this duration in seconds.
    ///
    /// This will return a value in the range `0.0 <= subsec_secs < 1.0`.
    ///
    /// # Example
    ///
    /// ```
    /// use sampled_data_duration::ConstantRateDuration;
    ///
    /// let crd = ConstantRateDuration::new(48000 + 24000, 48000);
    /// assert_eq!(crd.to_string(), "00:00:01;24000");
    /// assert_eq!(crd.subsec_secs(), 0.5);
    /// ```
    pub fn subsec_secs(&self) -> f64 {
        self.subsec_samples() as f64 / self.rate as f64
    }

    /// Returns the _whole_ number of seconds left over when this duration is measured in minutes.
    ///
    /// The returned value will always be 0 <= submin_secs <= 59.
    ///
    /// # Example
    ///
    /// ```
    /// use sampled_data_duration::ConstantRateDuration;
    ///
    /// let crd = ConstantRateDuration::new(48000 * 65 + 32000, 48000);
    /// assert_eq!(crd.to_string(), "00:01:05;32000");
    /// assert_eq!(crd.submin_secs(), 5);
    /// ```
    pub fn submin_secs(&self) -> u64 {
        self.as_secs() % SECS_PER_MIN
    }

    /// Returns the _whole_ number of minutes left over when this duration is measured in hours.
    ///
    /// The returned value will always be 0 <= subhour_mins <= 59.
    ///
    /// # Example
    ///
    /// ```
    /// use sampled_data_duration::ConstantRateDuration;
    ///
    /// let crd = ConstantRateDuration::new(48000 * 60 * 68, 48000);
    /// assert_eq!(crd.to_string(), "01:08:00;0");
    /// assert_eq!(crd.subhour_mins(), 8);
    /// ```
    pub fn subhour_mins(&self) -> u64 {
        self.as_mins() % MINS_PER_HOUR
    }

    /// Returns the _whole_ number of hours left over when this duration is measured in days.
    ///
    /// The returned value will always be 0 <= subday_hours <= 23.
    ///
    /// # Example
    ///
    /// ```
    /// use sampled_data_duration::ConstantRateDuration;
    ///
    /// let crd = ConstantRateDuration::new(48000 * 60 * 60 * 25, 48000);
    /// assert_eq!(crd.to_string(), "25:00:00;0");
    /// assert_eq!(crd.subday_hours(), 1);
    /// ```
    pub fn subday_hours(&self) -> u64 {
        self.as_hours() % HOURS_PER_DAY
    }

    /// Returns the _whole_ number of days left over when this duration is measured in weeks.
    ///
    /// The returned value will always be 0 <= subweek_days <= 6.
    ///
    /// # Example
    ///
    /// ```
    /// use sampled_data_duration::ConstantRateDuration;
    ///
    /// let crd = ConstantRateDuration::new(48000 * 60 * 60 * 24 * 9, 48000);
    /// assert_eq!(crd.to_string(), "216:00:00;0");
    /// assert_eq!(crd.subweek_days(), 2);
    /// ```
    pub fn subweek_days(&self) -> u64 {
        self.as_days() % DAYS_PER_WEEK
    }

    /// Returns this `ConstantRateDuration` as a `std::time::Duration`.
    ///
    /// # Example
    ///
    /// ```
    /// use sampled_data_duration::ConstantRateDuration;
    ///
    /// let crd = ConstantRateDuration::new(48000 * 42, 48000);
    /// assert_eq!(crd.to_string(), "00:00:42;0");
    /// assert_eq!(crd.to_duration().as_secs_f64(), 42.0);
    /// ```
    pub fn to_duration(&self) -> Duration {
        Duration::new(self.as_secs(), self.subsec_nanos())
    }

    /// Computes `self + other` returning `Ok(ConstantRateDuration)`
    /// if the sampling rates of `self` and `other` are the same, or
    /// `Err(MixedRateDuration)` if they are not.
    ///
    /// If the sample count of the new duration exceeds the maximum
    /// capacity of a `u64` then this method will panic with the error
    /// message `"overflow when adding constant rate durations"`.
    ///
    /// # Examples
    ///
    /// ```
    /// use sampled_data_duration::*;
    ///
    /// let a = ConstantRateDuration::new(48000, 48000);
    /// let b = ConstantRateDuration::new(48000 * 2, 48000);
    ///
    /// if let Ok(c) = a.try_add(b) {
    ///   assert_eq!(c.as_secs(), 3);
    /// } else {
    ///   assert!(false);
    /// }
    ///```
    pub fn try_add(
        self,
        other: ConstantRateDuration,
    ) -> Result<ConstantRateDuration, MixedRateDuration> {
        if self.rate == other.rate {
            return Ok(ConstantRateDuration::new(
                self.count
                    .checked_add(other.count)
                    .expect("overflow when adding constant rate durations"),
                self.rate,
            ));
        }

        let mut map: HashMap<u64, u64> = HashMap::with_capacity(2);
        map.insert(self.rate, self.count);
        map.insert(other.rate, other.count);
        let mut mrd = MixedRateDuration {
            duration: Duration::new(0, 0),
            map,
        };
        mrd.update_duration();

        Err(mrd)
    }

    /// Add-assigns `crd` to this `ConstantRateDuration` returning
    /// `Ok(())` if the sampling rates of `self` and `other` are the
    /// same, or `Err(MixedRateDuration)` if they are not.
    ///
    /// If the sample count of updated duration exceeds the maximum
    /// capacity of a `u64` then this method will panic with the error
    /// message `"overflow when add-assigning constant rate durations"`.
    ///
    /// # Examples
    ///
    /// ```
    /// use sampled_data_duration::*;
    ///
    /// let mut a = ConstantRateDuration::new(48000, 48000);
    /// let b = ConstantRateDuration::new(48000 * 2, 48000);
    ///
    /// if let Ok(()) = a.try_add_assign(b) {
    ///   assert_eq!(a.as_secs(), 3);
    /// } else {
    ///   assert!(false);
    /// }
    ///```
    pub fn try_add_assign(&mut self, crd: ConstantRateDuration) -> Result<(), MixedRateDuration> {
        if self.rate == crd.rate {
            self.count = self
                .count
                .checked_add(crd.count)
                .expect("overflow when add-assigning constant rate durations");
            return Ok(());
        }

        let mut map: HashMap<u64, u64> = HashMap::with_capacity(2);
        map.insert(self.rate, self.count);
        map.insert(crd.rate, crd.count);

        let mut mrd = MixedRateDuration {
            duration: Duration::new(0, 0),
            map,
        };
        mrd.update_duration();

        Err(mrd)
    }
}
impl fmt::Display for ConstantRateDuration {
    /// Display this `ConstantRateDuration` in the form `hh:mm:ss;samples`.
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let hours = self.as_hours();

        if hours < 10 {
            return write!(
                f,
                "{:02}:{:02}:{:02};{}",
                hours,
                self.subhour_mins(),
                self.submin_secs(),
                self.subsec_samples()
            );
        }

        write!(
            f,
            "{}:{:02}:{:02};{}",
            hours,
            self.subhour_mins(),
            self.submin_secs(),
            self.subsec_samples()
        )
    }
}

#[cfg(test)]
mod constant_rate_duration {
    use super::*;

    #[test]
    fn new() {
        let count: u64 = 15345873;
        let rate: u64 = 44100;
        let mut crd: ConstantRateDuration = ConstantRateDuration::new(count, rate);

        assert_eq!(crd.count, count);
        assert_eq!(crd.rate, rate);

        let updated_count: u64 = 26326888;
        crd.count = updated_count;
        assert_eq!(crd.count, updated_count);

        let updated_rate: u64 = 48000;
        crd.rate = updated_rate;
        assert_eq!(crd.rate, updated_rate);
    }

    #[test]
    fn as_and_sub_time_unit_methods() {
        let subsec_samples: u64 = 24_000;
        let submin_secs: u64 = 32;
        let subhour_mins: u64 = 23;
        let subday_hours: u64 = 19;
        let subweek_days: u64 = 4;
        let weeks: u64 = 1;
        let rate: u64 = 48_000;

        let as_days = weeks * DAYS_PER_WEEK + subweek_days;
        let as_hours = as_days * HOURS_PER_DAY + subday_hours;
        let as_mins = as_hours * MINS_PER_HOUR + subhour_mins;
        let as_secs = as_mins * SECS_PER_MIN + submin_secs;

        let total_secs: u64 = weeks * SECS_PER_WEEK
            + subweek_days * SECS_PER_DAY
            + subday_hours * SECS_PER_HOUR
            + subhour_mins * SECS_PER_MIN
            + submin_secs;
        let count: u64 = total_secs * rate + subsec_samples;
        let crd: ConstantRateDuration = ConstantRateDuration::new(count, rate);

        assert_eq!(crd.count, count);
        assert_eq!(crd.rate, rate);
        assert_eq!(crd.as_secs(), as_secs);
        assert_eq!(crd.as_mins(), as_mins);
        assert_eq!(crd.as_hours(), as_hours);
        assert_eq!(crd.as_days(), as_days);
        assert_eq!(crd.as_weeks(), weeks);
        assert_eq!(crd.subsec_samples(), subsec_samples);
        assert_eq!(crd.subsec_nanos(), 500_000_000);
        assert_eq!(crd.subsec_secs(), 0.5);
        assert_eq!(crd.submin_secs(), submin_secs);
        assert_eq!(crd.subhour_mins(), subhour_mins);
        assert_eq!(crd.subday_hours(), subday_hours);
        assert_eq!(crd.subweek_days(), subweek_days);

        // Check sample roll-over to seconds
        let mut crd: ConstantRateDuration = ConstantRateDuration::new(47999, 48000);
        assert_eq!(crd.as_secs(), 0);
        assert_eq!(crd.subsec_samples(), 47999);
        assert_eq!(crd.subsec_nanos(), 1000000000 - 20834);

        crd.count += 1;
        assert_eq!(crd.as_secs(), 1);
        assert_eq!(crd.subsec_samples(), 0);
        assert_eq!(crd.subsec_nanos(), 0);

        crd.count += 1;
        assert_eq!(crd.as_secs(), 1);
        assert_eq!(crd.subsec_samples(), 1);
        assert_eq!(crd.subsec_nanos(), 20833);
    }

    #[test]
    fn to_duration() {
        let crd = ConstantRateDuration::new(48000, 48000);
        assert_eq!(crd.to_duration(), Duration::new(1, 0));

        let crd = ConstantRateDuration::new(48001, 48000);
        assert_eq!(crd.to_duration(), Duration::new(1, 20833));
    }

    #[test]
    fn try_add() {
        // Add ConstantRateDurations with the same sample rate
        let a = ConstantRateDuration::new(48000, 48000);
        assert_eq!(a.as_secs(), 1);
        assert_eq!(a.subsec_samples(), 0);

        let b = ConstantRateDuration::new(1, 48000);
        assert_eq!(b.as_secs(), 0);
        assert_eq!(b.subsec_samples(), 1);

        if let Ok(c) = a.try_add(b) {
            assert_eq!(c.as_secs(), 1);
            assert_eq!(c.subsec_samples(), 1);
        } else {
            assert!(false);
        }

        // Add ConstantRateDurations with different sample rates
        let a = ConstantRateDuration::new(48000, 48000);
        assert_eq!(a.as_secs(), 1);
        assert_eq!(a.subsec_samples(), 0);

        let b = ConstantRateDuration::new(96000, 96000);
        assert_eq!(b.as_secs(), 1);
        assert_eq!(b.subsec_samples(), 0);

        if let Err(c) = a.try_add(b) {
            assert_eq!(c.as_secs(), 2);
        } else {
            assert!(false);
        }
    }

    #[test]
    #[should_panic(expected = "overflow when adding constant rate durations")]
    fn try_add_overflow() {
        // Add ConstantRateDurations which overflow
        let a = ConstantRateDuration::new(u64::MAX, 48000);
        let b = ConstantRateDuration::new(1, 48000);
        let _c = a.try_add(b);
    }

    #[test]
    fn try_add_assign() {
        // Add-assign ConstantRateDurations with the same sampling rate
        let mut a = ConstantRateDuration::new(48000, 48000);
        let b = ConstantRateDuration::new(48000, 48000);
        assert_eq!(a.as_secs(), 1);
        assert_eq!(a.subsec_samples(), 0);
        assert_eq!(b.as_secs(), 1);
        assert_eq!(b.subsec_samples(), 0);
        assert!(a.try_add_assign(b).is_ok());
        assert_eq!(a.as_secs(), 2);
        assert_eq!(a.subsec_samples(), 0);

        // Add-assign ConstantRateDurations with different sampling rates
        let mut a = ConstantRateDuration::new(48000, 48000);
        let b = ConstantRateDuration::new(96000, 96000);
        assert_eq!(a.as_secs(), 1);
        assert_eq!(a.subsec_samples(), 0);
        assert_eq!(b.as_secs(), 1);
        assert_eq!(b.subsec_samples(), 0);

        if let Err(c) = a.try_add_assign(b) {
            assert_eq!(c.as_secs(), 2);
        } else {
            assert!(false);
        }
    }

    #[test]
    #[should_panic(expected = "overflow when add-assigning constant rate durations")]
    fn try_add_assign_overflow() {
        // Add-assign ConstantRateDurations which overflow
        let mut a = ConstantRateDuration::new(u64::MAX, 48000);
        let b = ConstantRateDuration::new(1, 48000);
        let _c = a.try_add_assign(b);
    }

    #[test]
    fn display() {
        let crd = ConstantRateDuration::new(48000 + 12345, 48000);
        assert_eq!(crd.to_string(), "00:00:01;12345");
    }
}

/// Represents a duration of a collection of datasets which may have
/// been sampled at different rates.
///
/// # Examples
///
/// Consider an audio playlist which at first consists of a single
/// audio file of `12345678` samples per channel recorded with at
/// 44.1kHz sampling frequency. We then add another audio file to this
/// playlist which is recorded at 96kHz.
///
/// ```
/// use sampled_data_duration::ConstantRateDuration;
/// use sampled_data_duration::MixedRateDuration;
///
/// let mut mrd = MixedRateDuration::from(ConstantRateDuration::new(12345678, 48000));
///
/// // Note that the default string representation when there is only
/// // a single sampling rate is of the form `hh:mm:ss;samples`.
/// assert_eq!(mrd.to_string(), "00:04:17;9678");
///
/// // Now we add a 96kHz file
/// let crd = ConstantRateDuration::new(31415926, 96000);
/// assert_eq!(crd.to_string(), "00:05:27;23926");
///
/// mrd += crd;
///
/// // Note that the default string representation when there are
/// // multiple sampling rates is of the form `hh:mm:ss.s`.
/// assert_eq!(mrd.to_string(), "00:09:44.450854166");
///```
pub struct MixedRateDuration {
    duration: Duration,
    map: HashMap<u64, u64>,
}
impl MixedRateDuration {
    /// Construct an empty `MixedRateDuration`.
    ///
    /// # Examples
    ///
    /// ```
    /// use sampled_data_duration::MixedRateDuration;
    ///
    /// let mut mrd = MixedRateDuration::new();
    ///
    /// assert_eq!(mrd.to_string(), "00:00:00");
    ///```
    pub fn new() -> MixedRateDuration {
        MixedRateDuration {
            duration: Duration::new(0, 0),
            map: HashMap::new(),
        }
    }

    /// Returns the number of _whole_ seconds contained by this
    /// `MixedRateDuration`.
    ///
    /// The returned value does not include the fractional part of the
    /// duration which can be obtained with [`subsec_secs`].
    ///
    /// # Example
    ///
    /// ```
    /// use sampled_data_duration::ConstantRateDuration;
    /// use sampled_data_duration::MixedRateDuration;
    ///
    /// let mrd = MixedRateDuration::from(ConstantRateDuration::new(48000 * 62 + 123, 48000));
    /// assert_eq!(mrd.to_string(), "00:01:02;123");
    /// assert_eq!(mrd.as_secs(), 62);
    /// ```
    /// [`subsec_secs`]: MixedRateDuration::subsec_secs
    pub fn as_secs(&self) -> u64 {
        self.duration.as_secs()
    }

    /// Returns the number of _whole_ minutes contained by this `MixedRateDuration`.
    ///
    /// The returned value does not include the fractional part of the
    /// duration, and can be a value greater than 59.
    ///
    /// # Example
    ///
    /// ```
    /// use sampled_data_duration::ConstantRateDuration;
    /// use sampled_data_duration::MixedRateDuration;
    ///
    /// let mrd = MixedRateDuration::from(ConstantRateDuration::new(48000 * 60 * 91, 48000));
    /// assert_eq!(mrd.to_string(), "01:31:00;0");
    /// assert_eq!(mrd.as_mins(), 91);
    /// ```
    pub fn as_mins(&self) -> u64 {
        self.as_secs() / SECS_PER_MIN
    }

    /// Returns the number of _whole_ hours contained by this `MixedRateDuration`.
    ///
    /// The returned value does not include the fractional part of the
    /// duration, and can be a value greater than 23.
    ///
    /// # Example
    ///
    /// ```
    /// use sampled_data_duration::ConstantRateDuration;
    /// use sampled_data_duration::MixedRateDuration;
    ///
    /// let mrd = MixedRateDuration::from(ConstantRateDuration::new(48000 * 60 * 60 * 48 + 12345, 48000));
    /// assert_eq!(mrd.to_string(), "48:00:00;12345");
    /// assert_eq!(mrd.as_hours(), 48);
    /// ```
    pub fn as_hours(&self) -> u64 {
        self.as_secs() / SECS_PER_HOUR
    }

    /// Returns the number of _whole_ days contained by this `MixedRateDuration`.
    ///
    /// The returned value does not include the fractional part of the
    /// duration, and can be a value greater than 6.
    ///
    /// # Example
    ///
    /// ```
    /// use sampled_data_duration::ConstantRateDuration;
    /// use sampled_data_duration::MixedRateDuration;
    ///
    /// let mrd = MixedRateDuration::from(ConstantRateDuration::new(48000 * 60 * 60 * 48 + 12345, 48000));
    /// assert_eq!(mrd.to_string(), "48:00:00;12345");
    /// assert_eq!(mrd.as_days(), 2);
    /// ```
    pub fn as_days(&self) -> u64 {
        self.as_secs() / SECS_PER_DAY
    }

    /// Returns the number of _whole_ weeks contained by this `MixedRateDuration`.
    ///
    /// The returned value does not include the fractional part of the
    /// duration.
    ///
    /// # Example
    ///
    /// ```
    /// use sampled_data_duration::ConstantRateDuration;
    /// use sampled_data_duration::MixedRateDuration;
    ///
    /// let mrd = MixedRateDuration::from(ConstantRateDuration::new(48000 * 60 * 60 * 24 * 21 + 12345, 48000));
    /// assert_eq!(mrd.to_string(), "504:00:00;12345");
    /// assert_eq!(mrd.as_weeks(), 3);
    /// ```
    pub fn as_weeks(&self) -> u64 {
        self.as_secs() / SECS_PER_WEEK
    }

    /// Return the number of different sampling rates used in this
    /// `MixedRateDuration`.
    ///
    /// # Example
    ///
    /// ```
    /// use sampled_data_duration::ConstantRateDuration;
    /// use sampled_data_duration::MixedRateDuration;
    ///
    /// let mut mrd = MixedRateDuration::from(ConstantRateDuration::new(1, 44100));
    /// assert_eq!(mrd.num_rates(), 1);
    ///
    /// mrd += ConstantRateDuration::new(1, 48000);
    /// assert_eq!(mrd.num_rates(), 2);
    ///
    /// mrd += ConstantRateDuration::new(1, 96000);
    /// assert_eq!(mrd.num_rates(), 3);
    ///
    /// mrd += ConstantRateDuration::new(2, 44100);
    /// assert_eq!(mrd.num_rates(), 3);
    /// ```
    pub fn num_rates(&self) -> usize {
        self.map.len()
    }

    /// Returns the _whole_ number of nanoseconds in the fractional part of this `MixedRateDuration`.
    ///
    /// The returned value will always be less than one second i.e. >
    /// 1_000_000_000 nanoseconds.
    ///
    /// # Example
    ///
    /// ```
    /// use sampled_data_duration::ConstantRateDuration;
    /// use sampled_data_duration::MixedRateDuration;
    ///
    /// let mrd = MixedRateDuration::from(ConstantRateDuration::new(48000 + 24000, 48000));
    /// assert_eq!(mrd.to_string(), "00:00:01;24000");
    /// assert_eq!(mrd.subsec_nanos(), 500_000_000);
    /// ```
    pub fn subsec_nanos(&self) -> u32 {
        let nanos_as_f64 = self.subsec_secs() * NANOS_PER_SEC;

        nanos_as_f64 as u32
    }

    /// Returns the _whole_ number of seconds in the fractional part of this `MixedRateDuration`.
    ///
    /// The returned value will always be less than one second
    /// i.e. 0.0 <= subsec_secs < 1.0.
    ///
    /// # Example
    ///
    /// ```
    /// use sampled_data_duration::ConstantRateDuration;
    /// use sampled_data_duration::MixedRateDuration;
    ///
    /// let mrd = MixedRateDuration::from(ConstantRateDuration::new(48000 + 24000, 48000));
    /// assert_eq!(mrd.to_string(), "00:00:01;24000");
    /// assert_eq!(mrd.subsec_secs(), 0.5);
    /// ```
    pub fn subsec_secs(&self) -> f64 {
        self.duration.subsec_nanos() as f64 / NANOS_PER_SEC
    }

    /// Returns the _whole_ number of seconds left over when this
    /// duration is measured in minutes.
    ///
    /// The returned value will always be 0 <= submin_secs <= 59.
    ///
    /// # Example
    ///
    /// ```
    /// use sampled_data_duration::ConstantRateDuration;
    /// use sampled_data_duration::MixedRateDuration;
    ///
    /// let mrd = MixedRateDuration::from(ConstantRateDuration::new(48000 * 65 + 32000, 48000));
    /// assert_eq!(mrd.to_string(), "00:01:05;32000");
    /// assert_eq!(mrd.submin_secs(), 5);
    /// ```
    pub fn submin_secs(&self) -> u64 {
        self.as_secs() % SECS_PER_MIN
    }

    /// Returns the _whole_ number of minutes left over when this duration is measured in hours.
    ///
    /// The returned value will always be 0 <= subhour_mins <= 59.
    ///
    /// # Example
    ///
    /// ```
    /// use sampled_data_duration::ConstantRateDuration;
    /// use sampled_data_duration::MixedRateDuration;
    ///
    /// let mrd = MixedRateDuration::from(ConstantRateDuration::new(48000 * 60 * 68, 48000));
    /// assert_eq!(mrd.to_string(), "01:08:00;0");
    /// assert_eq!(mrd.subhour_mins(), 8);
    /// ```
    pub fn subhour_mins(&self) -> u64 {
        self.as_mins() % MINS_PER_HOUR
    }

    /// Returns the _whole_ number of hours left over when this duration is measured in days.
    ///
    /// The returned value will always be 0 <= subday_hours <= 23.
    ///
    /// # Example
    ///
    /// ```
    /// use sampled_data_duration::ConstantRateDuration;
    /// use sampled_data_duration::MixedRateDuration;
    ///
    /// let mrd = MixedRateDuration::from(ConstantRateDuration::new(48000 * 60 * 60 * 25, 48000));
    /// assert_eq!(mrd.to_string(), "25:00:00;0");
    /// assert_eq!(mrd.subday_hours(), 1);
    /// ```
    pub fn subday_hours(&self) -> u64 {
        self.as_hours() % HOURS_PER_DAY
    }

    /// Returns the _whole_ number of days left over when this duration is measured in weeks.
    ///
    /// The returned value will always be 0 <= subweek_days <= 6.
    ///
    /// # Example
    ///
    /// ```
    /// use sampled_data_duration::ConstantRateDuration;
    /// use sampled_data_duration::MixedRateDuration;
    ///
    /// let mrd = MixedRateDuration::from(ConstantRateDuration::new(48000 * 60 * 60 * 24 * 9, 48000));
    /// assert_eq!(mrd.to_string(), "216:00:00;0");
    /// assert_eq!(mrd.subweek_days(), 2);
    /// ```
    pub fn subweek_days(&self) -> u64 {
        self.as_days() % DAYS_PER_WEEK
    }

    /// Return this `MixedRateDuration` as a `std::time::Duration`.
    ///
    /// # Example
    ///
    /// ```
    /// use sampled_data_duration::ConstantRateDuration;
    /// use sampled_data_duration::MixedRateDuration;
    ///
    /// let mrd = MixedRateDuration::from(ConstantRateDuration::new(48000 * 42, 48000));
    /// assert_eq!(mrd.to_string(), "00:00:42;0");
    /// assert_eq!(mrd.to_duration().as_secs_f64(), 42.0);
    /// ```
    pub fn to_duration(&self) -> Duration {
        self.duration.clone()
    }

    /// Update this `MixedRateDuration`'s duration field by summing
    /// over all enteries in the internal `HashMap`.
    fn update_duration(&mut self) {
        let mut duration = Duration::new(0, 0);

        for (rate, count) in self.map.iter() {
            let crd = ConstantRateDuration::new(*count, *rate);
            duration += crd.to_duration();
        }

        self.duration = duration;
    }
}
impl From<ConstantRateDuration> for MixedRateDuration {
    /// Construct a `MixedRateDuration` from a `ConstantRateDuration`.
    fn from(crd: ConstantRateDuration) -> Self {
        let mut map: HashMap<u64, u64> = HashMap::with_capacity(1);
        map.insert(crd.rate, crd.count);

        MixedRateDuration {
            duration: crd.to_duration(),
            map,
        }
    }
}
impl fmt::Display for MixedRateDuration {
    /// Display this `MixedRateDuration` in the form
    /// `hh:mm:ss;samples` if there is only one sampling rate used,
    /// otherwise display in the form `hh:mm:ss.s`.
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.num_rates() == 1 {
            if let Some((rate, count)) = self.map.iter().next() {
                let crd = ConstantRateDuration::new(*count, *rate);
                return write!(
                    f,
                    "{:02}:{:02}:{:02};{}",
                    crd.as_hours(),
                    crd.subhour_mins(),
                    crd.submin_secs(),
                    crd.subsec_samples()
                );
            }
        }

        if self.submin_secs() < 10 {
            return write!(
                f,
                "{:02}:{:02}:0{}",
                self.as_hours(),
                self.subhour_mins(),
                self.submin_secs() as f64 + self.subsec_secs(),
            );
        }

        return write!(
            f,
            "{:02}:{:02}:{:2.}",
            self.as_hours(),
            self.subhour_mins(),
            self.submin_secs() as f64 + self.subsec_secs(),
        );
    }
}
impl Add<ConstantRateDuration> for MixedRateDuration {
    type Output = MixedRateDuration;

    /// Add a `ConstantRateDuration` to this `MixedRateDuration`
    /// returning a new `MixedRateDuration`.
    fn add(self, crd: ConstantRateDuration) -> MixedRateDuration {
        let mut result_map = self.map.clone();

        if let Some(current_count) = result_map.get_mut(&crd.rate) {
            *current_count = current_count
                .checked_add(crd.count)
                .expect("overflow when adding a constant rate duration to a mixed rate duration");
        } else {
            result_map.insert(crd.rate, crd.count);
        }

        let mut result_mrd = MixedRateDuration {
            duration: Duration::new(0, 0),
            map: result_map,
        };
        result_mrd.update_duration();

        result_mrd
    }
}
impl Add<MixedRateDuration> for MixedRateDuration {
    type Output = MixedRateDuration;

    /// Add a `MixedRateDuration` to this `MixedRateDuration`
    /// returning a new `MixedRateDuration`.
    fn add(self, mrd: MixedRateDuration) -> MixedRateDuration {
        let mut result_map = self.map.clone();

        for (rate, count) in mrd.map.iter() {
            if let Some(current_count) = result_map.get_mut(rate) {
                *current_count = current_count.checked_add(*count).expect(
                    "overflow when adding a constant rate duration to a mixed rate duration",
                );
            } else {
                result_map.insert(*rate, *count);
            }
        }

        let mut result_mrd = MixedRateDuration {
            duration: Duration::new(0, 0),
            map: result_map,
        };
        result_mrd.update_duration();

        result_mrd
    }
}
impl AddAssign<ConstantRateDuration> for MixedRateDuration {
    /// Add a `ConstantRateDuration` to this `MixedRateDuration`.
    fn add_assign(&mut self, crd: ConstantRateDuration) {
        if let Some(current_count) = self.map.get_mut(&crd.rate) {
            *current_count = current_count.checked_add(crd.count).expect(
                "overflow when add-assigning a constant rate duration to a mixed rate duration",
            );
        } else {
            self.map.insert(crd.rate, crd.count);
        }

        self.update_duration();
    }
}
impl AddAssign<MixedRateDuration> for MixedRateDuration {
    /// Add a `MixedRateDuration` to this `MixedRateDuration`.
    fn add_assign(&mut self, rhs: MixedRateDuration) {
        for (rhs_rate, rhs_count) in rhs.map.iter() {
            if let Some(lhs_count) = self.map.get_mut(rhs_rate) {
                *lhs_count = lhs_count
                    .checked_add(*rhs_count)
                    .expect("overflow when add-assigning mixed rate durations");
            } else {
                self.map.insert(*rhs_rate, *rhs_count);
            }
        }

        self.update_duration();
    }
}

#[cfg(test)]
mod mixed_rate_duration {
    use super::*;

    #[test]
    fn new() {
        let count: u64 = 12345;
        let rate: u64 = 48000;
        let mrd = MixedRateDuration::from(ConstantRateDuration::new(count, rate));

        assert_eq!(mrd.map.len(), 1);
        assert!(mrd.map.contains_key(&rate));
        assert_eq!(mrd.map.get(&rate), Some(&count));
    }

    #[test]
    fn as_and_sub_time_unit_methods() {
        let subsec_samples: u64 = 24_000;
        let submin_secs: u64 = 32;
        let subhour_mins: u64 = 23;
        let subday_hours: u64 = 19;
        let subweek_days: u64 = 4;
        let weeks: u64 = 1;
        let rate: u64 = 48_000;

        let as_days = weeks * DAYS_PER_WEEK + subweek_days;
        let as_hours = as_days * HOURS_PER_DAY + subday_hours;
        let as_mins = as_hours * MINS_PER_HOUR + subhour_mins;
        let as_secs = as_mins * SECS_PER_MIN + submin_secs;

        let total_secs: u64 = weeks * SECS_PER_WEEK
            + subweek_days * SECS_PER_DAY
            + subday_hours * SECS_PER_HOUR
            + subhour_mins * SECS_PER_MIN
            + submin_secs;
        let count: u64 = total_secs * rate + subsec_samples;
        let mrd = MixedRateDuration::from(ConstantRateDuration::new(count, rate));

        assert_eq!(mrd.as_secs(), as_secs);
        assert_eq!(mrd.as_mins(), as_mins);
        assert_eq!(mrd.as_hours(), as_hours);
        assert_eq!(mrd.as_days(), as_days);
        assert_eq!(mrd.as_weeks(), weeks);
        assert_eq!(mrd.subsec_nanos(), 500_000_000);
        assert_eq!(mrd.subsec_secs(), 0.5);
        assert_eq!(mrd.submin_secs(), submin_secs);
        assert_eq!(mrd.subhour_mins(), subhour_mins);
        assert_eq!(mrd.subday_hours(), subday_hours);
        assert_eq!(mrd.subweek_days(), subweek_days);

        // Check multiple rates
        let mut mrd = MixedRateDuration::from(ConstantRateDuration::new(48000, 48000));
        assert_eq!(mrd.as_secs(), 1);
        assert_eq!(mrd.subsec_nanos(), 0);

        // Manually add 1.5 seconds of 96kHz samples
        mrd += ConstantRateDuration::new(96000 + 48000, 96000);
        assert_eq!(mrd.map.len(), 2);
        assert_eq!(mrd.as_secs(), 2);
        assert_eq!(mrd.subsec_nanos(), 500_000_000);
    }

    #[test]
    fn to_duration() {
        let mrd = MixedRateDuration::from(ConstantRateDuration::new(48000, 48000));
        assert_eq!(mrd.to_duration(), Duration::new(1, 0));

        //TODO more tests
    }

    #[test]
    fn display() {
        let mut mrd = MixedRateDuration::from(ConstantRateDuration::new(48000 + 24000, 48000));
        assert_eq!(mrd.to_string(), "00:00:01;24000");

        // Add 1 second of 96kHz samples
        mrd += ConstantRateDuration::new(96000, 96000);
        assert_eq!(mrd.map.len(), 2);
        assert_eq!(mrd.as_secs(), 2);
        assert_eq!(mrd.subsec_nanos(), 500_000_000);
        assert_eq!(mrd.to_string(), "00:00:02.5");

        // Manually add 10 seconds of 44.1kHz samples
        mrd += ConstantRateDuration::new(441000, 44100);
        assert_eq!(mrd.map.len(), 3);
        assert_eq!(mrd.as_secs(), 12);
        assert_eq!(mrd.subsec_nanos(), 500_000_000);
        assert_eq!(mrd.to_string(), "00:00:12.5");
    }

    #[test]
    fn add() {
        // Add a ConstantRateDuration
        let a = MixedRateDuration::from(ConstantRateDuration::new(48000, 48000));
        let b = ConstantRateDuration::new(48000, 48000);
        let c = a + b;
        assert_eq!(c.to_string(), "00:00:02;0");

        // Add-assign a MixedRateDuraiton
        let a = MixedRateDuration::from(ConstantRateDuration::new(48000, 48000));
        let b = MixedRateDuration::from(ConstantRateDuration::new(48000, 48000));
        let c = a + b;
        assert_eq!(c.to_string(), "00:00:02;0");

        // Add-assign a MixedRateDuraiton with different rates
        let a = MixedRateDuration::from(ConstantRateDuration::new(48000, 48000));
        let b = MixedRateDuration::from(ConstantRateDuration::new(96000 + 48000, 96000));
        let c = a + b;
        assert_eq!(c.to_string(), "00:00:02.5");
    }

    #[test]
    fn add_assign() {
        // Add-assign a ConstantRateDuration
        let mut a = MixedRateDuration::from(ConstantRateDuration::new(48000, 48000));
        let b = ConstantRateDuration::new(48000, 48000);
        a += b;
        assert_eq!(a.to_string(), "00:00:02;0");

        // Add-assign a MixedRateDuraiton
        let c = MixedRateDuration::from(ConstantRateDuration::new(96000 + 48000, 96000));
        a += c;
        assert_eq!(a.to_string(), "00:00:03.5");
    }
}
