<script lang="ts">
	import '$lib/utils/codemirror';
	import { basicSetup, EditorView } from 'codemirror';
	import { keymap } from '@codemirror/view';
	import { Compartment, EditorState } from '@codemirror/state';
	import { indentWithTab } from '@codemirror/commands';
	import { indentUnit, LanguageDescription } from '@codemirror/language';
	import { languages } from '@codemirror/language-data';
	import { oneDark } from '@codemirror/theme-one-dark';
	import { onMount, onDestroy } from 'svelte';

	export let value = '';
	export let filePath: string | null = null;
	export let onSave: ((content: string) => Promise<void>) | null = null;
	export let searchTarget: {
		line: number;
		column: number;
		length: number;
		requestId: number;
	} | null = null;

	let container: HTMLDivElement;
	let editor: EditorView | null = null;
	let editorTheme = new Compartment();
	let editorLanguage = new Compartment();
	let internalValue = '';
	let lastSearchTargetRequestId = 0;

	/** Return the current editor content */
	export const getValue = (): string => {
		return editor?.state.doc.toString() ?? value;
	};

	/** Replace editor content */
	export const setValue = (newValue: string) => {
		if (!editor) return;
		internalValue = newValue;
		editor.dispatch({
			changes: { from: 0, to: editor.state.doc.length, insert: newValue }
		});
	};

	export const focus = () => {
		editor?.focus();
	};

	const revealSearchTarget = () => {
		if (!editor || !searchTarget || searchTarget.requestId === lastSearchTargetRequestId) return;
		lastSearchTargetRequestId = searchTarget.requestId;
		const lineNumber = Math.min(Math.max(searchTarget.line, 1), editor.state.doc.lines);
		const line = editor.state.doc.line(lineNumber);
		const from = line.from + Math.min(Math.max(searchTarget.column - 1, 0), line.length);
		const to = Math.min(from + Math.max(searchTarget.length, 1), line.to);
		editor.dispatch({
			selection: { anchor: from, head: to },
			effects: EditorView.scrollIntoView(from, { y: 'center' })
		});
		editor.focus();
	};

	const detectLanguage = async (path: string | null) => {
		if (!path) return;
		const match = LanguageDescription.matchFilename(languages, path);
		if (match) {
			const lang = await match.load();
			if (lang && editor) {
				editor.dispatch({ effects: editorLanguage.reconfigure(lang) });
			}
		}
	};

	// React to external value changes (e.g. switching files)
	$: if (editor && value !== internalValue) {
		internalValue = value;
		editor.dispatch({
			changes: { from: 0, to: editor.state.doc.length, insert: value }
		});
	}

	// React to filePath changes for language detection
	$: if (editor && filePath) {
		detectLanguage(filePath);
	}

	$: if (editor && searchTarget) {
		revealSearchTarget();
	}

	onMount(() => {
		const isDark = document.documentElement.classList.contains('dark');
		internalValue = value;

		const extensions = [
			basicSetup,
			keymap.of([
				indentWithTab,
				{
					key: 'Mod-s',
					run: () => {
						if (onSave) {
							onSave(editor?.state.doc.toString() ?? '');
						}
						return true;
					}
				}
			]),
			indentUnit.of('    '),
			EditorView.updateListener.of((e) => {
				if (e.docChanged) {
					internalValue = e.state.doc.toString();
					value = internalValue;
				}
			}),
			editorTheme.of(isDark ? oneDark : []),
			editorLanguage.of([]),
			EditorView.theme({
				'&': { fontSize: '0.75rem', height: '100%' },
				'.cm-content': {
					padding: '0.5rem 0',
					fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace'
				},
				'.cm-scroller': { overflow: 'auto' },
				'.cm-focused': { outline: 'none' }
			})
		];

		editor = new EditorView({
			state: EditorState.create({ doc: value, extensions }),
			parent: container
		});

		detectLanguage(filePath);
		revealSearchTarget();

		// Watch dark mode
		const observer = new MutationObserver(() => {
			const dark = document.documentElement.classList.contains('dark');
			editor?.dispatch({ effects: editorTheme.reconfigure(dark ? oneDark : []) });
		});
		observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });

		return () => {
			observer.disconnect();
			editor?.destroy();
			editor = null;
		};
	});

	onDestroy(() => {
		editor?.destroy();
		editor = null;
	});
</script>

<div bind:this={container} class="file-code-editor" />

<style>
	.file-code-editor {
		width: 100%;
		height: 100%;
	}
	.file-code-editor :global(.cm-editor) {
		height: 100%;
	}
</style>
