<script lang="ts">
	import { CalendarDate } from '@internationalized/date';
	import { InputValidatorEvent, liveValidator, validate } from '@svelte-toolkit/validate';
	import { CalendarBlank } from 'phosphor-svelte';
	import { type Snippet } from 'svelte';
	import type { ClassValue, FormEventHandler, KeyboardEventHandler } from 'svelte/elements';
	import { generateIdentifier } from './util';
	import Label from './Label.svelte';

	type FormatString = 'year' | 'month' | 'day';
	const blankState = {
		year: 'yyyy',
		month: 'mm',
		day: 'dd'
	};

	interface Props {
		name?: string;
		value?: CalendarDate | null;
		min?: CalendarDate;
		// max?: CalendarDate; // TODO: Implement validation.
		label?: string;
		required?: boolean;
		invalidMessage?: string;
		class?: ClassValue | undefined | null;
		format?: FormatString[];
		onchange?: (date: CalendarDate | null) => void;
	}

	let {
		name,
		value = $bindable<CalendarDate | null>(null),
		/** min specifies lower bounds for the date input (WARNING: NOT IMPLEMENTED) */
		min = new CalendarDate(1900, 0, 1),
		/** max specifies upper bounds for the date input (WARNING: NOT IMPLEMENTED) */
		// max = new CalendarDate(2100, 11, 31),
		label,
		required = false,
		invalidMessage = 'Valid date is required',
		class: classValue,
		format = ['year', 'month', 'day'],
		onchange
	}: Props = $props();

	const id = $derived(generateIdentifier('dateinput', name));

	const controls: Record<
		FormatString,
		{ snippet: Snippet<[i: number]>; ref: HTMLDivElement | null; prevCaretPos?: caretPos }
	> = $state({
		year: { snippet: year, ref: null, beforeInput: null },
		month: { snippet: month, ref: null, beforeInput: null },
		day: { snippet: day, ref: null, beforeInput: null }
	});

	$effect(() => {
		// Throw an error if format includes duplicate or unsupported controls
		const found: Partial<Record<FormatString, boolean>> = {};
		format.forEach((f) => {
			if (found[f]) throw new Error(`Duplicate format string: ${f}`);
			found[f] = true;
			if (!controls[f]) throw new Error(`Unsupported format string: ${f}`);
		});
	});

	let valid = $state(true);
	let containerElement: HTMLDivElement;
	let previousYearValue = $state<string | undefined>(undefined);
	let yearValue = $derived.by(() => {
		if (!value && previousYearValue) {
			return previousYearValue;
		}
		if (value) {
			return value.year.toString();
		}
		return blankState['year'];
	});
	let previousMonthValue = $state<string | undefined>(undefined);
	let monthValue = $derived.by(() => {
		if (!value && previousMonthValue) {
			return previousMonthValue;
		}
		if (value) {
			return String(value.month).padStart(2, '0');
		}
		return blankState['month'];
	});
	let previousDayValue = $state<string | undefined>(undefined);
	let dayValue = $derived.by(() => {
		if (!value && previousDayValue) {
			return previousDayValue;
		}
		if (value) {
			return String(value.day).padStart(2, '0');
		}
		return blankState['day'];
	});

	const tryUpdateValue = () => {
		const setPrevious = () => {
			previousYearValue = yearValue;
			previousMonthValue = monthValue;
			previousDayValue = dayValue;
		};

		let year: number | undefined = undefined;
		if (controls.year.ref) {
			year = parseInt(controls.year.ref.textContent || '', 10);
			if (isNaN(year) || year < 0 || year > 9999) {
				if (value) {
					setPrevious();
					previousYearValue = undefined;
					value = null;
					onchange?.(value);
				}
				return;
			}
		}

		let month: number | undefined = undefined;
		if (controls.month.ref) {
			month = parseInt(controls.month.ref.textContent || '', 10);
			if (isNaN(month) || month < 1 || month > 12) {
				if (value) {
					setPrevious();
					previousMonthValue = undefined;
					value = null;
					onchange?.(value);
				}
				return;
			}
		}

		let day: number | undefined = undefined;
		if (controls.day.ref) {
			day = parseInt(controls.day.ref.textContent || '', 10);
			if (isNaN(day) || day < 1 || day > 31) {
				if (value) {
					setPrevious();
					previousDayValue = undefined;
					value = null;
					onchange?.(value);
				}
				return;
			}
		}

		const newDate = new CalendarDate(
			year ?? (value ? value.year : min.year),
			month ? month : value ? value.month : min.month,
			day ?? (value ? value.day : min.day)
		);
		value = newDate;
		onchange?.(value);
	};

	type caretPos = { start: number; end: number } | null;
	/** getCaretPosition returns the start and end of the text selection in a particular div */
	const getCaretPosition = (div: HTMLDivElement): caretPos => {
		const selection = window.getSelection();
		if (!selection || selection.rangeCount === 0) return null;

		const range = selection.getRangeAt(0);

		// Ensure the selection is within the target div
		if (!div.contains(range.startContainer) || !div.contains(range.endContainer)) {
			return null;
		}

		// Create a range from the start of the div to the selection start
		const preRange = document.createRange();
		preRange.selectNodeContents(div);
		preRange.setEnd(range.startContainer, range.startOffset);
		const start = preRange.toString().length;

		// Create a range from the start of the div to the selection end
		const postRange = document.createRange();
		postRange.selectNodeContents(div);
		postRange.setEnd(range.endContainer, range.endOffset);
		const end = postRange.toString().length;

		return { start, end };
	};

	/** focusNext focuses the next input */
	const focusNext = (i: number) => {
		if (i < format.length - 1) {
			const next = controls[format[i + 1]].ref;
			next?.focus();
		}
	};
	/** focusPrevious focuses the previous input */
	const focusPrevious = (i: number) => {
		if (i > 0) {
			const prev = controls[format[i - 1]].ref;
			prev?.focus();
		}
	};

	const createHandleKeydown = (i: number): KeyboardEventHandler<HTMLDivElement> => {
		return (e) => {
			if (!e.target) return;
			const target = e.target as HTMLDivElement;

			const pos = getCaretPosition(target);
			controls[format[i]].prevCaretPos = pos;

			if (
				(e.key === 'Backspace' || e.key === 'Delete') &&
				target.textContent === blankState[format[i]]
			) {
				target.textContent = '';
				e.preventDefault();
				return;
			}
		};
	};

	const createHandleKeyup = (i: number): KeyboardEventHandler<HTMLDivElement> => {
		return (e) => {
			if (!e.target) return;
			const target = e.target as HTMLDivElement;
			const prevPos = controls[format[i]].prevCaretPos;

			if (e.key === 'Enter') {
				e.preventDefault();
				target.blur(); // Remove focus to trigger blur event
				// Deselect any selected text at window-level
				const selection = window.getSelection();
				selection?.removeAllRanges();
			} else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
				// If the select has reached the end of this input, go on to the next
				// note: add `prevPos?.start === prevPos?.end` to disable moving with an active selection.
				if (prevPos?.end === target.textContent?.length) {
					focusNext(i);
				}
			} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
				// If the select has reached the start of this input, go back to the previous
				// note: add `prevPos?.start === prevPos?.end` to disable moving with an active selection.
				if (prevPos?.start === 0) {
					focusPrevious(i);
				}
			} else if (e.key === 'Backspace') {
				// If previous caret end position is at the start of the input, go back to the previous
				if (prevPos?.end === 0 && prevPos?.start === 0) {
					focusPrevious(i);
				}
			} else if (e.key === 'Delete') {
				// If previous caret start position is at the end of the input, go to the next
				if (
					prevPos?.start === target.textContent?.length &&
					prevPos?.end === target.textContent?.length
				) {
					focusNext(i);
				}
			}
		};
	};

	const createHandleInput = (i: number, maxLength: number): FormEventHandler<HTMLDivElement> => {
		return (e) => {
			const target = e.target as HTMLDivElement;

			// If we've typed to the end of the input, go on to the next input
			if (target.textContent?.length === maxLength) {
				focusNext(i);
			}
		};
	};

	const handleFocus = (e: FocusEvent) => {
		const target = e.target as HTMLDivElement;

		// Select all text in the input when focused
		if (target.isContentEditable) {
			setTimeout(() => {
				const range = document.createRange();
				const selection = window.getSelection();
				range.selectNodeContents(target);
				selection?.removeAllRanges();
				selection?.addRange(range);
			}, 1);
		}
	};

	const handleComponentValidate = (e: InputValidatorEvent) => {
		if (!e.detail.valid) {
			const target = e.target as HTMLDivElement;
			target.classList.add('invalid');
			setTimeout(() => {
				target.classList.remove('invalid');
			}, 500);
		}
	};

	const createHandleBlur = (defaultText: string) => {
		return (e: FocusEvent) => {
			const target = e.target as HTMLDivElement;
			if (!target.textContent || target.textContent.trim() === '') {
				target.textContent = defaultText;
			}

			if (e.relatedTarget === null || !containerElement.contains(e.relatedTarget as Node)) {
				tryUpdateValue();
			}
		};
	};
</script>

<div class={classValue}>
	<!-- Date input Label -->
	{#if label}
		<Label for={id}>{label}</Label>
	{/if}

	<div
		bind:this={containerElement}
		class={[
			'inline-flex w-54 items-center justify-start gap-1',
			'border-sui-accent rounded-sm border bg-white px-[1.125rem] py-3.5 font-normal transition-colors',
			'text-sui-text dark:border-sui-accent/50 dark:bg-sui-text-800 dark:text-sui-background dark:sm:bg-slate-800',
			'ring-sui-accent focus-within:ring-1',
			!valid && 'border-red-500!'
		]}
	>
		<input type="hidden" {name} {id} value={value?.toString() ?? ''} use:validate={{ required }} />

		{#each format as key, i (key)}
			{@render controls[key].snippet(i)}

			{#if i < format.length - 1}
				<span class="text-sui-text/60 dark:text-sui-background/60">-</span>
			{/if}
		{/each}

		<CalendarBlank size="1.5em" class="ml-auto" />
	</div>

	<!-- Error message if invalid -->
	<div class={['opacity-0 transition-opacity', !valid && 'opacity-100']}>
		<Label for={id} error>{invalidMessage}</Label>
	</div>
</div>

{#snippet year(i: number)}
	<div
		bind:this={controls.year.ref}
		bind:textContent={yearValue}
		role="textbox"
		tabindex="0"
		class="date-input"
		contenteditable
		spellcheck="false"
		onfocus={handleFocus}
		onblur={createHandleBlur(blankState.year)}
		onkeydown={createHandleKeydown(i)}
		onkeyup={createHandleKeyup(i)}
		oninput={createHandleInput(i, 4)}
		use:validate={{
			baseval: blankState.year,
			pattern: /^\d{1,4}$/
		}}
		use:liveValidator={{ constrain: true }}
		onvalidate={(e) => {
			handleComponentValidate(e);
		}}
	></div>
{/snippet}

{#snippet month(i: number)}
	<div
		bind:this={controls.month.ref}
		bind:textContent={monthValue}
		role="textbox"
		tabindex="0"
		class="date-input"
		contenteditable
		spellcheck="false"
		onfocus={handleFocus}
		onblur={createHandleBlur(blankState.month)}
		onkeydown={createHandleKeydown(i)}
		onkeyup={createHandleKeyup(i)}
		oninput={createHandleInput(i, 2)}
		use:validate={{
			baseval: blankState.month,
			pattern: /^([0-9]|0[1-9]|1[0-2])$/
		}}
		use:liveValidator={{ constrain: true }}
		onvalidate={(e) => {
			handleComponentValidate(e);
		}}
	></div>
{/snippet}

{#snippet day(i: number)}
	<div
		bind:this={controls.day.ref}
		bind:textContent={dayValue}
		role="textbox"
		tabindex="0"
		class="date-input"
		contenteditable
		spellcheck="false"
		onfocus={handleFocus}
		onblur={createHandleBlur(blankState.day)}
		onkeydown={createHandleKeydown(i)}
		onkeyup={createHandleKeyup(i)}
		oninput={createHandleInput(i, 2)}
		use:validate={{
			baseval: blankState.day,
			pattern: /^([0-9]|0[1-9]|[12][0-9]|3[01])$/
		}}
		use:liveValidator={{ constrain: true }}
		onvalidate={(e) => {
			handleComponentValidate(e);
		}}
	></div>
{/snippet}

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

	.date-input {
		@apply ring-sui-accent dark:ring-sui-accent/50 h-6 min-w-[1ch] outline-0 transition-all focus:ring-2;
	}
	.date-input.invalid {
		@apply ring-red-500!;
	}
</style>
