<script lang="ts">
	import { onMount, onDestroy } from 'svelte';
	import type { FlowMarkerConfig, Position } from './types';
	import { arrowPointAt } from './utils/arrow-path';

	interface Props {
		config: FlowMarkerConfig;
		start: Position;
		end: Position;
		controlPoints: Position[];
		/** Slide loop duration in ms — used to round the marker cycle so the loop
		 * boundary lands cleanly. Without it, GIF/MP4 captures show a visible
		 * jump when frame N (= slide_duration) and frame 0 don't match. */
		slideDuration?: number;
	}

	let { config, start, end, controlPoints, slideDuration }: Props = $props();

	// One position per marker, expressed in arrow-local SVG coordinates.
	let positions = $state<{ x: number; y: number; visible: boolean; opacity: number }[]>([]);

	let raf = 0;

	function ease(t: number, mode: FlowMarkerConfig['easing']): number {
		if (mode === 'ease-in-out') return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
		return t;
	}

	function tick(now: number) {
		// Effective cycle = slide_duration / round(slide_duration / requested).
		// Guarantees an integer number of cycles fit in slide_duration so that
		// position(now) === position(now + slide_duration) — seamless GIF loop
		// regardless of when capture started. User's speedMs is preserved up
		// to a small rounding (close to their intent in /present too).
		const requested = config.speedMs > 0 ? config.speedMs : 2000;
		const cycle = slideDuration && slideDuration > 0
			? slideDuration / Math.max(1, Math.round(slideDuration / requested))
			: requested;
		const elapsed = now;

		const out: typeof positions = [];
		for (let i = 0; i < config.count; i++) {
			const offset = i / config.count;
			const raw = ((elapsed / cycle) + offset) % 1;
			let t = raw;

			let direction: 1 | -1 = 1;
			if (config.direction === 'reverse') direction = -1;
			else if (config.direction === 'bidirectional') direction = i % 2 === 0 ? 1 : -1;

			if (config.pattern === 'bounce') {
				// 0 -> 1 -> 0 within one cycle
				t = raw < 0.5 ? raw * 2 : (1 - raw) * 2;
			} else if (config.pattern === 'once') {
				const totalElapsed = elapsed - offset * cycle;
				if (totalElapsed > cycle) {
					out.push({ x: 0, y: 0, visible: false, opacity: 0 });
					continue;
				}
				t = Math.min(1, Math.max(0, totalElapsed / cycle));
			}

			t = ease(t, config.easing);
			if (direction === -1) t = 1 - t;

			const p = arrowPointAt(start, end, controlPoints, t);

			// Pulse fades in/out over its travel.
			let opacity = 1;
			if (config.shape === 'pulse') {
				opacity = 0.4 + 0.6 * Math.sin(t * Math.PI);
			}

			out.push({ x: p.x, y: p.y, visible: true, opacity });
		}
		positions = out;
		raf = requestAnimationFrame(tick);
	}

	function restart() {
		if (raf) cancelAnimationFrame(raf);
		positions = [];
		phase = 0;
		lastNow = null;
		if (config.enabled) raf = requestAnimationFrame(tick);
	}

	onMount(() => {
		if (config.enabled) raf = requestAnimationFrame(tick);
		// Register so the video-export pipeline can restart under the virtual
		// clock. Without this, the marker's first RAF and startTime get captured
		// in real time before __startVirtualClock(), then subsequent ticks run
		// in virtual time — the time bases diverge and the marker visibly resets
		// mid-cycle in GIF/MP4 captures.
		if (typeof window !== 'undefined') {
			const reg = ((window as any).__svgAnimRestart ||= [] as Array<() => void>);
			reg.push(restart);
		}
	});

	onDestroy(() => {
		if (raf) cancelAnimationFrame(raf);
		if (typeof window !== 'undefined') {
			const reg = (window as any).__svgAnimRestart as Array<() => void> | undefined;
			if (reg) {
				const idx = reg.indexOf(restart);
				if (idx >= 0) reg.splice(idx, 1);
			}
		}
	});

	$effect(() => {
		// Restart animation when the enabled flag toggles.
		if (config.enabled && !raf) {
			phase = 0;
			lastNow = null;
			raf = requestAnimationFrame(tick);
		} else if (!config.enabled && raf) {
			cancelAnimationFrame(raf);
			raf = 0;
			positions = [];
		}
	});
</script>

{#if config.enabled}
	<g class="flow-markers" pointer-events="none">
		{#each positions as p, i (i)}
			{#if p.visible}
				{#if config.shape === 'square'}
					<rect
						x={p.x - config.size / 2}
						y={p.y - config.size / 2}
						width={config.size}
						height={config.size}
						fill={config.color}
						opacity={p.opacity}
					/>
				{:else if config.shape === 'pulse'}
					<circle cx={p.x} cy={p.y} r={config.size / 2} fill={config.color} opacity={p.opacity} />
					<circle
						cx={p.x}
						cy={p.y}
						r={config.size}
						fill="none"
						stroke={config.color}
						stroke-width="1"
						opacity={p.opacity * 0.4}
					/>
				{:else}
					<circle cx={p.x} cy={p.y} r={config.size / 2} fill={config.color} opacity={p.opacity} />
				{/if}
			{/if}
		{/each}
	</g>
{/if}

<style>
	.flow-markers {
		pointer-events: none;
	}
</style>
