// Small DOM helpers shared by the transforms.
//
// The Python scripts these were ported from parse the roster into a tree of
// text offsets and edit the file as text, so that everything they do not touch
// survives byte for byte.  In the browser there is nothing to preserve: the
// document goes straight into the roster parser and is never written back out,
// so the transforms mutate the DOM directly and all of that machinery
// (Node/Edit/apply_edits/attr escaping/XML re-validation) is gone.

// Roster files declare a default namespace (the BattleScribe roster schema), so
// everything here matches on `localName` and creates elements in the document's
// own namespace.  Matching on `tagName` would still work for these files, but
// only because nothing in them carries a prefix.

/** Element children of `node`, as a real array (safe to mutate while iterating). */
export const elementChildren = (node) => Array.from(node.children);

/** The first element child named `name`, or null. */
export const childElement = (node, name) =>
	elementChildren(node).find((child) => child.localName === name) ?? null;

/** Every descendant element named `name`, in document order. */
export const descendants = (node, name) =>
	Array.from(node.getElementsByTagNameNS("*", name));

/** Create an element in the same namespace as the document's root. */
export const createElement = (doc, name) =>
	doc.createElementNS(doc.documentElement.namespaceURI, name);

/**
 * Remove `node`, taking the whitespace that only separated it from its sibling.
 *
 * Rosters come out of BattleScribe on a single line, so usually there is no
 * whitespace to take; this only keeps the result tidy for a roster that has
 * been pretty-printed at some point.
 */
export function removeElement(node) {
	const previous = node.previousSibling;
	const TEXT_NODE = 3;
	if (
		previous &&
		previous.nodeType === TEXT_NODE &&
		previous.data.trim() === ""
	) {
		previous.remove();
	}
	node.remove();
}

/**
 * Insert `child` into `parent`, keeping `order` among the containers listed in
 * it.  A container not mentioned in `order` is appended.
 */
export function insertOrdered(parent, child, order) {
	const rank = order.indexOf(child.localName);
	if (rank !== -1) {
		const before = elementChildren(parent).find((existing) => {
			const existingRank = order.indexOf(existing.localName);
			return existingRank !== -1 && existingRank > rank;
		});
		if (before) {
			parent.insertBefore(child, before);
			return;
		}
	}
	parent.appendChild(child);
}

/**
 * Joins the parts of a key or a generated id, so that ("ab", "c") and
 * ("a", "bc") cannot collide.
 *
 * ASCII unit separator: a control character cannot occur in a unit name, an
 * ability name or an id, so nothing in a roster can contain it and no pair of
 * different parts can ever join to the same string.
 */
export const KEY_SEPARATOR = String.fromCharCode(31);

/**
 * A stable id derived from `parts`, shaped like BattleScribe's
 * ``xxxx-xxxx-xxxx-xxxx``.
 *
 * Deterministic so that re-running against an updated roster produces the same
 * ids for the same ability rather than churning them, which is what lets the
 * merge recognise two generated profiles as the same catalogue object.  This is
 * a plain string hash rather than the SHA-1 the Python script uses: the ids only
 * have to be unique and stable within one document, and `crypto.subtle` is
 * async, which would push a promise through every call site for no gain.
 */
export function makeId(...parts) {
	const input = parts.join(KEY_SEPARATOR);
	// Two independent FNV-1a passes, giving 64 bits of output.
	let h1 = 0x811c9dc5;
	let h2 = 0x01000193;
	for (let i = 0; i < input.length; i++) {
		const code = input.charCodeAt(i);
		h1 = Math.imul(h1 ^ code, 0x01000193) >>> 0;
		h2 = Math.imul(h2 ^ code, 0x811c9dc5) >>> 0;
	}
	const hex =
		h1.toString(16).padStart(8, "0") + h2.toString(16).padStart(8, "0");
	return `${hex.slice(0, 4)}-${hex.slice(4, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}`;
}

/**
 * The datasheet an element sits on, for the run report: the outermost
 * `<selection>` above it, since that is the force-level unit.
 */
export function enclosingUnit(node) {
	let unit = null;
	for (
		let current = node.parentElement;
		current;
		current = current.parentElement
	) {
		if (current.localName === "selection") {
			unit = current.getAttribute("name") ?? "?";
		} else if (current.localName === "force" && !unit) {
			return current.getAttribute("name") ?? "Force";
		}
	}
	return unit ?? "Roster";
}
