// Strip Leader/Support abilities from a roster.  Ported from
// remove_leader_abilities.py; see README.md on the fixtures.
//
// A printed datasheet is a reference card you hold during a game, and the
// Leader / Support abilities are not something you look up mid-turn: they only
// say which units a character may attach to.  Once the army is built that
// decision is already made, and the block is long enough to push the rules you
// *do* need off the card.
//
// Each ability arrives from the catalogue in two places - an ``Abilities``
// profile holding the "This model can be attached to the following units"
// text, and a same-named ``rule`` that puts *Leader* on the datasheet's RULES
// line and its full text in the rules appendix - so both are taken out.
// Containers left empty by the removal (``<profiles>``, ``<rules>``) are
// dropped too, rather than left behind as empty elements.
//
// Points are deliberately left alone.  Removing a ``<cost>`` only removes it
// from the roster; the renderer prints the title bar unconditionally, so a
// stripped unit would read ``0pts`` rather than nothing - worse than just
// leaving the cost in.

import { ABILITY_TYPE_NAME, PRUNABLE_CONTAINERS } from "./config.js";
import { elementChildren, enclosingUnit, removeElement } from "./dom.js";

/** Should this element be stripped in its own right? */
const isTarget = (node, strip) => {
	const name = node.getAttribute("name");
	if (node.localName === "rule") return strip.has(name);
	if (node.localName === "profile") {
		return (
			node.getAttribute("typeName") === ABILITY_TYPE_NAME && strip.has(name)
		);
	}
	return false;
};

const describe = (node) =>
	`${node.localName === "profile" ? "ability" : "rule"}: ${
		node.getAttribute("name") ?? "?"
	}`;

/**
 * Flag `node` and its descendants for removal, bottom-up, returning whether
 * `node` itself is going.
 *
 * A container goes when everything it held is going: a unit whose only ability
 * was *Leader* should not be left with an empty ``<profiles/>``.
 */
function markDoomed(node, strip, doomed) {
	if (isTarget(node, strip)) {
		doomed.add(node);
		return true;
	}

	// Not `.every()` - every child has to be visited, not just until one stays.
	const children = elementChildren(node);
	const gone = children.map((child) => markDoomed(child, strip, doomed));

	if (
		PRUNABLE_CONTAINERS.includes(node.localName) &&
		gone.length > 0 &&
		gone.every(Boolean)
	) {
		doomed.add(node);
		return true;
	}
	return false;
}

/** The strip targets inside a doomed subtree, for the run report. */
function* iterTargets(node, strip) {
	if (isTarget(node, strip)) {
		yield node;
		return;
	}
	for (const child of elementChildren(node)) yield* iterTargets(child, strip);
}

/**
 * The outermost doomed elements, in document order.
 *
 * Doomed-ness propagates downwards - a container is only doomed when every
 * child is - so removing these removes everything, and each one is a single
 * entry in the report rather than one per profile inside it.
 */
function collectOutermost(node, doomed, found) {
	if (doomed.has(node)) {
		found.push(node);
		return;
	}
	for (const child of elementChildren(node)) {
		collectOutermost(child, doomed, found);
	}
}

/**
 * Remove the Leader/Support abilities from `doc`, in place.
 *
 * Returns a report of `{ unit: [what was removed] }`; an empty report means
 * there was nothing to strip (already stripped, or no such abilities).
 */
export function removeLeaderAbilities(doc, config) {
	const strip = new Set(config.abilitiesToStrip);
	const root = doc.documentElement;
	if (!root) return {};

	const doomed = new Set();
	markDoomed(root, strip, doomed);

	const outermost = [];
	collectOutermost(root, doomed, outermost);

	// The report is built before anything is removed, so `enclosingUnit` can
	// still walk up to the datasheet the element sat on.
	const report = {};
	for (const node of outermost) {
		const unit = enclosingUnit(node);
		report[unit] = [
			...(report[unit] ?? []),
			...Array.from(iterTargets(node, strip), describe),
		];
	}

	for (const node of outermost) removeElement(node);

	return report;
}
