// spell-checker:ignore itertools, iproduct, bitnesses

use std::fmt::{self, Display, Formatter};

use super::{Bitness, Type, Version};

/// Holds information about operating system (type, version, etc.).
///
/// The best way to get string representation of the operation system information is to use its
/// `Display` implementation.
///
/// # Examples
///
/// ```
/// use os_info;
///
/// let info = os_info::get();
/// println!("OS information: {info}");
/// ```
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Info {
    /// Operating system type. See `Type` for details.
    pub(crate) os_type: Type,
    /// Operating system version. See `Version` for details.
    pub(crate) version: Version,
    /// Operating system edition.
    pub(crate) edition: Option<String>,
    /// Operating system edition.
    pub(crate) codename: Option<String>,
    /// Operating system architecture in terms of how many bits compose the basic values it can deal
    /// with. See `Bitness` for details.
    pub(crate) bitness: Bitness,
}

impl Info {
    /// Constructs a new `Info` instance with unknown type, version and bitness.
    ///
    /// # Examples
    ///
    /// ```
    /// use os_info::{Info, Type, Version, Bitness};
    ///
    /// let info = Info::unknown();
    /// assert_eq!(Type::Unknown, info.os_type());
    /// assert_eq!(&Version::Unknown, info.version());
    /// assert_eq!(None, info.edition());
    /// assert_eq!(None, info.codename());
    /// assert_eq!(Bitness::Unknown, info.bitness());
    /// ```
    pub fn unknown() -> Self {
        Self {
            os_type: Type::Unknown,
            version: Version::Unknown,
            edition: None,
            codename: None,
            bitness: Bitness::Unknown,
        }
    }

    /// Constructs a new `Info` instance with the specified operating system type.
    ///
    /// # Examples
    ///
    /// ```
    /// use os_info::{Info, Type, Version, Bitness};
    ///
    /// let os_type = Type::Linux;
    /// let info = Info::with_type(os_type);
    /// assert_eq!(os_type, info.os_type());
    /// assert_eq!(&Version::Unknown, info.version());
    /// assert_eq!(None, info.edition());
    /// assert_eq!(None, info.codename());
    /// assert_eq!(Bitness::Unknown, info.bitness());
    /// ```
    pub fn with_type(os_type: Type) -> Self {
        Self {
            os_type,
            ..Default::default()
        }
    }

    /// Returns operating system type. See `Type` for details.
    ///
    /// # Examples
    ///
    /// ```
    /// use os_info::{Info, Type};
    ///
    /// let info = Info::unknown();
    /// assert_eq!(Type::Unknown, info.os_type());
    /// ```
    pub fn os_type(&self) -> Type {
        self.os_type
    }

    /// Returns operating system version. See `Version` for details.
    ///
    /// # Examples
    ///
    /// ```
    /// use os_info::{Info, Version};
    ///
    /// let info = Info::unknown();
    /// assert_eq!(&Version::Unknown, info.version());
    /// ```
    pub fn version(&self) -> &Version {
        &self.version
    }

    /// Returns optional operation system edition.
    ///
    /// # Examples
    ///
    /// ```
    /// use os_info::Info;
    ///
    /// let info = Info::unknown();
    /// assert_eq!(None, info.edition());
    pub fn edition(&self) -> Option<&str> {
        self.edition.as_ref().map(String::as_ref)
    }

    /// Returns optional operation system 'codename'.
    ///
    /// # Examples
    ///
    /// ```
    /// use os_info::Info;
    ///
    /// let info = Info::unknown();
    /// assert_eq!(None, info.codename());
    pub fn codename(&self) -> Option<&str> {
        self.codename.as_ref().map(String::as_ref)
    }

    /// Returns operating system bitness. See `Bitness` for details.
    ///
    /// # Examples
    ///
    /// ```
    /// use os_info::{Info, Bitness};
    ///
    /// let info = Info::unknown();
    /// assert_eq!(Bitness::Unknown, info.bitness());
    /// ```
    pub fn bitness(&self) -> Bitness {
        self.bitness
    }
}

impl Default for Info {
    fn default() -> Self {
        Self::unknown()
    }
}

impl Display for Info {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        write!(f, "{}", self.os_type)?;
        if self.version != Version::Unknown {
            write!(f, " {}", self.version)?;
        }
        if let Some(ref edition) = self.edition {
            write!(f, " ({edition})")?;
        }
        if let Some(ref codename) = self.codename {
            write!(f, " ({codename})")?;
        }
        write!(f, " [{}]", self.bitness)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use pretty_assertions::assert_eq;

    #[test]
    fn unknown() {
        let info = Info::unknown();
        assert_eq!(Type::Unknown, info.os_type());
        assert_eq!(&Version::Unknown, info.version());
        assert_eq!(None, info.edition());
        assert_eq!(None, info.codename());
        assert_eq!(Bitness::Unknown, info.bitness());
    }

    #[test]
    fn with_type() {
        let types = [
            Type::Redox,
            Type::Alpine,
            Type::Amazon,
            Type::Android,
            Type::Arch,
            Type::CentOS,
            Type::Debian,
            Type::Emscripten,
            Type::EndeavourOS,
            Type::Fedora,
            Type::Gentoo,
            Type::Linux,
            Type::Macos,
            Type::Manjaro,
            Type::Mariner,
            Type::NixOS,
            Type::OpenCloudOS,
            Type::openEuler,
            Type::openSUSE,
            Type::OracleLinux,
            Type::Pop,
            Type::Redhat,
            Type::RedHatEnterprise,
            Type::Redox,
            Type::Solus,
            Type::SUSE,
            Type::Ubuntu,
            Type::Mint,
            Type::Unknown,
            Type::Windows,
        ];

        for t in &types {
            let info = Info::with_type(*t);
            assert_eq!(t, &info.os_type());
        }
    }

    #[test]
    fn default() {
        assert_eq!(Info::default(), Info::unknown());
    }

    #[test]
    fn display() {
        let data = [
            // All unknown.
            (Info::unknown(), "Unknown [unknown bitness]"),
            // Type.
            (
                Info {
                    os_type: Type::Redox,
                    ..Default::default()
                },
                "Redox [unknown bitness]",
            ),
            // Type and version.
            (
                Info {
                    os_type: Type::Linux,
                    version: Version::Semantic(2, 3, 4),
                    ..Default::default()
                },
                "Linux 2.3.4 [unknown bitness]",
            ),
            (
                Info {
                    os_type: Type::Arch,
                    version: Version::Rolling(None),
                    ..Default::default()
                },
                "Arch Linux Rolling Release [unknown bitness]",
            ),
            (
                Info {
                    os_type: Type::Manjaro,
                    version: Version::Rolling(Some("2020.05.24".to_owned())),
                    ..Default::default()
                },
                "Manjaro Rolling Release (2020.05.24) [unknown bitness]",
            ),
            (
                Info {
                    os_type: Type::Windows,
                    version: Version::Custom("Special Version".to_owned()),
                    ..Default::default()
                },
                "Windows Special Version [unknown bitness]",
            ),
            // Bitness.
            (
                Info {
                    bitness: Bitness::X32,
                    ..Default::default()
                },
                "Unknown [32-bit]",
            ),
            (
                Info {
                    bitness: Bitness::X64,
                    ..Default::default()
                },
                "Unknown [64-bit]",
            ),
            // All info.
            (
                Info {
                    os_type: Type::Macos,
                    version: Version::Semantic(10, 2, 0),
                    edition: Some("edition".to_owned()),
                    codename: Some("codename".to_owned()),
                    bitness: Bitness::X64,
                },
                "Mac OS 10.2.0 (edition) (codename) [64-bit]",
            ),
        ];

        for (info, expected) in &data {
            assert_eq!(expected, &info.to_string());
        }
    }
}
