use core::time::Duration;

use super::ValueError;

const fn abs_diff(a: u64, b: u64) -> u64 {
    if a > b {
        a - b
    } else {
        b - a
    }
}

/// Timeout argument.
///
/// This is used by:
/// * [`set_rx`]
/// * [`set_tx`]
/// * [`TcxoMode`]
///
/// Each timeout has 3 bytes, with a resolution of 15.625µs per bit, giving a
/// range of 0s to 262.143984375s.
///
/// [`set_rx`]: super::SubGhz::set_rx
/// [`set_tx`]: super::SubGhz::set_tx
/// [`TcxoMode`]: super::TcxoMode
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct Timeout {
    bits: u32,
}

impl Timeout {
    const BITS_PER_MILLI: u32 = 64; // 1e-3 / 15.625e-6
    const BITS_PER_SEC: u32 = 64_000; // 1 / 15.625e-6

    /// Disable the timeout (0s timeout).
    ///
    /// # Example
    ///
    /// ```
    /// use core::time::Duration;
    /// use stm32wlxx_hal::subghz::Timeout;
    ///
    /// const TIMEOUT: Timeout = Timeout::DISABLED;
    /// assert_eq!(TIMEOUT.as_duration(), Duration::from_secs(0));
    /// ```
    pub const DISABLED: Timeout = Timeout { bits: 0x0 };

    /// Minimum timeout, 15.625µs.
    ///
    /// # Example
    ///
    /// ```
    /// use core::time::Duration;
    /// use stm32wlxx_hal::subghz::Timeout;
    ///
    /// const TIMEOUT: Timeout = Timeout::MIN;
    /// assert_eq!(TIMEOUT.into_bits(), 1);
    /// ```
    pub const MIN: Timeout = Timeout { bits: 1 };

    /// Maximum timeout, 262.143984375s.
    ///
    /// # Example
    ///
    /// ```
    /// use core::time::Duration;
    /// use stm32wlxx_hal::subghz::Timeout;
    ///
    /// const TIMEOUT: Timeout = Timeout::MAX;
    /// assert_eq!(TIMEOUT.as_duration(), Duration::from_nanos(262_143_984_375));
    /// ```
    pub const MAX: Timeout = Timeout { bits: 0x00FF_FFFF };

    /// Timeout resolution in nanoseconds, 15.625µs.
    pub const RESOLUTION_NANOS: u16 = 15_625;

    /// Timeout resolution, 15.625µs.
    ///
    /// # Example
    ///
    /// ```
    /// use stm32wlxx_hal::subghz::Timeout;
    ///
    /// assert_eq!(
    ///     Timeout::RESOLUTION.as_nanos(),
    ///     Timeout::RESOLUTION_NANOS as u128
    /// );
    /// ```
    pub const RESOLUTION: Duration = Duration::from_nanos(Self::RESOLUTION_NANOS as u64);

    /// Create a new timeout from a [`Duration`].
    ///
    /// This will return the nearest timeout value possible, or a
    /// [`ValueError`] if the value is out of bounds.
    ///
    /// Use [`from_millis_sat`](Self::from_millis_sat) for runtime timeout
    /// construction.
    /// This is not _that_ useful right now, it is simply future proofing for a
    /// time when `Result::unwrap` is available for `const fn`.
    ///
    /// # Example
    ///
    /// Value within bounds:
    ///
    /// ```
    /// use core::time::Duration;
    /// use stm32wlxx_hal::subghz::{Timeout, ValueError};
    ///
    /// const MIN: Duration = Timeout::RESOLUTION;
    /// assert_eq!(Timeout::from_duration(MIN).unwrap(), Timeout::MIN);
    /// ```
    ///
    /// Value too low:
    ///
    /// ```
    /// use core::time::Duration;
    /// use stm32wlxx_hal::subghz::{Timeout, ValueError};
    ///
    /// const LOWER_LIMIT_NANOS: u128 = 7813;
    /// const TOO_LOW_NANOS: u128 = LOWER_LIMIT_NANOS - 1;
    /// const TOO_LOW_DURATION: Duration = Duration::from_nanos(TOO_LOW_NANOS as u64);
    /// assert_eq!(
    ///     Timeout::from_duration(TOO_LOW_DURATION),
    ///     Err(ValueError::too_low(TOO_LOW_NANOS, LOWER_LIMIT_NANOS))
    /// );
    /// ```
    ///
    /// Value too high:
    ///
    /// ```
    /// use core::time::Duration;
    /// use stm32wlxx_hal::subghz::{Timeout, ValueError};
    ///
    /// const UPPER_LIMIT_NANOS: u128 = Timeout::MAX.as_nanos() as u128 + 7812;
    /// const TOO_HIGH_NANOS: u128 = UPPER_LIMIT_NANOS + 1;
    /// const TOO_HIGH_DURATION: Duration = Duration::from_nanos(TOO_HIGH_NANOS as u64);
    /// assert_eq!(
    ///     Timeout::from_duration(TOO_HIGH_DURATION),
    ///     Err(ValueError::too_high(TOO_HIGH_NANOS, UPPER_LIMIT_NANOS))
    /// );
    /// ```
    pub const fn from_duration(duration: Duration) -> Result<Timeout, ValueError<u128>> {
        // at the time of development many methods in
        // `core::Duration` were not `const fn`, which leads to the hacks
        // you see here.
        let nanos: u128 = duration.as_nanos();
        const UPPER_LIMIT: u128 = Timeout::MAX.as_nanos() as u128 + (Timeout::RESOLUTION_NANOS as u128) / 2;
        const LOWER_LIMIT: u128 = (((Timeout::RESOLUTION_NANOS as u128) + 1) / 2) as u128;

        if nanos > UPPER_LIMIT {
            Err(ValueError::too_high(nanos, UPPER_LIMIT))
        } else if nanos < LOWER_LIMIT {
            Err(ValueError::too_low(nanos, LOWER_LIMIT))
        } else {
            // safe to truncate here because of previous bounds check.
            let duration_nanos: u64 = nanos as u64;

            let div_floor: u64 = duration_nanos / (Self::RESOLUTION_NANOS as u64);
            let div_ceil: u64 = 1 + (duration_nanos - 1) / (Self::RESOLUTION_NANOS as u64);

            let timeout_ceil: Timeout = Timeout::from_raw(div_ceil as u32);
            let timeout_floor: Timeout = Timeout::from_raw(div_floor as u32);

            let error_ceil: u64 = abs_diff(timeout_ceil.as_nanos(), duration_nanos);
            let error_floor: u64 = abs_diff(timeout_floor.as_nanos(), duration_nanos);

            if error_ceil < error_floor {
                Ok(timeout_ceil)
            } else {
                Ok(timeout_floor)
            }
        }
    }

    /// Create a new timeout from a [`Duration`].
    ///
    /// This will return the nearest timeout value possible, saturating at the
    /// limits.
    ///
    /// This is an expensive function to call outside of `const` contexts.
    /// Use [`from_millis_sat`](Self::from_millis_sat) for runtime timeout
    /// construction.
    ///
    /// # Example
    ///
    /// ```
    /// use core::time::Duration;
    /// use stm32wlxx_hal::subghz::Timeout;
    ///
    /// const DURATION_MAX_NS: u64 = 262_143_984_376;
    ///
    /// assert_eq!(
    ///     Timeout::from_duration_sat(Duration::from_millis(0)),
    ///     Timeout::MIN
    /// );
    /// assert_eq!(
    ///     Timeout::from_duration_sat(Duration::from_nanos(DURATION_MAX_NS)),
    ///     Timeout::MAX
    /// );
    /// assert_eq!(
    ///     Timeout::from_duration_sat(Timeout::RESOLUTION).into_bits(),
    ///     1
    /// );
    /// ```
    pub const fn from_duration_sat(duration: Duration) -> Timeout {
        // at the time of development many methods in
        // `core::Duration` were not `const fn`, which leads to the hacks
        // you see here.
        let nanos: u128 = duration.as_nanos();
        const UPPER_LIMIT: u128 = Timeout::MAX.as_nanos() as u128;

        if nanos > UPPER_LIMIT {
            Timeout::MAX
        } else if nanos < (Timeout::RESOLUTION_NANOS as u128) {
            Timeout::from_raw(1)
        } else {
            // safe to truncate here because of previous bounds check.
            let duration_nanos: u64 = duration.as_nanos() as u64;

            let div_floor: u64 = duration_nanos / (Self::RESOLUTION_NANOS as u64);
            let div_ceil: u64 = 1 + (duration_nanos - 1) / (Self::RESOLUTION_NANOS as u64);

            let timeout_ceil: Timeout = Timeout::from_raw(div_ceil as u32);
            let timeout_floor: Timeout = Timeout::from_raw(div_floor as u32);

            let error_ceil: u64 = abs_diff(timeout_ceil.as_nanos(), duration_nanos);
            let error_floor: u64 = abs_diff(timeout_floor.as_nanos(), duration_nanos);

            if error_ceil < error_floor {
                timeout_ceil
            } else {
                timeout_floor
            }
        }
    }

    /// Create a new timeout from a milliseconds value.
    ///
    /// This will round towards zero and saturate at the limits.
    ///
    /// This is the preferred method to call when you need to generate a
    /// timeout value at runtime.
    ///
    /// # Example
    ///
    /// ```
    /// use stm32wlxx_hal::subghz::Timeout;
    ///
    /// assert_eq!(Timeout::from_millis_sat(0), Timeout::MIN);
    /// assert_eq!(Timeout::from_millis_sat(262_144), Timeout::MAX);
    /// assert_eq!(Timeout::from_millis_sat(1).into_bits(), 64);
    /// ```
    pub const fn from_millis_sat(millis: u32) -> Timeout {
        if millis == 0 {
            Timeout::MIN
        } else if millis >= 262_144 {
            Timeout::MAX
        } else {
            Timeout::from_raw(millis * Self::BITS_PER_MILLI)
        }
    }

    /// Create a timeout from raw bits, where each bit has the resolution of
    /// [`Timeout::RESOLUTION`].
    ///
    /// **Note:** Only the first 24 bits of the `u32` are used, the `bits`
    /// argument will be masked.
    ///
    /// # Example
    ///
    /// ```
    /// use stm32wlxx_hal::subghz::Timeout;
    ///
    /// assert_eq!(Timeout::from_raw(u32::MAX), Timeout::MAX);
    /// assert_eq!(Timeout::from_raw(0x00_FF_FF_FF), Timeout::MAX);
    /// assert_eq!(Timeout::from_raw(1).as_duration(), Timeout::RESOLUTION);
    /// assert_eq!(Timeout::from_raw(0), Timeout::DISABLED);
    /// ```
    pub const fn from_raw(bits: u32) -> Timeout {
        Timeout {
            bits: bits & 0x00FF_FFFF,
        }
    }

    /// Get the timeout as nanoseconds.
    ///
    /// # Example
    ///
    /// ```
    /// use stm32wlxx_hal::subghz::Timeout;
    ///
    /// assert_eq!(Timeout::MAX.as_nanos(), 262_143_984_375);
    /// assert_eq!(Timeout::DISABLED.as_nanos(), 0);
    /// assert_eq!(Timeout::from_raw(1).as_nanos(), 15_625);
    /// assert_eq!(Timeout::from_raw(64_000).as_nanos(), 1_000_000_000);
    /// ```
    pub const fn as_nanos(&self) -> u64 {
        (self.bits as u64) * (Timeout::RESOLUTION_NANOS as u64)
    }

    /// Get the timeout as microseconds, rounding towards zero.
    ///
    /// # Example
    ///
    /// ```
    /// use stm32wlxx_hal::subghz::Timeout;
    ///
    /// assert_eq!(Timeout::MAX.as_micros(), 262_143_984);
    /// assert_eq!(Timeout::DISABLED.as_micros(), 0);
    /// assert_eq!(Timeout::from_raw(1).as_micros(), 15);
    /// assert_eq!(Timeout::from_raw(64_000).as_micros(), 1_000_000);
    /// ```
    pub const fn as_micros(&self) -> u32 {
        (self.as_nanos() / 1_000) as u32
    }

    /// Get the timeout as milliseconds, rounding towards zero.
    ///
    /// # Example
    ///
    /// ```
    /// use stm32wlxx_hal::subghz::Timeout;
    ///
    /// assert_eq!(Timeout::MAX.as_millis(), 262_143);
    /// assert_eq!(Timeout::DISABLED.as_millis(), 0);
    /// assert_eq!(Timeout::from_raw(1).as_millis(), 0);
    /// assert_eq!(Timeout::from_raw(64_000).as_millis(), 1_000);
    /// ```
    pub const fn as_millis(&self) -> u32 {
        self.into_bits() / Self::BITS_PER_MILLI
    }

    /// Get the timeout as seconds, rounding towards zero.
    ///
    /// # Example
    ///
    /// ```
    /// use stm32wlxx_hal::subghz::Timeout;
    ///
    /// assert_eq!(Timeout::MAX.as_secs(), 262);
    /// assert_eq!(Timeout::DISABLED.as_secs(), 0);
    /// assert_eq!(Timeout::from_raw(1).as_secs(), 0);
    /// assert_eq!(Timeout::from_raw(64_000).as_secs(), 1);
    /// ```
    pub const fn as_secs(&self) -> u16 {
        (self.into_bits() / Self::BITS_PER_SEC) as u16
    }

    /// Get the timeout as a [`Duration`].
    ///
    /// # Example
    ///
    /// ```
    /// use core::time::Duration;
    /// use stm32wlxx_hal::subghz::Timeout;
    ///
    /// assert_eq!(
    ///     Timeout::MAX.as_duration(),
    ///     Duration::from_nanos(262_143_984_375)
    /// );
    /// assert_eq!(Timeout::DISABLED.as_duration(), Duration::from_nanos(0));
    /// assert_eq!(Timeout::from_raw(1).as_duration(), Timeout::RESOLUTION);
    /// ```
    pub const fn as_duration(&self) -> Duration {
        Duration::from_nanos((self.bits as u64) * (Timeout::RESOLUTION_NANOS as u64))
    }

    /// Get the bit value for the timeout.
    ///
    /// # Example
    ///
    /// ```
    /// use stm32wlxx_hal::subghz::Timeout;
    ///
    /// assert_eq!(Timeout::from_raw(u32::MAX).into_bits(), 0x00FF_FFFF);
    /// assert_eq!(Timeout::from_raw(1).into_bits(), 1);
    /// ```
    pub const fn into_bits(self) -> u32 {
        self.bits
    }

    /// Get the byte value for the timeout.
    ///
    /// # Example
    ///
    /// ```
    /// use stm32wlxx_hal::subghz::Timeout;
    ///
    /// assert_eq!(Timeout::from_raw(u32::MAX).as_bytes(), [0xFF, 0xFF, 0xFF]);
    /// assert_eq!(Timeout::from_raw(1).as_bytes(), [0, 0, 1]);
    /// ```
    pub const fn as_bytes(self) -> [u8; 3] {
        [
            ((self.bits >> 16) & 0xFF) as u8,
            ((self.bits >> 8) & 0xFF) as u8,
            (self.bits & 0xFF) as u8,
        ]
    }

    /// Saturating timeout addition.  Computes `self + rhs`, saturating at the
    /// numeric bounds instead of overflowing.
    ///
    /// # Example
    ///
    /// ```
    /// use stm32wlxx_hal::subghz::Timeout;
    ///
    /// assert_eq!(
    ///     Timeout::from_raw(0xFF_FF_F0).saturating_add(Timeout::from_raw(0xFF)),
    ///     Timeout::from_raw(0xFF_FF_FF)
    /// );
    /// assert_eq!(
    ///     Timeout::from_raw(100).saturating_add(Timeout::from_raw(23)),
    ///     Timeout::from_raw(123)
    /// );
    /// ```
    #[must_use = "saturating_add returns a new Timeout"]
    pub const fn saturating_add(self, rhs: Self) -> Self {
        // TODO: use core::cmp::min when it is const
        let bits: u32 = self.bits.saturating_add(rhs.bits);
        if bits > Self::MAX.bits {
            Self::MAX
        } else {
            Self { bits }
        }
    }
}

impl From<Timeout> for Duration {
    fn from(to: Timeout) -> Self {
        to.as_duration()
    }
}

impl From<Timeout> for [u8; 3] {
    fn from(to: Timeout) -> Self {
        to.as_bytes()
    }
}

impl From<Timeout> for embassy_time::Duration {
    fn from(to: Timeout) -> Self {
        embassy_time::Duration::from_micros(to.as_micros().into())
    }
}

#[cfg(test)]
mod tests {
    use core::time::Duration;

    use super::{Timeout, ValueError};

    #[test]
    fn saturate() {
        assert_eq!(Timeout::from_duration_sat(Duration::from_secs(u64::MAX)), Timeout::MAX);
    }

    #[test]
    fn rounding() {
        const NANO1: Duration = Duration::from_nanos(1);
        let res_sub_1_ns: Duration = Timeout::RESOLUTION - NANO1;
        let res_add_1_ns: Duration = Timeout::RESOLUTION + NANO1;
        assert_eq!(Timeout::from_duration_sat(res_sub_1_ns).into_bits(), 1);
        assert_eq!(Timeout::from_duration_sat(res_add_1_ns).into_bits(), 1);
    }

    #[test]
    fn lower_limit() {
        let low: Duration = (Timeout::RESOLUTION + Duration::from_nanos(1)) / 2;
        assert_eq!(Timeout::from_duration(low), Ok(Timeout::from_raw(1)));

        let too_low: Duration = low - Duration::from_nanos(1);
        assert_eq!(
            Timeout::from_duration(too_low),
            Err(ValueError::too_low(too_low.as_nanos(), low.as_nanos()))
        );
    }

    #[test]
    fn upper_limit() {
        let high: Duration = Timeout::MAX.as_duration() + Timeout::RESOLUTION / 2;
        assert_eq!(Timeout::from_duration(high), Ok(Timeout::from_raw(0xFFFFFF)));

        let too_high: Duration = high + Duration::from_nanos(1);
        assert_eq!(
            Timeout::from_duration(too_high),
            Err(ValueError::too_high(too_high.as_nanos(), high.as_nanos()))
        );
    }
}
