<svelte:head>
	<title>UUID Tools</title>
</svelte:head>

<script lang="js">
	import { copy } from '$lib/clipboard.js';

	// 100-ns intervals from 1582-10-15 to 1970-01-01
	const GREGORIAN_OFFSET = 122192928000000000n;

	/** @param {Uint8Array} bytes @returns {string} */
	function bytesToUUID(bytes) {
		const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
		return `${hex.slice(0,8)}-${hex.slice(8,12)}-${hex.slice(12,16)}-${hex.slice(16,20)}-${hex.slice(20)}`;
	}

	function uuidv1() {
		const bytes = crypto.getRandomValues(new Uint8Array(16));
		const t = BigInt(Date.now()) * 10000n + GREGORIAN_OFFSET;
		const tLow = Number(t & 0xffffffffn);
		const tMid = Number((t >> 32n) & 0xffffn);
		const tHi  = Number((t >> 48n) & 0x0fffn);
		bytes[0] = (tLow >> 24) & 0xff;
		bytes[1] = (tLow >> 16) & 0xff;
		bytes[2] = (tLow >> 8)  & 0xff;
		bytes[3] =  tLow        & 0xff;
		bytes[4] = (tMid >> 8)  & 0xff;
		bytes[5] =  tMid        & 0xff;
		bytes[6] = ((tHi >> 8)  & 0x0f) | 0x10;
		bytes[7] =  tHi         & 0xff;
		bytes[8] = (bytes[8] & 0x3f) | 0x80;
		bytes[10] |= 0x01; // multicast bit → random node
		return bytesToUUID(bytes);
	}

	function uuidv4() {
		return crypto.randomUUID();
	}

	function uuidv6() {
		const bytes = crypto.getRandomValues(new Uint8Array(16));
		const t = BigInt(Date.now()) * 10000n + GREGORIAN_OFFSET;
		const tTop = t >> 12n;
		bytes[0] = Number((tTop >> 40n) & 0xffn);
		bytes[1] = Number((tTop >> 32n) & 0xffn);
		bytes[2] = Number((tTop >> 24n) & 0xffn);
		bytes[3] = Number((tTop >> 16n) & 0xffn);
		bytes[4] = Number((tTop >> 8n)  & 0xffn);
		bytes[5] = Number( tTop         & 0xffn);
		bytes[6] = 0x60 | Number((t >> 8n) & 0x0fn);
		bytes[7] = Number(t & 0xffn);
		bytes[8] = (bytes[8] & 0x3f) | 0x80;
		bytes[10] |= 0x01;
		return bytesToUUID(bytes);
	}

	function uuidv7() {
		const bytes = crypto.getRandomValues(new Uint8Array(16));
		const ms = Date.now();
		bytes[0] = (ms / 2**40) & 0xff;
		bytes[1] = (ms / 2**32) & 0xff;
		bytes[2] = (ms / 2**24) & 0xff;
		bytes[3] = (ms / 2**16) & 0xff;
		bytes[4] = (ms / 2**8)  & 0xff;
		bytes[5] =  ms          & 0xff;
		bytes[6] = (bytes[6] & 0x0f) | 0x70;
		bytes[8] = (bytes[8] & 0x3f) | 0x80;
		return bytesToUUID(bytes);
	}

	/**
	 * @typedef {{
	 *   version: number,
	 *   variant: string,
	 *   timestamp?: number,
	 *   date?: string,
	 *   dateLocal?: string,
	 *   randA?: string,
	 *   randB?: string,
	 *   clockSeq?: string,
	 *   node?: string
	 * }} ParsedUUID
	 */

	/** @param {string} input @returns {ParsedUUID | { error: string }} */
	function parseUUID(input) {
		const s = input.trim().toLowerCase();
		if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(s)) {
			return { error: 'Invalid UUID format (expected xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)' };
		}
		const hex = s.replace(/-/g, '');
		const version = parseInt(hex[12], 16);
		const variantNibble = parseInt(hex[16], 16);

		let variant;
		if      ((variantNibble & 0x8) === 0)   variant = 'NCS (0xxx)';
		else if ((variantNibble & 0xc) === 0x8) variant = 'RFC 4122 (10xx)';
		else if ((variantNibble & 0xe) === 0xc) variant = 'Microsoft (110x)';
		else                                    variant = 'Reserved (111x)';

		/** @type {ParsedUUID} */
		const result = { version, variant };

		if (version === 7) {
			const tsMs = parseInt(hex.slice(0, 12), 16);
			const date = new Date(tsMs);
			result.timestamp  = tsMs;
			result.date       = date.toISOString();
			result.dateLocal  = date.toLocaleString();
			result.randA      = hex.slice(13, 16);
			result.randB      = hex.slice(16);
		} else if (version === 1) {
			const tHi    = hex.slice(13, 16);
			const tMid   = hex.slice(8, 12);
			const tLow   = hex.slice(0, 8);
			const t100ns = BigInt('0x' + tHi + tMid + tLow);
			const unixMs = (t100ns - GREGORIAN_OFFSET) / 10000n;
			const date   = new Date(Number(unixMs));
			result.timestamp = Number(unixMs);
			result.date      = date.toISOString();
			result.dateLocal = date.toLocaleString();
			result.clockSeq  = hex.slice(16, 20);
			result.node      = hex.slice(20);
		} else if (version === 6) {
			const tHigh48 = hex.slice(0, 12);
			const tLow12  = hex.slice(13, 16);
			const t100ns  = (BigInt('0x' + tHigh48) << 12n) | BigInt('0x' + tLow12);
			const unixMs  = (t100ns - GREGORIAN_OFFSET) / 10000n;
			const date    = new Date(Number(unixMs));
			result.timestamp = Number(unixMs);
			result.date      = date.toISOString();
			result.dateLocal = date.toLocaleString();
			result.clockSeq  = hex.slice(16, 20);
			result.node      = hex.slice(20);
		}

		return result;
	}

	// --- State ---

	/** @type {{ label: string, uuid: string, gen: () => string }[]} */
	let generators = $state([
		{ label: 'v1', uuid: uuidv1(), gen: uuidv1 },
		{ label: 'v4', uuid: uuidv4(), gen: uuidv4 },
		{ label: 'v6', uuid: uuidv6(), gen: uuidv6 },
		{ label: 'v7', uuid: uuidv7(), gen: uuidv7 },
	]);

	let parseInput = $state('');
	const parseResult = $derived(parseInput.trim().length === 36 ? parseUUID(parseInput) : null);
	const parsed      = $derived(parseResult && !('error' in parseResult) ? parseResult : null);
	const parseError  = $derived(parseResult && 'error' in parseResult ? parseResult.error : '');
</script>

<h1>UUID</h1>

<h2>Generate</h2>
<table>
	<tbody>
		{#each generators as g}
			<tr>
				<td>{g.label}</td>
				<td><code>{g.uuid}</code></td>
				<td><button onclick={() => { g.uuid = g.gen(); }}>new</button></td>
				<td><button onclick={() => copy(g.uuid)}>copy</button></td>
			</tr>
		{/each}
	</tbody>
</table>

<hr>

<h2>Parse / Inspect</h2>
<p>
	<input
		type="text"
		bind:value={parseInput}
		placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
		size="38"
		spellcheck="false"
	>
</p>

{#if parseError}
	<p class="error">{parseError}</p>
{/if}

{#if parsed}
	<table>
		<tbody>
			<tr><td>Version</td><td><b>{parsed.version}</b></td><td></td></tr>
			<tr><td>Variant</td><td>{parsed.variant}</td><td></td></tr>
			{#if parsed.date}
				<tr><td>Timestamp (ms)</td><td><code>{parsed.timestamp}</code></td><td><button onclick={() => copy(String(parsed.timestamp))}>copy</button></td></tr>
				<tr><td>Date (UTC)</td><td><code>{parsed.date}</code></td><td><button onclick={() => copy(parsed.date ?? '')}>copy</button></td></tr>
				<tr><td>Date (local)</td><td><code>{parsed.dateLocal}</code></td><td><button onclick={() => copy(parsed.dateLocal ?? '')}>copy</button></td></tr>
			{/if}
			{#if parsed.randA}
				<tr><td>rand_a (12 bits)</td><td><code>{parsed.randA}</code></td><td></td></tr>
				<tr><td>rand_b (62 bits)</td><td><code>{parsed.randB}</code></td><td></td></tr>
			{/if}
			{#if parsed.clockSeq}
				<tr><td>Clock seq</td><td><code>{parsed.clockSeq}</code></td><td></td></tr>
				<tr><td>Node</td><td><code>{parsed.node}</code></td><td><button onclick={() => copy(parsed.node ?? '')}>copy</button></td></tr>
			{/if}
		</tbody>
	</table>
{/if}

<style>
	.error { color: var(--err); }
</style>
