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

use serde_derive::{Deserialize, Serialize};

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, Serialize, 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 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!(Bitness::Unknown, info.bitness());
    /// ```
    pub fn unknown() -> Self {
        Self {
            os_type: Type::Unknown,
            version: Version::unknown(),
            bitness: Bitness::Unknown,
        }
    }

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

    /// 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 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)?;
        write!(f, " ({})", self.version)?;
        write!(f, " ({})", self.bitness)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use itertools::iproduct;
    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!(Bitness::Unknown, info.bitness());
    }

    #[test]
    fn new() {
        let types = [
            Type::Unknown,
            Type::Android,
            Type::Emscripten,
            Type::Linux,
            Type::Redhat,
            Type::Ubuntu,
            Type::Debian,
            Type::Arch,
            Type::Centos,
            Type::Fedora,
            Type::Alpine,
            Type::Macos,
            Type::Redox,
            Type::Windows,
        ];

        let versions = [
            Version::unknown(),
            Version::semantic(0, 0, 0, None),
            Version::semantic(1, 2, 3, Some("e".to_owned())),
            Version::custom("version".to_owned(), None),
            Version::custom("different version".to_owned(), Some("edition".to_owned())),
        ];

        let bitnesses = [Bitness::Unknown, Bitness::X32, Bitness::X64];

        for (os_type, version, bitness) in iproduct!(&types, &versions, &bitnesses) {
            let info = Info::new(*os_type, version.clone(), *bitness);
            assert_eq!(*os_type, info.os_type());
            assert_eq!(version, info.version());
        }
    }
}
