//! Platform requirements

use crate::error::Error;
use crate::platform::Platform;
use std::{fmt, str::FromStr, string::String};

#[cfg(feature = "serde")]
use serde::{de, ser, Deserialize, Serialize};

/// Platform requirements: glob-like expressions for matching Rust platforms
/// as identified by a "target triple", e.g. `i686-apple-darwin`.
///
/// For a list of all valid platforms, "target triples", see:
///
/// <https://doc.rust-lang.org/nightly/rustc/platform-support.html>
///
/// Platforms can be grouped with simple globbing rules:
///
/// - Start with wildcard: `*-gnu`
/// - End with wildcard: `x86_64-*`
/// - Start and end with wildcard: `*windows*`
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PlatformReq(String);

/// Wildcard character used for globbing
pub const WILDCARD: char = '*';

impl PlatformReq {
    /// Borrow this platform requirement as a string slice
    pub fn as_str(&self) -> &str {
        self.0.as_ref()
    }

    /// Does this platform requirement match the given platform string?
    ///
    /// This matcher accepts a platform "target triple" string ala
    /// `x86_64-unknown-linux-gnu` and matches it against this
    /// `Platform`, using simple glob like rules.
    pub fn matches(&self, platform: &Platform) -> bool {
        let self_len = self.as_str().len();

        // Universal matcher
        if self.0.len() == 1 && self.0.chars().next().unwrap() == WILDCARD {
            return true;
        }

        let mut chars = self.as_str().chars();
        let starts_with_wildcard = chars.next().unwrap() == WILDCARD;
        let ends_with_wildcard = chars.last() == Some(WILDCARD);

        if starts_with_wildcard {
            if ends_with_wildcard {
                // Contains expression: `*windows*`
                platform
                    .target_triple
                    .contains(&self.0[1..self_len.checked_sub(1).unwrap()])
            } else {
                // Suffix expression: `*-gnu`
                platform.target_triple.ends_with(&self.0[1..])
            }
        } else if ends_with_wildcard {
            // Prefix expression: `x86_64-*`
            platform
                .target_triple
                .starts_with(&self.0[..self_len.checked_sub(1).unwrap()])
        } else {
            // No wildcards: direct comparison
            self.as_str() == platform.target_triple
        }
    }

    /// Expand glob expressions into a list of all known matching platforms
    pub fn matching_platforms(&self) -> impl Iterator<Item = &Platform> {
        matching_platforms(self, Platform::ALL)
    }
}

// Split into its own function for unit testing
#[inline]
fn matching_platforms<'a>(
    req: &'a PlatformReq,
    platforms: &'a [Platform],
) -> impl Iterator<Item = &'a Platform> {
    platforms
        .iter()
        .filter(move |&platform| req.matches(platform))
}

impl FromStr for PlatformReq {
    type Err = Error;

    /// Create a new platform requirement. Platforms support glob-like
    /// wildcards on the beginning and end, e.g. `*windows*`.
    ///
    /// Must match at least one known Rust platform "target triple"
    /// (e.g. `x86_64-unknown-linux-gnu`) to be considered valid.
    fn from_str(req_str: &str) -> Result<PlatformReq, Error> {
        let platform_req = PlatformReq(req_str.into());

        if platform_req.0.is_empty() || platform_req.matching_platforms().next().is_none() {
            Err(Error)
        } else {
            Ok(platform_req)
        }
    }
}

impl fmt::Display for PlatformReq {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

#[cfg(feature = "serde")]
impl Serialize for PlatformReq {
    fn serialize<S: ser::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_str(self.as_str())
    }
}

#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for PlatformReq {
    fn deserialize<D: de::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        use de::Error;
        String::deserialize(deserializer)?
            .parse()
            .map_err(D::Error::custom)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::{str::FromStr, vec::Vec};

    use crate::platform::platforms::*;
    const TEST_PLATFORM_LIST: &[Platform] = &[
        AARCH64_PC_WINDOWS_MSVC,
        AARCH64_UNKNOWN_LINUX_MUSL,
        ARMV7_UNKNOWN_LINUX_MUSLEABI,
        ARMV7_UNKNOWN_LINUX_MUSLEABIHF,
        SPARC_UNKNOWN_LINUX_GNU,
        SPARC64_UNKNOWN_LINUX_GNU,
        SPARC64_UNKNOWN_NETBSD,
        SPARC64_UNKNOWN_OPENBSD,
        SPARCV9_SUN_SOLARIS,
        AARCH64_UWP_WINDOWS_MSVC,
        I686_PC_WINDOWS_GNU,
        I686_PC_WINDOWS_MSVC,
        I686_UWP_WINDOWS_GNU,
        I686_UWP_WINDOWS_MSVC,
        MIPS64_UNKNOWN_LINUX_GNUABI64,
        MIPS64_UNKNOWN_LINUX_MUSLABI64,
        THUMBV7A_PC_WINDOWS_MSVC,
        THUMBV7A_UWP_WINDOWS_MSVC,
        RISCV64GC_UNKNOWN_LINUX_MUSL,
        X86_64_PC_WINDOWS_GNU,
    ];

    #[test]
    fn prefix_glob_test() {
        let req = PlatformReq::from_str("sparc*").unwrap();

        assert_eq!(
            matching_platforms(&req, TEST_PLATFORM_LIST)
                .map(|p| p.target_triple)
                .collect::<Vec<_>>(),
            [
                "sparc-unknown-linux-gnu",
                "sparc64-unknown-linux-gnu",
                "sparc64-unknown-netbsd",
                "sparc64-unknown-openbsd",
                "sparcv9-sun-solaris"
            ]
        );
    }

    #[test]
    fn suffix_glob_test() {
        let req = PlatformReq::from_str("*-musl").unwrap();

        assert_eq!(
            matching_platforms(&req, TEST_PLATFORM_LIST)
                .map(|p| p.target_triple)
                .collect::<Vec<_>>(),
            ["aarch64-unknown-linux-musl", "riscv64gc-unknown-linux-musl"]
        );
    }

    #[test]
    fn contains_glob_test() {
        let req = PlatformReq::from_str("*windows*").unwrap();

        assert_eq!(
            matching_platforms(&req, TEST_PLATFORM_LIST)
                .map(|p| p.target_triple)
                .collect::<Vec<_>>(),
            [
                "aarch64-pc-windows-msvc",
                "aarch64-uwp-windows-msvc",
                "i686-pc-windows-gnu",
                "i686-pc-windows-msvc",
                "i686-uwp-windows-gnu",
                "i686-uwp-windows-msvc",
                "thumbv7a-pc-windows-msvc",
                "thumbv7a-uwp-windows-msvc",
                "x86_64-pc-windows-gnu",
            ]
        );
    }

    #[test]
    fn direct_match_test() {
        let req = PlatformReq::from_str("x86_64-unknown-dragonfly").unwrap();

        assert_eq!(
            req.matching_platforms()
                .map(|p| p.target_triple)
                .collect::<Vec<_>>(),
            ["x86_64-unknown-dragonfly"]
        );
    }

    #[test]
    fn wildcard_test() {
        let req = PlatformReq::from_str("*").unwrap();
        assert_eq!(req.matching_platforms().count(), Platform::ALL.len())
    }

    // How to handle this is debatable...
    #[test]
    fn double_wildcard_test() {
        let req = PlatformReq::from_str("**").unwrap();
        assert_eq!(req.matching_platforms().count(), Platform::ALL.len())
    }

    #[test]
    fn invalid_req_tests() {
        assert!(PlatformReq::from_str("").is_err());
        assert!(PlatformReq::from_str(" ").is_err());
        assert!(PlatformReq::from_str("derp").is_err());
        assert!(PlatformReq::from_str("***").is_err());
    }
}
