<script lang="ts">
	import filesize from 'filesize';
	import { formatRelative, fromUnixTime, parseISO } from 'date-fns';
	import Ellipsis from '../utils/Ellipsis.svelte';
	import UpObject from '../display/UpObject.svelte';
	import { createEventDispatcher } from 'svelte';
	import type { WidgetChange } from '$lib/types/base';
	import type { UpEntry, UpListing } from '@upnd/upend';
	import IconButton from '../utils/IconButton.svelte';
	import Selector, { type SelectorValue, selectorValueAsValue } from '../utils/Selector.svelte';
	import Editable from '../utils/Editable.svelte';
	import { query } from '$lib/entity';
	import { type Readable, readable } from 'svelte/store';
	import { defaultEntitySort, entityValueSort } from '$lib/util/sort';
	import { attributeLabels } from '$lib/util/labels';
	import { formatDuration } from '$lib/util/fragments/time';
	import { i18n } from '$lib/i18n';
	import UpLink from '../display/UpLink.svelte';
	import { ATTR_ADDED, ATTR_LABEL } from '@upnd/upend/constants';

	const dispatch = createEventDispatcher<{ change: WidgetChange }>();

	export let columns: string | undefined = undefined;
	export let header = true;

	export let orderByValue = false;
	export let columnWidths: string[] | undefined = undefined;

	export let entries: UpEntry[];
	export let attributes: string[] | undefined = undefined;

	// Display
	$: displayColumns = (columns || 'entity, attribute, value').split(',').map((c) => c.trim());

	const TIMESTAMP_COL = 'timestamp';
	const PROVENANCE_COL = 'provenance';
	const ENTITY_COL = 'entity';
	const ATTR_COL = 'attribute';
	const VALUE_COL = 'value';

	$: templateColumns = (
		(displayColumns || []).map((column, idx) => {
			if (columnWidths?.[idx]) return columnWidths[idx];
			return 'minmax(6em, auto)';
		}) as string[]
	)
		.concat(['auto'])
		.join(' ');

	// Editing
	let adding = false;
	let addHover = false;
	let addFocus = false;
	let newAttrSelector: Selector;
	let newEntryAttribute = '';
	let newEntryValue: SelectorValue | undefined;

	$: if (adding && newAttrSelector) newAttrSelector.focus();
	$: if (!addFocus && !addHover && !newEntryAttribute && !newEntryValue) adding = false;

	async function addEntry(attribute: string, value: SelectorValue) {
		dispatch('change', {
			type: 'create',
			attribute,
			value: await selectorValueAsValue(value)
		});
		newEntryAttribute = '';
		newEntryValue = undefined;
	}
	async function removeEntry(address: string) {
		if (confirm($i18n.t('Are you sure you want to remove the property?') || '')) {
			dispatch('change', { type: 'delete', address } as WidgetChange);
		}
	}
	async function updateEntry(oldEntry: UpEntry, value: SelectorValue) {
		dispatch('change', [
			{
				type: 'delete',
				address: oldEntry.address
			},
			{
				type: 'create',
				attribute: oldEntry.attribute,
				value: await selectorValueAsValue(value)
			}
		]);
	}

	// Labelling
	let labelListing: Readable<UpListing | undefined> = readable(undefined);
	$: {
		const addresses: string[] = [];
		entries
			.flatMap((e) => (e.value.t === 'Address' ? [e.entity, e.value.c] : [e.entity]))
			.forEach((addr) => {
				if (!addresses.includes(addr)) {
					addresses.push(addr);
				}
			});

		const addressesString = addresses.map((addr) => `@${addr}`).join(' ');

		labelListing = query(`(matches (in ${addressesString}) "${ATTR_LABEL}" ? )`).result;
	}

	// Sorting
	let sortedEntries = entries;

	let sortKeys: { [key: string]: string[] } = {};
	function addSortKeys(key: string, vals: string[], resort: boolean) {
		if (!sortKeys[key]) {
			sortKeys[key] = [];
		}
		let changed = false;
		vals.forEach((val) => {
			if (!sortKeys[key].includes(val)) {
				changed = true;
				sortKeys[key].push(val);
			}
		});

		if (resort && changed) sortEntries();
	}

	function sortEntries() {
		sortedEntries = orderByValue
			? entityValueSort(entries, Object.assign(sortKeys, $attributeLabels))
			: defaultEntitySort(entries, Object.assign(sortKeys, $attributeLabels));
	}

	$: {
		if ($labelListing) {
			entries.forEach((entry) => {
				addSortKeys(entry.entity, $labelListing!.getObject(entry.entity).identify(), false);

				if (entry.value.t === 'Address') {
					addSortKeys(
						entry.value.c,
						$labelListing!.getObject(String(entry.value.c)).identify(),
						false
					);
				}
			});
			sortEntries();
		}
	}

	entries.forEach((entry) => {
		addSortKeys(entry.entity, entry.listing?.getObject(entry.entity).identify() || [], false);
		if (entry.value.t === 'Address') {
			addSortKeys(
				entry.value.c,
				entry.listing?.getObject(String(entry.value.c)).identify() || [],
				false
			);
		}
	});
	sortEntries();

	// Visibility
	let visible: Set<string> = new Set();
	let observer = new IntersectionObserver((intersections) => {
		intersections.forEach((intersection) => {
			const address = (intersection.target as HTMLElement).dataset['address'];
			if (!address) {
				console.warn('Intersected wrong element?');
				return;
			}

			if (intersection.isIntersecting) {
				visible.add(address);
			}
			visible = visible;
		});
	});

	function observe(node: HTMLElement) {
		observer.observe(node);

		return {
			destroy() {
				observer.unobserve(node);
			}
		};
	}

	// Formatting & Display
	const COLUMN_LABELS: { [key: string]: string } = {
		timestamp: $i18n.t('Added at'),
		provenance: $i18n.t('Provenance'),
		entity: $i18n.t('Entity'),
		attribute: $i18n.t('Attribute'),
		value: $i18n.t('Value')
	};

	function formatValue(value: string | number | null, attribute: string): string {
		try {
			switch (attribute) {
				case 'FILE_SIZE':
					return filesize(parseInt(String(value), 10), { base: 2 });
				case ATTR_ADDED:
				case 'LAST_VISITED':
					return formatRelative(fromUnixTime(parseInt(String(value), 10)), new Date());
				case 'NUM_VISITED':
					return `${value} times`;
				case 'MEDIA_DURATION':
					return formatDuration(parseInt(String(value), 10));
			}
		} catch {
			// noop.
		}
		return String(value);
	}

	// Unused attributes
	let unusedAttributes: string[] = [];

	$: (async () => {
		unusedAttributes = await Promise.all(
			(attributes || []).filter((attr) => !entries.some((entry) => entry.attribute === attr))
		);
	})();
</script>

<div class="entry-list" style:--template-columns={templateColumns}>
	{#if header}
		<header>
			{#each displayColumns as column}
				<div class="label">
					{COLUMN_LABELS[column] || $attributeLabels[column] || column}
				</div>
			{/each}
			<div class="attr-action"></div>
		</header>
	{/if}

	{#each sortedEntries as entry (entry.address)}
		{#if visible.has(entry.address)}
			{#each displayColumns as column}
				{#if column == TIMESTAMP_COL}
					<div class="cell" title={entry.timestamp}>
						{formatRelative(parseISO(entry.timestamp), new Date())}
					</div>
				{:else if column == PROVENANCE_COL}
					<div class="cell">{entry.provenance}</div>
				{:else if column == ENTITY_COL}
					<div class="cell entity mark-entity">
						<UpObject
							link
							labels={$labelListing?.getObject(String(entry.entity))?.identify() || []}
							address={entry.entity}
							on:resolved={(event) => {
								addSortKeys(entry.entity, event.detail, true);
							}}
						/>
					</div>
				{:else if column == ATTR_COL}
					<div
						class="cell mark-attribute"
						class:formatted={Boolean(Object.keys($attributeLabels).includes(entry.attribute))}
					>
						<UpLink to={{ attribute: entry.attribute }}>
							<Ellipsis
								value={$attributeLabels[entry.attribute] || entry.attribute}
								title={$attributeLabels[entry.attribute]
									? `${$attributeLabels[entry.attribute]} (${entry.attribute})`
									: entry.attribute}
							/>
						</UpLink>
					</div>
				{:else if column == VALUE_COL}
					<div
						class="cell value mark-value"
						data-address={entry.value.t === 'Address' ? entry.value.c : undefined}
					>
						<Editable value={entry.value} on:edit={(ev) => updateEntry(entry, ev.detail)}>
							{#if entry.value.t === 'Address'}
								<UpObject
									link
									address={String(entry.value.c)}
									labels={$labelListing?.getObject(String(entry.value.c))?.identify() || []}
									on:resolved={(event) => {
										addSortKeys(String(entry.value.c), event.detail, true);
									}}
								/>
							{:else}
								<div class:formatted={Boolean(formatValue(entry.value.c, entry.attribute))}>
									<Ellipsis
										value={formatValue(entry.value.c, entry.attribute) || String(entry.value.c)}
									/>
								</div>
							{/if}
						</Editable>
					</div>
				{:else}
					<div>?</div>
				{/if}
			{/each}
			<div class="attr-action">
				<IconButton
					plain
					subdued
					name="x-circle"
					color="#dc322f"
					on:click={() => removeEntry(entry.address)}
				/>
			</div>
		{:else}
			<div class="skeleton" data-address={entry.address} use:observe>...</div>
		{/if}
	{/each}

	{#each unusedAttributes as attribute}
		{#each displayColumns as column}
			{#if column == ATTR_COL}
				<div
					class="cell mark-attribute"
					class:formatted={Boolean(Object.keys($attributeLabels).includes(attribute))}
				>
					<UpLink to={{ attribute }}>
						<Ellipsis
							value={$attributeLabels[attribute] || attribute}
							title={$attributeLabels[attribute]
								? `${$attributeLabels[attribute]} (${attribute})`
								: attribute}
						/>
					</UpLink>
				</div>
			{:else if column == VALUE_COL}
				<div class="cell">
					<Editable on:edit={(ev) => addEntry(attribute, ev.detail)}>
						<span class="unset">{$i18n.t('(unset)')}</span>
					</Editable>
				</div>
			{:else}
				<div class="cell"></div>
			{/if}
		{/each}
		<div class="attr-action"></div>
	{/each}

	{#if !attributes?.length}
		{#if adding}
			<!-- svelte-ignore a11y-no-static-element-interactions -->
			<div
				class="add-row"
				on:mouseenter={() => (addHover = true)}
				on:mouseleave={() => (addHover = false)}
			>
				{#each displayColumns as column}
					{#if column == ATTR_COL}
						<div class="cell mark-attribute">
							<Selector
								types={['Attribute', 'NewAttribute']}
								on:input={(ev) => (newEntryAttribute = ev.detail?.name)}
								on:focus={(ev) => (addFocus = ev.detail)}
								keepFocusOnSet
								bind:this={newAttrSelector}
							/>
						</div>
					{:else if column === VALUE_COL}
						<div class="cell mark-value">
							<Selector
								on:input={(ev) => (newEntryValue = ev.detail)}
								on:focus={(ev) => (addFocus = ev.detail)}
								keepFocusOnSet
							/>
						</div>
					{:else}
						<div class="cell"></div>
					{/if}
				{/each}
				<div class="attr-action">
					<IconButton
						name="save"
						on:click={() => newEntryValue && addEntry(newEntryAttribute, newEntryValue)}
					/>
				</div>
			</div>
		{:else}
			<div class="add-button">
				<IconButton outline subdued name="plus-circle" on:click={() => (adding = true)} />
			</div>
		{/if}
	{/if}
</div>

<style lang="scss">
	.entry-list {
		display: grid;
		grid-template-columns: var(--template-columns);
		gap: 0.05rem 0.5rem;

		header {
			display: contents;

			.label {
				font-weight: 600;
			}
		}

		.cell {
			font-family: var(--monospace-font);
			line-break: anywhere;
			min-width: 0;

			border-radius: 4px;
			padding: 2px;

			&.formatted,
			.formatted {
				font-family: var(--default-font);
			}
		}

		.attr-action {
			display: flex;
			justify-content: center;
			align-items: center;
		}

		.add-row {
			display: contents;
		}

		.add-button {
			display: flex;
			flex-direction: column;
			grid-column: 1 / -1;
		}

		.unset {
			opacity: 0.66;
			pointer-events: none;
		}
	}
</style>
