<script lang="ts">
	import { onMount } from 'svelte';
	import { EditorView, Decoration, keymap, placeholder as cmPlaceholder, type DecorationSet } from '@codemirror/view';
	import { EditorState, StateField, StateEffect, Compartment } from '@codemirror/state';
	import { StreamLanguage } from '@codemirror/language';
	import { oneDark } from '@codemirror/theme-one-dark';
	import { acceptCompletion } from '@codemirror/autocomplete';
	import { getTheme } from '$lib/theme.svelte';
	import { jinjaAutocomplete, type SlotVariables } from '$lib/editor/jinja-autocomplete';

	let { value = '', onchange, rows = 6, placeholder = '', errorLine = null, variables = undefined } = $props<{
		value: string;
		onchange: (val: string) => void;
		rows?: number;
		placeholder?: string;
		errorLine?: number | null;
		variables?: SlotVariables;
	}>();

	const autocompleteCompartment = new Compartment();

	let container: HTMLDivElement;
	let view: EditorView;
	const theme = getTheme();

	// Error line highlight effect and field
	const setErrorLine = StateEffect.define<number | null>();
	const errorLineField = StateField.define<DecorationSet>({
		create() { return Decoration.none; },
		update(decorations, tr) {
			for (const e of tr.effects) {
				if (e.is(setErrorLine)) {
					if (e.value === null) return Decoration.none;
					const lineNum = e.value;
					if (lineNum < 1 || lineNum > tr.state.doc.lines) return Decoration.none;
					const line = tr.state.doc.line(lineNum);
					return Decoration.set([
						Decoration.line({ class: 'cm-error-line' }).range(line.from),
					]);
				}
			}
			return decorations;
		},
		provide: f => EditorView.decorations.from(f),
	});

	// Simple Jinja2 stream parser for syntax highlighting
	const jinjaLang = StreamLanguage.define({
		token(stream) {
			if (stream.match('{#')) {
				stream.skipTo('#}') && stream.match('#}');
				return 'comment';
			}
			if (stream.match('{{')) {
				while (!stream.eol()) {
					if (stream.match('}}')) return 'variableName';
					stream.next();
				}
				return 'variableName';
			}
			if (stream.match('{%')) {
				while (!stream.eol()) {
					if (stream.match('%}')) return 'keyword';
					stream.next();
				}
				return 'keyword';
			}
			while (stream.next()) {
				if (stream.peek() === '{') break;
			}
			return null;
		},
	});

	function buildExtensions(isDark: boolean) {
		const extensions = [
			jinjaLang,
			errorLineField,
			autocompleteCompartment.of(variables ? jinjaAutocomplete(variables) : []),
			keymap.of([{ key: 'Tab', run: acceptCompletion }]),
			EditorView.updateListener.of((update) => {
				if (update.docChanged) {
					onchange(update.state.doc.toString());
				}
			}),
			EditorView.lineWrapping,
		];
		// Apply oneDark first so its syntax-token colors are kept,
		// then override with our Aurora-aware theme so background,
		// borders, and gutters match the rest of the design.
		if (isDark) extensions.push(oneDark);
		extensions.push(EditorView.theme({
			'&': {
				fontSize: '13px',
				fontFamily: 'var(--font-mono)',
				backgroundColor: 'var(--color-input-bg) !important',
				borderRadius: '14px',
				border: '1px solid var(--color-rule-strong)',
				color: 'var(--color-foreground)',
				overflow: 'hidden',
			},
			'.cm-editor': { backgroundColor: 'transparent !important', borderRadius: '14px' },
			'.cm-scroller': { backgroundColor: 'transparent !important' },
			'.cm-content': { minHeight: `${rows * 1.5}em`, padding: '12px 14px', caretColor: 'var(--color-primary)' },
			'.cm-gutters': {
				backgroundColor: 'transparent',
				color: 'var(--color-muted-foreground)',
				borderRight: '1px solid var(--color-border)',
			},
			'.cm-activeLineGutter': { backgroundColor: 'var(--color-glass-strong)' },
			'.cm-activeLine': { backgroundColor: 'var(--color-glass-strong)' },
			'.cm-cursor': { borderLeftColor: 'var(--color-primary)' },
			'.cm-selectionBackground, ::selection': { backgroundColor: 'var(--color-glass-elev) !important' },
			'&.cm-focused .cm-selectionBackground': { backgroundColor: 'var(--color-glow) !important' },
			'.cm-focused': { outline: 'none' },
			'&.cm-focused': { borderColor: 'var(--color-primary)', boxShadow: '0 0 0 3px var(--color-glow)' },
			'.cm-error-line': { backgroundColor: 'rgba(255, 138, 120, 0.18)', outline: '1px solid rgba(255, 138, 120, 0.4)' },
			'.ͼc': { color: 'var(--color-orchid)' },
			'.ͼd': { color: 'var(--color-sky)' },
			'.ͼ5': { color: 'var(--color-muted-foreground)' },
			'.cm-tooltip-autocomplete': {
				background: 'color-mix(in srgb, var(--color-background) 92%, transparent)',
				backdropFilter: 'blur(28px) saturate(160%)',
				border: '1px solid var(--color-rule-strong)',
				borderRadius: '12px',
				fontSize: '12px',
				boxShadow: '0 12px 30px -12px rgba(0,0,0,0.4)',
				overflow: 'hidden',
			},
			'.cm-tooltip-autocomplete > ul > li[aria-selected]': {
				backgroundColor: 'var(--color-glass-elev)',
				color: 'var(--color-primary)',
			},
		}));
		if (placeholder) extensions.push(cmPlaceholder(placeholder));
		return extensions;
	}

	onMount(() => {
		view = new EditorView({
			state: EditorState.create({ doc: value, extensions: buildExtensions(theme.isDark) }),
			parent: container,
		});

		return () => view.destroy();
	});

	$effect(() => {
		if (view && view.state.doc.toString() !== value) {
			view.dispatch({
				changes: { from: 0, to: view.state.doc.length, insert: value },
			});
		}
	});

	$effect(() => {
		if (view) {
			view.dispatch({ effects: setErrorLine.of(errorLine ?? null) });
		}
	});

	// Update autocomplete when variables change
	$effect(() => {
		if (view) {
			view.dispatch({
				effects: autocompleteCompartment.reconfigure(
					variables ? jinjaAutocomplete(variables) : [],
				),
			});
		}
	});

	// Recreate editor when theme changes
	let lastIsDark: boolean | undefined;
	$effect(() => {
		const isDark = theme.isDark;
		if (lastIsDark !== undefined && lastIsDark !== isDark && view) {
			const currentDoc = view.state.doc.toString();
			view.destroy();

			view = new EditorView({
				state: EditorState.create({ doc: currentDoc, extensions: buildExtensions(isDark) }),
				parent: container,
			});
		}
		lastIsDark = isDark;
	});
</script>

<div bind:this={container}></div>
