use lazy_static::lazy_static;
use libxml::{
    parser::Parser,
    tree::{node::Node, nodetype::NodeType},
};
use log::warn;
use regex::Regex;
use std::collections::HashSet;
use std::fmt::Write;
use std::str;

lazy_static! {
    static ref IGNORED_ELEMENTS: HashSet<&'static str> = {
        let mut s = HashSet::new();
        s.insert("base");
        s.insert("link");
        s.insert("meta");
        s.insert("head");
        s.insert("script");
        s.insert("style");
        s.insert("template");
        s.insert("img");
        s
    };
    static ref SPACING_ELEMENTS: HashSet<&'static str> = {
        let mut s = HashSet::new();
        s.insert("dt");
        s.insert("dd");
        s.insert("td");
        s.insert("th");
        s
    };
    static ref ALT_TEXT_ELEMENTS: HashSet<&'static str> = {
        let s = HashSet::new();
        s
    };
    static ref BREAKING_ELEMENTS: HashSet<&'static str> = {
        let mut s = HashSet::new();
        s.insert("address");
        s.insert("blockquote");
        s.insert("br");
        s.insert("caption");
        s.insert("center");
        s.insert("div");
        s.insert("dt");
        s.insert("embed");
        s.insert("form");
        s.insert("hr");
        s.insert("iframe");
        s.insert("li");
        s.insert("map");
        s.insert("menu");
        s.insert("tr");
        s.insert("pre");
        s.insert("p");
        s.insert("object");
        s.insert("noscript");
        s.insert("h1");
        s.insert("h2");
        s.insert("h3");
        s.insert("h4");
        s.insert("h5");
        s.insert("h6");
        s
    };
}

pub struct Html2Text;

impl Html2Text {
    pub fn to_summary(plain_text: &str) -> String {
        plain_text.chars().take(300).collect()
    }

    pub fn process(html: &str) -> Option<String> {
        let parser = Parser::default_html();
        if let Ok(doc) = parser.parse_string(html) {
            if let Some(root_node) = doc.get_root_element() {
                let mut text = String::new();
                Self::recurse_html_nodes_for_text(&root_node, &mut text);

                let text = match escaper::decode_html(&text) {
                    Ok(text) => text,
                    Err(e) => {
                        warn!("Error {:?} at character {}", e.kind, e.position);
                        text
                    }
                };
                let text = text.trim();
                let text = str::replace(&text, "\n", " ");
                let text = str::replace(&text, "\r", " ");
                let text = str::replace(&text, "_", " ");

                let compress_whitespace = Regex::new(r#"/\s+/g"#).expect("Failed to create RegEx");
                let text = compress_whitespace.replace_all(&text, " ");

                return Some(text.into_owned());
            }
        }
        None
    }

    fn recurse_html_nodes_for_text(node: &Node, text: &mut String) {
        for n in node.get_child_nodes() {
            let node_type = n.get_type();

            if let Some(NodeType::TextNode) = node_type {
                write!(text, "{}", n.get_content()).unwrap();
            } else if let Some(NodeType::ElementNode) = node_type {
                let name = n.get_name();

                if ALT_TEXT_ELEMENTS.contains::<str>(&name) {
                    if let Some(alt_text) = n.get_property("alt") {
                        write!(text, "{}", alt_text).unwrap();
                    }
                }

                if !IGNORED_ELEMENTS.contains::<str>(&name) {
                    Self::recurse_html_nodes_for_text(&n, text);
                }

                if SPACING_ELEMENTS.contains::<str>(&name) {
                    write!(text, " ").unwrap();
                }

                if BREAKING_ELEMENTS.contains::<str>(&name) {
                    write!(text, "\n").unwrap();
                }
            }
        }
    }
}

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

    #[test]
    pub fn hardwareluxx() {
        let article = "<p><img src=\"https://www.hardwareluxx.de/images/stories/2017/stadia.jpg\" alt=\"stadia\">Am vergangenen Dienstag präsentierte Google im Rahmen der Game Developers Conference in San Francisco seinen neuen  <a href=\"https://www.hardwareluxx.de/index.php/news/software/spiele/48994-googles-cloud-gaming-plattform-stadia-geht-an-den-start.html\" rel=\"noopener noreferrer\" target=\"_blank\" referrerpolicy=\"no-referrer\">Spiele-Streaming-Dienst Stadia</a> , der noch im Sommer dieses Jahres an den Start gehen soll. Auch einen eigenen  <a href=\"https://www.hardwareluxx.de/index.php/news/hardware/eingabegeraete/49001-googles-stadia-controller-mit-zwei-sondertasten-zum-heimlichen-star.html\" rel=\"noopener noreferrer\" target=\"_blank\" referrerpolicy=\"no-referrer\">Controller mit vielen interessanten Features</a>  hatte der Konzern den anwesenden Journalisten gezeigt.</p><p>Die Vorteile von Stadia liegen klar auf der Hand: Die Hardware im Rechenzentrum ist dank skalierbarer Infrastruktur schneller als jede Heimkonsole und jeder Spiele-PC zu Hause und erlaubt damit theoretisch die höchste Bildqualität. Hinzu kommt, dass langwierige Downloads und Installations-Prozesse entfallen und teure Hardware für die Nutzung des Dienstes nicht benötigt wird. Ein leistungsschwaches Notebook oder gar ein herkömmliches Smartphone sollen laut Google genügen.</p>";
        let needle = "Am vergangenen Dienstag";

        let summary = Html2Text::process(&article).unwrap();

        assert_eq!(needle, &summary[..needle.len()]);
    }
}
