use raui_core::prelude::*;
use std::{
    collections::HashMap,
    fmt::{Error, Write},
};

#[allow(unused_macros)]
macro_rules! table {
    () => (HashMap::new());
    { $( $key:expr => $value:expr ),* } => {
        {
            let mut result = HashMap::new();
            $(
                result.insert($key.to_string(), $value.to_string());
            )*
            result
        }
    };
}

#[allow(unused_macros)]
macro_rules! node {
    (
        $context:ident : $tag:ident
        [ $writer:expr ]
        $( level = { $level:expr } )?
        $( styles = { $( $style_key:expr => $style_value:expr ),* } )?
        $( attribs = { $( $attrib_key:expr => $attrib_value:expr ),* } )?
        ($writer_name:ident, $level_name:ident)
    ) => {
        #[allow(unused_variables)]
        #[allow(unused_mut)]
        #[allow(unused_assignments)]
        {
            let mut level = 0;
            $(
                level = $level;
            )?
            let mut styles = Styles::new();
            $(
                styles = table!{ $( $style_key => $style_value ),* };
            )?
            let mut attribs = Attribs::new();
            $(
                attribs = table!{ $( $attrib_key => $attrib_value ),* };
            )?
            $context.inline_node(
                &stringify!($tag),
                &styles,
                &attribs,
                $writer,
                level,
            )?;
        }
    };
    (
        $context:ident : $tag:ident
        [ $writer:expr ]
        $( level = { $level:expr } )?
        $( styles = { $( $style_key:expr => $style_value:expr ),* } )?
        $( attribs = { $( $attrib_key:expr => $attrib_value:expr ),* } )?
        $code:block
        ($writer_name:ident, $level_name:ident)
    ) => {
        #[allow(unused_variables)]
        #[allow(unused_mut)]
        #[allow(unused_assignments)]
        {
            let mut level = 0;
            $(
                level = $level;
            )?
            let mut styles = Styles::new();
            $(
                styles = table!{ $( $style_key => $style_value ),* };
            )?
            let mut attribs = Attribs::new();
            $(
                attribs = table!{ $( $attrib_key => $attrib_value ),* };
            )?
            $context.with_node(
                &stringify!($tag),
                &styles,
                &attribs,
                $writer,
                level,
                |$writer_name, $level_name| {
                    $code
                    Ok(())
                }
            )?;
        }
    };
}

type Styles = HashMap<String, String>;
type Attribs = HashMap<String, String>;

#[derive(Debug, Clone)]
pub struct HtmlRenderer {
    pub indent: usize,
    pub title: Option<String>,
}

impl Default for HtmlRenderer {
    fn default() -> Self {
        Self {
            indent: 2,
            title: None,
        }
    }
}

impl Renderer<String, Error> for HtmlRenderer {
    fn render(
        &mut self,
        tree: &WidgetUnit,
        _: &CoordsMapping,
        _layout: &Layout,
    ) -> Result<String, Error> {
        let mut result = String::new();
        self.write_document(&mut result, tree)?;
        Ok(result)
    }
}

#[allow(dead_code)]
impl HtmlRenderer {
    fn write_line<W>(&self, line: &str, writer: &mut W, level: usize) -> Result<(), Error>
    where
        W: Write,
    {
        for _ in 0..(level * self.indent) {
            writer.write_char(' ')?;
        }
        writer.write_str(line)?;
        writer.write_char('\n')?;
        Ok(())
    }

    fn inline_node<W>(
        &self,
        name: &str,
        styles: &Styles,
        attribs: &Attribs,
        writer: &mut W,
        level: usize,
    ) -> Result<(), Error>
    where
        W: Write,
    {
        let styles = Self::stringify_styles_attr(styles)?;
        let attribs = Self::stringify_attribs(attribs);
        self.write_line(
            &format!(r#"<{} {} {}/>"#, name, styles, attribs),
            writer,
            level,
        )
    }

    fn with_node<W, F>(
        &self,
        name: &str,
        styles: &Styles,
        attribs: &Attribs,
        writer: &mut W,
        level: usize,
        mut f: F,
    ) -> Result<(), Error>
    where
        W: Write,
        F: FnMut(&mut W, usize) -> Result<(), Error>,
    {
        self.start_node(name, styles, attribs, writer, level)?;
        f(writer, level + 1)?;
        self.stop_node(name, writer, level)
    }

    fn start_node<W>(
        &self,
        name: &str,
        styles: &Styles,
        attribs: &Attribs,
        writer: &mut W,
        level: usize,
    ) -> Result<(), Error>
    where
        W: Write,
    {
        let styles = Self::stringify_styles_attr(styles)?;
        let attribs = Self::stringify_attribs(attribs);
        self.write_line(
            &format!(r#"<{} {} {}>"#, name, styles, attribs),
            writer,
            level,
        )
    }

    fn stop_node<W>(&self, name: &str, writer: &mut W, level: usize) -> Result<(), Error>
    where
        W: Write,
    {
        self.write_line(&format!(r#"</{}>"#, name), writer, level)
    }

    fn stringify_styles_attr(styles: &Styles) -> Result<String, Error> {
        let mut result = String::new();
        if !styles.is_empty() {
            result.write_str(r#"style=""#)?;
            for (key, value) in styles {
                result.write_str(key)?;
                result.write_str(": ")?;
                result.write_str(value)?;
                result.write_char(';')?;
            }
            result.write_str(r#"""#)?;
        }
        Ok(result)
    }

    fn stringify_attribs(attribs: &Attribs) -> String {
        attribs
            .iter()
            .map(|(k, v)| format!(r#"{}="{}""#, k, v))
            .collect::<Vec<String>>()
            .join(" ")
    }

    fn write_document<W>(&self, writer: &mut W, tree: &WidgetUnit) -> Result<(), Error>
    where
        W: Write,
    {
        self.write_line(r#"<!DOCTYPE html>"#, writer, 0)?;
        node!(self: html [writer] attribs={"lang" => "en", "dir" => "ltr"} {
            node!(self: head [writer] level={level} {
                self.write_line(r#"<meta charset="utf-8">"#, writer, level)?;
                if let Some(title) = &self.title {
                    self.write_line(&format!(r#"<title>{}</title>"#, title), writer, level)?;
                }
            } (writer, level));
            node!(self: body [writer] level={level} {
                self.write_node(writer, tree, level)?;
            } (writer, level));
        } (writer, level));
        Ok(())
    }

    fn write_node<W>(&self, writer: &mut W, tree: &WidgetUnit, level: usize) -> Result<(), Error>
    where
        W: Write,
    {
        self.write_node_with_styles_and_attribs(writer, tree, level, Styles::new(), Attribs::new())
    }

    fn write_node_with_styles_and_attribs<W>(
        &self,
        writer: &mut W,
        tree: &WidgetUnit,
        level: usize,
        _styles: Styles,
        _attribs: Attribs,
    ) -> Result<(), Error>
    where
        W: Write,
    {
        match tree {
            WidgetUnit::None => {}
            WidgetUnit::AreaBox(AreaBox { slot, .. }) => {
                node!(self: div [writer] level={level} {
                    self.write_node(writer, slot, level)?;
                } (writer, level));
            }
            WidgetUnit::ContentBox(ContentBox { items, .. }) => {
                node!(self: div [writer] level={level} {
                    for item in items {
                        self.write_node(writer, &item.slot, level)?;
                    }
                } (writer, level));
            }
            WidgetUnit::FlexBox(FlexBox { items, .. }) => {
                node!(self: div [writer] level={level} {
                    for item in items {
                        self.write_node(writer, &item.slot, level)?;
                    }
                } (writer, level));
            }
            WidgetUnit::GridBox(GridBox { items, .. }) => {
                node!(self: div [writer] level={level} {
                    for item in items {
                        self.write_node(writer, &item.slot, level)?;
                    }
                } (writer, level));
            }
            WidgetUnit::SizeBox(SizeBox { slot, .. }) => {
                node!(self: div [writer] level={level} {
                    self.write_node(writer, slot, level)?;
                } (writer, level));
            }
            WidgetUnit::ImageBox(ImageBox { .. }) => {
                node!(self: div [writer] level={level} {
                } (writer, level));
            }
            WidgetUnit::TextBox(TextBox { text, .. }) => {
                node!(self: span [writer] level={level} {
                    self.write_line(text, writer, level)?;
                } (writer, level));
            }
        }
        Ok(())
    }
}
