<script lang="ts" module>
	/**
	 * Defines the features available in dialog event callbacks.
	 */
	export interface DialogAPI {
		/** shows an error message at the top of the dialog */
		error: (message: ErrorMessage | null) => void;
		/** closes the dialog */
		close: () => void;
		/** opens the dialog */
		open: () => void;
		/** returns true if the dialog is open */
		isOpen: () => boolean;
		/** if default controls are used, sets the submit button loading state */
		loading: () => void;
		/** if default controls are used, sets the submit button loaded state */
		loaded: () => void;
		/** if default controls are used, returns true if the submit button is in loading state */
		isLoading: () => boolean;
		/** if default controls are used, disables submission and exiting */
		freeze: () => void;
		/** if default controls are used, enables submission and exiting */
		unfreeze: () => void;
		/** if the default controls are used, returns true if the dialog is frozen */
		isFrozen: () => boolean;
	}

	/**
	 * Configures the default dialog controls.
	 */
	export type DialogControlOpts = {
		cancel?: {
			label?: string;
			class?: ClassValue;
			action?: (dialog: DialogAPI) => void;
		};
		ok?: {
			label?: string;
			class?: ClassValue;
			action?: (dialog: DialogAPI) => void;
		};
		close?: {
			class?: ClassValue;
			action?: (dialog: DialogAPI) => void;
		};
	};
</script>

<script lang="ts">
	import { Portal } from '@jsrob/svelte-portal';
	import { type Snippet } from 'svelte';
	import type { ClassValue } from 'svelte/elements';
	import { fade } from 'svelte/transition';
	import { flyAndScale } from './transition';
	import Button from './Button.svelte';
	import { X } from 'phosphor-svelte';
	import { ErrorMessage } from './error';
	import ErrorBox from './ErrorBox.svelte';

	interface Props {
		open?: boolean;
		title: string;
		description?: string;
		size?: 'sm' | 'md' | 'lg' | 'max';
		class?: ClassValue;
		children?: Snippet;
		controls?: Snippet | DialogControlOpts;
		onopen?: (dialog: DialogAPI) => void;
		onclose?: (dialog: DialogAPI) => void;
	}

	let {
		open = $bindable(false),
		title,
		description,
		size = 'sm',
		class: classValue,
		children,
		controls,
		onopen,
		onclose
	}: Props = $props();

	let dialogContainer = $state<HTMLDivElement | null>(null);
	let error = $state<ErrorMessage | null>(null);
	let loading = $state(false);
	let frozen = $state(false);

	// disable window scroll when dialog is open
	$effect(() => {
		if (open) {
			document.body.style.overflow = 'hidden';
			onopen?.(dialogAPI);
		} else {
			document.body.style.overflow = '';
			onclose?.(dialogAPI);
		}
	});

	const dialogAPI: DialogAPI = {
		error: (message) => (error = message),
		close: () => (open = false),
		open: () => (open = true),
		isOpen: () => open,
		loading: () => (loading = true),
		loaded: () => (loading = false),
		isLoading: () => loading,
		freeze: () => (frozen = true),
		unfreeze: () => (frozen = false),
		isFrozen: () => frozen
	};
</script>

<Portal target="body">
	{#if open}
		{@render dialog()}
	{/if}
</Portal>

{#snippet dialog()}
	<div
		class={[
			'fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm',
			classValue
		]}
		transition:fade={{ duration: 150 }}
		onclick={(e) => {
			const target = e.target as HTMLElement;
			if (open && !dialogContainer?.contains(target) && target !== dialogContainer) open = false;
		}}
		onkeydown={(e) => {
			if (e.key === 'Escape') {
				open = false;
			}
		}}
		role="dialog"
		tabindex="-1"
	>
		<div
			bind:this={dialogContainer}
			class={[
				'relative max-h-[85vh] w-[90vw] rounded-xl bg-white p-6 shadow-lg',
				size === 'sm' && 'max-w-[450px]',
				size === 'md' && 'max-w-[650px]',
				size === 'lg' && 'max-w-[850px]',
				size === 'max' && 'max-w-[95vw]'
			]}
			transition:flyAndScale={{
				duration: 150,
				y: -8,
				start: 0.96
			}}
		>
			<h2 class="pointer-events-none mb-2 text-lg font-medium text-black select-none">{title}</h2>

			{#if error}
				<ErrorBox {error} />
			{/if}

			{#if description}
				<p class="mb-3 leading-normal text-zinc-600">
					{description}
				</p>
			{/if}

			{#if children}{@render children()}{:else}Dialog is empty{/if}

			<div class="mt-6 flex justify-end gap-4">
				{#if controls && typeof controls === 'function'}{@render controls()}{:else}
					<Button
						class={controls?.cancel?.class}
						onclick={() => {
							if (controls?.cancel?.action) {
								controls.cancel.action(dialogAPI);
							} else if (!frozen) {
								open = false;
							}
						}}
						disabled={frozen}
					>
						{controls?.cancel?.label || 'Cancel'}
					</Button>
					<Button
						class={controls?.ok?.class}
						onclick={() => {
							if (controls?.ok?.action) {
								controls.ok.action(dialogAPI);
							} else if (!frozen && !loading) {
								open = false;
							}
						}}
						disabled={frozen}
						{loading}
					>
						{controls?.ok?.label || 'OK'}
					</Button>
				{/if}
			</div>
			<button
				aria-label="close"
				class="absolute top-4 right-4 inline-flex cursor-pointer items-center
				justify-center disabled:cursor-not-allowed disabled:opacity-50"
				onclick={() => {
					if (!frozen) open = false;
				}}
				disabled={frozen}
			>
				<X size="1.5em" weight="bold" />
			</button>
		</div>
	</div>
{/snippet}
