import {
	DOMParser,
	Element,
	NodeType,
	Text,
} from "https://deno.land/x/deno_dom@v0.1.38/deno-dom-wasm.ts";
import { assert } from "https://deno.land/std/testing/asserts.ts";
import katex from "https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.mjs";

// this is similar to delimiter object in auto-render.js
interface IKaTeXAutoRenderSurround {
	display: boolean;
	test: RegExp;
}

// options to be given to renderMathInElement
interface IKaTeXAutoRenderOption {
	// delimiters
	surronds: IKaTeXAutoRenderSurround[];

	// tag names which are excluded
	ignoreTags: string[];
}

// the default option is same with auto-render.js
const DefaultOptions: IKaTeXAutoRenderOption = {
	surronds: [
		{ test: /\$\$(.+?)\$\$/, display: true },
		{ test: /\$(.+?)\$/, display: false },
	],
	ignoreTags: [
		"script",
		"noscript",
		"style",
		"textarea",
		"pre",
		"code",
	],
};

function renderText(
	element: Text,
	options: IKaTeXAutoRenderOption,
): string | null {
	let restText = element.textContent;
	const partialResults = new Array<string>();

	let anyMatch = false;

	while (true) {
		let match: RegExpExecArray | null = null;
		let surround: IKaTeXAutoRenderSurround | null = null;
		for (const sr of options.surronds) {
			match = sr.test.exec(restText);
			surround = sr;
			if (match) break;
		}
		if (!match) {
			break;
		} else {
			anyMatch = true;
		}

		// Insert text before the match
		partialResults.push(restText.substr(0, match.index));

		// Renders and inserts the KaTeX text
		assert(surround !== null);
		const renderOption = Object.assign({}, options, {
			displayMode: surround.display,
		});
		partialResults.push(
			katex.renderToString(match[1], renderOption),
		);

		restText = restText.substr(match.index + match[0].length);
	}

	if (!anyMatch) {
		return null;
	}

	// Inserts the leftover text
	partialResults.push(restText);
	return partialResults.join("");
}

function renderElement(
	element: Element,
	options: IKaTeXAutoRenderOption,
): void {
	for (const child of element.childNodes) {
		switch (child.nodeType) {
			case NodeType.ELEMENT_NODE:
				{
					if (
						!options.ignoreTags.includes(
							child.nodeName.toLowerCase(),
						)
					) {
						renderElement(child as Element, options);
					}
					break;
				}
			case NodeType.TEXT_NODE:
				{
					const newText = renderText(child as Text, options);
					if (newText != null) {
						const document = child.ownerDocument;
						assert(document !== null);
						const newChild = document.createElement("");
						newChild.innerHTML = newText;
						child.replaceWith(...newChild.childNodes);
					}
					break;
				}
			default:
				continue;
		}
	}
}

async function renderMathInFile(filename: string): Promise<void> {
	const decoder = new TextDecoder();
	const document = new DOMParser().parseFromString(
		decoder.decode(await Deno.readFile(filename)),
		"text/html",
	)!;

	renderElement(document.body, DefaultOptions);

	assert(document.documentElement !== null);
	await Deno.writeTextFile(filename, '<!DOCTYPE html>' + document.documentElement.outerHTML);
}

const filenames = Deno.args;
for (const filename of filenames) {
	await renderMathInFile(filename);
}
