// Copyright © 2026 Stephan Kunz
//! Implementation of the [`XmlReader`](crate::xml_reader::XmlReader)s core type `XmlElement`.

use core::ops::Range;

// ── low-level helpers ────────────────────────────────────────────────────────

pub(crate) fn skip_whitespace(text: &str, mut pos: usize) -> usize {
	let b = text.as_bytes();
	while pos < b.len() && matches!(b[pos], b' ' | b'\t' | b'\n' | b'\r') {
		pos += 1;
	}
	pos
}

/// Search `bytes[start..]` for `needle`; return absolute index of first byte.
pub(crate) fn find_bytes(bytes: &[u8], start: usize, needle: &[u8]) -> Option<usize> {
	if needle.is_empty() {
		return Some(start);
	}
	let end = bytes.len().checked_sub(needle.len() - 1)?;
	if start >= end {
		return None;
	}
	bytes[start..end + needle.len() - 1]
		.windows(needle.len())
		.position(|w| w == needle)
		.map(|p| p + start)
}

/// Scan from `start` (position after `<`) for the unquoted `>` that closes a
/// tag.  Returns the index *after* `>`.
pub(crate) fn find_tag_end(text: &str, start: usize) -> Option<usize> {
	let b = text.as_bytes();
	let mut i = start;
	let mut q: u8 = 0;
	while i < b.len() {
		match b[i] {
			c @ (b'"' | b'\'') if q == 0 => q = c,
			c if c == q => q = 0,
			b'>' if q == 0 => return Some(i + 1),
			_ => {}
		}
		i += 1;
	}
	None
}

/// Extract the tag name starting at `pos` (first char after `<` or `</`).
pub(crate) fn extract_tag_name(text: &str, pos: usize) -> &str {
	let b = text.as_bytes();
	let mut end = pos;
	while end < b.len() && !matches!(b[end], b' ' | b'\t' | b'\n' | b'\r' | b'>' | b'/' | b'?') {
		end += 1;
	}
	&text[pos..end]
}

fn self_closing(bytes: &[u8], tag_end: usize) -> bool {
	tag_end >= 2 && bytes[tag_end - 2] == b'/'
}

/// Find the range of the close tag `</tag_name>` that matches the open tag
/// ending at `start`, counting nested same-named elements.
fn find_close_tag(text: &str, start: usize, tag_name: &str) -> Option<Range<usize>> {
	let b = text.as_bytes();
	let tag = tag_name.as_bytes();
	let mut pos = start;
	let mut depth: usize = 1;

	while pos < b.len() {
		let lt = b[pos..].iter().position(|&c| c == b'<')?;
		pos += lt;

		// Comment
		if b.get(pos + 1..pos + 4) == Some(b"!--") {
			pos = find_bytes(b, pos + 4, b"-->")? + 3;
			continue;
		}
		// PI
		if b.get(pos + 1) == Some(&b'?') {
			pos = find_bytes(b, pos + 2, b"?>")? + 2;
			continue;
		}
		// CDATA
		if b.get(pos + 1..pos + 9) == Some(b"![CDATA[") {
			pos = find_bytes(b, pos + 9, b"]]>")? + 3;
			continue;
		}

		if b.get(pos + 1) == Some(&b'/') {
			// Close tag — check if it matches our name
			let ns = pos + 2;
			if b.get(ns..ns + tag.len()) == Some(tag)
				&& matches!(
					b.get(ns + tag.len()),
					Some(&b'>') | Some(&b' ') | Some(&b'\t') | Some(&b'\n') | Some(&b'\r')
				) {
				depth -= 1;
				if depth == 0 {
					let gt = b[ns + tag.len()..]
						.iter()
						.position(|&c| c == b'>')?;
					return Some(pos..ns + tag.len() + gt + 1);
				}
			}
			// Skip past this close tag
			pos += b[pos..].iter().position(|&c| c == b'>')? + 1;
			continue;
		}

		// Opening tag — depth-track only same name
		let ns = pos + 1;
		if b.get(ns..ns + tag.len()) == Some(tag)
			&& matches!(
				b.get(ns + tag.len()),
				Some(&b'>') | Some(&b' ') | Some(&b'\t') | Some(&b'\n') | Some(&b'\r') | Some(&b'/')
			) {
			let te = find_tag_end(text, ns)?;
			if !self_closing(b, te) {
				depth += 1;
			}
			pos = te;
			continue;
		}

		// Any other opening tag: skip its opening marker
		pos = find_tag_end(text, pos + 1)?;
	}

	None
}

/// Parse one node starting at `pos` (must point to `<`).
/// `content_end` is the limit for sibling searches on the returned element.
pub(crate) fn parse_element_at(xml_text: &str, pos: usize, content_end: usize) -> Option<XmlElement<'_>> {
	let b = xml_text.as_bytes();
	if b.get(pos) != Some(&b'<') {
		return None;
	}

	// Comment
	if b.get(pos + 1..pos + 4) == Some(b"!--") {
		let end = find_bytes(b, pos + 4, b"-->")? + 3;
		return Some(XmlElement {
			xml_text,
			open: pos..end,
			close: end..end,
			element_type: XmlElementType::Comment,
			content_end,
		});
	}

	// PI
	if b.get(pos + 1) == Some(&b'?') {
		let end = find_bytes(b, pos + 2, b"?>")? + 2;
		return Some(XmlElement {
			xml_text,
			open: pos..end,
			close: end..end,
			element_type: XmlElementType::PI,
			content_end,
		});
	}

	// CDATA (exposed as Text)
	if b.get(pos + 1..pos + 9) == Some(b"![CDATA[") {
		let end = find_bytes(b, pos + 9, b"]]>")? + 3;
		return Some(XmlElement {
			xml_text,
			open: pos..end,
			close: end..end,
			element_type: XmlElementType::Text,
			content_end,
		});
	}

	// Close tag — caller should not land here
	if b.get(pos + 1) == Some(&b'/') {
		return None;
	}

	// Regular element
	let tag_end = find_tag_end(xml_text, pos + 1)?;
	if self_closing(b, tag_end) {
		return Some(XmlElement {
			xml_text,
			open: pos..tag_end,
			close: tag_end..tag_end,
			element_type: XmlElementType::Element,
			content_end,
		});
	}

	let tag_name = extract_tag_name(xml_text, pos + 1);
	let close = find_close_tag(xml_text, tag_end, tag_name)?;
	Some(XmlElement {
		xml_text,
		open: pos..tag_end,
		close,
		element_type: XmlElementType::Element,
		content_end,
	})
}

// ── public types ─────────────────────────────────────────────────────────────

/// An XML node produced by zero-copy parsing.
///
/// All string slices borrow from the original source passed to
/// [`XmlReader::parse`](crate::XmlReader::parse).
#[derive(Clone)]
pub struct XmlElement<'xml> {
	xml_text: &'xml str,
	/// Range of the opening tag (or the whole node for self-closing / leaf nodes).
	open: Range<usize>,
	/// Range of the closing tag, or `open.end..open.end` when absent.
	close: Range<usize>,
	element_type: XmlElementType,
	/// Upper bound for sibling navigation (parent's close-tag start, or doc len).
	content_end: usize,
}

impl<'xml> XmlElement<'xml> {
	/// Value of attribute `name`, or `None` if the attribute is absent.
	pub fn attribute(&self, name: &str) -> Option<&'xml str> {
		self.attributes()
			.find(|a| a.name() == name)
			.map(|a| a.value())
	}

	/// Iterator over the element's attributes (empty for non-Element nodes).
	pub fn attributes(&self) -> XmlAttributes<'xml> {
		if !matches!(self.element_type, XmlElementType::Element) {
			return XmlAttributes {
				xml_text: self.xml_text,
				pos: 0,
				end: 0,
			};
		}
		let name_len = extract_tag_name(self.xml_text, self.open.start + 1).len();
		let attr_start = self.open.start + 1 + name_len;
		// attr_end is just before the final '>'
		let attr_end = self.open.end.saturating_sub(1);
		XmlAttributes {
			xml_text: self.xml_text,
			pos: attr_start,
			end: attr_end,
		}
	}

	/// Tag name of an Element node, or `""` for all other node types.
	pub fn tag_name(&self) -> &'xml str {
		match self.element_type {
			XmlElementType::Element => extract_tag_name(self.xml_text, self.open.start + 1),
			_ => "",
		}
	}

	/// First child node, or `None` for leaf / self-closing elements.
	pub fn first_child(&self) -> Option<XmlElement<'xml>> {
		if self.close.start <= self.open.end {
			return None;
		}
		let b = self.xml_text.as_bytes();
		let pos = skip_whitespace(self.xml_text, self.open.end);
		if pos >= self.close.start {
			return None;
		}
		if b[pos] == b'<' {
			if b.get(pos + 1) == Some(&b'/') {
				return None;
			}
			parse_element_at(self.xml_text, pos, self.close.start)
		} else {
			// Text node
			let mut end = pos;
			while end < self.close.start && b[end] != b'<' {
				end += 1;
			}
			Some(XmlElement {
				xml_text: self.xml_text,
				open: pos..end,
				close: end..end,
				element_type: XmlElementType::Text,
				content_end: self.close.start,
			})
		}
	}

	/// Next sibling node, or `None` when no more siblings exist.
	pub fn next_sibling(&self) -> Option<XmlElement<'xml>> {
		let after = if self.close.is_empty() {
			self.open.end
		} else {
			self.close.end
		};
		let b = self.xml_text.as_bytes();
		let pos = skip_whitespace(self.xml_text, after);
		if pos >= self.content_end {
			return None;
		}
		if b[pos] == b'<' {
			if b.get(pos + 1) == Some(&b'/') {
				return None;
			}
			parse_element_at(self.xml_text, pos, self.content_end)
		} else {
			// Text node
			let mut end = pos;
			while end < self.content_end && b[end] != b'<' {
				end += 1;
			}
			Some(XmlElement {
				xml_text: self.xml_text,
				open: pos..end,
				close: end..end,
				element_type: XmlElementType::Text,
				content_end: self.content_end,
			})
		}
	}

	/// Iterator over all direct child nodes (elements, text, comments, PIs).
	pub fn children(&self) -> XmlChildren<'xml> {
		if self.close.start <= self.open.end {
			return XmlChildren {
				xml_text: self.xml_text,
				pos: self.open.end,
				end: self.open.end,
			};
		}
		XmlChildren {
			xml_text: self.xml_text,
			pos: self.open.end,
			end: self.close.start,
		}
	}

	/// Element type.
	pub fn element_type(&self) -> XmlElementType {
		self.element_type
	}

	/// Text content of Text / CDATA nodes, or the comment body (without
	/// `<!--`/`-->`) for Comment nodes.  Returns `""` for all other types.
	pub fn text(&self) -> &'xml str {
		match self.element_type {
			XmlElementType::Text => &self.xml_text[self.open.clone()],
			XmlElementType::Comment => {
				let s = &self.xml_text[self.open.clone()];
				s.trim_start_matches("<!--")
					.trim_end_matches("-->")
			}
			_ => "",
		}
	}

	/// Byte range of this node in the source text.
	pub fn range(&self) -> Range<usize> {
		let end = if self.close.is_empty() {
			self.open.end
		} else {
			self.close.end
		};
		self.open.start..end
	}
}

// ── element type ─────────────────────────────────────────────────────────────

/// Discriminant for [`XmlElement`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum XmlElementType {
	/// `<TagName attr="val">…</TagName>` or `<TagName/>`.
	Element,
	/// `<!-- … -->`
	Comment,
	/// Character data or CDATA section content.
	Text,
	/// `<?target content?>`
	PI,
}

// ── attribute ────────────────────────────────────────────────────────────────

/// A single `name="value"` pair inside an element's opening tag.
pub struct XmlAttribute<'xml> {
	xml_text: &'xml str,
	name: Range<usize>,
	value: Range<usize>,
}

impl<'xml> XmlAttribute<'xml> {
	/// Attribute name.
	pub fn name(&self) -> &'xml str {
		&self.xml_text[self.name.clone()]
	}

	/// Attribute value, without surrounding quotes.
	pub fn value(&self) -> &'xml str {
		&self.xml_text[self.value.clone()]
	}
}

// ── iterators ────────────────────────────────────────────────────────────────

/// Iterator over attributes in an element's opening tag.
pub struct XmlAttributes<'xml> {
	xml_text: &'xml str,
	pos: usize,
	end: usize,
}

impl<'xml> Iterator for XmlAttributes<'xml> {
	type Item = XmlAttribute<'xml>;

	fn next(&mut self) -> Option<Self::Item> {
		let b = self.xml_text.as_bytes();
		// Skip whitespace
		while self.pos < self.end && matches!(b[self.pos], b' ' | b'\t' | b'\n' | b'\r') {
			self.pos += 1;
		}
		if self.pos >= self.end || matches!(b[self.pos], b'/' | b'>') {
			return None;
		}
		// Name
		let name_start = self.pos;
		while self.pos < self.end && !matches!(b[self.pos], b'=' | b' ' | b'\t' | b'\n' | b'\r') {
			self.pos += 1;
		}
		let name_end = self.pos;
		// Advance to '='
		while self.pos < self.end && b[self.pos] != b'=' {
			self.pos += 1;
		}
		if self.pos >= self.end {
			return None;
		}
		self.pos += 1; // skip '='
		// Skip whitespace
		while self.pos < self.end && matches!(b[self.pos], b' ' | b'\t' | b'\n' | b'\r') {
			self.pos += 1;
		}
		if self.pos >= self.end {
			return None;
		}
		let quote = b[self.pos];
		if quote != b'"' && quote != b'\'' {
			return None;
		}
		self.pos += 1; // skip opening quote
		let val_start = self.pos;
		while self.pos < self.end && b[self.pos] != quote {
			self.pos += 1;
		}
		if self.pos >= self.end {
			return None;
		}
		let val_end = self.pos;
		self.pos += 1; // skip closing quote
		Some(XmlAttribute {
			xml_text: self.xml_text,
			name: name_start..name_end,
			value: val_start..val_end,
		})
	}
}

/// Iterator over the direct child nodes of an element.
pub struct XmlChildren<'xml> {
	xml_text: &'xml str,
	pos: usize,
	end: usize,
}

impl<'xml> Iterator for XmlChildren<'xml> {
	type Item = XmlElement<'xml>;

	fn next(&mut self) -> Option<Self::Item> {
		let b = self.xml_text.as_bytes();
		// Skip whitespace
		while self.pos < self.end && matches!(b[self.pos], b' ' | b'\t' | b'\n' | b'\r') {
			self.pos += 1;
		}
		if self.pos >= self.end {
			return None;
		}
		if b[self.pos] == b'<' && b.get(self.pos + 1) == Some(&b'/') {
			return None;
		}
		if b[self.pos] == b'<' {
			let elem = parse_element_at(self.xml_text, self.pos, self.end)?;
			self.pos = if elem.close.is_empty() {
				elem.open.end
			} else {
				elem.close.end
			};
			return Some(elem);
		}
		// Text node (reached only when `pos < end` and `b[pos] != '<'`,
		// so the loop below always advances `pos` past `text_start`).
		let text_start = self.pos;
		while self.pos < self.end && b[self.pos] != b'<' {
			self.pos += 1;
		}
		Some(XmlElement {
			xml_text: self.xml_text,
			open: text_start..self.pos,
			close: self.pos..self.pos,
			element_type: XmlElementType::Text,
			content_end: self.end,
		})
	}
}

#[cfg(test)]
mod tests {
	use super::*;

	const fn is_normal<T: Sized + Send + Sync>() {}

	#[test]
	const fn normal_types() {
		is_normal::<XmlElement>();
		is_normal::<&XmlElement>();
		is_normal::<XmlElementType>();
		is_normal::<&XmlElementType>();
	}

	// ── low-level helper branches that the public API cannot reach ──────────

	#[test]
	fn find_bytes_empty_needle() {
		assert_eq!(find_bytes(b"abc", 2, b""), Some(2));
	}

	#[test]
	fn find_bytes_needle_larger_than_haystack() {
		// `bytes.len() < needle.len() - 1` triggers the `checked_sub` None branch.
		assert_eq!(find_bytes(b"", 0, b"abc"), None);
		assert_eq!(find_bytes(b"x", 0, b"abc"), None);
	}

	#[test]
	fn parse_element_at_rejects_non_open_bracket() {
		assert!(parse_element_at("abc", 0, 3).is_none());
	}

	#[test]
	fn parse_element_at_rejects_close_tag() {
		assert!(parse_element_at("</a>", 0, 4).is_none());
	}

	#[test]
	fn parse_element_at_fails_on_unclosed_comment() {
		assert!(parse_element_at("<!--x", 0, 5).is_none());
	}

	#[test]
	fn parse_element_at_fails_on_unclosed_pi() {
		assert!(parse_element_at("<?x", 0, 3).is_none());
	}

	#[test]
	fn parse_element_at_fails_on_unclosed_cdata() {
		assert!(parse_element_at("<![CDATA[x", 0, 10).is_none());
	}

	#[test]
	fn parse_element_at_fails_on_unclosed_tag() {
		// `find_tag_end` returns None — no `>` in the input.
		assert!(parse_element_at("<unterminated", 0, 13).is_none());
	}

	#[test]
	fn parse_element_at_fails_on_unmatched_close() {
		// Tag opens but no matching close tag exists.
		assert!(parse_element_at("<a>body", 0, 7).is_none());
	}

	#[test]
	fn parse_element_at_fails_on_close_tag_with_no_gt() {
		// The close tag's name matches but the `>` is missing.
		assert!(parse_element_at("<a>x</a   ", 0, 10).is_none());
	}

	#[test]
	fn parse_element_at_fails_on_foreign_close_tag_with_no_gt() {
		// A different close tag is encountered whose `>` is missing.
		assert!(parse_element_at("<a>x</y", 0, 7).is_none());
	}

	#[test]
	fn parse_element_at_fails_on_nested_same_name_unclosed() {
		// Same-named nested open tag whose own `>` is unreachable (unterminated
		// attribute quote) — exercises `find_tag_end` failure inside the
		// same-name branch of `find_close_tag`.
		assert!(parse_element_at("<a><a attr=\"unclosed", 0, 20).is_none());
	}

	#[test]
	fn parse_element_at_fails_on_foreign_open_tag_unclosed() {
		// Different-named open tag whose `>` is unreachable — exercises the
		// fallback `find_tag_end` call in `find_close_tag`.
		assert!(parse_element_at("<a><b attr=\"unclosed", 0, 20).is_none());
	}

	#[test]
	fn parse_element_at_fails_on_unclosed_comment_in_body() {
		// `find_close_tag` encounters `<!--` inside the element body and
		// `find_bytes` for `-->` returns None.
		assert!(parse_element_at("<a><!-- unclosed", 0, 16).is_none());
	}

	#[test]
	fn parse_element_at_fails_on_unclosed_pi_in_body() {
		// `find_close_tag` encounters `<?` inside the element body and
		// `find_bytes` for `?>` returns None.
		assert!(parse_element_at("<a><?unclosed", 0, 13).is_none());
	}

	#[test]
	fn parse_element_at_fails_on_unclosed_cdata_in_body() {
		// `find_close_tag` encounters `<![CDATA[` inside the body and
		// `find_bytes` for `]]>` returns None.
		assert!(parse_element_at("<a><![CDATA[unclosed", 0, 20).is_none());
	}

	// ── defensive branches reachable only by constructing nodes directly ────

	#[test]
	fn first_child_returns_none_when_body_starts_with_close_tag() {
		// Constructed element whose body (between open.end and close.start)
		// begins with `</` — can't occur in well-formed parse output, but the
		// guard exists.
		let xml = "<a></a></a>";
		let elem = XmlElement {
			xml_text: xml,
			open: 0..3,
			close: 7..11,
			element_type: XmlElementType::Element,
			content_end: xml.len(),
		};
		assert!(elem.first_child().is_none());
	}

	#[test]
	fn children_iter_returns_none_on_leading_close_tag() {
		let xml = "</a>";
		let mut iter = XmlChildren {
			xml_text: xml,
			pos: 0,
			end: xml.len(),
		};
		assert!(iter.next().is_none());
	}

	#[test]
	fn attributes_iter_returns_none_on_unterminated_quote() {
		// attr_end sits past the opening quote, but there is no closing quote
		// before it — the scan walks off the end.
		let xml = " attr=\"value";
		let mut iter = XmlAttributes {
			xml_text: xml,
			pos: 0,
			end: xml.len(),
		};
		assert!(iter.next().is_none());
	}

	#[test]
	fn public_api_smoke() {
		// Exercises every public method on XmlElement, XmlAttribute and the
		// iterators from inside the crate, so coverage-per-instantiation of the
		// in-crate build is non-zero.
		use crate::XmlReader;

		let xml = "<root a=\"v\"><!-- c --><child/>text</root>";
		let reader = XmlReader::parse(xml);
		let root = reader.root_element().unwrap();

		let _ = root.clone();
		assert_eq!(root.tag_name(), "root");
		assert_eq!(root.element_type(), XmlElementType::Element);
		assert_eq!(root.attribute("a"), Some("v"));
		assert_eq!(root.text(), "");
		assert_eq!(&xml[root.range()], xml);
		let mut attrs = root.attributes();
		let a0 = attrs.next().unwrap();
		assert_eq!(a0.name(), "a");
		assert_eq!(a0.value(), "v");
		assert!(attrs.next().is_none());

		let first = root.first_child().unwrap();
		assert_eq!(first.element_type(), XmlElementType::Comment);
		let second = first.next_sibling().unwrap();
		assert_eq!(second.tag_name(), "child");
		assert_eq!(root.children().count(), 3);
	}

	#[test]
	fn children_iter_returns_none_when_parse_fails() {
		// `parse_element_at` fails (unclosed tag), so `?` inside the iterator
		// short-circuits.
		let xml = "<unclosed";
		let mut iter = XmlChildren {
			xml_text: xml,
			pos: 0,
			end: xml.len(),
		};
		assert!(iter.next().is_none());
	}
}
