use core::fmt;

use binrw::{BinRead, BinWrite};

/// OS version and patch level
///
/// # Warning
///
/// Please note that the version information may be incorrect, for example, on OnePlus devices.
///
/// # Bitwise format
///
/// * 7 bits indicate first part of version
/// * 7 bits indicate second part of version
/// * 7 bits indicate third part of version
/// * 12 bits indicate patch year
/// * 4 bits indicate patch month
#[derive(BinRead, BinWrite, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[br(little)]
pub struct OsVersionPatch(pub u32);

impl OsVersionPatch {
    /// Creates a new `OsVersionPatch`.
    #[must_use]
    pub const fn new(version: OsVersion, patch: OsPatch) -> Self {
        Self((version.0 << 11) + patch.0 as u32)
    }
    /// Returns the version part.
    #[must_use]
    pub const fn version(self) -> OsVersion {
        OsVersion(self.0 >> 11)
    }
    /// Returns the patch part.
    #[must_use]
    pub const fn patch(self) -> OsPatch {
        OsPatch((self.0 & 0x7ff) as u16)
    }
}

impl fmt::Debug for OsVersionPatch {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "OsVersionPatch({}, {})", self.version(), self.patch())
    }
}

/// OS patch level
///
/// # Bitwise format
///
/// * 12 bits indicate year
/// * 4 bits indicate month
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct OsPatch(pub u16);
impl OsPatch {
    /// Creates a new `OsPatch`.
    #[must_use]
    pub const fn new(year: u16, month: u8) -> Self {
        Self(((year - 2000) << 4) + month as u16)
    }
    /// Returns the year.
    #[must_use]
    pub const fn year(self) -> u16 {
        // Highest 12 bits indicate year
        (self.0 >> 4) + 2000
    }
    /// Returns the month.
    #[must_use]
    pub const fn month(self) -> u8 {
        // Lowest 4 bits indicate month
        (self.0 & 0xf) as u8
    }
}

impl fmt::Display for OsPatch {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}-{:02}", self.year(), self.month())
    }
}
impl fmt::Debug for OsPatch {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Display::fmt(self, f)
    }
}

/// OS version
///
/// # Warning
///
/// Please note that this information may be incorrect, for example, on OnePlus devices.
///
/// # Bitwise format
///
/// * 7 bits indicate first part
/// * 7 bits indicate second part
/// * 7 bits indicate third part
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct OsVersion(pub u32);
impl OsVersion {
    /// Creates a new `OsVersion`.
    #[must_use]
    pub const fn new(a: u8, b: u8, c: u8) -> Self {
        Self(((a as u32) << 14) | ((b as u32) << 7) | c as u32)
    }
    /// Returns the version parts.
    #[must_use]
    pub const fn version_parts(self) -> (u8, u8, u8) {
        let x = self.0;
        let a = (x >> 14) & 0x7f; // Top 7 bits
        let b = (x >> 7) & 0x7f; // Middle 7 bits
        let c = x & 0x7f; // Low 7 bits
        (a as u8, b as u8, c as u8)
    }
}

impl fmt::Display for OsVersion {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let (a, b, c) = self.version_parts();
        write!(f, "{a}.{b}.{c}")
    }
}
impl fmt::Debug for OsVersion {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Display::fmt(self, f)
    }
}

#[cfg(test)]
mod tests {
    use alloc::{format, string::ToString as _};

    use super::*;
    #[test]
    fn basic() {
        let vp = OsVersionPatch(0x1800_0186);
        assert_eq!(format!("{vp:?}"), "OsVersionPatch(12.0.0, 2024-06)");
        assert_eq!(vp.version().to_string(), "12.0.0");
        assert_eq!(vp.patch().to_string(), "2024-06");
        assert_eq!(vp, OsVersionPatch::new(vp.version(), vp.patch()));
        assert_eq!(vp.version(), OsVersion::new(12, 0, 0));
        assert_eq!(vp.patch(), OsPatch::new(2024, 6));
    }
    #[test]
    fn truncating_behavior() {
        let ver = OsVersion(0b1111_1111 << 14);
        assert_eq!(ver.version_parts(), (0b0111_1111, 0, 0));
        assert_eq!(format!("{ver:?}"), "127.0.0");
        assert_eq!(ver.to_string(), "127.0.0");
    }
}
