//! Types to parse all OCPI enumerations.

use crate::{json, warning};

/// A type used to deserialize a JSON string value into a structured Rust enum.
///
/// The deserialized value may not map to a `Known` variant in the enum and therefore be `Unknown`.
/// The caller can then decide what to do with the `Unknown` variant.
#[derive(Clone, Debug)]
pub(crate) enum Enum<T> {
    Known(T),
    Unknown(String),
}

/// Create an `Enum<T>` from a `&str`.
///
/// This is used in conjunction with `FromJson`.
pub(crate) trait IntoEnum: Sized {
    fn enum_from_str(s: &str) -> Enum<Self>;
}

/// A wrapper around `WarningKind` that associates the `enum` name with the warning.
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
pub struct Warning {
    /// The type name of the enum that raised this warning.
    type_name: &'static str,

    /// The kind of warning raised.
    kind: WarningKind,
}

#[cfg(test)]
impl Warning {
    pub(crate) fn type_name(&self) -> &'static str {
        self.type_name
    }

    pub(crate) fn kind(&self) -> &WarningKind {
        &self.kind
    }
}

impl std::fmt::Display for Warning {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match &self.kind {
            WarningKind::ContainsEscapeCodes => write!(
                f,
                "The {} value contains escape codes but it does not need them.",
                self.type_name
            ),
            WarningKind::Decode(warning) => std::fmt::Display::fmt(&warning, f),
            WarningKind::PreferUpperCase => {
                write!(f, "The `{}` should be uppercase.", self.type_name)
            }
            WarningKind::InvalidVariant => {
                write!(f, "The value is not a valid `{}`.", self.type_name)
            }
            WarningKind::InvalidType { type_found } => {
                write!(
                    f,
                    "A {} value should be a string but `{}` was found.",
                    self.type_name, type_found
                )
            }
        }
    }
}

impl crate::Warning for Warning {
    fn id(&self) -> warning::Id {
        match &self.kind {
            WarningKind::ContainsEscapeCodes => warning::Id::from_static("contains_escape_codes"),
            WarningKind::Decode(warning) => warning.id(),
            WarningKind::PreferUpperCase => warning::Id::from_static("prefer_upper_case"),
            WarningKind::InvalidVariant => warning::Id::from_static("invalid_variant"),

            WarningKind::InvalidType { type_found } => {
                warning::Id::from_string(format!("invalid_type({type_found})"))
            }
        }
    }
}

#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum WarningKind {
    /// The enum variant doesn't need to have escape-codes.
    ContainsEscapeCodes,

    /// The field at the path could not be decoded.
    Decode(json::decode::Warning),

    /// Each enum should be uppercase.
    PreferUpperCase,

    /// The value is not a valid.
    InvalidVariant,

    /// The JSON value given is not a string.
    InvalidType { type_found: json::ValueKind },
}

impl WarningKind {
    pub(crate) fn invalid_type(elem: &json::Element<'_>) -> Self {
        Self::InvalidType {
            type_found: elem.value().kind(),
        }
    }

    /// Convert a `WarningKind` into a `Warning` by supplying a type name.
    pub(crate) fn into_warning(self, type_name: &'static str) -> Warning {
        Warning {
            type_name,
            kind: self,
        }
    }
}

/// This macro defines the `FromJson` impl for the given enum.
///
/// Note: The `IntoEnum` should already be defined for the enum.
#[macro_export]
macro_rules! define_enum_from_json {
    ($kind:ident, display_name: $name:literal, warning_id: $warning:literal) => {
        impl $crate::json::FromJson<'_> for $kind {
            type Warning = $crate::enumeration::Warning;

            fn from_json(elem: &$crate::json::Element<'_>) -> $crate::Verdict<Self, Self::Warning> {
                use $crate::warning::IntoCaveat as _;

                let value = Enum::<$kind>::from_json(elem)?;
                let (value, warnings) = value.into_parts();

                let value = match value {
                    $crate::Enum::Known(v) => v,
                    $crate::Enum::Unknown(_) => {
                        return warnings.bail(
                            elem,
                            $crate::enumeration::WarningKind::InvalidVariant
                                .into_warning(stringify!($kind)),
                        );
                    }
                };

                Ok(value.into_caveat(warnings))
            }
        }

        impl $crate::json::FromJson<'_> for $crate::Enum<$kind> {
            type Warning = $crate::enumeration::Warning;

            fn from_json(elem: &$crate::json::Element<'_>) -> $crate::Verdict<Self, Self::Warning> {
                use $crate::warning::IntoCaveat as _;

                let mut warnings = $crate::warning::Set::new();
                let value = elem.as_value();

                let Some(s) = value.to_raw_str() else {
                    return warnings.bail(
                        elem,
                        $crate::enumeration::WarningKind::invalid_type(elem)
                            .into_warning(stringify!($kind)),
                    );
                };

                // We can't use `fn gather_warnings_into` here as we want to convert the warnings
                // using the type name from the macro instead of the `impl From<WarningA> for WarningB`.
                // This requires a manual conversion from `decode::Warning` to `Warning`.
                let (s, inner_warnings) = s.has_escapes(elem).into_parts();
                let inner_warnings =
                    inner_warnings
                        .into_inner()
                        .into_iter()
                        .map(|(elem_id, group)| {
                            (
                                elem_id,
                                group.map(|w| {
                                    $crate::enumeration::WarningKind::Decode(w)
                                        .into_warning(stringify!($kind))
                                }),
                            )
                        });

                warnings.extend(inner_warnings);

                let s = match s {
                    $crate::json::PendingStr::NoEscapes(s) => s,
                    $crate::json::PendingStr::HasEscapes(_) => {
                        return warnings.bail(
                            elem,
                            $crate::enumeration::WarningKind::ContainsEscapeCodes
                                .into_warning(stringify!($kind)),
                        );
                    }
                };

                if !s.chars().all(|c| c.is_uppercase() || c == '_') {
                    warnings.insert(
                        elem,
                        $crate::enumeration::WarningKind::PreferUpperCase
                            .into_warning(stringify!($kind)),
                    );
                }

                let value = $kind::enum_from_str(s);

                Ok(value.into_caveat(warnings))
            }
        }
    };
}
