use std::fmt::Write;

use chrono::{DateTime, Utc};

use crate::{
    config::Config,
    dirs::Dirs,
    html::EscapeAttribute,
    sources::Sources,
    state::{FileId, Treehouse},
    tree::{
        attributes::{Content, Stage, Visibility},
        mini_template,
        pull::BranchKind,
        SemaBranchId,
    },
    vfs::{self, VPath, VPathBuf},
};

use super::{djot, EscapeHtml};

pub struct Renderer<'a> {
    pub sources: &'a Sources,
    pub dirs: &'a Dirs,
    pub file_id: FileId,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum HasChildren {
    No,
    Yes,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LinkButton {
    Tree,
    Branch,
}

struct OpenBranch {
    has_children: HasChildren,
}

impl Renderer<'_> {
    fn treehouse(&self) -> &Treehouse {
        &self.sources.treehouse
    }

    fn config(&self) -> &Config {
        &self.sources.config
    }

    fn open_branch(&self, s: &mut String, id: &str) {
        write!(s, "<li id=\"{}\"", EscapeAttribute(id)).unwrap();
    }

    fn attr(&self, s: &mut String, key: &'static str, value: &str) {
        write!(s, r#" {key}="{}""#, EscapeAttribute(value)).unwrap()
    }

    fn attr_class_begin(&self, s: &mut String, has_children: HasChildren) {
        write!(
            s,
            r#" class="{}"#,
            EscapeAttribute(match has_children {
                HasChildren::Yes => "branch",
                HasChildren::No => "leaf",
            })
        )
        .unwrap();
    }

    fn attr_class_push(&self, s: &mut String, class: &str) {
        write!(s, " {}", EscapeAttribute(class)).unwrap();
    }

    fn attr_class_end(&self, s: &mut String) {
        s.push('"');
    }

    fn attr_cast_begin(&self, s: &mut String) {
        s.push_str(r#" data-cast=""#);
    }

    fn attr_cast_push(&self, s: &mut String, spell: &str) {
        if s.as_bytes().last() != Some(&b'"') {
            s.push(' ');
        }
        write!(s, "{}", EscapeAttribute(spell)).unwrap();
    }

    fn attr_cast_end(&self, s: &mut String) {
        s.push('"');
    }

    fn attr_link(&self, s: &mut String, linked: &VPath) {
        self.attr(s, "th-link", linked.as_str());
    }

    fn attr_ts(&self, s: &mut String, timestamp: DateTime<Utc>) {
        self.attr(s, "th-ts", &timestamp.timestamp_millis().to_string())
    }

    fn attr_do_not_persist(&self, s: &mut String) {
        s.push_str(" th-do-not-persist");
    }

    fn end_attrs(&self, s: &mut String) {
        s.push('>');
    }

    fn begin_container(
        &self,
        s: &mut String,
        has_children: HasChildren,
        branch_kind: BranchKind,
    ) -> OpenBranch {
        match has_children {
            HasChildren::Yes => {
                s.push_str(match branch_kind {
                    BranchKind::Expanded => "<details open>",
                    BranchKind::Collapsed => "<details>",
                });
                s.push_str("<summary class=\"branch-container\">");
            }
            HasChildren::No => {
                s.push_str("<div class=\"branch-container\">");
            }
        }

        OpenBranch { has_children }
    }

    fn begin_children(&self, s: &mut String, open: &OpenBranch) -> HasChildren {
        if open.has_children == HasChildren::Yes {
            s.push_str("</summary>");
        }
        open.has_children
    }

    fn close_branch(&self, s: &mut String, open: OpenBranch) {
        match open.has_children {
            HasChildren::Yes => {
                s.push_str("</details>");
            }
            HasChildren::No => {
                s.push_str("</div>");
            }
        }
        s.push_str("</li>");
    }

    fn bullet_point(&self, s: &mut String) {
        s.push_str("<th-bp></th-bp>");
    }

    fn branch_content(&self, s: &mut String, markup: &str, linked: Option<&VPath>) {
        s.push_str("<th-bc>");

        let events: Vec<_> = jotdown::Parser::new(markup).into_offset_iter().collect();
        // TODO: Report rendering diagnostics.
        let render_diagnostics = djot::Renderer {
            page_id: self
                .treehouse()
                .tree_path(self.file_id)
                .expect(".tree file expected")
                .to_string(),

            config: self.config(),
            dirs: self.dirs,

            treehouse: self.treehouse(),
            file_id: self.file_id,
        }
        .render(&events, s);

        if let Some(linked) = linked {
            write!(
                s,
                "<noscript><a class=\"navigate icon-go\" href=\"{}/{}\">Go to linked tree: <code>{}</code></a></noscript>",
                EscapeAttribute(&self.config().site),
                EscapeAttribute(linked.as_str()),
                EscapeHtml(linked.as_str()),
            )
            .unwrap();
        }
        s.push_str("</th-bc>");
    }

    fn button_bar(
        &self,
        s: &mut String,
        date_time: Option<DateTime<Utc>>,
        link_button: LinkButton,
        link: &str,
    ) {
        s.push_str("<th-bb>");
        {
            if let Some(date_time) = date_time {
                write!(s, "<th-bd>{}</th-bd>", date_time.format("%F")).unwrap();
            }

            match link_button {
                LinkButton::Tree => {
                    write!(
                        s,
                        "<a class=\"icon icon-go\" href=\"{}\" title=\"linked tree\"></a>",
                        EscapeAttribute(link)
                    )
                    .unwrap();
                }
                LinkButton::Branch => {
                    write!(
                        s,
                        "<a th-p class=\"icon icon-permalink\" href=\"{}\" title=\"permalink\"></a>",
                        EscapeAttribute(link)
                    )
                    .unwrap();
                }
            }
        }
        s.push_str("</th-bb>");
    }

    fn branch_children_empty(&self, s: &mut String) {
        s.push_str("<ul></ul>");
    }

    fn branch_children(&self, s: &mut String, branch_id: SemaBranchId) {
        let branch = self.treehouse().tree.branch(branch_id);

        s.push_str("<ul");
        if !branch.attributes.classes.branch_children.is_empty() {
            write!(
                s,
                " class=\"{}\"",
                EscapeAttribute(&branch.attributes.classes.branch_children)
            )
            .unwrap();
        }
        s.push('>');
        let num_children = branch.children.len();
        for i in 0..num_children {
            let child_id = self.treehouse().tree.branch(branch_id).children[i];
            self.branch(s, child_id);
        }
        s.push_str("</ul>");
    }

    fn preprocess_markup(&self, branch_id: SemaBranchId) -> String {
        let branch = self.treehouse().tree.branch(branch_id);

        let raw_block_content =
            &self.treehouse().source(self.file_id).input()[branch.content.clone()];
        let mut markup = String::with_capacity(raw_block_content.len());
        for line in raw_block_content.lines() {
            // Bit of a jank way to remove at most branch.indent_level spaces from the front.
            let mut space_count = 0;
            for i in 0..branch.indent_level {
                if line.as_bytes().get(i).copied() == Some(b' ') {
                    space_count += 1;
                } else {
                    break;
                }
            }

            markup.push_str(&line[space_count..]);
            markup.push('\n');
        }

        if branch.attributes.template {
            markup = mini_template::render(self.config(), self.treehouse(), self.dirs, &markup);
        }

        markup
    }

    pub fn branch(&self, s: &mut String, branch_id: SemaBranchId) {
        let branch = self.treehouse().tree.branch(branch_id);

        if !cfg!(debug_assertions) && branch.attributes.stage == Stage::Draft {
            return;
        }

        let has_children = match !branch.children.is_empty()
            || matches!(branch.attributes.content, Content::ResolvedLink(_))
        {
            true => HasChildren::Yes,
            false => HasChildren::No,
        };

        let linked_tree = match branch.attributes.content {
            Content::Inline | Content::Link(_) => None,
            Content::ResolvedLink(file_id) => self.treehouse().tree_path(file_id),
        };

        self.open_branch(s, &branch.html_id);
        {
            // data-cast
            self.attr_cast_begin(s);
            self.attr_cast_push(
                s,
                match linked_tree {
                    Some(_) => "b-linked",
                    None => "b",
                },
            );
            if !branch.attributes.cast.is_empty() {
                self.attr_cast_push(s, &branch.attributes.cast);
            }
            self.attr_cast_end(s);

            // th-link
            if let Some(tree_path) = linked_tree {
                self.attr_link(s, tree_path);
            }

            // class
            self.attr_class_begin(s, has_children);
            if !branch.attributes.classes.branch.is_empty() {
                self.attr_class_push(s, &branch.attributes.classes.branch);
            }
            if branch.attributes.stage == Stage::Draft {
                self.attr_class_push(s, "draft");
            }
            self.attr_class_end(s);

            // th-do-not-persist
            if branch.attributes.do_not_persist {
                self.attr_do_not_persist(s);
            }
        }
        self.end_attrs(s);

        let open = self.begin_container(s, has_children, branch.kind);
        {
            self.bullet_point(s);

            self.branch_content(s, &self.preprocess_markup(branch_id), linked_tree);

            let date_time = branch.attributes.timestamp();
            let link_button = match linked_tree {
                Some(_) => LinkButton::Tree,
                None => LinkButton::Branch,
            };
            let link = match linked_tree {
                Some(tree_path) => format!("{}/{}", self.config().site, tree_path),
                None => format!("{}/b?{}", self.config().site, &branch.named_id),
            };
            self.button_bar(s, date_time, link_button, &link);

            if self.begin_children(s, &open) == HasChildren::Yes {
                self.branch_children(s, branch_id);
            }
        }
        self.close_branch(s, open);
    }

    pub fn root(&self, s: &mut String) {
        let roots = self
            .treehouse()
            .roots
            .get(&self.file_id)
            .expect("tree should have been added to the treehouse");

        s.push_str("<ul>");
        for &child in &roots.branches {
            self.branch(s, child);
        }

        let path = self.treehouse().path(self.file_id);
        let children_path = if path == const { VPath::new_const("index.tree") } {
            VPath::ROOT
        } else {
            path
        };
        let tree_path = children_path.with_extension("");

        let child_pages = self.get_child_pages(&tree_path);
        if !child_pages.is_empty() {
            s.push_str(r#"<li class="child-pages">"#);
            s.push_str("<ul>");
            for child_page in &child_pages {
                self.open_branch(s, &format!("p-{}", child_page.tree_path));
                {
                    self.attr_cast_begin(s);
                    self.attr_cast_push(s, "b-linked");
                    self.attr_cast_end(s);

                    self.attr_link(s, &child_page.tree_path);

                    self.attr_class_begin(s, HasChildren::Yes);
                    self.attr_class_end(s);

                    if let Some(timestamp) = child_page.timestamp {
                        self.attr_ts(s, timestamp);
                    }
                }
                self.end_attrs(s);

                let open = self.begin_container(s, HasChildren::Yes, BranchKind::Collapsed);
                {
                    self.bullet_point(s);
                    self.branch_content(
                        s,
                        &format!(":{}: {}", child_page.icon, child_page.title),
                        Some(&child_page.tree_path),
                    );
                    self.button_bar(
                        s,
                        child_page.timestamp,
                        LinkButton::Tree,
                        &format!("{}/{}", self.config().site, child_page.tree_path),
                    );
                    self.begin_children(s, &open);
                    self.branch_children_empty(s);
                }
                self.close_branch(s, open);
            }
            s.push_str("</ul>");
            s.push_str("</li>");
        }

        s.push_str("</ul>");
    }
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct ChildPage {
    timestamp: Option<DateTime<Utc>>,
    title: String,
    icon: String,
    tree_path: VPathBuf,
}

impl Renderer<'_> {
    fn get_child_pages(&self, parent_page: &VPath) -> Vec<ChildPage> {
        let mut child_pages = vfs::entries(&self.dirs.content, parent_page);
        child_pages.retain(|path| matches!(path.extension(), Some("tree")));
        for child_page in &mut child_pages {
            child_page.set_extension("");
        }
        child_pages.sort();
        child_pages.dedup();

        let mut child_pages: Vec<_> = child_pages
            .into_iter()
            .filter_map(|tree_path| {
                self.treehouse()
                    .files_by_tree_path
                    .get(&tree_path)
                    .and_then(|file_id| {
                        let roots = &self.treehouse().roots[file_id];
                        let visible = roots.attributes.visibility == Visibility::Public;
                        visible.then(|| ChildPage {
                            tree_path,
                            title: roots.attributes.title.clone(),
                            icon: roots.attributes.icon.clone(),
                            timestamp: roots.attributes.timestamps.as_ref().map(|t| t.updated),
                        })
                    })
            })
            .collect();
        child_pages.sort_by(|a, b| b.cmp(a));
        child_pages
    }
}
