use regex::Regex;
use serde::{
    de::{self, MapAccess, Visitor},
    ser::{SerializeMap, SerializeSeq, SerializeStruct},
    Deserialize, Deserializer, Serializer,
};
use std::{
    cmp::Ordering,
    collections::BTreeMap,
    convert::{Infallible, TryFrom, TryInto},
    fmt::{self, Debug},
    marker::Copy,
    ops::Deref,
    str::FromStr,
};
pub use url::Url;
pub mod temporal;

#[cfg(test)]
mod test;

/// A helper type for representing a list of [property values][PropertyValue].
pub type NodeList = Vec<PropertyValue>;

/// A helper type for representing a map of named [property values][PropertyValue].
pub type Properties = std::collections::BTreeMap<String, NodeList>;

/// A concrete reference of the supported Microformats class by this library.
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "kebab-case")]
pub enum KnownClass {
    /// Represents a general container for content.
    /// https://microformats.org/wiki/h-entry
    #[serde(alias = "h-entry")]
    Entry,

    /// Represents a rewference to content at a different location.
    /// https://microformats.org/wiki/h-cite
    #[serde(alias = "h-cite")]
    Cite,

    /// Represents a contact card or vCard.
    /// https://microformats.org/wiki/h-card
    #[serde(alias = "h-card")]
    Card,

    /// https://microformats.org/wiki/h-feed
    #[serde(alias = "h-feed")]
    Feed,

    /// https://microformats.org/wiki/h-event
    #[serde(alias = "h-event")]
    Event,

    /// https://microformats.org/wiki/h-product
    #[serde(alias = "h-product")]
    Product,

    /// https://microformats.org/wiki/h-adr
    #[serde(alias = "h-adr")]
    Adr,

    /// https://microformats.org/wiki/h-geo
    #[serde(alias = "h-geo")]
    Geo,

    /// https://microformats.org/wiki/h-resume
    #[serde(alias = "h-resume")]
    Resume,

    /// https://microformats.org/wiki/h-review
    #[serde(alias = "h-review")]
    Review,

    /// https://microformats.org/wiki/h-recipe
    #[serde(alias = "h-recipe")]
    Recipe,
}

#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error("The type {0:?} is not a known Microformats class supported by this library.")]
    NotKnownClass(String),

    #[error("JSON: {0}")]
    JSON(#[from] serde_json::Error),

    #[error("The provided JSON value was not an object.")]
    NotAnObject,

    #[error("Missing property {0:?} when converting from JSON.")]
    JsonObjectMissingProperty(String),

    #[error(transparent)]
    Temporal(#[from] temporal::Error),
}

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

impl Eq for Error {}

impl FromStr for KnownClass {
    type Err = Error;

    /// Converts this concrete known class into a string.
    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-recipe" | "recipe" => Ok(Self::Recipe),
            _ => Err(Error::NotKnownClass(s.to_string())),
        }
    }
}

impl std::fmt::Display for KnownClass {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> 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",
        })
    }
}

/// Represents a Microformat class.
/// https://microformats.org/wiki/Category:Draft_Specifications
#[derive(Debug, Clone, Eq)]
pub enum Class {
    /// Represents a known Microformat class (h-entry, h-card, etc).
    Known(KnownClass),

    /// Represents a class that's not spec-compliant (h-cookies, h-monster, etc).
    Custom(String),
}

impl PartialOrd for Class {
    fn partial_cmp(&self, other: &Self) -> std::option::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 = Infallible;

    /// Parses a string as a Microformat class.
    ///
    /// # Examples
    /// ```
    /// # use std::str::FromStr;
    /// # use microformats_types::{Class, KnownClass};
    ///
    /// assert_eq!(Class::from_str("entry"), Ok(Class::Known(KnownClass::Entry)));
    /// assert_eq!(Class::from_str("h-card"), Ok(Class::Known(KnownClass::Card)));
    /// assert_eq!(Class::from_str("x-plane"), Ok(Class::Custom("x-plane".to_string())));
    /// ```
    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 fmt::Formatter<'_>) -> 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 {
    /// Serializes this class reference into a string.
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(self.to_string().as_str())
    }
}

impl Class {
    /// Checks if this class is a recognized one by the Microformats spec.
    pub fn is_recognized(&self) -> bool {
        !matches!(self, Self::Custom(_))
    }
}

struct ClassVisitor;

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

    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> 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 From<KnownClass> for Class {
    fn from(kc: KnownClass) -> Self {
        Self::Known(kc)
    }
}

fn short_circuit_url_deserialization<'de, D>(d: D) -> Result<Url, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let string_form = String::deserialize(d)?;
    let url_form = Url::parse(&string_form).map_err(serde::de::Error::custom)?;

    if url_form.as_str() != string_form {
        // This is called in the event a string happens to match the parsing of a URL but doesn't
        // convert back into one.
        Err(serde::de::Error::custom(
            "This string doesn't represent a valid URL despite looking like one.",
        ))
    } else {
        Ok(url_form)
    }
}

fn short_circuit_plain_text_deserialization<'de, D>(d: D) -> Result<String, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let string_form = String::deserialize(d)?;

    Url::from_str(&string_form)
        .map_err(serde::de::Error::custom)
        .map(|u| u.as_str().to_string())
        .and_then(|u| {
            if u == string_form && !u.contains(|c: char| c.is_whitespace()) && !u.contains('\n') {
                Err(serde::de::Error::invalid_type(
                    de::Unexpected::Other("URL"),
                    &"plain 'ol string",
                ))
            } else {
                Ok(string_form.clone())
            }
        })
        .or_else(|r: D::Error| {
            if r.to_string().starts_with("invalid type: URL") {
                Err(r)
            } else {
                temporal::Value::from_str(&string_form)
                    .map_err(serde::de::Error::custom)
                    .map(|u| u.to_string())
                    .and_then(|u| {
                        if u == string_form {
                            Err(serde::de::Error::invalid_type(
                                de::Unexpected::Str("temporal data"),
                                &"plain 'ol string",
                            ))
                        } else {
                            Ok(string_form.clone())
                        }
                    })
            }
        })
        .or_else(|r: D::Error| {
            if r.to_string().starts_with("invalid type: URL")
                || r.to_string().contains("temporal data")
            {
                Err(r)
            } else {
                Ok(string_form)
            }
        })
}

/// Represents the multiple forms in which a property's value is represented.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(untagged, rename_all = "kebab-case")]
pub enum PropertyValue {
    /// Represents plain-text, usually stored in properties like "p-name". It can be
    /// displayed directly to the user agent.
    /// <https://microformats.org/wiki/microformats2-parsing#parsing_a_p-_property>
    #[serde(deserialize_with = "short_circuit_plain_text_deserialization")]
    Plain(String),

    /// Represents a linked value.
    /// It's a companion of `Plain`; meant to make detecting of absolute URLs easier.
    #[serde(deserialize_with = "short_circuit_url_deserialization")]
    Url(Url),

    /// Represents a compatible datetime parser defined by <https://microformats.org/wiki/value-class-pattern#Date_and_time_parsing>
    /// <https://microformats.org/wiki/microformats2-parsing#parsing_a_dt-_property>
    Temporal(temporal::Value),

    Fragment(Fragment),

    /// Represents a structured form of information presented by Microformats as an
    /// `Item`. This will usually require a bit more processing before showing it.
    /// <https://microformats.org/wiki/microformats2-parsing#parsing_a_u-_property>
    #[serde(with = "referenced_item")]
    Item(Item),

    Image(Image),
}

impl From<Item> for PropertyValue {
    fn from(item: Item) -> Self {
        PropertyValue::Item(item)
    }
}

/// Represents markup and the plain text representation accompanying it.
/// <https://microformats.org/wiki/microformats2-parsing#parsing_a_e-_property>
#[derive(
    Debug, Clone, PartialEq, Eq, serde::Serialize, Default, serde::Deserialize, PartialOrd, Ord,
)]
#[serde(rename_all = "kebab-case")]
pub struct Fragment {
    /// Provides the HTML representation of this fragment.
    #[serde(skip_serializing_if = "String::is_empty")]
    pub html: String,

    /// Provides the plain-text form of the HTML.
    #[serde(default, skip_serializing_if = "String::is_empty")]
    pub value: String,

    /// Provides the language that this fragment is represented in.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub lang: Option<String>,

    /// A list of direct links extracted from the HTML.
    #[serde(skip)]
    pub links: Vec<String>,
}

impl Fragment {
    /// Determines if this fragment has any information represented in it.
    ///
    /// The presence of HTML does not determine if this fragment is empty; a
    /// fragment can be defined with no HTML (meaning the HTML would implictly look the same).
    pub fn is_empty(&self) -> bool {
        self.value.is_empty()
    }

    pub fn links(&self) -> &[String] {
        &self.links
    }
}

/// Represents the structured form of an image.
/// <https://microformats.org/wiki/microformats2-parsing#parse_an_img_element_for_src_and_alt>
#[derive(Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize, PartialOrd, Ord)]
#[serde(rename_all = "kebab-case")]
pub struct Image {
    pub value: Url,

    #[serde(default)]
    pub alt: Option<String>,
}

impl std::fmt::Debug for Image {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Image")
            .field("value", &self.value.to_string())
            .field("alt", &self.alt)
            .finish()
    }
}

mod referenced_item {

    use super::*;

    type Value = Item;

    struct ItemVisitor;

    #[derive(serde::Deserialize, Debug)]
    #[serde(field_identifier, rename_all = "kebab-case")]
    enum ItemDeserializationFields {
        Children,
        Value,
        Id,
        Properties,
        r#Type,
    }

    impl<'de> Visitor<'de> for ItemVisitor {
        type Value = Value;
        fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
            formatter.write_str("expecting null or an map representing an item")
        }

        fn visit_map<A>(self, mut item_map: A) -> Result<Self::Value, A::Error>
        where
            A: MapAccess<'de>,
        {
            let mut children: Items = Default::default();
            let mut value: Option<ValueKind> = Default::default();
            let mut id: Option<String> = Default::default();
            let mut types = Vec::new();
            let mut properties = Properties::default();

            while let Some(property) = item_map.next_key()? {
                match property {
                    ItemDeserializationFields::Children => {
                        let new_items = item_map.next_value::<Vec<Item>>()?.into_iter();

                        if children.is_empty() && new_items.len() > 0 {
                            children = new_items.collect::<Vec<Item>>().into();
                        } else {
                            children.extend(new_items);
                        }
                    }
                    ItemDeserializationFields::Value => {
                        if value.is_none() {
                            value = item_map.next_value::<Option<ValueKind>>()?;
                        }
                    }
                    ItemDeserializationFields::Id => {
                        if id.is_none() {
                            id = item_map.next_value::<Option<String>>()?;
                        }
                    }
                    ItemDeserializationFields::Type => {
                        types.extend(item_map.next_value::<Vec<Class>>()?);
                    }
                    ItemDeserializationFields::Properties => {
                        properties.extend(item_map.next_value::<BTreeMap<String, _>>()?);
                    }
                }
            }

            let item = Item {
                r#type: types,
                properties,
                id,
                value,
                children,
                ..Default::default()
            };

            Ok(item)
        }
    }

    pub fn serialize<S>(item: &Value, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_some(&Some(item))
    }

    pub fn deserialize<'de, D>(deserializer: D) -> Result<Value, D::Error>
    where
        D: Deserializer<'de>,
    {
        // TODO: Confirm that these are all of the top-level fields for an item.
        deserializer.deserialize_struct(
            "Item",
            &["type", "properties", "id", "value", "children"],
            ItemVisitor,
        )
    }
}

impl PropertyValue {
    /// Determines if this node's internal value is empty
    pub fn is_empty(&self) -> bool {
        match self {
            Self::Temporal(_) | Self::Url(_) | Self::Image(_) => false,
            Self::Plain(s) => s.is_empty(),
            Self::Fragment(f) => f.is_empty(),
            Self::Item(i) => i.is_empty(),
        }
    }
}

impl From<Url> for PropertyValue {
    fn from(u: Url) -> Self {
        Self::Url(u)
    }
}

impl From<temporal::Stamp> for PropertyValue {
    fn from(t: temporal::Stamp) -> Self {
        Self::Temporal(temporal::Value::Timestamp(t))
    }
}

impl From<temporal::Duration> for PropertyValue {
    fn from(t: temporal::Duration) -> Self {
        Self::Temporal(temporal::Value::Duration(t))
    }
}

/// Represents the structured form of an 'object' in Microformats.
#[derive(serde::Serialize, serde::Deserialize, Default, PartialEq, Eq, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Item {
    pub r#type: Vec<Class>,

    /// Represents the directly associated attributes for this item.
    #[serde(default, with = "referenced_properties")]
    pub properties: Properties,

    /// Represents a list of children for this item.
    #[serde(
        default,
        with = "referenced_children",
        skip_serializing_if = "referenced_children::is_empty"
    )]
    pub children: Items,

    /// The ID string of this item, if any is resolved.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub id: Option<String>,

    /// The language of this item, if any is resolved.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub lang: Option<String>,

    /// Represents the precise value of this item (if it's defined as a property to another).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub value: Option<ValueKind>,
}

impl std::fmt::Debug for Item {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Item")
            .field("type", &self.r#type)
            .field("id", &self.id)
            .field("value", &self.value)
            .field("lang", &self.lang)
            .finish()
    }
}

impl TryFrom<serde_json::Map<String, serde_json::Value>> for Item {
    type Error = crate::Error;

    fn try_from(obj: serde_json::Map<String, serde_json::Value>) -> Result<Self, Self::Error> {
        if !obj.contains_key("type") {
            return Err(Self::Error::JsonObjectMissingProperty("type".to_string()));
        }
        if !obj.contains_key("properties") {
            return Err(Self::Error::JsonObjectMissingProperty(
                "properties".to_string(),
            ));
        }

        serde_json::from_value(serde_json::Value::Object(obj)).map_err(Self::Error::JSON)
    }
}

impl TryFrom<serde_json::Value> for Item {
    type Error = crate::Error;

    fn try_from(v: serde_json::Value) -> Result<Self, Self::Error> {
        if let serde_json::Value::Object(o) = v {
            Self::try_from(o)
        } else {
            Err(Self::Error::NotAnObject)
        }
    }
}

impl TryInto<serde_json::Value> for Item {
    type Error = crate::Error;

    fn try_into(self) -> Result<serde_json::Value, Self::Error> {
        serde_json::to_value(self).map_err(crate::Error::JSON)
    }
}

impl IntoIterator for Item {
    type Item = Item;
    type IntoIter = std::vec::IntoIter<Self::Item>;

    fn into_iter(self) -> Self::IntoIter {
        let mut items = self
            .children
            .iter()
            .flat_map(|i| i.clone().into_iter())
            .collect::<Vec<Self::Item>>();
        items.push(self);
        items.into_iter()
    }
}

#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[serde(untagged, rename_all = "kebab-case")]
pub enum ValueKind {
    Url(Url),
    Plain(String),
}

impl Default for ValueKind {
    fn default() -> Self {
        Self::Plain(String::default())
    }
}

mod referenced_properties {
    use super::*;
    type Value = Properties;

    struct PropertyVisitor;

    #[derive(serde::Deserialize, Debug)]
    #[serde(untagged)]
    enum PotentialPropertyValue {
        List(NodeList),
        Value(PropertyValue),
    }

    impl<'de> Visitor<'de> for PropertyVisitor {
        type Value = Value;

        fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
            formatter.write_str("a map of properties with values that could be null, a string, a list of either strings, maps or both")
        }

        fn visit_map<A>(self, mut map_visitor: A) -> Result<Self::Value, A::Error>
        where
            A: de::MapAccess<'de>,
        {
            let mut property_map = Properties::default();

            while let Some(key) = map_visitor.next_key()? {
                // NOTE: This is a lossy operation.
                let concrete_value: NodeList =
                    match map_visitor.next_value::<PotentialPropertyValue>() {
                        Ok(PotentialPropertyValue::List(values)) => values,
                        Ok(PotentialPropertyValue::Value(node)) => vec![node],
                        Err(_) => vec![],
                    };

                if let Some(values) = property_map.get_mut(&key) {
                    values.extend(concrete_value);
                } else {
                    property_map.insert(key, concrete_value);
                }
            }

            Ok(property_map)
        }
    }

    pub fn serialize<S>(properties: &Value, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::ser::Serializer,
    {
        let mut properties_seq = serializer.serialize_map(Some(properties.len()))?;

        for (key, value) in properties.iter() {
            properties_seq.serialize_entry(key, value)?;
        }

        properties_seq.end()
    }

    pub fn deserialize<'de, D>(deserializer: D) -> Result<Value, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        deserializer.deserialize_map(PropertyVisitor)
    }
}

mod referenced_children {

    use super::*;
    type Value = Items;

    struct ChildrenVisitor;

    impl<'de> Visitor<'de> for ChildrenVisitor {
        type Value = Value;
        fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
            formatter.write_str("expecting a list of children nodes, an empty list or null")
        }

        fn visit_seq<ChildrenSequenceAccessor>(
            self,
            mut seq: ChildrenSequenceAccessor,
        ) -> Result<Self::Value, ChildrenSequenceAccessor::Error>
        where
            ChildrenSequenceAccessor: de::SeqAccess<'de>,
        {
            let size_hint = seq.size_hint().unwrap_or(0);
            let mut children: Items = Items::with_capacity(size_hint);

            while let Some(item) = seq.next_element()? {
                children.push(item);
            }

            Ok(children)
        }
    }

    #[allow(clippy::ptr_arg)]
    pub fn serialize<S>(children: &Value, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::ser::Serializer,
    {
        let mut seq = serializer.serialize_seq(Some(children.deref().len()))?;
        let safe_items = children
            .iter()
            .filter(|item| !item.is_empty())
            .cloned()
            .collect::<Vec<_>>();
        for concrete_item in safe_items {
            seq.serialize_element(&concrete_item)?;
        }
        seq.end()
    }

    pub fn deserialize<'de, D>(deserializer: D) -> Result<Value, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        deserializer.deserialize_seq(ChildrenVisitor)
    }

    pub fn is_empty(items: &Items) -> bool {
        items.is_empty()
    }
}

impl Item {
    /// Creates a new item with the provided `ItemParent` as its parent.
    pub fn new(types: Vec<Class>) -> Self {
        Item {
            r#type: types,
            ..Default::default()
        }
    }

    /// Determines if this item is undefined - an empty one.
    pub fn is_empty(&self) -> bool {
        self.children.is_empty() && self.r#type.is_empty()
    }

    pub fn remove_whole_property(&mut self, property_name: &str) {
        self.properties.remove(property_name);
    }

    pub fn content(&self) -> Option<Vec<PropertyValue>> {
        self.properties.get("content").cloned()
    }

    pub fn append_property(&mut self, property_name: &str, property_value: PropertyValue) {
        let mut new_values = if let Some(values) = self.properties.get(property_name) {
            values.to_vec()
        } else {
            Vec::default()
        };

        new_values.push(property_value);
        self.properties.insert(property_name.to_owned(), new_values);
    }

    /// Checks if this item has any properties with a nested item.
    pub fn has_nested_microformats(&self) -> bool {
        let has_nested_value_microformats = self
            .properties
            .values()
            .flatten()
            .any(|v| matches!(v, PropertyValue::Item(_)));

        has_nested_value_microformats || !self.children.is_empty()
    }

    pub fn nested_children(&self) -> Vec<Item> {
        self.properties
            .values()
            .flatten()
            .filter_map(|value| {
                if let PropertyValue::Item(item) = value {
                    Some(item)
                } else {
                    None
                }
            })
            .cloned()
            .collect::<Vec<_>>()
    }

    /// Obtains a list of values for a property.
    pub fn get_property(&self, arg: &str) -> Option<Vec<PropertyValue>> {
        self.properties.get(arg).cloned()
    }
}

#[derive(
    Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default, serde::Serialize, serde::Deserialize,
)]
pub struct Relation {
    pub rels: Vec<String>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub hreflang: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub media: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub r#type: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub text: Option<String>,
}

fn expand_text<T: std::fmt::Display>(value: &Option<T>, attr: &str) -> String {
    if let Some(value) = value {
        format!(" {attr}=\"{value}\"")
    } else {
        Default::default()
    }
}

impl Relation {
    /// Fuses the values of the other relation with this one.
    pub fn merge_with(&mut self, other: Self) {
        self.rels.extend_from_slice(&other.rels);
        self.rels.sort();
        self.rels.dedup();

        if self.hreflang.is_none() {
            self.hreflang = other.hreflang;
        }

        if self.media.is_none() {
            self.media = other.media;
        }
        if self.title.is_none() {
            self.title = other.title;
        }
        if self.r#type.is_none() {
            self.r#type = other.r#type;
        }
        if self.text.is_none() {
            self.text = other.text;
        }
    }

    /// Generates a value to be used in a header.
    pub fn to_header_value(&self, base_url: &Url) -> String {
        todo!("convert this relation to a header value with its URLs resolved to {base_url}")
    }
    /// Generates HTML to represent this relation.
    pub fn to_html(&self, rel_url: &str) -> String {
        format!(
            "<link href=\"{rel_url}\"{hreflang}{rel}{type_}{title}{media} />",
            hreflang = expand_text(&self.hreflang, "hreflang"),
            rel = expand_text(&Some(self.rels.join(" ")), "rel"),
            type_ = expand_text(&self.r#type, "type"),
            title = expand_text(&self.title, "title"),
            media = expand_text(&self.media, "media")
        )
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Default, serde::Deserialize, serde::Serialize)]
pub struct Relations {
    #[serde(flatten)]
    pub items: BTreeMap<Url, Relation>,
}

impl Relations {
    pub fn by_rels(&self) -> BTreeMap<String, Vec<Url>> {
        let mut rels: BTreeMap<String, Vec<Url>> = BTreeMap::default();
        self.items
            .iter()
            .flat_map(|(u, rel)| {
                rel.rels
                    .iter()
                    .map(move |rel_name| (rel_name.to_owned(), u.to_owned()))
            })
            .for_each(|(rel_name, url)| {
                if let Some(rel_urls) = rels.get_mut(&rel_name) {
                    rel_urls.push(url);
                } else {
                    rels.insert(rel_name, vec![url]);
                }
            });

        rels.iter_mut().for_each(|(_, urls)| {
            urls.dedup();
            urls.sort()
        });

        rels
    }
}

#[derive(Default, Debug, PartialEq, Eq, Clone)]
pub struct Items(Vec<Item>);

impl From<Vec<Item>> for Items {
    fn from(value: Vec<Item>) -> Self {
        Self(value)
    }
}

impl Items {
    /// Creates a new child item for this list of children.
    pub fn create_child_item(&mut self, types: &[Class]) -> Item {
        let item = Item::new(types.to_vec());
        self.0.push(item.to_owned());
        item
    }

    pub fn get_by_id(&self, id: &str) -> Option<Item> {
        self.iter()
            .flat_map(|item| item.clone().into_iter())
            .find(|item| item.id == Some(id.to_string()))
            .clone()
    }

    pub fn get_by_url(&self, url: &Url) -> Option<Item> {
        self.iter()
            .flat_map(|item| item.clone().into_iter())
            .find(|item| item.value == Some(ValueKind::Url(url.to_owned())))
            .clone()
    }

    pub fn with_capacity(size_hint: usize) -> Items {
        Items(Vec::with_capacity(size_hint))
    }
}

impl std::ops::DerefMut for Items {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

impl Deref for Items {
    type Target = Vec<Item>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

/// Represents a parsed document of Microformats items and its relating rel links.
#[derive(Clone, Debug, PartialEq, Default, Eq)]
pub struct Document {
    pub items: Vec<Item>,
    pub url: Option<url::Url>,
    pub rels: Relations,
    pub lang: Option<String>,
}

impl Document {
    pub fn new(url: Option<Url>) -> Self {
        Self {
            url,
            ..Default::default()
        }
    }

    /// Adds a new [relation][Relation] pointing to the [url][Url] provided.
    pub fn add_relation(&mut self, url: Url, relation: Relation) {
        if let Some(rel) = self.rels.items.get_mut(&url) {
            rel.merge_with(relation);
        } else {
            self.rels.items.insert(url.to_owned(), relation);
        }
    }
}

impl serde::Serialize for Document {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let mut s = serializer.serialize_struct("Document", 4)?;

        s.serialize_field("items", &self.items)?;
        s.serialize_field("rel-urls", &self.rels.items)?;
        s.serialize_field("rels", &self.rels.by_rels())?;
        if let Some(lang) = &self.lang {
            s.serialize_field("lang", lang)?;
        } else {
            s.skip_field("lang")?;
        }
        s.end()
    }
}

#[derive(serde::Deserialize, Debug)]
#[serde(field_identifier, rename_all = "kebab-case")]
enum DocumentDeserializationFields {
    Items,
    RelUrls,
    Rels,
    Url,
    Lang,
}

impl<'de> serde::Deserialize<'de> for Document {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        struct DocumentVisitor;

        impl<'de> Visitor<'de> for DocumentVisitor {
            type Value = Document;

            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                formatter.write_str("a Microformat document represented with the expected fields")
            }

            fn visit_map<V>(self, mut map: V) -> Result<Document, V::Error>
            where
                V: de::MapAccess<'de>,
            {
                let mut document = Document::default();
                let mut rel_urls: Option<Relations> = None;

                while let Ok(Some(key)) = map.next_key() {
                    match key {
                        DocumentDeserializationFields::Items => {
                            let raw_items = map.next_value::<Vec<Item>>()?;
                            document.items.extend(raw_items);
                        }
                        DocumentDeserializationFields::Url => {
                            if document.url.is_some() {
                                return Err(de::Error::duplicate_field("url"));
                            }

                            document.url = map.next_value()?;
                        }
                        DocumentDeserializationFields::RelUrls => {
                            if rel_urls.is_some() {
                                return Err(de::Error::duplicate_field("rel-urls"));
                            }

                            rel_urls = map.next_value()?;
                        }
                        DocumentDeserializationFields::Lang => {
                            if document.lang.is_some() {
                                return Err(de::Error::duplicate_field("lang"));
                            }

                            document.lang = map.next_value()?;
                        }
                        DocumentDeserializationFields::Rels => {
                            map.next_value::<BTreeMap<String, Vec<String>>>()?;
                        }
                    }
                }

                document.rels = rel_urls.unwrap_or_default();

                Ok(document)
            }
        }

        deserializer.deserialize_struct(
            "Document",
            &["items", "rel-urls", "url", "lang"],
            DocumentVisitor,
        )
    }
}

impl IntoIterator for Document {
    type Item = Item;
    type IntoIter = std::vec::IntoIter<Self::Item>;

    fn into_iter(self) -> Self::IntoIter {
        self.items
            .into_iter()
            .flat_map(|i| i.into_iter())
            .collect::<Vec<Item>>()
            .into_iter()
    }
}

pub trait FindItemByProperty {
    fn find_items_with_matching_property_value_by<F>(&self, predicate: F) -> Vec<(String, Item)>
    where
        F: Fn(String, PropertyValue) -> bool + Copy;

    fn find_items_with_matching_property_value(
        &self,
        needle: PropertyValue,
    ) -> Vec<(String, Item)> {
        self.find_items_with_matching_property_value_by(|_name, property_value| {
            property_value == needle
        })
    }
}

impl FindItemByProperty for Item {
    fn find_items_with_matching_property_value_by<F>(&self, predicate: F) -> Vec<(String, Item)>
    where
        F: Fn(String, PropertyValue) -> bool + Copy,
    {
        let mut values = self
            .properties
            .iter()
            .filter_map(|(name, values)| {
                if values
                    .iter()
                    .any(|value| predicate(name.to_owned(), value.to_owned()))
                {
                    Some((name.to_owned(), self.to_owned()))
                } else {
                    None
                }
            })
            .collect::<Vec<_>>();

        self.children.iter().for_each(|child| {
            values.extend(child.find_items_with_matching_property_value_by(predicate));
        });

        values
    }
}

impl FindItemByProperty for Document {
    fn find_items_with_matching_property_value_by<F>(&self, predicate: F) -> Vec<(String, Item)>
    where
        F: Fn(String, PropertyValue) -> bool + std::marker::Copy,
    {
        self.items
            .iter()
            .flat_map(|item| item.find_items_with_matching_property_value_by(predicate))
            .collect()
    }
}

pub trait FindItemByUrl: FindItemByProperty {
    fn find_item_by_url(&self, expected_url: &Url) -> Option<Item> {
        let url_property_value = PropertyValue::Url(expected_url.to_owned());
        self.find_items_with_matching_property_value(url_property_value)
            .first()
            .map(|(_name, value)| value.to_owned())
    }
}

impl FindItemByUrl for Document {}

impl FindItemByUrl for Item {}

pub trait FindItemById {
    fn find_item_by_id(&self, expected_id: &str) -> Option<Item>;
}

impl FindItemById for Item {
    fn find_item_by_id(&self, expected_id: &str) -> Option<Item> {
        if self.id == Some(expected_id.to_string()) {
            Some(self.to_owned())
        } else {
            self.children
                .iter()
                .find_map(|item| item.find_item_by_id(expected_id))
        }
    }
}

impl FindItemById for Document {
    fn find_item_by_id(&self, expected_id: &str) -> Option<Item> {
        self.items
            .iter()
            .find_map(|item| item.find_item_by_id(expected_id))
    }
}
