<script lang="ts">
	import { onMount } from 'svelte';
	import type { ColorsPrompt, ColorsImage } from '$lib/types';
	import { randomSeed, extractPalette, uploadImage } from '$lib/utils';
	import { isLoading, loadingState } from '$lib/store';
	import { PUBLIC_WS_ENDPOINT, PUBLIC_API} from '$env/static/public';
	import Pallette from '$lib/Palette.svelte';
	import ArrowRight from '$lib/ArrowRight.svelte';
	import ArrowLeft from '$lib/ArrowLeft.svelte';

	let promptsData: ColorsPrompt[] = [];
	let prompt: string;
	let promptInputEl: HTMLElement;

	onMount(() => {
		fetchData();
		const interval = window.setInterval(fetchData, 5000);
		return () => {
			clearInterval(interval);
		};
	});

	async function fetchData() {
		const palettes = await fetch(PUBLIC_API + '/data').then((d) => d.json());
		if (!promptsData || palettes?.length > promptsData?.length) {
			promptsData = sortData(palettes);
		}
	}

	$: promptsTotal = promptsData?.length || null;

	let page: number = 0;
	const maxPerPage: number = 10;
	$: totalPages = Math.ceil(promptsData?.length / maxPerPage) || 0;
	$: promptsDataPage = [...promptsData].slice(page * maxPerPage, (page + 1) * maxPerPage);
	let pagesLinks: number[] = [];
	$: if (totalPages) {
		const pagesNums = Array(totalPages)
			.fill([])
			.map((_, i) => ({ value: i, label: i + 1 }));
		pagesLinks = pagesNums
			.slice(0, 3)
			.concat([{ value: -1, label: '...' }])
			.concat(pagesNums.length > 3 ? pagesNums.slice(-1) : []);
		console.log(pagesLinks);
	}

	function sortData(_promptData: ColorsPrompt[]) {
		return _promptData
			.sort((a, b) => b.id - a.id)
			.map((p) => p.data)
			.filter((d) => d.images.length > 0);
	}
	async function savePaletteDB(colorPrompt: ColorsPrompt) {
		try {
			const newPalettes: ColorsPrompt[] = await fetch(PUBLIC_API + '/new_palette', {
				method: 'POST',
				headers: {
					'Content-Type': 'application/json'
				},
				body: JSON.stringify({
					prompt: colorPrompt.prompt,
					images: colorPrompt.images.map((i) => ({
						imgURL: i.imgURL,
						colors: i.colors.map((c) => c.formatHex())
					}))
				})
			}).then((d) => d.json());

			promptsData = sortData(newPalettes);
		} catch (e) {
			console.error(e);
		}
	}

	async function generatePalette(_prompt: string) {
		if (!_prompt || $isLoading == true) return;
		$loadingState = 'Pending';
		$isLoading = true;

		const sessionHash = crypto.randomUUID();

		const hashpayload = {
			fn_index: 2,
			session_hash: sessionHash
		};

		const datapayload = {
			data: [_prompt]
		};

		const websocket = new WebSocket(PUBLIC_WS_ENDPOINT);
		// websocket.onopen = async function (event) {
		// 	websocket.send(JSON.stringify({ hash: sessionHash }));
		// };
		websocket.onclose = (evt) => {
			if (!evt.wasClean) {
				$loadingState = 'Error';
				$isLoading = false;
			}
		};
		websocket.onmessage = async function (event) {
			try {
				const data = JSON.parse(event.data);
				$loadingState = '';
				switch (data.msg) {
					case 'send_hash':
						websocket.send(JSON.stringify(hashpayload));
						break;
					case 'send_data':
						$loadingState = 'Sending Data';
						websocket.send(JSON.stringify({ ...hashpayload, ...datapayload }));
						break;
					case 'queue_full':
						$loadingState = 'Queue full';
						websocket.close();
						$isLoading = false;
						return;
					case 'estimation':
						const { msg, rank, queue_size } = data;
						$loadingState = `On queue ${rank}/${queue_size}`;
						break;
					case 'process_generating':
						$loadingState = data.success ? 'Generating' : 'Error';
						break;
					case 'process_completed':
						try {
							const images = await extractColorsImages(data.output.data[0], _prompt);
							savePaletteDB({
								prompt: _prompt,
								images
							});
							$loadingState = data.success ? 'Complete' : 'Error';
						} catch (e) {
							$loadingState = e.message;
						}
						websocket.close();
						$isLoading = false;
						return;
					case 'process_starts':
						$loadingState = 'Processing';
						break;
				}
			} catch (e) {
				console.error(e);
				$isLoading = false;
				$loadingState = 'Error';
			}
		};
	}
	async function extractColorsImages(images: string[], _prompt: string): Promise<ColorsImage[]> {
		const nsfwColors = ['#040404', '#B7B7B7', '#565656', '#747474', '#6C6C6C'];

		const colorImages = [];
		let isNSFW = false;
		for (const base64img of images) {
			const { colors, imgBlob } = await extractPalette(base64img);
			if (
				!colors.map((color) => color.formatHex().toUpperCase()).every((c) => nsfwColors.includes(c))
			) {
				const url = await uploadImage(imgBlob, _prompt);
				const colorsImage: ColorsImage = {
					colors,
					imgURL: url
				};
				colorImages.push(colorsImage);
			} else {
				isNSFW = true;
			}
		}

		if (colorImages.length === 0 && isNSFW) {
			console.error('Possible NSFW image');
			throw new Error('Possible NSFW image');
		}
		return colorImages;
	}
	function remix(e: CustomEvent) {
		prompt = e.detail.prompt;
		promptInputEl.scrollIntoView({ behavior: 'smooth' });
		scrollTop();
	}
	function scrollTop() {
		window.scrollTo(0, 0);
		if ('parentIFrame' in window) {
			window.parentIFrame.scrollTo(0, promptInputEl.offsetTop);
		}
	}
</script>

<div class="max-w-screen-md mx-auto px-3 py-8 relative z-0">
	<h1 class="text-3xl font-bold leading-normal">Palette generation with Stable Diffusion</h1>
	<p class="text-sm">
		Original ideas:

		<a
			class="link"
			target="_blank"
			rel="nofollow noopener"
			href="https://twitter.com/mattdesl/status/1569457653298139136"
		>
			Matt DesLauriers
		</a>,
		<a class="link" href="https://drib.net/homage"> dribnet </a>
	</p>
	<div class="relative top-0 z-50 bg-white dark:bg-black py-3">
		<form class="grid grid-cols-6" on:submit|preventDefault={() => generatePalette(prompt)}>
			<input
				bind:this={promptInputEl}
				class="input"
				placeholder="A photo of a beautiful sunset in San Francisco"
				title="Input prompt to generate image and obtain palette"
				type="text"
				name="prompt"
				bind:value={prompt}
				disabled={$isLoading}
			/>
			<button
				class="button"
				on:click|preventDefault={() => generatePalette(prompt)}
				disabled={$isLoading}
				title="Generate Palette"
			>
				Create Palette
			</button>
		</form>
		{#if $loadingState}
			<h3 class="text-xs font-bold ml-3 inline-block">{$loadingState}</h3>
			{#if $isLoading}
				<svg
					xmlns="http://www.w3.org/2000/svg"
					fill="none"
					viewBox="0 0 24 24"
					class="animate-spin max-w-[1rem] inline-block"
				>
					<path
						fill="currentColor"
						d="M20 12a8 8 0 0 1-8 8v4a12 12 0 0 0 12-12h-4Zm-2-5.3a8 8 0 0 1 2 5.3h4c0-3-1.1-5.8-3-8l-3 2.7Z"
					/>
				</svg>
			{/if}
		{/if}
	</div>

	<div class="flex items-center gap-4 my-10">
		<div class="font-bold text-sm">
			{promptsTotal ? `${promptsTotal} submitted palettes` : 'Loading...'}
		</div>
		<div class="grow border-b border-gray-200" />
	</div>

	{#if promptsDataPage}
		<div>
			{#each promptsDataPage as promptData}
				<Pallette {promptData} on:remix={remix} />
				<div class="border-b border-gray-200 py-2" />
			{/each}
		</div>
		<nav role="navigation">
			<ul
				class="items-center sm:justify-center space-x-2  select-none w-full flex justify-center mt-6 mb-4"
			>
				<li />
				<li>
					<a
						on:click|preventDefault={() => {
							page = page - 1 < 0 ? 0 : page - 1;
							scrollTop();
						}}
						class="px-2.5 py-1 hover:bg-gray-50 dark:hover:bg-gray-800 flex items-center rounded-lg"
						href="#"
						><ArrowLeft /> Previous
					</a>
				</li>
				<li class="text-sm">
					<span class="inline-block min-w-[3ch] text-right">{page + 1} </span>/<span
						class="inline-block min-w-[3ch]"
						>{totalPages}
					</span>
				</li>
				<li>
					<a
						on:click|preventDefault={() => {
							page = page + 1 >= totalPages - 1 ? totalPages - 1 : page + 1;
							scrollTop();
						}}
						class="px-2.5 py-1 hover:bg-gray-50 dark:hover:bg-gray-800 flex items-center rounded-lg"
						href="#"
						>Next <ArrowRight />
					</a>
				</li>
			</ul>
		</nav>
	{/if}
</div>

<style lang="postcss" scoped>
	.link {
		@apply text-xs underline font-bold hover:no-underline hover:text-gray-500 visited:text-gray-500;
	}
	.input {
		@apply text-sm disabled:opacity-50 col-span-4 md:col-span-5 italic dark:placeholder:text-black placeholder:text-white text-white dark:text-black placeholder:text-opacity-30 dark:placeholder:text-opacity-10 dark:bg-white bg-slate-900 border-2 border-black rounded-2xl px-2 shadow-sm focus:outline-none focus:border-gray-400 focus:ring-1;
	}
	.button {
		@apply disabled:opacity-50 col-span-2 md:col-span-1 dark:bg-white dark:text-black border-2 border-black rounded-2xl ml-2 px-2 py-2  text-xs shadow-sm font-bold focus:outline-none focus:border-gray-400;
	}
</style>
