<script lang="ts">
	import { onMount } from 'svelte';
	import { highlightCode } from './highlighter';
	import type { CodeAnimationMode } from '../types';

	interface Props {
		oldCode: string;
		newCode: string;
		language: string;
		theme: string;
		mode: CodeAnimationMode;
		speed?: number;
		highlightColor?: string;
		highlightDuration?: number;
		showLineNumbers?: boolean;
		onComplete?: () => void;
	}

	let {
		oldCode, newCode, language = 'javascript', theme = 'github-dark',
		mode = 'highlight-changes', speed = 50, highlightColor = '#facc15',
		highlightDuration = 1000, showLineNumbers = false, onComplete
	}: Props = $props();

	const durationSec = $derived(highlightDuration / 1000);
	let displayedHtml = $state('');
	let changedWordIndices = $state<Set<number>>(new Set());
	let isAnimating = $state(false);

	function tokenize(code: string): string[] {
		return code.match(/[a-zA-Z_][a-zA-Z0-9_]*|\d+|\s+|./g) || [];
	}

	function findChangedWords(oldStr: string, newStr: string): Set<number> {
		const oldTokens = tokenize(oldStr);
		const newTokens = tokenize(newStr);
		const changed = new Set<number>();
		let oldIdx = 0, newIdx = 0;
		while (newIdx < newTokens.length) {
			if (oldIdx < oldTokens.length && oldTokens[oldIdx] === newTokens[newIdx]) {
				oldIdx++; newIdx++;
			} else {
				if (newTokens[newIdx].trim()) changed.add(newIdx);
				newIdx++;
				if (oldIdx < oldTokens.length && oldTokens[oldIdx] !== newTokens[newIdx - 1]) oldIdx++;
			}
		}
		return changed;
	}

	function sleep(ms: number): Promise<void> { return new Promise(r => setTimeout(r, ms)); }

	async function animateTypewriter() {
		isAnimating = true;
		let diffStart = 0;
		while (diffStart < oldCode.length && diffStart < newCode.length && oldCode[diffStart] === newCode[diffStart]) diffStart++;
		const delayMs = 1000 / speed;
		for (let i = diffStart; i <= newCode.length; i++) {
			displayedHtml = await highlightCode(newCode.substring(0, i), language, theme, { showLineNumbers });
			await sleep(delayMs);
		}
		isAnimating = false;
		onComplete?.();
	}

	async function animateHighlight() {
		isAnimating = true;
		changedWordIndices = findChangedWords(oldCode, newCode);
		const baseHtml = await highlightCode(newCode, language, theme, { showLineNumbers });
		displayedHtml = wrapChangedWords(newCode, baseHtml, changedWordIndices);
		await sleep(highlightDuration);
		displayedHtml = baseHtml;
		changedWordIndices = new Set();
		isAnimating = false;
		onComplete?.();
	}

	function wrapChangedWords(code: string, html: string, changed: Set<number>): string {
		if (changed.size === 0) return html;
		const tokens = tokenize(code);
		const changedTokens = new Set<string>();
		for (const idx of changed) {
			if (tokens[idx] && tokens[idx].trim()) changedTokens.add(tokens[idx]);
		}
		let result = html;
		for (const token of changedTokens) {
			const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
			const regex = new RegExp(`(>)([^<]*?)(\\b${escaped}\\b)([^<]*?)(<)`, 'g');
			result = result.replace(regex, (_, open, before, word, after, close) => {
				return `${open}${before}<mark class="word-highlight">${word}</mark>${after}${close}`;
			});
		}
		return result;
	}

	onMount(async () => {
		if (mode === 'instant' || oldCode === newCode) {
			displayedHtml = await highlightCode(newCode, language, theme, { showLineNumbers });
		} else if (mode === 'typewriter') {
			await animateTypewriter();
		} else if (mode === 'highlight-changes') {
			await animateHighlight();
		}
	});
</script>

<div class="code-morph" class:animating={isAnimating} style:--highlight-color={highlightColor} style:--duration="{durationSec}s">{@html displayedHtml}</div>

<style>
	.code-morph {
		width: 100%; height: 100%; margin: 0; padding: 0;
		background: transparent; font-family: var(--font-mono, 'JetBrains Mono', monospace);
		font-size: inherit; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word;
		color: #e6edf3; box-sizing: border-box; overflow: visible;
	}
	.code-morph :global(pre) {
		margin: 0; padding: 16px; background: transparent !important;
		font-family: var(--font-mono); font-size: inherit; line-height: 1.6; overflow: visible;
	}
	.code-morph :global(code) { font-family: inherit; font-size: inherit; font-weight: inherit; }
	.code-morph :global(.line-number) {
		display: inline-block; width: 2.5em; margin-right: 1em;
		text-align: right; color: #6e7681; user-select: none; opacity: 0.6;
	}
	.code-morph :global(.word-highlight) {
		background: var(--highlight-color, #facc15); color: #000;
		border-radius: 3px; padding: 1px 3px; margin: 0 -3px;
		box-decoration-break: clone;
		animation: fadeHighlight var(--duration, 1s) ease-out forwards;
	}
	@keyframes fadeHighlight {
		0% { background: var(--highlight-color, #facc15); color: #000; }
		70% { background: var(--highlight-color, #facc15); color: #000; }
		100% { background: transparent; color: inherit; }
	}
</style>
