<script lang="ts" module>
	export type StateMachinePage = {
		onMount?: () => void;
		disableBack?: boolean;
		hero: {
			text: string | (() => string);
			fontSize: 'small' | 'large';
			size: 'small' | 'large';
			colour: 'light' | 'dark';
		};
		snippet: Snippet;
		button:
			| false
			| {
					text: string;
					icon: IconDef;
					onclick?: MouseEventHandler<HTMLButtonElement>;
			  };

		// a post-submit action to be called after the form is submitted
		// if it returns a string, it marks an error and prevents the step from advancing
		onComplete?: () => void | Promise<void>;
	};

	type Circle = {
		size: number;
		x: number;
		y: number;
		color: string;
	};
</script>

<script lang="ts">
	import { browser } from '$app/environment';

	import Button from './Button.svelte';
	import { onMount, tick, type Snippet } from 'svelte';
	import { cubicOut } from 'svelte/easing';
	import type { MouseEventHandler } from 'svelte/elements';
	import { tweened } from 'svelte/motion';
	import { fade, fly } from 'svelte/transition';
	import { validateForm } from '@svelte-toolkit/validate';
	import { ArrowLeft, Check } from 'phosphor-svelte';
	import type { IconDef } from './util';

	let {
		pages,
		success,
		failure,
		index = $bindable(0),
		action
	}: {
		pages: StateMachinePage[];
		success: StateMachinePage;
		failure: StateMachinePage;
		index?: number;
		action?: string;
	} = $props();

	pages.push(success, failure); // add success and failure pages to the end of the pages array
	// let progressVisible = $state(false);

	let lastIndex = $state(index);
	let page = $derived(pages[index]);
	let hero = $derived(page.hero);
	let buttonLoading: boolean = $state(false);

	let height = tweened(0, {
		duration: 250,
		easing: cubicOut
	});

	let pageContainer: HTMLDivElement | undefined = $state<HTMLDivElement>();
	let formElement: HTMLFormElement;
	let circleBox: HTMLDivElement;
	let heroText: HTMLSpanElement;
	let buttonContainer: HTMLDivElement | undefined = $state<HTMLDivElement>();
	let buttonComponent: Button | undefined = $state<Button>();
	let collatedFormData: Record<string, string> = {};

	let backButtonVisible = $derived(index > 0 && !page.disableBack);

	$effect.pre(() => {
		if (!browser || !pageContainer) return;

		index = index; // reference the index to trigger the effect when it changes

		tick().then(() => {
			height.set(pageContainer?.offsetHeight ?? 0);
			updateCircles();

			// Make sure hero text fits
			heroText.style.fontSize = '';
			heroText.style.lineHeight = '';
			updateHeroText();

			// Make sure we can see the button
			updateScrollHeight();

			// Call onmount if it exists
			if (page.onMount) page.onMount();
		});
	});

	const updateHeroText = () => {
		if (
			heroText.scrollWidth > heroText.offsetWidth ||
			heroText.scrollHeight > heroText.offsetHeight
		) {
			const fontSize = parseInt(window.getComputedStyle(heroText).fontSize);
			heroText.style.fontSize = `${fontSize - 5}px`;
			heroText.style.lineHeight = `${(fontSize - 5) * 1.2}px`;

			updateHeroText();
		}
	};

	const updateScrollHeight = () => {
		if (!buttonContainer) return;
		const bottom = buttonContainer.getBoundingClientRect().bottom;
		const height = window.innerHeight;

		window.scrollTo({ top: bottom - height + 32, behavior: 'smooth' });
	};

	const handleContinueClick: MouseEventHandler<HTMLButtonElement> = async (event) => {
		if (!buttonComponent) return; // what in the ghost

		buttonComponent.animateRipple(event);

		// validate the form
		const result = await validateForm(formElement);
		if (!result) {
			buttonComponent.animateBounce();
			setTimeout(() => focusFirstInput(true), 50);
			return;
		}

		// we don't care about the result of onComplete
		// just that it resolves without error
		const res = await Promise.resolve(page.onComplete?.())
			.then(() => true)
			.catch(() => false);

		if (!res) {
			buttonComponent.animateBounce();
			return;
		}

		// collate form data for eventual submission
		const formData = new FormData(formElement);

		for (const [key, value] of formData.entries()) {
			collatedFormData[key] = value.toString();
		}

		// bubble up the event
		if (page.button) page.button.onclick?.(event);
		if (event.defaultPrevented) return;

		// if this is the last page, submit the form
		if (index === pages.length - 3) {
			submitCollatedData();
		} else {
			buttonComponent.animateLoop();
			index += 1;
			setTimeout(() => focusFirstInput(), 350);
		}
	};

	const getText = (text: string | (() => string)) => {
		return typeof text === 'function' ? text() : text;
	};

	let dirquery = 0;
	const dir = (val: number) => {
		let result = 1;
		if (index < lastIndex) {
			result = -1;
			dirquery = result;
		} else if (dirquery !== 0) {
			result = dirquery;
			dirquery = 0;
		}
		lastIndex = index;
		if (dirquery > 1) dirquery = 0;
		return result * val;
	};

	const flyIn = (node: Element) => {
		return fly(node, { x: dir(200), duration: 200, delay: 225 });
	};

	const flyOut = (node: Element) => {
		return fly(node, { x: dir(-200), duration: 200 });
	};

	const focusFirstInput = (firstInvalid?: boolean) => {
		let selector = 'input:not([type="hidden"])';
		if (firstInvalid) selector = 'input[data-validate-state="invalid"]:not([type="hidden"])';

		const inputs = formElement.querySelectorAll(selector);
		if (inputs.length > 0) {
			const input = inputs[0] as HTMLInputElement;
			input.focus();
		}
	};

	const submitCollatedData = async () => {
		if (!action) action = window.location.href;

		// update button state
		buttonLoading = true;

		const form_data = new FormData();
		for (const [key, value] of Object.entries(collatedFormData)) {
			form_data.append(key, value);
		}

		const response = await fetch(action, {
			method: 'POST',
			headers: {
				accept: 'application/json',
				'x-sveltekit-action': 'true'
			},
			cache: 'no-store',
			body: form_data
		});

		buttonLoading = false;

		const json = await response.json();

		if (json.status && json.status === 200) {
			index = pages.length - 2;
		} else {
			index = pages.length - 1;
		}
	};

	function getRandomColor() {
		var letters = '0123456789ABCDEF';
		var color = '#';
		for (var i = 0; i < 6; i++) {
			color += letters[Math.floor(Math.random() * 16)];
		}
		return color;
	}

	const randomFromInterval = (min: number, max: number) => {
		return Math.floor(Math.random() * (max - min + 1) + min);
	};

	const circles: Circle[][] = [];

	const updateCircles = () => {
		const elems = circleBox.querySelectorAll('.circle-initial');
		elems.forEach((circle, key) => {
			const el = circle as HTMLDivElement;
			el.style.width = `${circles[index][key].size}rem`;
			el.style.height = el.style.width;
			el.style.top = `${circles[index][key].y}%`;
			el.style.left = `${circles[index][key].x}%`;
			el.style.backgroundColor = circles[index][key].color;
		});
	};

	onMount(() => {
		// pages.push(success, failure); // add success and failure pages to the end of the pages array
		// progressVisible = true;

		for (let i = 0; i < 15; i++) {
			for (let j = 0; j < pages.length; j++) {
				if (!circles[j]) circles[j] = [];

				circles[j].push({
					size: randomFromInterval(8, 15),
					x: randomFromInterval(0, 100),
					y: randomFromInterval(0, 100),
					color: getRandomColor()
				});
			}

			const div = document.createElement('div');
			div.style.borderRadius = '100%';
			div.style.position = 'absolute';
			div.style.transition =
				'background-color 0.5s, opacity 1s, top 1s, left 1s, width 1s, height 1s';
			div.style.width = `${circles[index][i].size}rem`;
			div.style.height = div.style.width;
			div.style.top = `${circles[index][i].y}%`;
			div.style.left = `${circles[index][i].x}%`;
			div.style.backgroundColor = circles[index][i].color;
			div.classList.add('circle-initial');
			circleBox.appendChild(div);
		}

		setTimeout(() => {
			const circles = circleBox.querySelectorAll('.circle-initial');
			circles.forEach((circle) => {
				circle.classList.add('circle-final');
			});
		}, 1000);

		setTimeout(() => focusFirstInput(), 50);
	});
</script>

<!-- Form progress bar -->
{#snippet progress()}
	{#each { length: pages.length - 2 } as _, i (i)}
		<div
			class="relative flex h-6 w-7 items-center justify-center rounded-3xl bg-white
					text-sm dark:bg-slate-600 {index === i
				? 'bg-sui-accent-500!  text-sui-background dark:bg-sui-accent-700!'
				: ''} {i < index ? 'text-sui-background bg-green-600!' : ''} {i > index
				? 'scale-[0.85] opacity-80'
				: ''}
					transition-[transform,background-color,color]"
		>
			{#if i >= index}
				<span class="mb-[0.0625rem]">{i + 1}</span>
			{:else}
				<Check class="mt-0.5" size="1.5rem" weight="bold" />
			{/if}
		</div>
	{/each}
{/snippet}

<div
	class="transition-height relative mt-5 mb-5 flex h-8 items-center justify-between lg:mt-0 lg:mb-3
	{!backButtonVisible ? 'lg:h-0' : ''}"
>
	<!-- Back button -->
	{#if backButtonVisible}
		<button
			type="button"
			class="text-sui-text hover:text-sui-text-700 dark:text-sui-background dark:hover:text-sui-background/80 flex cursor-pointer items-center
			gap-2.5 font-medium transition-colors"
			onclick={() => (index -= 1)}
			transition:fly={{ x: -200, duration: 200 }}
		>
			<ArrowLeft />
			Back
		</button>
	{/if}

	<!-- Progress bar (mobile only, above form) -->
	<div
		class="progress absolute transition-transform duration-500 lg:!hidden {backButtonVisible
			? 'right-0'
			: 'left-1/2 -translate-x-1/2'}"
	>
		{@render progress()}
	</div>
</div>

<!-- Form container -->
<div
	class="sm:shadow-centre relative flex flex-col rounded-md sm:bg-white/85 lg:h-auto
	lg:max-h-[60rem] lg:min-h-[30rem] lg:flex-row lg:items-center dark:sm:bg-gray-900"
>
	<!-- Progress bar (desktop only, in form space) -->
	<div class="progress absolute top-8 left-[70%] hidden! -translate-x-1/2 lg:flex!">
		{@render progress()}
	</div>

	<!-- Hero container -->
	<div
		class={[
			'hero group relative mb-5 min-w-36 basis-2/5 self-stretch overflow-hidden rounded-md duration-500',
			'sm:rounded-b-none lg:mb-0 lg:rounded-md lg:rounded-r-none',
			hero.size === 'small' && 'min-h-52',
			hero.size === 'large' && 'min-h-72'
		]}
	>
		<!-- Circle decoration -->
		<div class="absolute inset-0 bg-gray-600 dark:bg-gray-900" bind:this={circleBox}></div>
		<div
			class={[
				'absolute inset-0 backdrop-blur-md transition-colors',
				hero.colour === 'light' ? 'bg-sui-accent/80' : 'bg-[#00283C]/80'
			]}
		></div>

		<!-- Hero text -->
		<span
			class={[
				'absolute right-4 bottom-4 left-4 text-white lg:bottom-1/2 lg:pr-4 lg:text-right',
				hero.fontSize === 'small'
					? 'max-h-[7.2rem] text-5xl leading-[3.6rem] font-semibold'
					: 'max-h-[10.6rem] text-7xl leading-[5.3rem] font-bold'
			]}
			transition:fade
			bind:this={heroText}
		>
			{getText(hero.text)}
		</span>
	</div>

	<form
		class="basis-3/5 sm:overflow-hidden sm:px-6 sm:pb-6 lg:mt-24 lg:px-12"
		novalidate
		onsubmit={(e) => e.preventDefault()}
		bind:this={formElement}
	>
		<div class="relative" style="height: {$height}px">
			<!-- Form page -->
			{#key index}
				<div
					class="text-sui-text dark:text-sui-background absolute top-0 right-0 left-0"
					in:flyIn
					out:flyOut
					bind:this={pageContainer}
				>
					{@render page.snippet()}
				</div>
			{/key}
		</div>

		<!-- Continue / submit button -->
		{#if index < pages.length - 1 && page.button && $height > 0}
			<div class="mt-4" out:fade bind:this={buttonContainer}>
				<Button
					icon={page.button.icon}
					onclick={handleContinueClick}
					animate={false}
					loading={buttonLoading}
					bind:this={buttonComponent}
				>
					{page.button.text}
				</Button>
			</div>
		{/if}
	</form>
</div>

<style lang="postcss">
	@reference "./styles/reference.css";

	:global(.circle-initial) {
		transition: opacity 1s;
		opacity: 0;
	}

	:global(.circle-final) {
		opacity: 1;
	}

	.progress {
		@apply flex cursor-default items-center justify-center gap-3 overflow-hidden rounded-3xl px-3 py-2;
		@apply dark:bg-sui-text-800 bg-gray-200/80 dark:lg:bg-slate-800;
		@apply text-sui-text/60 dark:text-sui-background/60 font-semibold;
	}
</style>
