use std::fmt;

use rustc_macros::{Decodable, Encodable, StableHash};
use rustc_span::{Span, Symbol};
use thin_vec::ThinVec;

use crate::attr::version::RustcVersion;

#[derive(Encodable, Decodable, Clone, Debug, PartialEq, Eq, Hash, StableHash)]
pub enum CfgEntry {
    All(ThinVec<CfgEntry>, Span),
    Any(ThinVec<CfgEntry>, Span),
    Not(Box<CfgEntry>, Span),
    Bool(bool, Span),
    NameValue { name: Symbol, value: Option<Symbol>, span: Span },
    Version(Option<RustcVersion>, Span),
}

impl CfgEntry {
    pub fn lower_spans(&mut self, lower_span: impl Copy + Fn(Span) -> Span) {
        match self {
            CfgEntry::All(subs, span) | CfgEntry::Any(subs, span) => {
                *span = lower_span(*span);
                subs.iter_mut().for_each(|sub| sub.lower_spans(lower_span));
            }
            CfgEntry::Not(sub, span) => {
                *span = lower_span(*span);
                sub.lower_spans(lower_span);
            }
            CfgEntry::Bool(_, span)
            | CfgEntry::NameValue { span, .. }
            | CfgEntry::Version(_, span) => {
                *span = lower_span(*span);
            }
        }
    }

    pub fn span(&self) -> Span {
        let (Self::All(_, span)
        | Self::Any(_, span)
        | Self::Not(_, span)
        | Self::Bool(_, span)
        | Self::NameValue { span, .. }
        | Self::Version(_, span)) = self;
        *span
    }

    /// Same as `PartialEq` but doesn't check spans and ignore order of cfgs.
    pub fn is_equivalent_to(&self, other: &Self) -> bool {
        match (self, other) {
            (Self::All(a, _), Self::All(b, _)) | (Self::Any(a, _), Self::Any(b, _)) => {
                a.len() == b.len() && a.iter().all(|a| b.iter().any(|b| a.is_equivalent_to(b)))
            }
            (Self::Not(a, _), Self::Not(b, _)) => a.is_equivalent_to(b),
            (Self::Bool(a, _), Self::Bool(b, _)) => a == b,
            (
                Self::NameValue { name: name1, value: value1, .. },
                Self::NameValue { name: name2, value: value2, .. },
            ) => name1 == name2 && value1 == value2,
            (Self::Version(a, _), Self::Version(b, _)) => a == b,
            _ => false,
        }
    }
}

impl fmt::Display for CfgEntry {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fn write_entries(
            name: &str,
            entries: &[CfgEntry],
            f: &mut fmt::Formatter<'_>,
        ) -> fmt::Result {
            write!(f, "{name}(")?;
            for (nb, entry) in entries.iter().enumerate() {
                if nb != 0 {
                    f.write_str(", ")?;
                }
                entry.fmt(f)?;
            }
            f.write_str(")")
        }
        match self {
            Self::All(entries, _) => write_entries("all", entries, f),
            Self::Any(entries, _) => write_entries("any", entries, f),
            Self::Not(entry, _) => write!(f, "not({entry})"),
            Self::Bool(value, _) => write!(f, "{value}"),
            Self::NameValue { name, value, .. } => {
                match value {
                    // We use `as_str` and debug display to have characters escaped and `"`
                    // characters surrounding the string.
                    Some(value) => write!(f, "{name} = {:?}", value.as_str()),
                    None => write!(f, "{name}"),
                }
            }
            Self::Version(version, _) => match version {
                Some(version) => write!(f, "{version}"),
                None => Ok(()),
            },
        }
    }
}
