use std::{error, fmt, str::FromStr};
use globset::{Error as GlobError, Glob, GlobBuilder, GlobMatcher};
use crate::version::{self, PartialVersion, VersionError};
use super::Runtime;
#[derive(Debug)]
pub enum ConstraintError {
    GlobError(GlobError),
    VersionError(version::VersionError),
}
impl fmt::Display for ConstraintError {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        use ConstraintError::*;
        match self {
            GlobError(ref error) => write!(fmt, "could not parse constraint: {error}",),
            VersionError(ref error) => write!(
                fmt,
                "could not parse version constraint {text:?}: {error}",
                text = error.text().unwrap_or("<unknown>")
            ),
        }
    }
}
impl error::Error for ConstraintError {
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
        match *self {
            ConstraintError::GlobError(ref error) => Some(error),
            ConstraintError::VersionError(ref error) => Some(error),
        }
    }
}
impl From<GlobError> for ConstraintError {
    fn from(error: GlobError) -> ConstraintError {
        ConstraintError::GlobError(error)
    }
}
impl From<version::VersionError> for ConstraintError {
    fn from(error: version::VersionError) -> ConstraintError {
        ConstraintError::VersionError(error)
    }
}
#[derive(Clone, Debug)]
pub enum Constraint {
    BinDir(GlobMatcher),
    Version(PartialVersion),
    Either(Box<Constraint>, Box<Constraint>),
    Both(Box<Constraint>, Box<Constraint>),
    Not(Box<Constraint>),
    Anything,
    Nothing,
}
impl Constraint {
    pub fn path(pattern: &str) -> Result<Self, GlobError> {
        Ok(Self::BinDir(
            GlobBuilder::new(pattern)
                .literal_separator(true)
                .empty_alternates(true)
                .build()?
                .compile_matcher(),
        ))
    }
    pub fn glob(glob: &Glob) -> Self {
        Self::BinDir(glob.compile_matcher())
    }
    pub fn version(version: &str) -> Result<Self, VersionError> {
        Ok(Self::Version(version.parse()?))
    }
    pub fn any<C: IntoIterator<Item = Constraint>>(constraints: C) -> Self {
        constraints
            .into_iter()
            .reduce(|a, b| a | b)
            .unwrap_or(Self::Nothing)
    }
    pub fn all<C: IntoIterator<Item = Constraint>>(constraints: C) -> Self {
        constraints
            .into_iter()
            .reduce(|a, b| a & b)
            .unwrap_or(Self::Anything)
    }
    pub fn matches(&self, runtime: &Runtime) -> bool {
        match self {
            Self::BinDir(matcher) => matcher.is_match(&runtime.bindir),
            Self::Version(version) => version.compatible(runtime.version),
            Self::Either(ca, cb) => ca.matches(runtime) || cb.matches(runtime),
            Self::Both(ca, cb) => ca.matches(runtime) && cb.matches(runtime),
            Self::Not(constraint) => !constraint.matches(runtime),
            Self::Anything => true,
            Self::Nothing => false,
        }
    }
}
impl std::ops::Not for Constraint {
    type Output = Self;
    fn not(self) -> Self::Output {
        match self {
            Self::Anything => Self::Nothing,
            Self::Nothing => Self::Anything,
            Self::Not(constraint) => *constraint,
            _ => Self::Not(Box::new(self)),
        }
    }
}
impl std::ops::BitOr for Constraint {
    type Output = Self;
    fn bitor(self, rhs: Self) -> Self::Output {
        match (self, rhs) {
            (Self::Anything, _) | (_, Self::Anything) => Self::Anything,
            (Self::Nothing, c) | (c, Self::Nothing) => c,
            (ca, cb) => Self::Either(Box::new(ca), Box::new(cb)),
        }
    }
}
impl std::ops::BitAnd for Constraint {
    type Output = Self;
    fn bitand(self, rhs: Self) -> Self::Output {
        match (self, rhs) {
            (Self::Anything, c) | (c, Self::Anything) => c,
            (Self::Nothing, _) | (_, Self::Nothing) => Self::Nothing,
            (ca, cb) => Self::Both(Box::new(ca), Box::new(cb)),
        }
    }
}
impl From<PartialVersion> for Constraint {
    fn from(version: PartialVersion) -> Self {
        Self::Version(version)
    }
}
impl FromStr for Constraint {
    type Err = ConstraintError;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if s.contains(std::path::MAIN_SEPARATOR) {
            Ok(Self::path(s)?)
        } else {
            Ok(Self::version(s)?)
        }
    }
}
#[cfg(test)]
mod tests {
    use super::Constraint;
    use super::PartialVersion;
    const CONSTRAINT: Constraint = Constraint::Version(PartialVersion::Post10m(13));
    #[test]
    fn test_not() {
        let c1 = Constraint::Version(PartialVersion::Post10m(13));
        assert!(matches!(c1, Constraint::Version(_)));
        let c2 = !c1;
        assert!(matches!(c2, Constraint::Not(_)));
        let c3 = !c2;
        assert!(matches!(c3, Constraint::Version(_)));
    }
    #[test]
    fn test_not_anything_and_nothing() {
        let c1 = Constraint::Anything;
        let c2 = !c1;
        assert!(matches!(c2, Constraint::Nothing));
        let c3 = !c2;
        assert!(matches!(c3, Constraint::Anything));
    }
    #[test]
    fn test_or() {
        assert!(matches!(
            Constraint::Anything | CONSTRAINT.clone(),
            Constraint::Anything
        ));
        assert!(matches!(
            CONSTRAINT.clone() | Constraint::Anything,
            Constraint::Anything
        ));
        assert!(matches!(
            Constraint::Nothing | CONSTRAINT.clone(),
            Constraint::Version(_)
        ));
        assert!(matches!(
            CONSTRAINT.clone() | Constraint::Nothing,
            Constraint::Version(_)
        ));
    }
    #[test]
    fn test_or_anything_and_nothing() {
        assert!(matches!(
            Constraint::Anything | Constraint::Anything,
            Constraint::Anything
        ));
        assert!(matches!(
            Constraint::Nothing | Constraint::Anything,
            Constraint::Anything
        ));
        assert!(matches!(
            Constraint::Anything | Constraint::Nothing,
            Constraint::Anything
        ));
    }
    #[test]
    fn test_and() {
        assert!(matches!(
            Constraint::Anything & CONSTRAINT.clone(),
            Constraint::Version(_)
        ));
        assert!(matches!(
            CONSTRAINT.clone() & Constraint::Anything,
            Constraint::Version(_)
        ));
        assert!(matches!(
            Constraint::Nothing & CONSTRAINT.clone(),
            Constraint::Nothing
        ));
        assert!(matches!(
            CONSTRAINT.clone() & Constraint::Nothing,
            Constraint::Nothing
        ));
    }
    #[test]
    fn test_and_anything_and_nothing() {
        assert!(matches!(
            Constraint::Anything & Constraint::Anything,
            Constraint::Anything
        ));
        assert!(matches!(
            Constraint::Nothing & Constraint::Anything,
            Constraint::Nothing
        ));
        assert!(matches!(
            Constraint::Anything & Constraint::Nothing,
            Constraint::Nothing
        ));
    }
    #[test]
    fn test_any() {
        assert!(matches!(Constraint::any([]), Constraint::Nothing));
        assert!(matches!(
            Constraint::any([
                Constraint::Anything,
                Constraint::Nothing,
                Constraint::Nothing
            ]),
            Constraint::Anything
        ));
        assert!(matches!(
            Constraint::any([Constraint::Nothing, CONSTRAINT.clone(), Constraint::Nothing]),
            Constraint::Version(_)
        ));
        assert!(matches!(
            Constraint::any([
                Constraint::Anything,
                CONSTRAINT.clone(),
                Constraint::Nothing
            ]),
            Constraint::Anything
        ));
        assert!(matches!(
            Constraint::any([CONSTRAINT.clone(), CONSTRAINT.clone()]),
            Constraint::Either(ca, cb)
                if matches!(*ca, Constraint::Version(_))
                && matches!(*cb, Constraint::Version(_))
        ));
    }
    #[test]
    fn test_all() {
        assert!(matches!(Constraint::all([]), Constraint::Anything));
        assert!(matches!(
            Constraint::all([
                Constraint::Anything,
                Constraint::Anything,
                Constraint::Anything
            ]),
            Constraint::Anything
        ));
        assert!(matches!(
            Constraint::all([
                Constraint::Anything,
                CONSTRAINT.clone(),
                Constraint::Anything,
            ]),
            Constraint::Version(_),
        ));
        assert!(matches!(
            Constraint::all([
                Constraint::Anything,
                CONSTRAINT.clone(),
                Constraint::Nothing,
            ]),
            Constraint::Nothing,
        ));
        assert!(matches!(
            Constraint::all([CONSTRAINT.clone(), CONSTRAINT.clone()]),
            Constraint::Both(ca, cb)
                if matches!(*ca, Constraint::Version(_))
                && matches!(*cb, Constraint::Version(_))
        ));
    }
}