const BLOCK = /
([\s\S]*?)<\/code><\/pre>/g;

const MAX_BLOCK_CHARS = 200_000;

const ENTITIES: Record = {
	amp: "&",
	lt: "<",
	gt: ">",
	quot: '"',
	"#39": "'",
	"#x27": "'",
	"#x2F": "/"
};

const decodeEntities = (value: string): string =>
	value.replace(/&(#x[0-9a-fA-F]+|#\d+|[a-zA-Z#0-9]+);/g, (match, body: string) => {
		if (body.startsWith("#x") || body.startsWith("#X"))
			try {
				return String.fromCodePoint(parseInt(body.slice(2), 16));
			} catch {
				return match;
			}
		if (body.startsWith("#")) {
			const point = parseInt(body.slice(1), 10);
			return Number.isNaN(point) ? match : String.fromCodePoint(point);
		}
		return ENTITIES[body] ?? match;
	});

const INNER = /]*>([\s\S]*)<\/code>\s*<\/pre>/;

type Highlighter = Awaited>;

type Loaded = { highlighter: Highlighter; theme: string };

// importing this lazily keeps shiki out of markdown pages with no code
const loadHighlighter = async (langs: string[]): Promise => {
	try {
		const { SYNTAX_THEME_NAME, getSharedHighlighter } = await import("$lib/syntax");
		const highlighter = await getSharedHighlighter({ themes: [SYNTAX_THEME_NAME], langs });
		return { highlighter, theme: SYNTAX_THEME_NAME };
	} catch {
		return null;
	}
};

const highlightInner = (loaded: Loaded, code: string, lang: string): string | null => {
	let full: string;
	try {
		full = loaded.highlighter.codeToHtml(code, {
			lang,
			theme: loaded.theme,
			defaultColor: false,
			cssVariablePrefix: "--diffs-token-"
		});
	} catch {
		return null;
	}
	return INNER.exec(full)?.[1] ?? null;
};

export const highlightCodeblocks = async (html: string): Promise => {
	const found = [...html.matchAll(BLOCK)];
	if (found.length === 0) return html;

	const blocks = found.flatMap((match) => {
		const [raw, before, lang, after, escaped] = match as unknown as [
			string,
			string,
			string,
			string,
			string
		];
		const code = decodeEntities(escaped);
		return code.length > 0 && code.length <= MAX_BLOCK_CHARS
			? [{ raw, before, lang, after, code }]
			: [];
	});
	if (blocks.length === 0) return html;

	const loaded = await loadHighlighter([...new Set(blocks.map((block) => block.lang))]);
	if (!loaded) return html;

	const swapped = new Map();
	for (const block of blocks) {
		if (swapped.has(block.raw)) continue;
		const inner = highlightInner(loaded, block.code, block.lang);
		if (inner !== null)
			swapped.set(
				block.raw,
				`
${inner}
` ); } if (swapped.size === 0) return html; return html.replace(BLOCK, (raw) => swapped.get(raw) ?? raw); };