mod component_button;
mod component_checkbox;
mod component_slider;
mod style;
mod widget_div;
mod widget_label;
mod widget_rectangle;
mod widget_sprite;

use crate::{
	assets::{AssetPath, AssetPathOwned, normalize_path},
	components::{Component, ComponentWeak},
	drawing::{self},
	globals::WguiGlobals,
	layout::{Layout, LayoutParams, LayoutState, Widget, WidgetID, WidgetMap, WidgetPair},
	parser::{
		component_button::parse_component_button, component_checkbox::parse_component_checkbox,
		component_slider::parse_component_slider, widget_div::parse_widget_div, widget_label::parse_widget_label,
		widget_rectangle::parse_widget_rectangle, widget_sprite::parse_widget_sprite,
	},
	widget::ConstructEssentials,
};
use ouroboros::self_referencing;
use smallvec::SmallVec;
use std::{cell::RefMut, collections::HashMap, path::Path, rc::Rc};

#[self_referencing]
struct XmlDocument {
	xml: String,

	#[borrows(xml)]
	#[covariant]
	doc: roxmltree::Document<'this>,
}

pub struct Template {
	node_document: Rc<XmlDocument>,
	node: roxmltree::NodeId, // belongs to node_document which could be included in another file
}

struct ParserFile {
	path: AssetPathOwned,
	document: Rc<XmlDocument>,
	template_parameters: HashMap<Rc<str>, Rc<str>>,
}

/*
	`components` could contain connected listener handles.
		Do not drop them unless you don't need to handle any events,
		including mouse-hover animations.
*/
#[derive(Default, Clone)]
pub struct ParserData {
	pub components_by_id: HashMap<Rc<str>, ComponentWeak>,
	pub components_by_widget_id: HashMap<WidgetID, ComponentWeak>,
	pub components: Vec<Component>,
	pub ids: HashMap<Rc<str>, WidgetID>,
	pub templates: HashMap<Rc<str>, Rc<Template>>,
	pub var_map: HashMap<Rc<str>, Rc<str>>,
	macro_attribs: HashMap<Rc<str>, MacroAttribs>,
}

pub trait Fetchable {
	/// Return a component by its string ID
	fn fetch_component_by_id(&self, id: &str) -> anyhow::Result<Component>;

	/// Return a component by the ID of the widget that owns it
	fn fetch_component_by_widget_id(&self, widget_id: WidgetID) -> anyhow::Result<Component>;

	/// Fetch a component by string ID and down‑cast it to a concrete component type `T` (see `components/mod.rs`)
	fn fetch_component_as<T: 'static>(&self, id: &str) -> anyhow::Result<Rc<T>>;

	/// Fetch a component by widget ID and down‑cast it to a concrete component type `T` (see `components/mod.rs`)
	fn fetch_component_from_widget_id_as<T: 'static>(&self, widget_id: WidgetID) -> anyhow::Result<Rc<T>>;

	/// Return a widget by its string ID
	fn get_widget_id(&self, id: &str) -> anyhow::Result<WidgetID>;

	/// Retrieve the widget associated with a string ID, returning a `WidgetPair` (id and widget itself)
	fn fetch_widget(&self, state: &LayoutState, id: &str) -> anyhow::Result<WidgetPair>;

	/// Retrieve a widget by string ID and down‑cast its inner value to type `T` (see `widget/mod.rs`)
	fn fetch_widget_as<'a, T: 'static>(&self, state: &'a LayoutState, id: &str) -> anyhow::Result<RefMut<'a, T>>;
}

impl ParserData {
	fn take_results_from(&mut self, from: &mut Self) {
		let ids = std::mem::take(&mut from.ids);
		let components = std::mem::take(&mut from.components);
		let components_by_id = std::mem::take(&mut from.components_by_id);
		let components_by_widget_id = std::mem::take(&mut from.components_by_widget_id);

		for (id, key) in ids {
			self.ids.insert(id, key);
		}

		for c in components {
			self.components.push(c);
		}

		for (k, v) in components_by_id {
			self.components_by_id.insert(k, v);
		}

		for (k, v) in components_by_widget_id {
			self.components_by_widget_id.insert(k, v);
		}
	}
}

impl Fetchable for ParserData {
	fn fetch_component_by_id(&self, id: &str) -> anyhow::Result<Component> {
		let Some(weak) = self.components_by_id.get(id) else {
			anyhow::bail!("Component by ID \"{id}\" doesn't exist");
		};

		let Some(component) = weak.upgrade() else {
			anyhow::bail!("Component by ID \"{id}\" doesn't exist");
		};

		Ok(Component(component))
	}

	fn fetch_component_by_widget_id(&self, widget_id: WidgetID) -> anyhow::Result<Component> {
		let Some(weak) = self.components_by_widget_id.get(&widget_id) else {
			anyhow::bail!("Component by widget ID \"{widget_id:?}\" doesn't exist");
		};

		let Some(component) = weak.upgrade() else {
			anyhow::bail!("Component by widget ID \"{widget_id:?}\" doesn't exist");
		};

		Ok(Component(component))
	}

	fn fetch_component_as<T: 'static>(&self, id: &str) -> anyhow::Result<Rc<T>> {
		let component = self.fetch_component_by_id(id)?;

		if !(*component.0).as_any().is::<T>() {
			anyhow::bail!("fetch_component_as({id}): type not matching");
		}

		// safety: we just checked the type
		unsafe { Ok(Rc::from_raw(Rc::into_raw(component.0).cast())) }
	}

	fn fetch_component_from_widget_id_as<T: 'static>(&self, widget_id: WidgetID) -> anyhow::Result<Rc<T>> {
		let component = self.fetch_component_by_widget_id(widget_id)?;

		if !(*component.0).as_any().is::<T>() {
			anyhow::bail!("fetch_component_by_widget_id({widget_id:?}): type not matching");
		}

		// safety: we just checked the type
		unsafe { Ok(Rc::from_raw(Rc::into_raw(component.0).cast())) }
	}

	fn get_widget_id(&self, id: &str) -> anyhow::Result<WidgetID> {
		match self.ids.get(id) {
			Some(id) => Ok(*id),
			None => anyhow::bail!("Widget by ID \"{id}\" doesn't exist"),
		}
	}

	fn fetch_widget(&self, state: &LayoutState, id: &str) -> anyhow::Result<WidgetPair> {
		let widget_id = self.get_widget_id(id)?;
		let widget = state
			.widgets
			.get(widget_id)
			.ok_or_else(|| anyhow::anyhow!("fetch_widget({id}): widget not found"))?;
		Ok(WidgetPair {
			id: widget_id,
			widget: widget.clone(),
		})
	}

	fn fetch_widget_as<'a, T: 'static>(&self, state: &'a LayoutState, id: &str) -> anyhow::Result<RefMut<'a, T>> {
		let widget_id = self.get_widget_id(id)?;
		let widget = state
			.widgets
			.get(widget_id)
			.ok_or_else(|| anyhow::anyhow!("fetch_widget_as({id}): widget not found"))?;

		let casted = widget
			.get_as_mut::<T>()
			.ok_or_else(|| anyhow::anyhow!("fetch_widget_as({id}): failed to cast"))?;

		Ok(casted)
	}
}

/*
	WARNING: this struct could contain valid components with already bound listener handles.
	Make sure to store them somewhere in your code.
*/
#[derive(Default)]
pub struct ParserState {
	pub data: ParserData,
	pub path: AssetPathOwned,
}

impl ParserState {
	/// This function is suitable in cases if you don't want to pollute main parser state with dynamic IDs
	/// Use `instantiate_template` instead unless you want to handle `components` results yourself.
	/// Make sure not to drop them if you want to have your listener handles valid
	pub fn parse_template(
		&mut self,
		doc_params: &ParseDocumentParams,
		template_name: &str,
		layout: &mut Layout,
		widget_id: WidgetID,
		template_parameters: HashMap<Rc<str>, Rc<str>>,
	) -> anyhow::Result<ParserData> {
		let Some(template) = self.data.templates.get(template_name) else {
			anyhow::bail!("no template named \"{template_name}\" found");
		};

		let mut ctx = ParserContext {
			layout,
			data_global: &self.data,
			data_local: ParserData::default(),
			doc_params,
		};

		let file = ParserFile {
			document: template.node_document.clone(),
			path: self.path.clone(),
			template_parameters: template_parameters.clone(), // FIXME: prevent copying
		};

		parse_widget_other_internal(&template.clone(), template_parameters, &file, &mut ctx, widget_id)?;
		Ok(ctx.data_local)
	}

	/// Instantinate template by saving all the results into the main `ParserState`
	pub fn instantiate_template(
		&mut self,
		doc_params: &ParseDocumentParams,
		template_name: &str,
		layout: &mut Layout,
		widget_id: WidgetID,
		template_parameters: HashMap<Rc<str>, Rc<str>>,
	) -> anyhow::Result<()> {
		let mut data_local = self.parse_template(doc_params, template_name, layout, widget_id, template_parameters)?;

		self.data.take_results_from(&mut data_local);
		Ok(())
	}
}

// convenience wrapper functions for `data`
impl Fetchable for ParserState {
	fn fetch_component_by_id(&self, id: &str) -> anyhow::Result<Component> {
		self.data.fetch_component_by_id(id)
	}

	fn fetch_component_by_widget_id(&self, widget_id: WidgetID) -> anyhow::Result<Component> {
		self.data.fetch_component_by_widget_id(widget_id)
	}

	fn fetch_component_as<T: 'static>(&self, id: &str) -> anyhow::Result<Rc<T>> {
		self.data.fetch_component_as(id)
	}

	fn fetch_component_from_widget_id_as<T: 'static>(&self, widget_id: WidgetID) -> anyhow::Result<Rc<T>> {
		self.data.fetch_component_from_widget_id_as(widget_id)
	}

	fn get_widget_id(&self, id: &str) -> anyhow::Result<WidgetID> {
		self.data.get_widget_id(id)
	}

	fn fetch_widget(&self, state: &LayoutState, id: &str) -> anyhow::Result<WidgetPair> {
		self.data.fetch_widget(state, id)
	}

	fn fetch_widget_as<'a, T: 'static>(&self, state: &'a LayoutState, id: &str) -> anyhow::Result<RefMut<'a, T>> {
		self.data.fetch_widget_as(state, id)
	}
}

#[derive(Debug, Clone)]
struct MacroAttribs {
	attribs: HashMap<Rc<str>, Rc<str>>,
}

struct ParserContext<'a> {
	doc_params: &'a ParseDocumentParams<'a>,
	layout: &'a mut Layout,
	data_global: &'a ParserData, // current parser state at a given moment
	data_local: ParserData,      // newly processed items in a given template
}

impl ParserContext<'_> {
	const fn get_construct_essentials(&mut self, parent: WidgetID) -> ConstructEssentials<'_> {
		ConstructEssentials {
			layout: self.layout,
			parent,
		}
	}

	fn get_template(&self, name: &str) -> Option<Rc<Template>> {
		// find in local
		if let Some(template) = self.data_local.templates.get(name) {
			return Some(template.clone());
		}

		// find in global
		if let Some(template) = self.data_global.templates.get(name) {
			return Some(template.clone());
		}

		None
	}

	fn get_var(&self, name: &str) -> Option<Rc<str>> {
		// find in local
		if let Some(value) = self.data_local.var_map.get(name) {
			return Some(value.clone());
		}

		// find in global
		if let Some(value) = self.data_global.var_map.get(name) {
			return Some(value.clone());
		}

		None
	}

	fn get_macro_attrib(&self, value: &str) -> Option<&MacroAttribs> {
		// find in local
		if let Some(macro_attribs) = self.data_local.macro_attribs.get(value) {
			return Some(macro_attribs);
		}

		// find in global
		if let Some(macro_attribs) = self.data_global.macro_attribs.get(value) {
			return Some(macro_attribs);
		}

		None
	}

	fn insert_template(&mut self, name: Rc<str>, template: Rc<Template>) {
		self.data_local.templates.insert(name, template);
	}

	fn insert_var(&mut self, key: &str, value: &str) {
		self.data_local.var_map.insert(Rc::from(key), Rc::from(value));
	}

	fn insert_macro_attrib(&mut self, name: Rc<str>, attribs: MacroAttribs) {
		self.data_local.macro_attribs.insert(name, attribs);
	}

	fn insert_component(&mut self, widget_id: WidgetID, component: Component, id: Option<Rc<str>>) {
		self
			.data_local
			.components_by_widget_id
			.insert(widget_id, component.weak());

		if let Some(id) = id
			&& self
				.data_local
				.components_by_id
				.insert(id.clone(), component.weak())
				.is_some()
		{
			log::warn!("duplicate component ID \"{id}\" in the same layout file!");
		}

		self.data_local.components.push(component);
	}

	fn insert_id(&mut self, id: &Rc<str>, widget_id: WidgetID) {
		if self.data_local.ids.insert(id.clone(), widget_id).is_some() {
			log::warn!("duplicate widget ID \"{id}\" in the same layout file!");
		}
	}
}

// Parses a color from a HTML hex string
pub fn parse_color_hex(html_hex: &str) -> Option<drawing::Color> {
	if html_hex.len() == 7 {
		if let (Ok(r), Ok(g), Ok(b)) = (
			u8::from_str_radix(&html_hex[1..3], 16),
			u8::from_str_radix(&html_hex[3..5], 16),
			u8::from_str_radix(&html_hex[5..7], 16),
		) {
			return Some(drawing::Color::new(
				f32::from(r) / 255.,
				f32::from(g) / 255.,
				f32::from(b) / 255.,
				1.,
			));
		}
	} else if html_hex.len() == 9
		&& let (Ok(r), Ok(g), Ok(b), Ok(a)) = (
			u8::from_str_radix(&html_hex[1..3], 16),
			u8::from_str_radix(&html_hex[3..5], 16),
			u8::from_str_radix(&html_hex[5..7], 16),
			u8::from_str_radix(&html_hex[7..9], 16),
		) {
		return Some(drawing::Color::new(
			f32::from(r) / 255.,
			f32::from(g) / 255.,
			f32::from(b) / 255.,
			f32::from(a) / 255.,
		));
	}
	log::warn!("failed to parse color \"{html_hex}\"");
	None
}

fn get_tag_by_name<'a>(node: &roxmltree::Node<'a, 'a>, name: &str) -> Option<roxmltree::Node<'a, 'a>> {
	node.children().find(|&child| child.tag_name().name() == name)
}

fn require_tag_by_name<'a>(node: &roxmltree::Node<'a, 'a>, name: &str) -> anyhow::Result<roxmltree::Node<'a, 'a>> {
	get_tag_by_name(node, name).ok_or_else(|| anyhow::anyhow!("Tag \"{name}\" not found"))
}

fn print_invalid_attrib(key: &str, value: &str) {
	log::warn!("Invalid value \"{value}\" in attribute \"{key}\"");
}

fn print_missing_attrib(tag_name: &str, attr: &str) {
	log::warn!("Missing attribute {attr} in tag <{tag_name}>");
}

fn print_invalid_value(value: &str) {
	log::warn!("Invalid value \"{value}\"");
}

fn parse_val(value: &str) -> Option<f32> {
	let Ok(val) = value.parse::<f32>() else {
		print_invalid_value(value);
		return None;
	};
	Some(val)
}

fn is_percent(value: &str) -> bool {
	value.ends_with('%')
}

fn parse_percent(value: &str) -> Option<f32> {
	let Some(val_str) = value.split('%').next() else {
		print_invalid_value(value);
		return None;
	};

	let Ok(val) = val_str.parse::<f32>() else {
		print_invalid_value(value);
		return None;
	};
	Some(val / 100.0)
}

fn parse_i32(value: &str) -> Option<i32> {
	value.parse::<i32>().ok()
}

fn parse_f32(value: &str) -> Option<f32> {
	value.parse::<f32>().ok()
}

fn parse_check_i32(value: &str, num: &mut i32) -> bool {
	if let Some(value) = parse_i32(value) {
		*num = value;
		true
	} else {
		print_invalid_value(value);
		false
	}
}

fn parse_check_f32(value: &str, num: &mut f32) -> bool {
	if let Some(value) = parse_f32(value) {
		*num = value;
		true
	} else {
		print_invalid_value(value);
		false
	}
}

fn parse_size_unit<T>(value: &str) -> Option<T>
where
	T: taffy::prelude::FromPercent + taffy::prelude::FromLength,
{
	if is_percent(value) {
		Some(taffy::prelude::percent(parse_percent(value)?))
	} else {
		Some(taffy::prelude::length(parse_f32(value)?))
	}
}

fn parse_widget_other_internal(
	template: &Rc<Template>,
	template_parameters: HashMap<Rc<str>, Rc<str>>,
	file: &ParserFile,
	ctx: &mut ParserContext,
	parent_id: WidgetID,
) -> anyhow::Result<()> {
	let template_file = ParserFile {
		document: template.node_document.clone(),
		path: file.path.clone(),
		template_parameters,
	};

	let doc = template_file.document.clone();

	let template_node = doc
		.borrow_doc()
		.get_node(template.node)
		.ok_or_else(|| anyhow::anyhow!("template node invalid"))?;

	parse_children(&template_file, ctx, template_node, parent_id)?;

	Ok(())
}

fn parse_widget_other(
	xml_tag_name: &str,
	file: &ParserFile,
	ctx: &mut ParserContext,
	parent_id: WidgetID,
	attribs: &[AttribPair],
) -> anyhow::Result<()> {
	let Some(template) = ctx.get_template(xml_tag_name) else {
		log::error!("Undefined tag named \"{xml_tag_name}\"");
		return Ok(()); // not critical
	};

	let template_parameters: HashMap<Rc<str>, Rc<str>> =
		attribs.iter().map(|a| (a.attrib.clone(), a.value.clone())).collect();

	parse_widget_other_internal(&template, template_parameters, file, ctx, parent_id)
}

fn parse_tag_include(
	file: &ParserFile,
	ctx: &mut ParserContext,
	parent_id: WidgetID,
	attribs: &[AttribPair],
) -> anyhow::Result<()> {
	for pair in attribs {
		#[allow(clippy::single_match)]
		match pair.attrib.as_ref() {
			"src" => {
				let new_path = {
					let this = &file.path.clone();
					let include: &str = &pair.value;
					let buf = this.get_path_buf();
					let mut new_path = buf.parent().unwrap_or_else(|| Path::new("/")).to_path_buf();
					new_path.push(include);
					let new_path = normalize_path(&new_path);

					match this {
						AssetPathOwned::WguiInternal(_) => AssetPathOwned::WguiInternal(new_path),
						AssetPathOwned::BuiltIn(_) => AssetPathOwned::BuiltIn(new_path),
						AssetPathOwned::Filesystem(_) => AssetPathOwned::Filesystem(new_path),
					}
				};
				let new_path_ref = new_path.as_ref();
				let (new_file, node_layout) = get_doc_from_asset_path(ctx, new_path_ref)?;
				parse_document_root(&new_file, ctx, parent_id, node_layout)?;

				return Ok(());
			}
			_ => {
				print_invalid_attrib(pair.attrib.as_ref(), pair.value.as_ref());
			}
		}
	}

	Ok(())
}

fn parse_tag_var<'a>(ctx: &mut ParserContext, node: roxmltree::Node<'a, 'a>) {
	let mut out_key: Option<&str> = None;
	let mut out_value: Option<&str> = None;

	for attrib in node.attributes() {
		let (key, value) = (attrib.name(), attrib.value());

		match key {
			"key" => {
				out_key = Some(value);
			}
			"value" => {
				out_value = Some(value);
			}
			_ => {
				print_invalid_attrib(key, value);
			}
		}
	}

	let Some(key) = out_key else {
		print_missing_attrib("var", "key");
		return;
	};

	let Some(value) = out_value else {
		print_missing_attrib("var", "value");
		return;
	};

	ctx.insert_var(key, value);
}

pub fn replace_vars(input: &str, vars: &HashMap<Rc<str>, Rc<str>>) -> Rc<str> {
	let re = regex::Regex::new(r"\$\{([^}]*)\}").unwrap();

	/*if !vars.is_empty() {
		log::error!("template parameters {:?}", vars);
	}*/

	let out = re.replace_all(input, |captures: &regex::Captures| {
		let input_var = &captures[1];

		if let Some(replacement) = vars.get(input_var) {
			replacement.clone()
		} else {
			// failed to find var, return an empty string
			Rc::from("")
		}
	});

	Rc::from(out)
}

#[allow(clippy::manual_strip)]
fn process_attrib<'a>(file: &'a ParserFile, ctx: &'a ParserContext, key: &str, value: &str) -> AttribPair {
	if value.starts_with('~') {
		let name = &value[1..];

		match ctx.get_var(name) {
			Some(name) => AttribPair::new(key, name),
			None => AttribPair::new(key, "undefined"),
		}
	} else {
		AttribPair::new(key, replace_vars(value, &file.template_parameters))
	}
}

fn raw_attribs<'a>(node: &'a roxmltree::Node<'a, 'a>) -> Vec<AttribPair> {
	let mut res = vec![];
	for attrib in node.attributes() {
		let (key, value) = (attrib.name(), attrib.value());
		res.push(AttribPair::new(key, value));
	}
	res
}

fn process_attribs<'a>(
	file: &'a ParserFile,
	ctx: &'a ParserContext,
	node: &'a roxmltree::Node<'a, 'a>,
	is_tag_macro: bool,
) -> Vec<AttribPair> {
	if is_tag_macro {
		// return as-is, no attrib post-processing
		return raw_attribs(node);
	}
	let mut res = vec![];

	for attrib in node.attributes() {
		let (key, value) = (attrib.name(), attrib.value());

		if key == "macro" {
			if let Some(macro_attrib) = ctx.get_macro_attrib(value) {
				for (macro_key, macro_value) in &macro_attrib.attribs {
					res.push(process_attrib(file, ctx, macro_key, macro_value));
				}
			} else {
				log::warn!("requested macro named \"{value}\" not found!");
			}
		} else {
			res.push(process_attrib(file, ctx, key, value));
		}
	}

	res
}

fn parse_tag_theme<'a>(ctx: &mut ParserContext, node: roxmltree::Node<'a, 'a>) {
	for child_node in node.children() {
		let child_name = child_node.tag_name().name();
		match child_name {
			"var" => {
				parse_tag_var(ctx, child_node);
			}
			"" => { /* ignore */ }
			_ => {
				print_invalid_value(child_name);
			}
		}
	}
}

fn parse_tag_template(file: &ParserFile, ctx: &mut ParserContext, node: roxmltree::Node<'_, '_>) {
	let mut template_name: Option<Rc<str>> = None;

	let attribs = process_attribs(file, ctx, &node, false);

	for pair in attribs {
		match pair.attrib.as_ref() {
			"name" => {
				template_name = Some(pair.value);
			}
			_ => {
				print_invalid_attrib(pair.value.as_ref(), pair.value.as_ref());
			}
		}
	}

	let Some(name) = template_name else {
		log::error!("Template name not specified, ignoring");
		return;
	};

	ctx.insert_template(
		name,
		Rc::new(Template {
			node: node.id(),
			node_document: file.document.clone(),
		}),
	);
}

fn parse_tag_macro(file: &ParserFile, ctx: &mut ParserContext, node: roxmltree::Node<'_, '_>) {
	let mut macro_name: Option<Rc<str>> = None;

	let attribs = process_attribs(file, ctx, &node, true);
	let mut macro_attribs = HashMap::<Rc<str>, Rc<str>>::new();

	for pair in attribs {
		match pair.attrib.as_ref() {
			"name" => {
				macro_name = Some(pair.value);
			}
			_ => {
				if macro_attribs.insert(pair.attrib.clone(), pair.value).is_some() {
					log::warn!("macro attrib \"{}\" already defined!", pair.attrib);
				}
			}
		}
	}

	let Some(name) = macro_name else {
		log::error!("Template name not specified, ignoring");
		return;
	};

	ctx.insert_macro_attrib(name, MacroAttribs { attribs: macro_attribs });
}

fn process_component(ctx: &mut ParserContext, component: Component, widget_id: WidgetID, attribs: &[AttribPair]) {
	let mut component_id: Option<Rc<str>> = None;

	for pair in attribs {
		#[allow(clippy::single_match)]
		match pair.attrib.as_ref() {
			"id" => {
				component_id = Some(pair.value.clone());
			}
			_ => {}
		}
	}

	ctx.insert_component(widget_id, component, component_id);
}

fn parse_widget_universal(ctx: &mut ParserContext, widget_id: WidgetID, attribs: &[AttribPair]) {
	for pair in attribs {
		#[allow(clippy::single_match)]
		match pair.attrib.as_ref() {
			"id" => {
				// Attach a specific widget to name-ID map (just like getElementById)
				ctx.insert_id(&pair.value, widget_id);
			}
			"interactable" => {
				if matches!(&pair.value.parse::<i32>(), Ok(0)) {
					log::info!("setting {widget_id:?} to noninteractable.");
					ctx.layout.state.widgets.get(widget_id).unwrap().state().interactable = false;
				} else {
					print_invalid_attrib(&pair.attrib, &pair.value);
				}
			}
			_ => {}
		}
	}
}

fn parse_child<'a>(
	file: &ParserFile,
	ctx: &mut ParserContext,
	parent_node: roxmltree::Node<'a, 'a>,
	child_node: roxmltree::Node<'a, 'a>,
	parent_id: WidgetID,
) -> anyhow::Result<()> {
	match parent_node.attribute("ignore_in_mode") {
		Some("dev") => {
			if !ctx.doc_params.extra.dev_mode {
				return Ok(()); // do not parse
			}
		}
		Some("live") => {
			if ctx.doc_params.extra.dev_mode {
				return Ok(()); // do not parse
			}
		}
		Some(s) => print_invalid_attrib("ignore_in_mode", s),
		_ => {}
	}

	let attribs = process_attribs(file, ctx, &child_node, false);

	let mut new_widget_id: Option<WidgetID> = None;

	match child_node.tag_name().name() {
		"include" => {
			parse_tag_include(file, ctx, parent_id, &attribs)?;
		}
		"div" => {
			new_widget_id = Some(parse_widget_div(file, ctx, child_node, parent_id, &attribs)?);
		}
		"rectangle" => {
			new_widget_id = Some(parse_widget_rectangle(file, ctx, child_node, parent_id, &attribs)?);
		}
		"label" => {
			new_widget_id = Some(parse_widget_label(file, ctx, child_node, parent_id, &attribs)?);
		}
		"sprite" => {
			new_widget_id = Some(parse_widget_sprite(file, ctx, child_node, parent_id, &attribs)?);
		}
		"Button" => {
			new_widget_id = Some(parse_component_button(file, ctx, child_node, parent_id, &attribs)?);
		}
		"Slider" => {
			new_widget_id = Some(parse_component_slider(ctx, parent_id, &attribs)?);
		}
		"CheckBox" => {
			new_widget_id = Some(parse_component_checkbox(ctx, parent_id, &attribs)?);
		}
		"" => { /* ignore */ }
		other_tag_name => {
			parse_widget_other(other_tag_name, file, ctx, parent_id, &attribs)?;
		}
	}

	// check for custom attributes (if the callback is set)
	if let Some(widget_id) = new_widget_id
		&& let Some(on_custom_attribs) = &ctx.doc_params.extra.on_custom_attribs
	{
		let mut pairs = SmallVec::<[AttribPair; 4]>::new();

		for pair in attribs {
			if !pair.attrib.starts_with('_') || pair.attrib.is_empty() {
				continue;
			}
			pairs.push(pair.clone());
		}

		if !pairs.is_empty() {
			on_custom_attribs(CustomAttribsInfo {
				widgets: &ctx.layout.state.widgets,
				parent_id,
				widget_id,
				pairs: &pairs,
			});
		}
	}

	Ok(())
}

fn parse_children<'a>(
	file: &ParserFile,
	ctx: &mut ParserContext,
	parent_node: roxmltree::Node<'a, 'a>,
	parent_id: WidgetID,
) -> anyhow::Result<()> {
	for child_node in parent_node.children() {
		parse_child(file, ctx, parent_node, child_node, parent_id)?;
	}

	Ok(())
}

fn create_default_context<'a>(
	doc_params: &'a ParseDocumentParams,
	layout: &'a mut Layout,
	data_global: &'a ParserData,
) -> ParserContext<'a> {
	ParserContext {
		doc_params,
		layout,
		data_local: ParserData::default(),
		data_global,
	}
}

#[derive(Clone)]
pub struct AttribPair {
	pub attrib: Rc<str>,
	pub value: Rc<str>,
}

impl AttribPair {
	fn new<A, V>(attrib: A, value: V) -> Self
	where
		A: Into<Rc<str>>,
		V: Into<Rc<str>>,
	{
		Self {
			attrib: attrib.into(),
			value: value.into(),
		}
	}
}

pub struct CustomAttribsInfo<'a> {
	pub parent_id: WidgetID,
	pub widget_id: WidgetID,
	pub widgets: &'a WidgetMap,
	pub pairs: &'a [AttribPair],
}

// helper functions
impl CustomAttribsInfo<'_> {
	pub fn get_widget(&self) -> Option<&Widget> {
		self.widgets.get(self.widget_id)
	}

	pub fn get_widget_as<T: 'static>(&self) -> Option<RefMut<'_, T>> {
		self.widgets.get(self.widget_id)?.get_as_mut::<T>()
	}

	pub fn get_value(&self, attrib_name: &str) -> Option<Rc<str>> {
		// O(n) search, these pairs won't be problematically big anyways
		for pair in self.pairs {
			if *pair.attrib == *attrib_name {
				return Some(pair.value.clone());
			}
		}

		None
	}

	pub fn to_owned(&self) -> CustomAttribsInfoOwned {
		CustomAttribsInfoOwned {
			parent_id: self.parent_id,
			widget_id: self.widget_id,
			pairs: self.pairs.to_vec(),
		}
	}
}

pub struct CustomAttribsInfoOwned {
	pub parent_id: WidgetID,
	pub widget_id: WidgetID,
	pub pairs: Vec<AttribPair>,
}

impl CustomAttribsInfoOwned {
	pub fn get_value(&self, attrib_name: &str) -> Option<&str> {
		// O(n) search, these pairs won't be problematically big anyways
		for pair in &self.pairs {
			if pair.attrib.as_ref() == attrib_name {
				return Some(pair.value.as_ref());
			}
		}

		None
	}
}

pub type OnCustomAttribsFunc = Box<dyn Fn(CustomAttribsInfo)>;

#[derive(Default)]
pub struct ParseDocumentExtra {
	pub on_custom_attribs: Option<OnCustomAttribsFunc>, // all attributes with '_' character prepended
	pub dev_mode: bool,
}

// filled-in by you in `new_layout_from_assets` function
pub struct ParseDocumentParams<'a> {
	pub globals: WguiGlobals,      // mandatory field
	pub path: AssetPath<'a>,       // mandatory field
	pub extra: ParseDocumentExtra, // optional field, can be Default-ed
}

pub fn parse_from_assets(
	doc_params: &ParseDocumentParams,
	layout: &mut Layout,
	parent_id: WidgetID,
) -> anyhow::Result<ParserState> {
	let parser_data = ParserData::default();
	let mut ctx = create_default_context(doc_params, layout, &parser_data);
	let (file, node_layout) = get_doc_from_asset_path(&ctx, doc_params.path)?;
	parse_document_root(&file, &mut ctx, parent_id, node_layout)?;

	// move everything essential to the result
	let result = ParserState {
		data: std::mem::take(&mut ctx.data_local),
		path: doc_params.path.to_owned(),
	};

	drop(ctx);

	Ok(result)
}

pub fn new_layout_from_assets(
	doc_params: &ParseDocumentParams,
	layout_params: &LayoutParams,
) -> anyhow::Result<(Layout, ParserState)> {
	let mut layout = Layout::new(doc_params.globals.clone(), layout_params)?;
	let widget = layout.content_root_widget;
	let state = parse_from_assets(doc_params, &mut layout, widget)?;
	Ok((layout, state))
}

fn get_doc_from_asset_path(
	ctx: &ParserContext,
	asset_path: AssetPath,
) -> anyhow::Result<(ParserFile, roxmltree::NodeId)> {
	let data = ctx.layout.state.globals.get_asset(asset_path)?;
	let xml = String::from_utf8(data)?;

	let document = Rc::new(XmlDocument::new(xml, |xml| {
		let opt = roxmltree::ParsingOptions {
			allow_dtd: true,
			..Default::default()
		};
		roxmltree::Document::parse_with_options(xml, opt).unwrap()
	}));

	let root = document.borrow_doc().root();
	let tag_layout = require_tag_by_name(&root, "layout")?;

	let file = ParserFile {
		path: asset_path.to_owned(),
		document: document.clone(),
		template_parameters: Default::default(),
	};

	Ok((file, tag_layout.id()))
}

fn parse_document_root(
	file: &ParserFile,
	ctx: &mut ParserContext,
	parent_id: WidgetID,
	node_layout: roxmltree::NodeId,
) -> anyhow::Result<()> {
	let node_layout = file
		.document
		.borrow_doc()
		.get_node(node_layout)
		.ok_or_else(|| anyhow::anyhow!("layout node not found"))?;

	for child_node in node_layout.children() {
		#[allow(clippy::single_match)]
		match child_node.tag_name().name() {
			/*  topmost include directly in <layout>  */
			"include" => parse_tag_include(file, ctx, parent_id, &raw_attribs(&child_node))?,
			"theme" => parse_tag_theme(ctx, child_node),
			"template" => parse_tag_template(file, ctx, child_node),
			"macro" => parse_tag_macro(file, ctx, child_node),
			_ => {}
		}
	}

	if let Some(tag_elements) = get_tag_by_name(&node_layout, "elements") {
		parse_children(file, ctx, tag_elements, parent_id)?;
	}

	Ok(())
}
