<script lang="ts">
	import UpObject from './display/UpObject.svelte';
	import api from '$lib/api';
	import Selector, { type SelectorValue } from './utils/Selector.svelte';
	import { createEventDispatcher, onMount, tick } from 'svelte';
	import type { ZoomBehavior, ZoomTransform, Selection } from 'd3';
	import Spinner from './utils/Spinner.svelte';
	import UpObjectCard from './display/UpObjectCard.svelte';
	import BlobPreview from './display/BlobPreview.svelte';
	import SurfacePoint from './display/SurfacePoint.svelte';
	import { i18n } from '../i18n';
	import debug from 'debug';
	import { Query } from '@upnd/upend';
	import { Any } from '@upnd/upend/query';
	import { isDefined } from '$lib/util/werk';
	const dbg = debug('kestrel:surface');
	const dispatch = createEventDispatcher();

	export let x: string | undefined = undefined;
	export let y: string | undefined = undefined;
	$: dispatch('updateAddress', { x, y });

	let loaded = false;

	let viewMode = 'point';

	let currentX = NaN;
	let currentY = NaN;

	let zoom: ZoomBehavior<Element, unknown> | undefined;
	let autofit: () => void | undefined;

	let view: Selection<HTMLElement, unknown, null, undefined>;
	let viewEl: HTMLElement | undefined;
	let viewHeight = 0;
	let viewWidth = 0;

	let selector: Selector | undefined;

	$: if (selector) selector.focus();

	$: {
		if ((x && !y) || (!x && y)) findPerpendicular();
	}
	async function findPerpendicular() {
		const presentAxis = x || y;
		const presentAxisAddress = await api.componentsToAddress({
			t: 'Attribute',
			c: presentAxis
		});
		const result = await api.query(
			Query.or(
				Query.matches(`@${presentAxisAddress}`, 'PERPENDICULAR', Any),
				Query.matches(Any, 'PERPENDICULAR', `@${presentAxisAddress}`)
			)
		);
		const perpendicular = [
			...result.entries.map((e) => e.entity),
			...result.values.filter((v) => v.t === 'Address').map((v) => v.c as string)
		].find((address) => address !== presentAxisAddress);

		if (perpendicular) {
			const perpendicularComponents = await api.addressToComponents(perpendicular);
			if (perpendicularComponents.t !== 'Attribute') return;
			const perpendicularName = perpendicularComponents.c;

			if (x) {
				y = perpendicularName;
			} else {
				x = perpendicularName;
			}
		}
	}

	interface IPoint {
		address: string;
		x: number;
		y: number;
	}
	let points: IPoint[] = [];
	async function loadPoints() {
		if (!x || !y) return;

		points = [];
		const result = await api.query(`(matches ? (in "${x}" "${y}") ?)`);

		points = Object.entries(result.objects)
			.map(([address, obj]) => {
				let objX = parseInt(String(obj.get(x!)));
				let objY = parseInt(String(obj.get(y!)));

				if (objX && objY) {
					return {
						address,
						x: objX,
						y: objY
					};
				}
			})
			.filter(isDefined);

		tick().then(() => {
			autofit();
		});
	}

	$: {
		if (x && y) {
			loadPoints();
		}
	}

	let selectorCoords: [number, number] | null = null;

	onMount(async () => {
		const d3 = await import('d3');

		function init() {
			if (!viewEl) {
				dbg("Couldn't find view element");
				return;
			}
			viewWidth = viewEl.clientWidth;
			viewHeight = viewEl.clientHeight;

			dbg('Initializing Surface view: %dx%d', viewWidth, viewHeight);
			view = d3.select(viewEl);
			const svg = view.select('svg');
			if (svg.empty()) {
				throw new Error("Failed initializing Surface - couldn't locate SVG element");
			}
			svg.selectAll('*').remove();

			const xScale = d3.scaleLinear().domain([0, viewWidth]).range([0, viewWidth]);

			const yScale = d3.scaleLinear().domain([0, viewHeight]).range([viewHeight, 0]);

			let xTicks = 10;
			let yTicks = viewHeight / (viewWidth / xTicks);

			const xAxis = d3
				.axisBottom(xScale)
				.ticks(xTicks)
				.tickSize(viewHeight)
				.tickPadding(5 - viewHeight);

			const yAxis = d3
				.axisRight(yScale)
				.ticks(yTicks)
				.tickSize(viewWidth)
				.tickPadding(5 - viewWidth);

			const gX = svg.append('g').call(xAxis);
			const gY = svg.append('g').call(yAxis);

			zoom = d3.zoom().on('zoom', zoomed);

			function zoomed({ transform }: { transform: ZoomTransform }) {
				const points = view.select('.content');
				points.style(
					'transform',
					`translate(${transform.x}px, ${transform.y}px) scale(${transform.k})`
				);
				const allPoints = view.selectAll('.point');
				allPoints.style('transform', `scale(${1 / transform.k})`);

				gX.call(xAxis.scale(transform.rescaleX(xScale)));
				gY.call(yAxis.scale(transform.rescaleY(yScale)));

				updateStyles();
			}

			autofit = () => {
				if (!zoom) return;

				zoom.translateTo(view as any, 0, viewHeight);

				if (points.length) {
					zoom.scaleTo(
						view as any,
						Math.min(
							viewWidth / 2 / Math.max(...points.map((p) => Math.abs(p.x))) - 0.3,
							viewHeight / 2 / Math.max(...points.map((p) => Math.abs(p.y))) - 0.3
						)
					);
				}
			};

			function updateStyles() {
				svg
					.selectAll('.tick line')
					.attr('stroke-width', (d) => {
						return d === 0 ? 2 : 1;
					})
					.attr('stroke', (d) => {
						return d === 0 ? 'var(--foreground-lightest)' : 'var(--foreground-lighter)';
					});
			}

			// function reset() {
			//   svg.transition().duration(750).call(zoom.transform, d3.zoomIdentity);
			// }

			view.on('mousemove', (ev: MouseEvent) => {
				// not using offsetXY because `translate` transforms on .inner mess it up
				const viewBBox = (view.node() as HTMLElement).getBoundingClientRect();
				const [x, y] = d3
					.zoomTransform(view.select('.content').node() as HTMLElement)
					.invert([ev.clientX - viewBBox.left, ev.clientY - viewBBox.top]);

				currentX = xScale.invert(x);
				currentY = yScale.invert(y);
			});

			d3.select(viewEl)
				.call(zoom as any)
				.on('dblclick.zoom', (_ev: MouseEvent) => {
					selectorCoords = [currentX, currentY];
				});

			autofit();

			loaded = true;
		}

		const resizeObserver = new ResizeObserver(() => {
			tick().then(() => init());
		});
		resizeObserver.observe(viewEl as any);
	});

	async function onSelectorInput(ev: CustomEvent<SelectorValue>) {
		const value = ev.detail;
		if (value.t !== 'Address') return;
		const address = value.c;

		const [xValue, yValue] = selectorCoords as any;
		selectorCoords = null;
		await Promise.all(
			(
				[
					[x, xValue],
					[y, yValue]
				] as any[]
			).map(([axis, value]: [string, number]) =>
				api.putEntityAttribute(address, axis, {
					t: 'Number',
					c: value
				})
			)
		);
		await loadPoints();
	}
</script>

<div class="surface">
	<div class="header ui">
		<div class="axis-selector">
			<div class="label">X</div>
			<Selector
				types={['Attribute', 'NewAttribute']}
				initial={x ? { t: 'Attribute', name: x } : undefined}
				on:input={(ev) => {
					if (ev.detail.t === 'Attribute') x = ev.detail.name;
				}}
			/>
			<div class="value">
				{(Math.round(currentX * 100) / 100).toLocaleString('en', {
					useGrouping: false,
					minimumFractionDigits: 2
				})}
			</div>
		</div>
		<div class="axis-selector">
			<div class="label">Y</div>
			<Selector
				types={['Attribute', 'NewAttribute']}
				initial={y ? { t: 'Attribute', name: y } : undefined}
				on:input={(ev) => {
					if (ev.detail.t === 'Attribute') y = ev.detail.name;
				}}
			/>
			<div class="value">
				{(Math.round(currentY * 100) / 100).toLocaleString('en', {
					useGrouping: false,
					minimumFractionDigits: 2
				})}
			</div>
		</div>
	</div>
	<div class="view" class:loaded bind:this={viewEl}>
		<div class="ui view-mode-selector">
			<div class="label">
				{$i18n.t('View as')}
			</div>
			<select bind:value={viewMode}>
				<option value="point">{$i18n.t('Point')}</option>
				<option value="link">{$i18n.t('Link')}</option>
				<option value="card">{$i18n.t('Card')}</option>
				<!-- <option value="preview">{$i18n.t("Preview")}</option> -->
			</select>
		</div>
		{#if !loaded}
			<div class="loading">
				<Spinner centered="absolute" />
			</div>
		{/if}
		<div class="content">
			{#if selectorCoords !== null}
				<div
					class="point selector"
					style="
            left: {selectorCoords[0]}px;
            top: {viewHeight - selectorCoords[1]}px"
				>
					<Selector
						types={['Address', 'NewAddress']}
						on:input={onSelectorInput}
						on:focus={(ev) => {
							if (!ev.detail) selectorCoords = null;
						}}
						bind:this={selector}
					/>
				</div>
			{/if}
			{#each points as point}
				<div class="point" style="left: {point.x}px; top: {viewHeight - point.y}px">
					<div class="inner">
						{#if viewMode == 'link'}
							<UpObject link address={point.address} />
						{:else if viewMode == 'card'}
							<UpObjectCard address={point.address} />
						{:else if viewMode == 'preview'}
							<BlobPreview address={point.address} />
						{:else if viewMode == 'point'}
							<SurfacePoint address={point.address} />
						{/if}
					</div>
				</div>
			{/each}
		</div>
		<svg />
	</div>
</div>

<style lang="scss">
	.surface {
		display: flex;
		flex-direction: column;
		height: 100%;
	}

	.header {
		display: flex;
		flex-wrap: wrap;
		gap: 1em;
		align-items: center;
		justify-content: space-between;
		margin: 0.5em 0;

		.axis-selector {
			display: flex;
			gap: 1em;
			align-items: center;

			.label {
				font-size: 1rem;
				&::after {
					content: ':';
				}
			}
		}
	}

	.view {
		flex-grow: 1;
		position: relative;

		overflow: hidden;

		:global(svg) {
			width: 100%;
			height: 100%;
		}

		:global(.tick text) {
			color: var(--foreground-lightest);
			font-size: 1rem;
			text-shadow: 0 0 0.25em var(--background);
		}

		.content {
			transform-origin: 0 0;
		}

		.point {
			position: absolute;
			transform-origin: 0 0;

			.inner {
				transform: translate(-50%, -50%);
			}

			&:hover {
				z-index: 99;
			}
		}

		.view-mode-selector {
			position: absolute;
			top: 2rem;
			right: 1.5em;
			padding: 0.66em;
			border-radius: 4px;
			border: 1px solid var(--foreground-lighter);
			background: var(--background);
			opacity: 0.66;
			transition: opacity 0.25s;
			&:hover {
				opacity: 1;
			}
		}

		&:not(.loaded) {
			pointer-events: none;
		}
	}

	.view-mode-selector {
		display: flex;
		flex-direction: column;
		gap: 0.5em;
		align-items: center;
	}

	.ui {
		font-size: 0.8rem;
	}

	.loading {
		position: absolute;
		top: 0;
		left: 0;
		width: 100%;
		height: 100%;
		background: rgba(0, 0, 0, 0.7);
		z-index: 99;
		transform: scale(3);
	}
</style>
