use std::collections::HashMap;
use std::iter::FromIterator;

fn default_h_value() -> String {
    "entry".to_string()
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Type {
    Like,
    Bookmark,
    Reply,
    Repost,
    Note,
    Article,
    Photo,
    Video,
    Audio,
    Media,
    Quote,
    GamePlay,
    #[serde(rename = "rsvp")]
    RSVP,
    #[serde(rename = "checkin")]
    CheckIn,
    Listen,
    Watch,
    Review,
    Read,
    Jam,
    Follow,
    Other(String),
}

impl ToString for Type {
    fn to_string(&self) -> String {
        serde_json::to_value(self)
            .unwrap_or_default()
            .as_str()
            .unwrap_or_default()
            .to_owned()
    }
}

impl PartialEq for Type {
    fn eq(&self, other: &Self) -> bool {
        std::mem::discriminant(self) == std::mem::discriminant(other)
    }
}

/// Represents the potential forms of defining a post type. The similar form
/// as represented by a single string is the one conventionally used. The
/// expanded form is one that's being experimented on to allow for the definition
/// of the constraints that a type can set.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(untagged, rename_all = "kebab-case")]
pub enum PostType {
    Simple(Type),
    Expanded {
        name: String,

        #[serde(alias = "type")]
        kind: Type,

        #[serde(default = "default_h_value")]
        h: String,

        #[serde(default)]
        properties: Vec<String>,

        #[serde(default)]
        required_properties: Vec<String>,
    },
}

impl From<PostType> for Type {
    fn from(post_type: PostType) -> Type {
        match post_type {
            PostType::Simple(kind) => kind,
            PostType::Expanded { kind, .. } => kind,
        }
    }
}

impl From<&PostType> for Type {
    fn from(post_type: &PostType) -> Type {
        match post_type {
            PostType::Simple(kind) => kind.clone(),
            PostType::Expanded { kind, .. } => kind.clone(),
        }
    }
}

impl PartialEq for PostType {
    fn eq(&self, other: &Self) -> bool {
        let ltype: Type = self.into();
        let rtype: Type = other.into();

        ltype == rtype
    }
}

impl PostType {
    pub fn name(&self) -> String {
        match self {
            Self::Simple(simple_type) => simple_type.to_string(),
            Self::Expanded { ref name, .. } => name.to_string(),
        }
    }

    pub fn kind(&self) -> String {
        match self {
            Self::Simple(simple_type) => simple_type.to_string(),
            Self::Expanded { ref name, .. } => name.to_string(),
        }
    }
}

fn properties_from_type() -> HashMap<String, Type> {
    HashMap::from_iter(
        vec![
            ("like-of".to_owned(), Type::Like),
            ("in-reply-to".to_owned(), Type::Reply),
            ("bookmark-of".to_owned(), Type::Bookmark),
            ("repost-of".to_owned(), Type::Repost),
            ("quote-of".to_owned(), Type::Quote),
            ("gameplay-of".to_owned(), Type::GamePlay),
            ("follow-of".to_owned(), Type::Follow),
            ("jam-of".to_owned(), Type::Jam),
            ("listen-of".to_owned(), Type::Listen),
            ("rsvp".to_owned(), Type::RSVP),
            ("photo".to_owned(), Type::Photo),
            ("video".to_owned(), Type::Video),
            ("audio".to_owned(), Type::Audio),
            ("checkin".to_owned(), Type::CheckIn),
            ("read-of".to_owned(), Type::Read),
            ("media".to_owned(), Type::Media),
        ]
        .iter()
        .cloned(),
    )
}

pub fn resolve_from_object(item_mf2: &serde_json::Map<String, serde_json::Value>) -> Option<Type> {
    item_mf2.get("properties").and_then(|properties| {
        properties
            .as_object()
            .map(|props| {
                let mut types: Vec<Type> = vec![];
                let has_content = props.contains_key(&"content".to_owned());
                let has_name = props.contains_key(&"name".to_owned());

                properties_from_type().iter().for_each(|(key, val)| {
                    if props.contains_key(&key.to_owned()) {
                        types.push(val.clone());
                    }
                });

                if has_name && has_content {
                    types.push(Type::Article)
                } else if !has_name && has_content {
                    types.push(Type::Note)
                } else {
                    types.push(Type::Other("unrecognized".to_string()))
                }

                types
            })
            .and_then(|types| {
                // FIXME: Do discovery for other combinatory post types.
                // 'photo' => ['photo', 'note']
                // 'video' => ['photo', 'video', 'note']
                // 'media' => ['photo', 'video', 'audio', 'note']
                if types.contains(&Type::RSVP) && types.contains(&Type::Reply) {
                    types
                        .iter()
                        .find(|post_type| *post_type == &Type::RSVP)
                        .cloned()
                } else {
                    types.first().cloned()
                }
            })
    })
}

pub fn dominant_type<Data>(mf2_data: Data, item_url: &url::Url) -> Option<Type>
where
    Data: ToString,
{
    None
    // let mf2_parsed = microformats_parser::mapparser::parse_string(mf2_data.to_string());
    // let page_mf2 = mf2_parsed.as_object().unwrap();
    // resolve_item_from_mf2(
    //     page_mf2
    //         .get(&"items".to_owned())
    //         .and_then(|v| v.as_array())
    //         .cloned()
    //         .unwrap_or_default()
    //         .iter()
    //         .filter_map(|i| i.as_object())
    //         .cloned()
    //         .collect(),
    //     item_url,
    // )
    // .or_else(|| {
    //     page_mf2
    //         .get("items")
    //         .and_then(|i| i.as_array())
    //         .and_then(|i| i.first())
    //         .and_then(|i| i.as_object())
    //         .cloned()
    // })
    // .and_then(|mf2| resolve_from_object(&mf2))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn post_type_from_json() {
        assert_eq!(
            serde_json::from_str::<PostType>(
                r#"
                {
                    "name": "Note",
                    "type": "note"
                }
                "#
            )
            .ok(),
            Some(PostType::Expanded {
                name: "Note".to_string(),
                kind: Type::Note,
                h: "entry".to_string(),
                properties: Vec::default(),
                required_properties: Vec::default()
            })
        );
    }

    #[test]
    fn post_type_partial_eq() {
        assert_eq!(
            PostType::Simple(Type::Like),
            PostType::Expanded {
                kind: Type::Like,
                name: "Like".to_string(),
                h: "entry".to_string(),
                properties: Vec::default(),
                required_properties: Vec::default()
            }
        );
    }

    // FIXME: Refactor to read HTML and JSON information from disk and generate tests from that.
    // This will let us use https://github.com/microformats/tests as a shared source of testing.
    #[test]
    fn type_insure_string_form() {
        assert_eq!(Type::Note.to_string(), "note".to_owned());
        assert_eq!(serde_json::from_str(r#""note""#).ok(), Some(Type::Note));
        assert_eq!(
            serde_json::from_str(r#""checkin""#).ok(),
            Some(Type::CheckIn)
        );
        assert_eq!(
            serde_json::from_str(r#""note""#)
                .ok()
                .map(|k: Type| k.to_string()),
            Some("note".to_owned())
        );
    }

    #[test]
    fn dominant_type_resolves_likes() {
        let _ = crate::test::init();
        let item_url: url::Url = "http://fake.url/omg".parse().unwrap();
        let text = format!(
            "
        <div class=\"h-entry\">
            <a class=\"u-url\" href=\"{}\"></a>
            <span href=\"/cokies\" class=\"u-like-of\">This content here.</span>
        </div>
        ",
            item_url
        );

        assert_eq!(dominant_type(text, &item_url), Some(Type::Like));
    }

    #[test]
    fn dominant_type_resolves_replies() {
        let _ = crate::test::init();
        let item_url: url::Url = "http://fake.url/omg".parse().unwrap();
        let text = format!(
            "
        <div class='h-entry'>
            <a class='u-url' href='{}'></a>
            <span class='u-in-reply-to'>
                <a href='well hello stranger' class='u-url'></a>
                This content here.
            </span>
        </div>
        ",
            item_url
        );

        assert_eq!(dominant_type(text, &item_url), Some(Type::Reply));
    }

    #[test]
    fn dominant_type_resolves_note() {
        let _ = crate::test::init();
        let item_url: url::Url = "http://fake.url/omg".parse().unwrap();
        let text = format!("<div class='h-entry'><p class='p-content'>Welp.</p></div>",);

        assert_eq!(dominant_type(text, &item_url), Some(Type::Note));
    }
}
