use std::cmp::Ordering;
use std::collections::HashMap;
use std::fmt::Display;

use anyhow::anyhow;
use anyhow::bail;
use anyhow::Context;
use chrono::DateTime;
use chrono::NaiveDate;
use chrono::TimeZone;
use jotdown::Alignment;
use jotdown::AttributeValue;
use jotdown::Container;
use jotdown::Event;
use jotdown::LinkType;
use jotdown::ListKind;
use jotdown::OrderedListNumbering::*;
use jotdown::SpanLinkType;
use tree_sitter::Language;
use tree_sitter_highlight::Highlight;
use tree_sitter_highlight::HighlightConfiguration;
use tree_sitter_highlight::HighlightEvent;
use tree_sitter_highlight::Highlighter;

use crate::metadata::has_role;
use crate::metadata::PageMetadata;
use crate::metadata::PageRoute;
use crate::template;
use crate::template::TemplateRenderer;

pub struct Writer<'s> {
	pub states: Vec<State<'s>>,
	list_tightness: Vec<bool>,
	footnotes: Footnotes<'s>,
	metadata: &'s PageMetadata<'s>,
	pages: &'s [PageMetadata<'s>],
	base_url: &'s str,
}

#[derive(Debug, Clone)]
pub enum State<'s> {
	TextOnly,
	Ignore,
	Raw,
	Figure,
	Math(bool),
	CodeBlock(String),
	Aside(TemplateRenderer<'s>),
	Article(TemplateRenderer<'s>),
	Footnote(Vec<jotdown::Event<'s>>),
	Datetime(String),
	Date(String),
}

impl<'s> Writer<'s> {
	pub fn new(metadata: &'s PageMetadata, pages: &'s [PageMetadata], base_url: &'s str) -> Self {
		Self {
			list_tightness: Vec::new(),
			states: Vec::new(),
			footnotes: Footnotes::default(),
			metadata,
			pages,
			base_url,
		}
	}

	#[allow(clippy::single_match)]
	pub fn render_event(
		&mut self,
		e: &Event<'s>,
		out: &mut impl std::fmt::Write,
	) -> anyhow::Result<()> {
		// {{{ Handle "footnote" states
		if matches!(self.states.last(), Some(State::Footnote(_))) {
			if let Event::End(Container::Footnote { label }) = e {
				let Some(State::Footnote(events)) = self.states.pop() else {
					unreachable!()
				};

				self.footnotes.insert(label, events);
			} else {
				let Some(State::Footnote(events)) = self.states.last_mut() else {
					unreachable!()
				};

				events.push(e.clone());
				return Ok(());
			}
		}
		// }}}
		// {{{ Handle "text-only" states
		if matches!(self.states.last(), Some(State::TextOnly)) {
			match e {
				Event::End(Container::Image(..)) => {
					self.states.pop();
				}
				Event::Str(s) => {
					write!(out, "{}", Escaped(s))?;
					return Ok(());
				}
				_ => return Ok(()),
			}
		}
		// }}}
		// {{{ Handle "ignore" states
		if matches!(self.states.last(), Some(State::Ignore)) {
			match e {
				Event::End(
					Container::RawBlock { .. }
					| Container::RawInline { .. }
					| Container::LinkDefinition { .. }
					| Container::Div { .. },
				) => {
					self.states.pop();
					return Ok(());
				}
				_ => return Ok(()),
			}
		}
		// }}}

		match e {
			// {{{ Container start
			Event::Start(c, attrs) => {
				match &c {
					// {{{ Section
					Container::Section { id } => {
						if self.metadata.title.id == *id {
							if matches!(self.metadata.route, PageRoute::Post(_)) {
								let mut renderer = template!("templates/post.html", out)?;

								assert_eq!(renderer.current(), Some("attrs"));
								write!(out, "{}", Attr("aria-labelledby", id))?;
								renderer.next(out)?;

								self.states.push(State::Article(renderer));
							}
						} else {
							write!(out, "<section {}>", Attr("aria-labelledby", id))?;
						}
					}
					// }}}
					// {{{ Aside
					Container::Div {
						class: class @ ("aside" | "long-aside" | "char-aside"),
					} => {
						let mut renderer = if *class == "aside" {
							template!("templates/aside.html", out)?
						} else if *class == "char-aside" {
							template!("templates/character-aside.html", out)?
						} else {
							template!("templates/long-aside.html", out)?
						};

						while let Some(label) = renderer.current() {
							match label {
								"id" => {
									let id = attrs.get_value("id").ok_or_else(|| {
										anyhow!("Cannot find `id` attribute on `aside` element")
									})?;

									write_attribute(out, &id)?;
								}
								"character" => {
									let character =
										attrs.get_value("character").ok_or_else(|| {
											anyhow!("Cannot find `character` attribute on `aside` element")
										})?;

									write_attribute(out, &character)?;
								}
								"title" => {
									let title = attrs.get_value("title").ok_or_else(|| {
										anyhow!("Cannot find `title` attribute on `aside` element")
									})?;

									write_attribute(out, &title)?;
								}
								_ => break,
							}
							renderer.next(out)?;
						}

						self.states.push(State::Aside(renderer));
					}
					// }}}
					// {{{ List
					Container::List { kind, tight } => {
						self.list_tightness.push(*tight);
						match kind {
							ListKind::Unordered(..) => out.write_str("<ul>")?,
							ListKind::Ordered {
								numbering, start, ..
							} => {
								out.write_str("<ol")?;
								if *start > 1 {
									write!(out, r#" start="{}""#, start)?;
								}

								if let Some(ty) = match numbering {
									Decimal => None,
									AlphaLower => Some('a'),
									AlphaUpper => Some('A'),
									RomanLower => Some('i'),
									RomanUpper => Some('I'),
								} {
									write!(out, r#" type="{}""#, ty)?;
								}

								write!(out, ">")?;
							}
							ListKind::Task(_) => bail!("Task lists are not supported"),
						}
					}
					// }}}
					// {{{ Link
					Container::Link(dst, ty) => {
						if matches!(ty, LinkType::Span(SpanLinkType::Unresolved)) {
							bail!("Unresolved url {dst:?}")
						} else {
							let prefix = if matches!(ty, LinkType::Email) {
								"mailto:"
							} else {
								""
							};

							write!(out, r#"<a href="{prefix}{}">"#, Escaped(dst))?;
						}
					}
					// }}}
					// {{{ Table cell
					Container::TableCell {
						head, alignment, ..
					} => {
						if *head {
							out.write_str("<td")?;
						} else {
							out.write_str("<th")?;
						}

						if !matches!(alignment, Alignment::Unspecified) {
							// TODO: move this to css
							let a = match alignment {
								Alignment::Unspecified => unreachable!(),
								Alignment::Left => "left",
								Alignment::Center => "center",
								Alignment::Right => "right",
							};

							write!(out, r#" style="text-align: {};""#, a)?;
						}

						write!(out, ">")?;
					}
					// }}}
					// {{{ Heading
					Container::Heading { level, id, .. } => {
						write!(
							out,
							r##"<h{level} id="{}"><a class="heading-anchor" href="#{}">◇</a> "##,
							Escaped(id),
							Escaped(id)
						)?;
					}
					// }}}
					// {{{ Paragraph
					Container::Paragraph => {
						if self.list_tightness.last() == Some(&true) {
							return Ok(());
						}

						out.write_str("<p>")?;
					}
					// }}}
					// {{{ Post list
					Container::Div { class: "posts" } => {
						write!(out, r#"<ol class="article-list">"#)?;
						for post in self.pages {
							// Skip non-posts
							if !matches!(post.route, PageRoute::Post(_)) {
								continue;
							}

							// Skip hidden pages
							if post.config.hidden {
								continue;
							}

							// Skip drafts
							if std::env::var("MOONYTHM_DRAFTS").unwrap_or_default() != "1"
								&& post.config.created_at.is_none()
							{
								continue;
							}

							template!("templates/post-summary.html", out)?.feed(
								out,
								|label, out| {
									match label {
										"id" => {
											if let PageRoute::Post(id) = &post.route {
												write!(out, "{id}")?;
											} else {
												unreachable!()
											}
										}
										"title" => {
											for event in &post.title.events {
												self.render_event(event, out)?;
											}
										}
										"description" => {
											for event in &post.description {
												self.render_event(event, out)?;
											}
										}
										_ => {
											if !Self::write_metadata_label(
												out,
												label,
												post,
												self.base_url,
											)? {
												bail!("Unknown label {label} in `post-summary` template");
											};
										}
									}

									Ok(true)
								},
							)?;
						}
						write!(out, "</ol>")?;
						// We don't care about the contents of this block
						self.states.push(State::Ignore);
					}
					// }}}
					// {{{ Table of contents
					// This component renders out a nice tree of all the headings in the article.
					Container::Div { class: "toc" } => {
						template!("templates/table-of-contents.html", out)?.feed_fully(
							out,
							|label, out| {
								if label == "content" {
									// Sometimes we can have TOCs that look like this:
									// # foo
									//   ## bar
									//     ### goo
									// # help
									//
									// In this case, we need to close two different sublists when
									// going from ### goo to # help. To achieve this, we use this
									// vec as a stack of all the different levels we are yet to
									// close out.
									//
									// Note that the list for the initial level is included in the
									// template, as it would never get opened/closed out otherwise
									// (there can be no level smaller than 1).
									let mut level_stack = Vec::with_capacity(6);
									level_stack.push(2);

									for (i, heading) in self.metadata.toc.iter().enumerate() {
										// We exclude the article title from the table of contents.
										if heading.level == 1 {
											continue;
										}

										write!(out, r##"<li><a href="#{}">"##, heading.id)?;
										for event in &heading.events {
											self.render_event(event, out)?;
										}

										// We only close the <a> here, as we might want to include a
										// sublist inside the same <li> element.
										write!(out, "</a>")?;

										let next_level =
											self.metadata.toc.get(i + 1).map_or(2, |h| h.level);

										match heading.level.cmp(&next_level) {
											Ordering::Equal => {
												write!(out, "</li>")?;
											}
											Ordering::Less => {
												write!(out, "<ol>")?;
												level_stack.push(next_level);
											}
											Ordering::Greater => {
												while level_stack.last().unwrap() > &next_level {
													write!(out, "</ol></li>")?;
													level_stack.pop();
												}
											}
										}
									}

									Ok(true)
								} else {
									Ok(false)
								}
							},
						)?;

						// We don't care about the contents of this block
						self.states.push(State::Ignore);
					}
					// }}}
					// {{{ Figure
					Container::Div { class: "figure" } => {
						self.states.push(State::Figure);
						let alt = attrs.get_value("alt").ok_or_else(|| {
							anyhow!("Figure element encountered without an `alt` attribute")
						})?;
						let src = attrs.get_value("src").ok_or_else(|| {
							anyhow!("Figure element encountered without a `src` attribute")
						})?;

						write!(out, r#"<figure><img alt=""#)?;
						write_attribute(out, &alt)?;
						write!(out, r#"" src=""#)?;
						write_attribute(out, &src)?;
						write!(out, r#""><figcaption>"#)?;
					}
					// }}}
					// {{{ Div
					Container::Div { class } => {
						if has_role(attrs, "description") {
							self.states.push(State::Ignore);
						} else {
							write!(out, "<div{}>", Attr("class", class))?;
						}
					}
					// }}}
					// {{{ Raw block
					Container::RawBlock { format } | Container::RawInline { format } => {
						if format == &"html" {
							self.states.push(State::Raw);
						} else {
							self.states.push(State::Ignore);
						};
					}
					// }}}
					Container::CodeBlock { .. } => {
						self.states.push(State::CodeBlock(String::new()));
						out.write_str("<pre><code>")?;
					}
					Container::Math { display } => {
						self.states.push(State::Math(*display));
					}
					Container::Image(_, _) => {
						self.states.push(State::TextOnly);
						out.write_str(r#"<img alt=""#)?;
					}
					Container::Footnote { .. } => self.states.push(State::Footnote(Vec::new())),
					Container::LinkDefinition { .. } => self.states.push(State::Ignore),
					Container::Blockquote => out.write_str("<blockquote>")?,
					Container::ListItem { .. } => out.write_str("<li>")?,
					Container::DescriptionList => out.write_str("<dl>")?,
					Container::DescriptionDetails => out.write_str("<dd>")?,
					Container::Table => out.write_str("<table>")?,
					Container::TableRow { .. } => out.write_str("<tr>")?,
					Container::Caption => out.write_str("<caption>")?,
					Container::DescriptionTerm => out.write_str("<dt>")?,
					Container::Span => {
						if has_role(attrs, "datetime") {
							self.states.push(State::Datetime(String::new()))
						} else if has_role(attrs, "date") {
							self.states.push(State::Date(String::new()))
						} else {
							out.write_str("<span>")?
						}
					}
					Container::Verbatim => out.write_str("<code>")?,
					Container::Subscript => out.write_str("<sub>")?,
					Container::Superscript => out.write_str("<sup>")?,
					Container::Insert => out.write_str("<ins>")?,
					Container::Delete => out.write_str("<del>")?,
					Container::Strong => out.write_str("<strong>")?,
					Container::Emphasis => out.write_str("<em>")?,
					Container::Mark => out.write_str("<mark>")?,
					e => bail!("DJot element {e:?} is not supported"),
				}
			}
			// }}}
			// {{{ Container end
			Event::End(c) => {
				match c {
					Container::Footnote { .. } => unreachable!(),
					// {{{ Raw block
					Container::RawBlock { .. } | Container::RawInline { .. } => {
						// Sanity check
						assert!(matches!(self.states.last(), Some(State::Raw)));

						self.states.pop();
					}
					// }}}
					// {{{ List
					Container::List { kind, .. } => {
						self.list_tightness.pop();
						match kind {
							ListKind::Unordered(..) => out.write_str("</ul>")?,
							ListKind::Ordered { .. } => out.write_str("</ol>")?,
							// We error out when the task list begins
							ListKind::Task(..) => unreachable!(),
						}
					}
					// }}}
					// {{{ Paragraph
					Container::Paragraph => {
						if matches!(self.list_tightness.last(), Some(true)) {
							return Ok(());
						}

						out.write_str("</p>")?;
					}
					// }}}
					// {{{ Math
					Container::Math { .. } => {
						// Sanity check
						assert!(matches!(self.states.last(), Some(State::Math(_))));
						self.states.pop();
					}
					// }}}
					// {{{ Section
					Container::Section { id, .. } => {
						if self.metadata.title.id == *id {
							if matches!(self.states.last(), Some(State::Article(_))) {
								let Some(State::Article(renderer)) = self.states.pop() else {
									unreachable!()
								};

								renderer.finish(out)?;
							}
						} else {
							out.write_str("</section>")?
						}
					}
					// }}}
					// {{{ Aside
					Container::Div {
						class: "aside" | "long-aside" | "char-aside",
					} => {
						let state = self.states.pop().unwrap();
						let State::Aside(renderer) = state else {
							panic!("Finished `aside` element without being in the `Aside` state.")
						};

						renderer.finish(out)?;
					}
					// }}}
					// {{{ Figure
					Container::Div { class: "figure" } => {
						let State::Figure = self.states.pop().unwrap() else {
							panic!(
								"Arrived at end of figure without being in the approriate state."
							);
						};

						write!(out, "</figcaption></figure>")?;
					}
					// }}}
					Container::Heading { level, .. } => {
						write!(out, "</h{}>", level)?;

						// {{{ Article title
						if let Some(State::Article(renderer)) = self.states.last_mut() {
							if renderer.current() == Some("title") {
								while let Some(label) = renderer.next(out)? {
									if !Self::write_metadata_label(
										out,
										label,
										self.metadata,
										self.base_url,
									)? {
										break;
									}
								}
							}
						}
						// }}}
					}
					Container::Image(src, ..) => {
						write!(out, r#"" {}>"#, Attr("src", src))?;
					}
					Container::Blockquote => out.write_str("</blockquote>")?,
					Container::ListItem { .. } => out.write_str("</li>")?,
					Container::DescriptionList => out.write_str("</dl>")?,
					Container::DescriptionDetails => out.write_str("</dd>")?,
					Container::Table => out.write_str("</table>")?,
					Container::TableRow { .. } => out.write_str("</tr>")?,
					Container::Div { .. } => out.write_str("</div>")?,
					Container::TableCell { head: false, .. } => out.write_str("</td>")?,
					Container::TableCell { head: true, .. } => out.write_str("</th>")?,
					Container::Caption => out.write_str("</caption>")?,
					Container::DescriptionTerm => out.write_str("</dt>")?,
					// {{{ Syntax highlighting
					Container::CodeBlock { language } => {
						let Some(State::CodeBlock(buffer)) = self.states.pop() else {
							panic!("Arrived at end of code block without being in the approriate state.");
						};

						let grammar = match *language {
							"rust" => Some((
								"rust",
								Language::new(tree_sitter_rust::LANGUAGE),
								tree_sitter_rust::HIGHLIGHTS_QUERY,
								tree_sitter_rust::INJECTIONS_QUERY,
							)),
							"djot" => Some((
								"dj",
								tree_sitter_djot::language(),
								tree_sitter_djot::HIGHLIGHTS_QUERY,
								tree_sitter_djot::INJECTIONS_QUERY,
							)),
							"html" => Some((
								"html",
								Language::new(tree_sitter_html::LANGUAGE),
								tree_sitter_html::HIGHLIGHTS_QUERY,
								tree_sitter_html::INJECTIONS_QUERY,
							)),
							// "tex" => Some((
							// 	"tex",
							// 	crate::bindings::tree_sitter_latex::language(),
							// 	"",
							// 	"",
							// )),
							_ => None,
						};

						if let Some((ft, ts_language, highlights, injections)) = grammar {
							let mut highlighter = Highlighter::new();

							let mut config = HighlightConfiguration::new(
								ts_language,
								ft,
								highlights,
								injections,
								"",
							)?;

							let highlight_names = [
								"attribute",
								"comment",
								"comment.documentation",
								"constant",
								"constant.builtin",
								"constructor",
								"function",
								"function.builtin",
								"function.macro",
								"function.method",
								"keyword",
								"label",
								"operator",
								"property",
								"punctuation",
								"punctuation.bracket",
								"punctuation.delimiter",
								"string",
								"string.special",
								"tag",
								"type",
								"type.builtin",
								"variable",
								"variable.builtin",
								"variable.parameter",
							];

							let highlight_classes = highlight_names
								.iter()
								.map(|s| s.replace(".", "-"))
								.collect::<Vec<_>>();

							config.configure(&highlight_names);

							let highlights =
								highlighter
									.highlight(&config, buffer.as_bytes(), None, |_| None)?;

							for event in highlights {
								match event? {
									HighlightEvent::Source { start, end } => {
										write!(out, "{}", Escaped(&buffer[start..end]))?;
									}
									HighlightEvent::HighlightStart(Highlight(index)) => {
										write!(
											out,
											r#"<span class="{}">"#,
											highlight_classes[index]
										)?;
									}
									HighlightEvent::HighlightEnd => {
										write!(out, r#"</span>"#)?;
									}
								}
							}
						} else {
							write!(out, "{}", Escaped(&buffer))?;
						}

						out.write_str("</code></pre>")?
					}
					// }}}
					Container::Span => {
						if matches!(self.states.last(), Some(State::Datetime(_))) {
							let Some(State::Datetime(buffer)) = self.states.pop() else {
								unreachable!()
							};

							write_datetime(out, &DateTime::parse_from_rfc3339(&buffer)?)?;
						} else if matches!(self.states.last(), Some(State::Date(_))) {
							let Some(State::Date(buffer)) = self.states.pop() else {
								unreachable!()
							};

							let date = NaiveDate::parse_from_str(&buffer, "%Y-%m-%d")
								.with_context(|| "Failed to parse date inside span")?;
							write!(
								out,
								r#"<time datetime="{}">{}</time>"#,
								date.format("%Y-%m-%d"),
								date.format("%a, %d %b %Y")
							)?;
						} else {
							out.write_str("</span>")?;
						}
					}

					Container::Link(..) => out.write_str("</a>")?,
					Container::Verbatim => out.write_str("</code>")?,
					Container::Subscript => out.write_str("</sub>")?,
					Container::Superscript => out.write_str("</sup>")?,
					Container::Insert => out.write_str("</ins>")?,
					Container::Delete => out.write_str("</del>")?,
					Container::Strong => out.write_str("</strong>")?,
					Container::Emphasis => out.write_str("</em>")?,
					Container::Mark => out.write_str("</mark>")?,
					e => bail!("DJot element {e:?} is not supported"),
				}
			}
			// }}}
			// {{{ Raw string
			Event::Str(s) => match self.states.last_mut() {
				Some(State::Raw) => out.write_str(s)?,
				Some(State::CodeBlock(buffer) | State::Datetime(buffer) | State::Date(buffer)) => {
					buffer.push_str(s)
				}
				// {{{ Math
				Some(State::Math(display)) => {
					let config = pulldown_latex::RenderConfig {
						display_mode: {
							use pulldown_latex::config::DisplayMode::*;
							if *display {
								Block
							} else {
								Inline
							}
						},
						annotation: None,
						error_color: (178, 34, 34),
						xml: true,
						math_style: pulldown_latex::config::MathStyle::TeX,
					};

					let mut mathml = String::new();
					let storage = pulldown_latex::Storage::new();
					let parser = pulldown_latex::Parser::new(s, &storage);
					pulldown_latex::push_mathml(&mut mathml, parser, config).unwrap();
					out.write_str(&mathml)?;
				}
				// }}}
				_ => write!(out, "{}", Escaped(s))?,
			},
			// }}}
			// {{{ Footnote reference
			Event::FootnoteReference(label) => {
				let number = self.footnotes.reference(label);
				if !matches!(self.states.last(), Some(State::TextOnly)) {
					write!(
						out,
						r##"
              <sup>
                <a id="fnref{number}" href="#fn{number}" role="doc-noteref">
                  {number}
                </a>
              </sup>
            "##
					)?;
				}
			}
			// }}}
			// {{{ Symbol
			Event::Symbol(sym) => write!(out, ":{}:", sym)?,
			Event::LeftSingleQuote => out.write_str("‘")?,
			Event::RightSingleQuote => out.write_str("’")?,
			Event::LeftDoubleQuote => out.write_str("“")?,
			Event::RightDoubleQuote => out.write_str("”")?,
			Event::Ellipsis => out.write_str("…")?,
			Event::EnDash => out.write_str("–")?,
			Event::EmDash => out.write_str("—")?,
			Event::NonBreakingSpace => out.write_str("&nbsp;")?,
			Event::Hardbreak => out.write_str("<br>")?,
			Event::Softbreak => out.write_char('\n')?,
			// }}}
			Event::ThematicBreak(_) => out.write_str("<hr />")?,
			Event::Escape | Event::Blankline | Event::Attributes(_) => {}
		}

		Ok(())
	}

	// {{{ Render epilogue
	pub fn render_epilogue(&mut self, out: &mut impl std::fmt::Write) -> anyhow::Result<()> {
		if self.footnotes.reference_encountered() {
			// TODO: rewrite this using a template
			out.write_str("<section role=\"doc-endnotes\"><hr/><ol>")?;

			while let Some((number, events)) = self.footnotes.next() {
				write!(out, r#"<li id="fn{number}">"#)?;

				for e in events.iter().flatten() {
					self.render_event(e, out)?;
				}

				write!(
					out,
					r##"
            <a href="#fnref{number}" role="doc-backlink">
                Return to content ↩︎
            </a></li>
          "##,
				)?;
			}

			out.write_str("</ol></section>")?;
		}

		Ok(())
	}
	// }}}
	// {{{ Fill in metadata labels
	fn write_metadata_label(
		out: &mut impl std::fmt::Write,
		label: &str,
		meta: &PageMetadata,
		base_url: &str,
	) -> anyhow::Result<bool> {
		match label {
			"posted_on" => {
				if let Some(d) = meta.config.created_at {
					write!(out, "Posted on ")?;
					write_datetime(out, &d)?;
				} else {
					write!(out, "Being conjured ")?;
				}
			}
			"base_url" => {
				write!(out, "{}", base_url)?;
			}
			"updated_on" => {
				write_datetime(out, &meta.last_modified)?;
			}
			"word_count" => {
				let wc = meta.word_count;
				if wc < 400 {
					write!(out, "{}", wc)?;
				} else if wc < 1000 {
					write!(out, "{}", wc / 10 * 10)?;
				} else if wc < 2000 {
					write!(out, "{}", wc / 100 * 100)?;
				} else {
					write!(out, "{} thousand", wc / 1000)?;
				}
			}
			"reading_duration" => {
				let minutes = meta.word_count / 200;
				if minutes == 0 {
					let seconds = meta.word_count * 60 / 200;
					write!(out, "very short {seconds} second")?;
				} else if minutes < 10 {
					write!(out, "short {minutes} minute")?;
				} else if minutes < 20 {
					write!(out, "somewhat short {minutes} minute")?;
				} else if minutes < 30 {
					write!(out, "somewhat long {minutes}")?;
				} else if minutes < 60 {
					write!(out, "long {minutes}")?;
				} else {
					let hours = minutes / 60;
					let minutes = minutes % 60;
					write!(out, "very long {hours} hour and {minutes} minute")?;
				}
			}
			"source_url" => {
				write!(
					out,
					"https://git.moonythm.dev/prescientmoon/moonythm/src/branch/main/{}",
					meta.source_path.to_str().unwrap()
				)?;
			}
			"changelog_url" => {
				write!(
					out,
					"https://git.moonythm.dev/prescientmoon/moonythm/commits/branch/main/{}",
					meta.source_path.to_str().unwrap()
				)?;
			}
			_ => {
				return Ok(false);
			}
		}

		Ok(true)
	}

	// }}}
}

// {{{ HTMl escaper
pub struct Escaped<'a>(&'a str);

impl<'s> Display for Escaped<'s> {
	fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		let mut s = self.0;
		let mut ent = "";
		while let Some(i) = s.find(|c| {
			match c {
				'<' => Some("&lt;"),
				'>' => Some("&gt;"),
				'&' => Some("&amp;"),
				'"' => Some("&quot;"),
				_ => None,
			}
			.map_or(false, |s| {
				ent = s;
				true
			})
		}) {
			out.write_str(&s[..i])?;
			out.write_str(ent)?;
			s = &s[i + 1..];
		}
		out.write_str(s)
	}
}
// }}}
// {{{ Render attributes
pub struct Attr<'a>(&'static str, &'a str);

impl<'s> Display for Attr<'s> {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		if !self.1.is_empty() {
			write!(f, r#" {}="{}""#, self.0, Escaped(self.1))?;
		}

		Ok(())
	}
}
// }}}
// {{{ Render datetimes
#[inline]
fn write_datetime<T: TimeZone>(
	out: &mut impl std::fmt::Write,
	datetime: &DateTime<T>,
) -> std::fmt::Result {
	let datetime = datetime.to_utc();

	write!(
		out,
		r#"<time datetime="{}">{}</time>"#,
		datetime.to_rfc3339_opts(chrono::SecondsFormat::Millis, false),
		datetime.format("%a, %d %b %Y")
	)
}
// }}}
// {{{ Jotdown attribute helpers
#[inline]
fn write_attribute(out: &mut impl std::fmt::Write, attr: &AttributeValue) -> std::fmt::Result {
	attr.parts()
		.try_for_each(|part| write!(out, "{}", Escaped(part)))
}
// }}}
// {{{ Footnotes
/// Helper to aggregate footnotes for rendering at the end of the document. It will cache footnote
/// events until they should be emitted at the end.
///
/// When footnotes should be rendered, they can be pulled with the [`Footnotes::next`] function in
/// the order they were first referenced.
#[derive(Default)]
struct Footnotes<'s> {
	/// Footnote references in the order they were first encountered.
	references: Vec<&'s str>,
	/// Events for each footnote.
	events: HashMap<&'s str, Vec<Event<'s>>>,
	/// Number of last footnote that was emitted.
	number: usize,
}

impl<'s> Footnotes<'s> {
	/// Returns `true` if any reference has been encountered.
	fn reference_encountered(&self) -> bool {
		!self.references.is_empty()
	}

	/// Add a footnote reference.
	fn reference(&mut self, label: &'s str) -> usize {
		self.references
			.iter()
			.position(|t| *t == label)
			.map_or_else(
				|| {
					self.references.push(label);
					self.references.len()
				},
				|i| i + 1,
			)
	}

	/// Insert a new footnote to be renderer later
	fn insert(&mut self, label: &'s str, events: Vec<jotdown::Event<'s>>) {
		self.events.insert(label, events);
	}
}

impl<'s> Iterator for Footnotes<'s> {
	type Item = (usize, Option<Vec<Event<'s>>>);

	fn next(&mut self) -> Option<Self::Item> {
		self.references.get(self.number).map(|label| {
			self.number += 1;
			(self.number, self.events.remove(label))
		})
	}
}
// }}}