use super::page::Page;
use regex::{Captures, Regex};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

pub const CSS_LINK_FRAGMENT: &'static str = "<link rel='stylesheet' href='{url}'>";
pub const SCRIPT_FRAGMENT: &'static str = "<script src='{url}'></script>";
pub const FAVICON_LINK_FRAGMENT: &'static str = "<link rel='icon' type='image/*' href='{url}'/>";
pub const IMAGE_LINKS_FRAGMENT: &'static str = "<meta name='image' content='{url}'/>
<meta property='og:image' content='{url}'/>
<meta property='twitter:image' content='{url}'/>";

const HTML_DOC_TEMPLATE: &'static str = "
<html lang='{lang}' prefix='og: https://ogp.me/ns#'>
<head>
    <meta charset='UTF-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <meta name='description' content='{description}'>
    <meta name='author' content='{author}'>

    <meta property='og:title' content='{title}'/>
    <meta property='og:description' content='{description}'/>
    {image}
    {favicon}
    <title>{title}</title>
    {css}
</head>

<body>
    {body}
</body>
{js}
</html>
";

pub fn replace_placeholders(template: &str, map: HashMap<String, String>) -> String {
    let re = Regex::new(r#"\{[a-z]+\}"#).unwrap();
    let def = String::new();
    re.replace_all(template, |captures: &Captures| {
        let placeholder = captures.iter().next().unwrap().unwrap().as_str();
        let placeholder = placeholder[1..placeholder.len() - 1].to_owned(); // strip the brackets
        match map.get(&placeholder) {
            Some(s) => s,
            None => &def,
        }
    })
    .to_string()
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct HtmlDoc(String);

impl HtmlDoc {
    pub fn from_page(page: &Page) -> Self {
        HtmlDoc(replace_placeholders(HTML_DOC_TEMPLATE, page.to_map()))
    }

    pub fn new() -> Self {
        HtmlDoc(String::new())
    }
}

impl std::fmt::Display for HtmlDoc {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct HtmlAttributes(pub HashMap<String, String>);

impl HtmlAttributes {
    pub fn new() -> Self {
        HtmlAttributes(HashMap::new())
    }

    pub fn get<S: Into<String>>(&self, key: S) -> Option<&String> {
        self.0.get(&key.into())
    }
}

impl std::fmt::Display for HtmlAttributes {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(
            f,
            "{}",
            self.0
                .iter()
                .map(|(key, value)| format!(" {}=\"{}\"", key, value))
                .collect::<Vec<String>>()
                .join("")
        )
    }
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct HtmlElement {
    pub tag: String,
    pub text: Option<String>,
    pub contents: Option<Vec<HtmlElement>>,
    #[serde(default = "HtmlAttributes::new")]
    pub attrs: HtmlAttributes,
}

impl HtmlElement {
    pub fn set_contents(&mut self, contents: Vec<HtmlElement>) {
        self.contents = Some(contents);
    }
}

impl std::fmt::Display for HtmlElement {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        let body = match &self.contents {
            Some(contents) => contents
                .iter()
                .map(|el| el.to_string())
                .collect::<Vec<String>>()
                .join(""),
            None => match &self.text {
                Some(text) => text.to_owned(),
                None => String::new(),
            },
        };

        write!(f, "<{}{}>{}</{}>", self.tag, self.attrs, body, self.tag)
    }
}

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

    #[test]
    fn text_html_element_to_string() {
        let html_el = HtmlElement {
            tag: String::from("p"),
            text: Some(String::from("Hello")),
            contents: None,
            attrs: HtmlAttributes::new(),
        };

        assert_eq!(html_el.to_string(), "<p>Hello</p>")
    }

    #[test]
    fn html_el_with_attrs_to_string() {
        let mut attrs = HtmlAttributes::new();
        attrs.0.insert(String::from("id"), String::from("some-id"));

        let html_el = HtmlElement {
            tag: String::from("p"),
            text: Some(String::from("Hello")),
            contents: None,
            attrs: attrs.clone(),
        };

        assert_eq!(html_el.to_string(), "<p id=\"some-id\">Hello</p>");
    }

    #[test]
    fn complex_html_el_to_string() {
        let html_el = HtmlElement {
            tag: String::from("p"),
            text: None,
            attrs: HtmlAttributes::new(),
            contents: Some(vec![
                HtmlElement {
                    tag: String::from("span"),
                    text: Some(String::from("Hello ")),
                    contents: None,
                    attrs: HtmlAttributes::new(),
                },
                HtmlElement {
                    tag: String::from("b"),
                    text: Some(String::from("World")),
                    contents: None,
                    attrs: HtmlAttributes::new(),
                },
            ]),
        };

        assert_eq!(
            html_el.to_string(),
            "<p><span>Hello </span><b>World</b></p>"
        )
    }
}