use core::fmt;

use crate::Node;

#[derive(Clone, Debug, PartialEq)]
pub struct Tag {
	pub name: String,
	/// Everything inside the tag that's not it's name. Includes a
	/// self-close if there is one.
	pub body: Option<String>,
	pub self_closing: bool,
	pub children: Vec<Node>,
}

impl Tag {
	pub fn self_closing(&self) -> bool {
		self.self_closing
	}

	pub fn get_attribute<'a>(&'a self, key: &str) -> Option<&'a str> {
		let body = match self.body.as_deref() {
			None => return None,
			Some(body) => body,
		};

		// get rid of potential self-close
		let trimmed = if let Some(suffix) = body.trim().strip_suffix('/') {
			suffix
		} else {
			body.trim()
		};

		let mut wrk = trimmed;
		loop {
			let key_end_idx = wrk.find(|c: char| c == ' ' || c == '=');

			match key_end_idx {
				None => {
					// boolean ends body
					if wrk == key {
						return Some("");
					} else {
						break;
					}
				}
				Some(idx) => match &wrk[idx..idx + 1] {
					" " => {
						// boolean
						if &wrk[..idx] == key {
							return Some("");
						} else {
							wrk = &wrk[idx + 1..];
						}
					}
					"=" => {
						// key-value
						let found_name = &wrk[..idx];

						// we're just assuming the attributes are properly
						// formed right now. Skips the `=` and the `"` that
						// should be there but we don't check for
						wrk = &wrk[idx + 2..];
						let end = wrk.find('"').unwrap();
						let value = &wrk[..end];
						wrk = &wrk[end + 1..].trim_start();

						if found_name == key {
							return Some(value);
						}
					}
					_ => unreachable!(),
				},
			}
		}

		None
	}

	pub fn id(&self) -> Option<&str> {
		match self.get_attribute("id") {
			None => None,
			Some("") => None,
			Some(id) => Some(id),
		}
	}

	pub fn has_class(&self, name: &str) -> bool {
		match self.get_attribute("class") {
			None => false,
			Some(classes) => {
				for class in classes.split(' ') {
					if class == name {
						return true;
					}
				}

				false
			}
		}
	}

	pub fn append_child<N: Into<Node>>(&mut self, node: N) {
		let node = node.into();
		self.children.push(node);
	}

	/// Replace all children with one [Node::Text]
	pub fn set_inner_text<S: Into<String>>(&mut self, txt: S) {
		self.children = vec![Node::Text(txt.into())];
	}

	pub fn child_tags(&self) -> TagIterator {
		TagIterator {
			inner: self.children.iter(),
		}
	}

	pub fn child_tags_mut(&mut self) -> TagIteratorMut {
		TagIteratorMut {
			inner: self.children.iter_mut(),
		}
	}

	pub fn has_tag(&self, tag: &str) -> bool {
		self.child_tags().any(|t| t.name == tag)
	}

	pub fn by_tag_mut<'a>(&'a mut self, looking: &str) -> Option<&'a mut Tag> {
		for tag in self.child_tags_mut() {
			if tag.name == looking {
				return Some(tag);
			}

			if let Some(found) = tag.by_tag_mut(looking) {
				return Some(found);
			}
		}

		None
	}
}

impl fmt::Display for Tag {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		let Tag {
			name,
			body,
			self_closing,
			children,
		} = self;

		let formatted_body = if let Some(body) = body {
			format!(" {body}")
		} else {
			String::from("")
		};

		if *self_closing {
			// we ignore our children if we're self-closing.
			write!(f, "<{name} {}/>", body.as_deref().unwrap_or_default())
		} else {
			write!(f, "<{name}{formatted_body}>")?;
			for child in children {
				write!(f, "{}", child)?;
			}
			write!(f, "</{name}>")
		}
	}
}

pub struct TagIterator<'a> {
	pub(crate) inner: std::slice::Iter<'a, Node>,
}

impl<'a> Iterator for TagIterator<'a> {
	type Item = &'a Tag;

	fn next(&mut self) -> Option<Self::Item> {
		loop {
			match self.inner.next() {
				None => break None,
				Some(Node::Tag(ref tag)) => break Some(tag),
				Some(_) => continue,
			}
		}
	}
}

pub struct TagIteratorMut<'a> {
	pub(crate) inner: std::slice::IterMut<'a, Node>,
}

impl<'a> Iterator for TagIteratorMut<'a> {
	type Item = &'a mut Tag;

	fn next(&mut self) -> Option<Self::Item> {
		loop {
			match self.inner.next() {
				None => break None,
				Some(Node::Tag(ref mut tag)) => break Some(tag),
				Some(_) => continue,
			}
		}
	}
}

#[cfg(test)]
mod test {
	use crate::Tag;

	#[test]
	fn tag_finds_boolen_attribute() {
		let tag = Tag {
			name: "div".into(),
			body: Some("contenteditable".into()),
			self_closing: false,
			children: vec![],
		};
		assert!(tag.get_attribute("contenteditable").is_some())
	}

	#[test]
	fn tag_finds_kv_attribute() {
		let tag = Tag {
			name: "script".into(),
			body: Some("src=\"script.js\"".into()),
			self_closing: false,
			children: vec![],
		};
		assert_eq!(tag.get_attribute("src"), Some("script.js"))
	}

	#[test]
	fn tag_finds_boolean_in_centre() {
		let tag = Tag {
			name: "div".into(),
			body: Some("id=\"divy\" contenteditable style=\"display: none;\"".into()),
			self_closing: false,
			children: vec![],
		};
		assert!(tag.get_attribute("contenteditable").is_some());
	}
}