use anyhow::bail;

// {{{ Templates & stops
#[derive(Clone, Debug)]
struct Stop<'s> {
	label: &'s str,
	start: usize,
	length: usize,
}

#[derive(Clone, Debug)]
pub struct Template<'s> {
	text: &'s str,
	stops: Vec<Stop<'s>>,
}

impl<'s> Template<'s> {
	// {{{ Parse template
	#[allow(clippy::iter_nth_zero)]
	pub fn parse(text: &'s str) -> anyhow::Result<Template<'s>> {
		let mut stops = Vec::new();

		let open_stop = "{{";
		let close_stop = "}}";

		let mut current_stop: Option<Stop> = None;
		let mut prev_ix = None;
		for (ix, c) in text.char_indices() {
			let l = c.len_utf8();
			if let Some(prev) = prev_ix {
				// This char, together with the previous one
				let last_two = &text[prev..ix + l];
				if close_stop == last_two {
					if let Some(mut stop) = current_stop.take() {
						// I think this is safe, as { and } are ascii
						stop.label = &text[stop.start + 2..=ix - 2];
						stop.length = ix + 1 - stop.start;
						stops.push(stop);
					}
				} else if open_stop == last_two && current_stop.is_none() {
					current_stop = Some(Stop {
						label: "",
						start: prev,
						length: 0,
					});
				}
			}

			prev_ix = Some(ix);
		}

		Ok(Template { text, stops })
	}
	// }}}
}
// }}}
// {{{ Template rendering
#[derive(Clone, Debug)]
pub struct TemplateRenderer<'a> {
	template: &'a Template<'a>,
	stop_index: Option<usize>,
}

impl<'a> TemplateRenderer<'a> {
	// {{{ Lifecycle (start / current / finish)
	#[inline]
	pub fn start(template: &'a Template, w: &mut impl std::fmt::Write) -> anyhow::Result<Self> {
		let stop_index = if !template.stops.is_empty() {
			Some(0)
		} else {
			None
		};

		let result = Self {
			template,
			stop_index,
		};

		let (next_pos, _) = result.current_stop_range();
		w.write_str(&template.text[..next_pos])?;

		Ok(result)
	}

	/// Get the current placeholder label
	pub fn current(&self) -> Option<&'a str> {
		self.stop_index.map(|ix| self.template.stops[ix].label)
	}

	/// Attempt to finish rendering.
	pub fn finish(mut self, w: &mut impl std::fmt::Write) -> anyhow::Result<()> {
		if let Some(label) = self.next(w)? {
			bail!("Attempting to finish template rendering before label `{label}` was handled");
		}

		Ok(())
	}
	// }}}
	// {{{ Advance to the next placeholder
	/// Move onto the next placeholder
	pub fn next(&mut self, w: &mut impl std::fmt::Write) -> anyhow::Result<Option<&'a str>> {
		let Some(stop_index) = self.stop_index else {
			return Ok(None);
		};

		let (_, current_pos) = self.current_stop_range();

		let next_stop_ix = stop_index + 1;

		self.stop_index = if next_stop_ix < self.template.stops.len() {
			Some(next_stop_ix)
		} else {
			None
		};

		let (next_pos, _) = self.current_stop_range();
		w.write_str(&self.template.text[current_pos..next_pos])?;

		Ok(self.current())
	}

	fn current_stop_range(&self) -> (usize, usize) {
		match self.stop_index {
			Some(stop_ix) => {
				let stop = &self.template.stops[stop_ix];
				(stop.start, stop.start + stop.length)
			}
			None => (self.template.text.len(), self.template.text.len()),
		}
	}
	// }}}
	// {{{ Stop feeding helpers
	/// Automatically fill in placeholders until the provided lambda returns false.
	pub fn feed<W: std::fmt::Write>(
		&mut self,
		out: &mut W,
		mut f: impl FnMut(&str, &mut W) -> anyhow::Result<bool>,
	) -> anyhow::Result<()> {
		while let Some(label) = self.current() {
			if f(label, out)? {
				self.next(out)?;
			} else {
				break;
			}
		}

		Ok(())
	}

	/// Equivalent to running [Self::feed] and then [Self::finish].
	pub fn feed_fully<W: std::fmt::Write>(
		mut self,
		out: &mut W,
		f: impl FnMut(&str, &mut W) -> anyhow::Result<bool>,
	) -> anyhow::Result<()> {
		self.feed(out, f)?;
		self.finish(out)?;

		Ok(())
	}
	// }}}
}
// }}}
// {{{ Macro
#[macro_export]
macro_rules! template {
	($path:literal,$w:expr) => {{
		use once_cell::sync::OnceCell;
		use $crate::template::{Template, TemplateRenderer};

		static TEMPLATE_TEXT: &str = include_str!($path);
		static CELL: OnceCell<Template<'static>> = OnceCell::new();

		CELL.get_or_try_init(|| Template::parse(TEMPLATE_TEXT))
			.and_then(|t| TemplateRenderer::start(t, $w))
	}};
}
// }}}