const addIdsToHeadings = (container: HTMLElement, headingFilter: string | null): void => {
	const selected = headingFilter ? headingFilter : "h1, h2, h3, h4, h5, h6";
	const headings = container.querySelectorAll<HTMLElement>(selected);

	headings.forEach((heading, index) => {
		const text = heading.textContent?.trim();
		if (!text) return;

		const id = `heading-${index + 1}`;
		if (heading.dataset.griddoid !== id) {
			heading.dataset.griddoid = id;
		}
	});
};

const observeAndAddIdsToHeadings = (container: HTMLElement, headingFilter: string | null): (() => void) => {
	addIdsToHeadings(container, headingFilter);

	const observer = new MutationObserver(() => {
		addIdsToHeadings(container, headingFilter);
	});

	observer.observe(container, {
		childList: true,
		subtree: true,
	});

	return () => observer.disconnect();
};

const KEYWORD_HIGHLIGHT_CLASS = "gdd-keyword";

const removeKeywordHighlights = (container: HTMLElement): void => {
	const marks = container.querySelectorAll<HTMLElement>(`mark.${KEYWORD_HIGHLIGHT_CLASS}`);

	marks.forEach((mark) => {
		const parent = mark.parentNode;
		if (!parent) return;
		parent.replaceChild(document.createTextNode(mark.textContent || ""), mark);
		parent.normalize();
	});
};

const buildKeywordsRegex = (keywords: string[]): RegExp | null => {
	const escapedKeywords = keywords
		.filter((k) => k.trim())
		.sort((a, b) => b.length - a.length)
		.map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));

	if (!escapedKeywords.length) return null;

	return new RegExp(`(${escapedKeywords.join("|")})`, "gi");
};

const collectTextNodes = (container: HTMLElement): Text[] => {
	const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, {
		acceptNode: (node) => {
			const parent = node.parentElement;
			if (!parent) return NodeFilter.FILTER_REJECT;
			if (["SCRIPT", "STYLE"].includes(parent.tagName)) return NodeFilter.FILTER_REJECT;
			if (parent.classList.contains(KEYWORD_HIGHLIGHT_CLASS)) return NodeFilter.FILTER_REJECT;
			return NodeFilter.FILTER_ACCEPT;
		},
	});

	const textNodes: Text[] = [];
	let current = walker.nextNode();
	while (current) {
		textNodes.push(current as Text);
		current = walker.nextNode();
	}

	return textNodes;
};

const wrapMatches = (text: string, regex: RegExp): DocumentFragment => {
	const fragment = document.createDocumentFragment();
	let lastIndex = 0;
	let match = regex.exec(text);

	while (match !== null) {
		if (match.index > lastIndex) {
			fragment.appendChild(document.createTextNode(text.slice(lastIndex, match.index)));
		}

		const mark = document.createElement("mark");
		mark.className = KEYWORD_HIGHLIGHT_CLASS;
		mark.textContent = match[0];
		fragment.appendChild(mark);

		lastIndex = regex.lastIndex;
		match = regex.exec(text);
	}

	if (lastIndex < text.length) {
		fragment.appendChild(document.createTextNode(text.slice(lastIndex)));
	}

	return fragment;
};

const highlightKeywords = (container: HTMLElement, keywords: string[]): void => {
	removeKeywordHighlights(container);

	const regex = buildKeywordsRegex(keywords);
	if (!regex) return;

	const textNodes = collectTextNodes(container);

	for (const textNode of textNodes) {
		const text = textNode.textContent || "";
		if (!regex.test(text)) continue;
		regex.lastIndex = 0;

		const fragment = wrapMatches(text, regex);
		textNode.parentNode?.replaceChild(fragment, textNode);
	}
};

const observeAndHighlightKeywords = (container: HTMLElement, keywords: string[]): (() => void) => {
	const observer = new MutationObserver(() => {
		observer.disconnect();
		highlightKeywords(container, keywords);
		observer.observe(container, { childList: true, subtree: true });
	});

	highlightKeywords(container, keywords);
	observer.observe(container, { childList: true, subtree: true });

	return () => {
		observer.disconnect();
		removeKeywordHighlights(container);
	};
};

export {
	addIdsToHeadings,
	observeAndAddIdsToHeadings,
	highlightKeywords,
	removeKeywordHighlights,
	observeAndHighlightKeywords,
	KEYWORD_HIGHLIGHT_CLASS,
};
