//! Microformat class types.

use crate::Error;
use std::str::FromStr;

/// Standard microformats2 class types recognized by this library.
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "kebab-case")]
pub enum KnownClass {
    /// A syndicatable piece of content.
    #[serde(alias = "h-entry")]
    Entry,
    /// A reference to another piece of content.
    #[serde(alias = "h-cite")]
    Cite,
    /// A person, organization, or venue.
    #[serde(alias = "h-card")]
    Card,
    /// A stream of entries.
    #[serde(alias = "h-feed")]
    Feed,
    /// An event with a date, time, and location.
    #[serde(alias = "h-event")]
    Event,
    /// A product for sale or review.
    #[serde(alias = "h-product")]
    Product,
    /// An address (street, city, etc.).
    #[serde(alias = "h-adr")]
    Adr,
    /// Geographic coordinates.
    #[serde(alias = "h-geo")]
    Geo,
    /// A resume or CV.
    #[serde(alias = "h-resume")]
    Resume,
    /// A review of something.
    #[serde(alias = "h-review")]
    Review,
    /// A recipe.
    #[serde(alias = "h-recipe")]
    Recipe,
}

impl FromStr for KnownClass {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_ascii_lowercase().as_str() {
            "h-entry" | "entry" => Ok(Self::Entry),
            "h-cite" | "cite" => Ok(Self::Cite),
            "h-card" | "card" => Ok(Self::Card),
            "h-event" | "event" => Ok(Self::Event),
            "h-product" | "product" => Ok(Self::Product),
            "h-feed" | "feed" => Ok(Self::Feed),
            "h-geo" | "geo" => Ok(Self::Geo),
            "h-adr" | "adr" => Ok(Self::Adr),
            "h-resume" | "resume" => Ok(Self::Resume),
            "h-review" | "review" => Ok(Self::Review),
            "h-recipe" | "recipe" => Ok(Self::Recipe),
            _ => Err(Error::NotKnownClass(s.to_string())),
        }
    }
}

impl std::fmt::Display for KnownClass {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(match self {
            KnownClass::Entry => "h-entry",
            KnownClass::Cite => "h-cite",
            KnownClass::Card => "h-card",
            KnownClass::Feed => "h-feed",
            KnownClass::Event => "h-event",
            KnownClass::Product => "h-product",
            KnownClass::Adr => "h-adr",
            KnownClass::Geo => "h-geo",
            KnownClass::Resume => "h-resume",
            KnownClass::Review => "h-review",
            KnownClass::Recipe => "h-recipe",
        })
    }
}

/// A microformats2 class, either a known standard type or a custom extension.
#[derive(Debug, Clone, Eq)]
pub enum Class {
    /// A recognized standard microformats2 class.
    Known(KnownClass),
    /// A custom or extension microformats class.
    Custom(String),
}

impl PartialOrd for Class {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        self.to_string().partial_cmp(&other.to_string())
    }
}

impl PartialEq for Class {
    fn eq(&self, other: &Self) -> bool {
        self.to_string().eq(&other.to_string())
    }
}

impl FromStr for Class {
    type Err = std::convert::Infallible;

    fn from_str(class_str: &str) -> Result<Self, Self::Err> {
        KnownClass::from_str(class_str)
            .or_else(|_| KnownClass::from_str(&class_str.replace("h-", "")))
            .map(Class::Known)
            .or_else(|_| Ok(Self::Custom(class_str.trim_start_matches("h-").to_string())))
    }
}

impl std::fmt::Display for Class {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Known(class) => f.write_fmt(format_args!("{}", class)),
            Self::Custom(class) => f.write_fmt(format_args!("h-{}", class)),
        }
    }
}

impl serde::Serialize for Class {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(self.to_string().as_str())
    }
}

struct ClassVisitor;

impl serde::de::Visitor<'_> for ClassVisitor {
    type Value = Class;

    fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter.write_str(
            "a string that follows Microformats class conventions of being prefixed by 'h-'",
        )
    }

    fn visit_str<E>(self, class_str: &str) -> Result<Self::Value, E>
    where
        E: serde::de::Error,
    {
        Class::from_str(class_str).map_err(|e| E::custom(e.to_string()))
    }
}

impl<'de> serde::Deserialize<'de> for Class {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        deserializer.deserialize_string(ClassVisitor)
    }
}

impl Class {
    /// Returns true if this is a recognized standard class.
    pub fn is_recognized(&self) -> bool {
        !matches!(self, Self::Custom(_))
    }
}

impl From<KnownClass> for Class {
    fn from(kc: KnownClass) -> Self {
        Self::Known(kc)
    }
}
