use stdweb::{
    unstable::{TryFrom, TryInto},
    web::{event::*, html_element::*, *},
};
use CATALOG;

macro_rules! mv {
    ( $( $var:ident ),* => $exp:expr ) => {
        {
            $( let $var = $var.clone(); )*
            $exp
        }
    }
}

fn get_elt_value(id: &'static str) -> String {
    let elt = document().get_element_by_id(id).unwrap();
    let inp: Result<InputElement, _> = elt.clone().try_into();
    let textarea: Result<TextAreaElement, _> = elt.try_into();
    inp.map(|i| i.raw_value())
        .unwrap_or_else(|_| textarea.unwrap().value())
}

fn set_value<S: AsRef<str>>(id: &'static str, val: S) {
    let elt = document().get_element_by_id(id).unwrap();
    let inp: Result<InputElement, _> = elt.clone().try_into();
    let textarea: Result<TextAreaElement, _> = elt.try_into();
    inp.map(|i| i.set_raw_value(val.as_ref()))
        .unwrap_or_else(|_| textarea.unwrap().set_value(val.as_ref()))
}

fn no_return(evt: KeyDownEvent) {
    if evt.key() == "Enter" {
        evt.prevent_default();
    }
}

#[derive(Debug)]
pub enum EditorError {
    NoneError,
    DOMError,
    TypeError,
}

impl From<std::option::NoneError> for EditorError {
    fn from(_: std::option::NoneError) -> Self {
        EditorError::NoneError
    }
}
impl From<stdweb::web::error::InvalidCharacterError> for EditorError {
    fn from(_: stdweb::web::error::InvalidCharacterError) -> Self {
        EditorError::DOMError
    }
}
impl From<stdweb::private::TODO> for EditorError {
    fn from(_: stdweb::private::TODO) -> Self {
        EditorError::DOMError
    }
}
impl From<stdweb::private::ConversionError> for EditorError {
    fn from(_: stdweb::private::ConversionError) -> Self {
        EditorError::TypeError
    }
}

fn init_widget(
    parent: &Element,
    tag: &'static str,
    placeholder_text: String,
    content: String,
    disable_return: bool,
) -> Result<HtmlElement, EditorError> {
    let widget = placeholder(make_editable(tag).try_into()?, &placeholder_text);
    if !content.is_empty() {
        widget.dataset().insert("edited", "true")?;
    }
    widget.append_child(&document().create_text_node(&content));
    if disable_return {
        widget.add_event_listener(no_return);
    }

    parent.append_child(&widget);
    // We need to do that to make sure the placeholder is correctly rendered
    widget.focus();
    widget.blur();

    filter_paste(&widget);

    Ok(widget)
}

fn filter_paste(elt: &HtmlElement) {
    // Only insert text when pasting something
    js! {
        @{&elt}.addEventListener("paste", function (evt) {
            evt.preventDefault();
            document.execCommand("insertText", false, evt.clipboardData.getData("text"));
        });
    };
}

pub fn init() -> Result<(), EditorError> {
    // Check if the user wants to use the basic editor
    if let Some(basic_editor) = window().local_storage().get("basic-editor") {
        if basic_editor == "true" {
            if let Some(editor) = document().get_element_by_id("plume-fallback-editor") {
                if let Ok(Some(title_label)) = document().query_selector("label[for=title]") {
                    let editor_button = document().create_element("a")?;
                    js! { @{&editor_button}.href = "#"; }
                    editor_button.add_event_listener(|_: ClickEvent| {
                        window().local_storage().remove("basic-editor");
                        window().history().go(0).ok(); // refresh
                    });
                    editor_button.append_child(
                        &document().create_text_node(&i18n!(CATALOG, "Open the rich text editor")),
                    );
                    editor.insert_before(&editor_button, &title_label).ok();
                    return Ok(());
                }
            }
        }
    }

    // If we didn't returned above
    init_editor()
}

fn init_editor() -> Result<(), EditorError> {
    if let Some(ed) = document().get_element_by_id("plume-editor") {
        // Show the editor
        js! { @{&ed}.style.display = "block"; };
        // And hide the HTML-only fallback
        let old_ed = document().get_element_by_id("plume-fallback-editor")?;
        let old_title = document().get_element_by_id("plume-editor-title")?;
        js! {
            @{&old_ed}.style.display = "none";
            @{&old_title}.style.display = "none";
        };

        // Get content from the old editor (when editing an article for instance)
        let title_val = get_elt_value("title");
        let subtitle_val = get_elt_value("subtitle");
        let content_val = get_elt_value("editor-content");
        // And pre-fill the new editor with this values
        let title = init_widget(&ed, "h1", i18n!(CATALOG, "Title"), title_val, true)?;
        let subtitle = init_widget(
            &ed,
            "h2",
            i18n!(CATALOG, "Subtitle, or summary"),
            subtitle_val,
            true,
        )?;
        let content = init_widget(
            &ed,
            "article",
            i18n!(CATALOG, "Write your article here. Markdown is supported."),
            content_val.clone(),
            false,
        )?;
        js! { @{&content}.innerHTML = @{content_val}; };

        // character counter
        content.add_event_listener(mv!(content => move |_: KeyDownEvent| {
            window().set_timeout(mv!(content => move || {
                if let Some(e) = document().get_element_by_id("char-count") {
                    let count = chars_left("#plume-fallback-editor", &content).unwrap_or_default();
                    let text = i18n!(CATALOG, "Around {} characters left"; count);
                    HtmlElement::try_from(e).map(|e| {
                        js!{@{e}.innerText = @{text}};
                    }).ok();
                };
            }), 0);
        }));

        document().get_element_by_id("publish")?.add_event_listener(
            mv!(title, subtitle, content, old_ed => move |_: ClickEvent| {
                let popup = document().get_element_by_id("publish-popup").or_else(||
                        init_popup(&title, &subtitle, &content, &old_ed).ok()
                    ).unwrap();
                let bg = document().get_element_by_id("popup-bg").or_else(||
                        init_popup_bg().ok()
                    ).unwrap();

                popup.class_list().add("show").unwrap();
                bg.class_list().add("show").unwrap();
            }),
        );

        show_errors();
        setup_close_button();
    }
    Ok(())
}

fn setup_close_button() {
    if let Some(button) = document().get_element_by_id("close-editor") {
        button.add_event_listener(|_: ClickEvent| {
            window()
                .local_storage()
                .insert("basic-editor", "true")
                .unwrap();
            window().history().go(0).unwrap(); // Refresh the page
        });
    }
}

fn show_errors() {
    if let Ok(Some(header)) = document().query_selector("header") {
        let list = document().create_element("header").unwrap();
        list.class_list().add("messages").unwrap();
        for error in document().query_selector_all("p.error").unwrap() {
            error
                .parent_element()
                .unwrap()
                .remove_child(&error)
                .unwrap();
            list.append_child(&error);
        }
        header
            .parent_element()
            .unwrap()
            .insert_before(&list, &header.next_sibling().unwrap())
            .unwrap();
    }
}

fn init_popup(
    title: &HtmlElement,
    subtitle: &HtmlElement,
    content: &HtmlElement,
    old_ed: &Element,
) -> Result<Element, EditorError> {
    let popup = document().create_element("div")?;
    popup.class_list().add("popup")?;
    popup.set_attribute("id", "publish-popup")?;

    let tags = get_elt_value("tags")
        .split(',')
        .map(str::trim)
        .map(str::to_string)
        .collect::<Vec<_>>();
    let license = get_elt_value("license");
    make_input(&i18n!(CATALOG, "Tags"), "popup-tags", &popup).set_raw_value(&tags.join(", "));
    make_input(&i18n!(CATALOG, "License"), "popup-license", &popup).set_raw_value(&license);

    let cover_label = document().create_element("label")?;
    cover_label.append_child(&document().create_text_node(&i18n!(CATALOG, "Cover")));
    cover_label.set_attribute("for", "cover")?;
    let cover = document().get_element_by_id("cover")?;
    cover.parent_element()?.remove_child(&cover).ok();
    popup.append_child(&cover_label);
    popup.append_child(&cover);

    if let Some(draft_checkbox) = document().get_element_by_id("draft") {
        let draft_label = document().create_element("label")?;
        draft_label.set_attribute("for", "popup-draft")?;

        let draft = document().create_element("input").unwrap();
        js! {
            @{&draft}.id = "popup-draft";
            @{&draft}.name = "popup-draft";
            @{&draft}.type = "checkbox";
            @{&draft}.checked = @{&draft_checkbox}.checked;
        };

        draft_label.append_child(&draft);
        draft_label.append_child(&document().create_text_node(&i18n!(CATALOG, "This is a draft")));
        popup.append_child(&draft_label);
    }

    let button = document().create_element("input")?;
    js! {
        @{&button}.type = "submit";
        @{&button}.value = @{i18n!(CATALOG, "Publish")};
    };
    button.append_child(&document().create_text_node(&i18n!(CATALOG, "Publish")));
    button.add_event_listener(
        mv!(title, subtitle, content, old_ed => move |_: ClickEvent| {
            title.focus(); // Remove the placeholder before publishing
            set_value("title", title.inner_text());
            subtitle.focus();
            set_value("subtitle", subtitle.inner_text());
            content.focus();
            set_value("editor-content", content.child_nodes().iter().fold(String::new(), |md, ch| {
                let to_append = match ch.node_type() {
                    NodeType::Element => {
                        if js!{ return @{&ch}.tagName; } == "DIV" {
                            (js!{ return @{&ch}.innerHTML; }).try_into().unwrap_or_default()
                        } else {
                            (js!{ return @{&ch}.outerHTML; }).try_into().unwrap_or_default()
                        }
                    },
                    NodeType::Text => ch.node_value().unwrap_or_default(),
                    _ => unreachable!(),
                };
                format!("{}\n\n{}", md, to_append)
            }));
            set_value("tags", get_elt_value("popup-tags"));
            if let Some(draft) = document().get_element_by_id("popup-draft") {
                js!{
                    document.getElementById("draft").checked = @{draft}.checked;
                };
            }
            let cover = document().get_element_by_id("cover").unwrap();
            cover.parent_element().unwrap().remove_child(&cover).ok();
            old_ed.append_child(&cover);
            set_value("license", get_elt_value("popup-license"));
            js! {
                @{&old_ed}.submit();
            };
        }),
    );
    popup.append_child(&button);

    document().body()?.append_child(&popup);
    Ok(popup)
}

fn init_popup_bg() -> Result<Element, EditorError> {
    let bg = document().create_element("div")?;
    bg.class_list().add("popup-bg")?;
    bg.set_attribute("id", "popup-bg")?;

    document().body()?.append_child(&bg);
    bg.add_event_listener(|_: ClickEvent| close_popup());
    Ok(bg)
}

fn chars_left(selector: &str, content: &HtmlElement) -> Option<i32> {
    match document().query_selector(selector) {
        Ok(Some(form)) => HtmlElement::try_from(form).ok().and_then(|form| {
            if let Some(len) = form
                .get_attribute("content-size")
                .and_then(|s| s.parse::<i32>().ok())
            {
                (js! {
                    let x = encodeURIComponent(@{content}.innerHTML)
                        .replace(/%20/g, "+")
                        .replace(/%0A/g, "%0D%0A")
                        .replace(new RegExp("[!'*()]", "g"), "XXX") // replace exceptions of encodeURIComponent with placeholder
                        .length + 2;
                    console.log(x);
                    return x;
                })
                .try_into()
                .map(|c: i32| len - c)
                .ok()
            } else {
                None
            }
        }),
        _ => None,
    }
}

fn close_popup() {
    let hide = |x: Element| x.class_list().remove("show");
    document().get_element_by_id("publish-popup").map(hide);
    document().get_element_by_id("popup-bg").map(hide);
}

fn make_input(label_text: &str, name: &'static str, form: &Element) -> InputElement {
    let label = document().create_element("label").unwrap();
    label.append_child(&document().create_text_node(label_text));
    label.set_attribute("for", name).unwrap();

    let inp: InputElement = document()
        .create_element("input")
        .unwrap()
        .try_into()
        .unwrap();
    inp.set_attribute("name", name).unwrap();
    inp.set_attribute("id", name).unwrap();

    form.append_child(&label);
    form.append_child(&inp);
    inp
}

fn make_editable(tag: &'static str) -> Element {
    let elt = document()
        .create_element(tag)
        .expect("Couldn't create editable element");
    elt.set_attribute("contenteditable", "true")
        .expect("Couldn't make the element editable");
    elt
}

fn placeholder(elt: HtmlElement, text: &str) -> HtmlElement {
    elt.dataset().insert("placeholder", text).unwrap();
    elt.dataset().insert("edited", "false").unwrap();

    elt.add_event_listener(mv!(elt => move |_: FocusEvent| {
        if elt.dataset().get("edited").unwrap().as_str() != "true" {
            clear_children(&elt);
        }
    }));
    elt.add_event_listener(mv!(elt => move |_: BlurEvent| {
        if elt.dataset().get("edited").unwrap().as_str() != "true" {
            clear_children(&elt);

            let ph = document().create_element("span").expect("Couldn't create placeholder");
            ph.class_list().add("placeholder").expect("Couldn't add class");
            ph.append_child(&document().create_text_node(&elt.dataset().get("placeholder").unwrap_or_default()));
            elt.append_child(&ph);
        }
    }));
    elt.add_event_listener(mv!(elt => move |_: KeyUpEvent| {
        elt.dataset().insert("edited", if elt.inner_text().trim_matches('\n').is_empty() {
            "false"
        } else {
            "true"
        }).expect("Couldn't update edition state");
    }));
    elt
}

fn clear_children(elt: &HtmlElement) {
    for child in elt.child_nodes() {
        elt.remove_child(&child).unwrap();
    }
}
