<script lang="ts">
	import EntryView, { type Widget } from './EntryView.svelte';
	import { useEntity } from '$lib/entity';
	import UpObject from './display/UpObject.svelte';
	import { createEventDispatcher } from 'svelte';
	import { derived, type Readable } from 'svelte/store';
	import { Query, type UpEntry } from '@upnd/upend';
	import Spinner from './utils/Spinner.svelte';
	import NotesEditor from './utils/NotesEditor.svelte';
	import type { WidgetChange } from '../types/base';
	import type { Address, EntityInfo } from '@upnd/upend/types';
	import IconButton from './utils/IconButton.svelte';
	import BlobViewer from './display/BlobViewer.svelte';
	import { i18n } from '../i18n';
	import EntryList from './widgets/EntryList.svelte';
	import api from '$lib/api';
	import EntityList from './widgets/EntityList.svelte';
	import { ATTR_IN, ATTR_KEY, ATTR_LABEL, ATTR_OF } from '@upnd/upend/constants';
	import InspectGroups from './InspectGroups.svelte';
	import InspectTypeEditor from './InspectTypeEditor.svelte';
	import LabelBorder from './utils/LabelBorder.svelte';
	import { debug } from 'debug';
	import { Any } from '@upnd/upend/query';
	import { isDefined } from '$lib/util/werk';
	const dbg = debug('kestrel:Inspect');

	const dispatch = createEventDispatcher<{
		resolved: string[];
		close: void;
	}>();

	export let address: string;
	export let detail: boolean;
	let showAsEntries = false;
	let highlightedType: string | undefined;

	let blobHandled = false;

	$: ({ entity, entityInfo, error, revalidate } = useEntity(address));

	$: allTypes = derived(
		entityInfo,
		($entityInfo, set) => {
			getAllTypes($entityInfo!).then((allTypes) => {
				set(allTypes);
			});
		},
		{}
	) as Readable<{
		[key: string]: {
			labels: string[];
			attributes: string[];
		};
	}>;
	$: sortedTypes = Object.entries($allTypes)
		.sort(([a, _], [b, __]) => a.localeCompare(b))
		.sort(([_, a], [__, b]) => a.attributes.length - b.attributes.length);

	async function getAllTypes(entityInfo: EntityInfo) {
		const allTypes: Record<Address, { labels: string[]; attributes: string[] }> = {};

		if (!entityInfo) {
			return {};
		}

		const typeAddresses: string[] = [
			await api.getAddress(entityInfo.t),
			...($entity?.attr[ATTR_IN] || []).map((e) => e.value.c as string)
		];
		const typeAddressesIn = typeAddresses.map((addr) => `@${addr}`).join(' ');

		const labelsQuery = await api.query(`(matches (in ${typeAddressesIn}) "${ATTR_LABEL}" ?)`);

		typeAddresses.forEach((address) => {
			let labels = labelsQuery.getObject(address).identify();

			let typeLabel: string | undefined;
			if (typeLabel) {
				labels.unshift(typeLabel);
			}

			allTypes[address] = {
				labels,
				attributes: []
			};
		});

		const attributes = await api.query(`(matches ? "${ATTR_OF}" (in ${typeAddressesIn}))`);
		await Promise.all(
			typeAddresses.map(async (address) => {
				allTypes[address].attributes = (
					await Promise.all(
						(attributes.getObject(address).attr[`~${ATTR_OF}`] || []).map(async (e) => {
							try {
								const { t, c } = await api.addressToComponents(e.entity);
								if (t == 'Attribute') {
									return c;
								}
							} catch (err) {
								console.error(err);
								return undefined;
							}
						})
					)
				).filter(isDefined);
			})
		);

		const result: Record<Address, { labels: string[]; attributes: string[] }> = {};
		Object.keys(allTypes).forEach((addr) => {
			if (allTypes[addr].attributes.length > 0) {
				result[addr] = allTypes[addr];
			}
		});

		return result;
	}

	let untypedProperties = [] as UpEntry[];
	let untypedLinks = [] as UpEntry[];

	$: {
		untypedProperties = [];
		untypedLinks = [];

		($entity?.attributes || []).forEach((entry) => {
			const entryTypes = Object.entries($allTypes || {}).filter(([_, t]) =>
				t.attributes.includes(entry.attribute)
			);
			if (entryTypes.length === 0) {
				if (entry.value.t === 'Address') {
					untypedLinks.push(entry);
				} else {
					untypedProperties.push(entry);
				}
			}
		});

		untypedProperties = untypedProperties;
		untypedLinks = untypedLinks;
	}

	$: filteredUntypedProperties = untypedProperties.filter(
		(entry) =>
			![
				ATTR_LABEL,
				ATTR_IN,
				ATTR_KEY,
				'NOTE',
				'LAST_VISITED',
				'NUM_VISITED',
				'LAST_ATTRIBUTE_WIDGET',
				'COVER'
			].includes(entry.attribute)
	);

	$: currentUntypedProperties = filteredUntypedProperties;

	$: filteredUntypedLinks = untypedLinks.filter(
		(entry) => ![ATTR_IN, ATTR_OF, 'COVER'].includes(entry.attribute)
	);

	$: currentUntypedLinks = filteredUntypedLinks;

	$: currentBacklinks =
		$entity?.backlinks.filter((entry) => ![ATTR_IN, ATTR_OF].includes(entry.attribute)) || [];

	$: tagged = $entity?.attr[`~${ATTR_IN}`] || [];

	let attributesUsed: UpEntry[] = [];
	$: {
		if ($entityInfo?.t === 'Attribute') {
			api
				.query(`(matches ? "${$entityInfo.c}" ?)`)
				.then((result) => (attributesUsed = result.entries));
		}
	}

	let correctlyTagged: Address[] | undefined;
	let incorrectlyTagged: Address[] | undefined;
	$: {
		if ($entity?.attr[`~${ATTR_OF}`]) {
			fetchCorrectlyTagged();
		}
	}

	async function fetchCorrectlyTagged() {
		if (!$entity?.attr[`~${ATTR_OF}`]?.length) {
			return;
		}

		const allAttributes = (
			await Promise.all(
				($entity?.attr[`~${ATTR_OF}`] ?? []).map(async (e) => {
					return { address: e.entity, components: await api.addressToComponents(e.entity) };
				})
			)
		)
			.filter((ac) => ac.components.t == 'Attribute')
			.filter(isDefined);

		const attributeRequiredQuery = await api.query(
			Query.matches(
				allAttributes.map((ac) => `@${ac.address}`),
				'TYPE_REQUIRED',
				Any
			)
		);

		const requiredAttributes = allAttributes
			.filter((ac) => {
				return attributeRequiredQuery.getObject(ac.address).attr['TYPE_REQUIRED']?.length;
			})
			.map((ac) => ac.components.c as string);

		const attributeQuery = await api.query(
			Query.matches(
				tagged.map((t) => `@${t.entity}`),
				requiredAttributes,
				Any
			)
		);

		correctlyTagged = [];
		incorrectlyTagged = [];

		for (const element of tagged) {
			const entity = attributeQuery.getObject(element.entity);
			if (requiredAttributes.every((attr) => entity.attr[attr])) {
				correctlyTagged = [...correctlyTagged, element.entity];
			} else {
				incorrectlyTagged = [...incorrectlyTagged, element.entity];
			}
		}
	}

	async function onChange(ev: CustomEvent<WidgetChange>) {
		dbg('onChange', ev.detail);
		const changes = Array.isArray(ev.detail) ? ev.detail : [ev.detail];
		for (const change of changes) {
			switch (change.type) {
				case 'create':
					await api.putEntry({
						entity: address,
						attribute: change.attribute,
						value: change.value
					});
					break;
				case 'delete':
					await api.deleteEntry(change.address);
					break;
				case 'upsert':
					await api.putEntityAttribute(address, change.attribute, change.value);
					break;
				case 'entry-add':
					await api.putEntry({
						entity: change.address,
						attribute: ATTR_IN,
						value: { t: 'Address', c: address }
					});
					break;
				case 'entry-delete': {
					const inEntry = $entity?.attr[`~${ATTR_IN}`]?.find((e) => e.entity === change.address);
					if (inEntry) {
						await api.deleteEntry(inEntry.address);
					} else {
						console.warn("Couldn't find IN entry for entity %s?!", change.address);
					}
					break;
				}
				default:
					console.error('Unimplemented AttributeChange', change);
					return;
			}
		}
		revalidate();
	}

	let identities = [address];

	function onResolved(ev: CustomEvent<string[]>) {
		identities = ev.detail;
		dispatch('resolved', ev.detail);
	}

	async function deleteObject() {
		if (confirm(`${$i18n.t('Really delete')} "${identities.join(' | ')}"?`)) {
			await api.deleteEntry(address);
			dispatch('close');
		}
	}

	const attributeWidgets: Widget[] = [
		{
			name: 'List',
			icon: 'list-check',
			components: ({ entries }) => [
				{
					component: EntryList,
					props: {
						entries,
						columns: 'attribute, value'
					}
				}
			]
		}
	];

	const linkWidgets: Widget[] = [
		{
			name: 'List',
			icon: 'list-check',
			components: ({ entries, group }) => [
				{
					component: EntryList,
					props: {
						entries,
						columns: 'attribute, value',
						attributes: group ? $allTypes[group]?.attributes : [] || []
					}
				}
			]
		},
		{
			name: 'Entity List',
			icon: 'image',
			components: ({ entries, address }) => [
				{
					component: EntityList,
					props: {
						address,
						entities: entries.filter((e) => e.value.t == 'Address').map((e) => e.value.c),
						thumbnails: true
					}
				}
			]
		}
	];

	const taggedWidgets: Widget[] = [
		{
			name: 'List',
			icon: 'list-check',
			components: ({ entries, address }) => [
				{
					component: EntityList,
					props: {
						address,
						entities: entries.map((e) => e.entity),
						thumbnails: false
					}
				}
			]
		},
		{
			name: 'EntityList',
			icon: 'image',
			components: ({ entries, address }) => [
				{
					component: EntityList,
					props: {
						address,
						entities: entries.map((e) => e.entity),
						thumbnails: true
					}
				}
			]
		}
	];

	$: entity.subscribe(async (object) => {
		if (object && object.listing?.entries.length) {
			dbg('Updating visit stats for %o', object);
			await api.putEntityAttribute(
				object.address,
				'LAST_VISITED',
				{
					t: 'Number',
					c: new Date().getTime() / 1000
				},
				'IMPLICIT'
			);

			await api.putEntityAttribute(
				object.address,
				'NUM_VISITED',
				{
					t: 'Number',
					c: (parseInt(String(object.get('NUM_VISITED'))) || 0) + 1
				},
				'IMPLICIT'
			);
		}
	});
</script>

<div
	class="inspect"
	class:detail
	class:blob={blobHandled}
	data-address-multi={($entity?.attr['~IN']?.map((e) => e.entity) || []).join(',')}
>
	<header>
		<h2>
			{#if $entity}
				{#key $entity}
					<UpObject banner {address} on:resolved={onResolved} on:change={onChange} />
				{/key}
			{:else}
				<Spinner centered />
			{/if}
		</h2>
	</header>
	{#if !showAsEntries}
		<div class="main-content">
			<div class="detail-col">
				<div class="blob-viewer">
					<BlobViewer {address} {detail} on:handled={(ev) => (blobHandled = ev.detail)} />
				</div>
				{#if !$error && $entity}
					<InspectGroups
						{entity}
						on:highlighted={(ev) => (highlightedType = ev.detail)}
						on:change={() => revalidate()}
					/>
					<div class="properties">
						<NotesEditor {address} on:change={onChange} />
						<InspectTypeEditor {entity} on:change={() => revalidate()} />
						{#each sortedTypes as [typeAddr, { labels, attributes }]}
							<EntryView
								entries={($entity?.attributes || []).filter((e) =>
									attributes.includes(e.attribute)
								)}
								widgets={linkWidgets}
								on:change={onChange}
								highlighted={highlightedType == typeAddr}
								title={labels.join(' | ')}
								group={typeAddr}
								{address}
							/>
						{/each}

						{#if currentUntypedProperties.length > 0}
							<EntryView
								title={$i18n.t('Other Properties') || ''}
								widgets={attributeWidgets}
								entries={currentUntypedProperties}
								on:change={onChange}
								{address}
							/>
						{/if}

						{#if currentUntypedLinks.length > 0}
							<EntryView
								title={$i18n.t('Links') || ''}
								widgets={linkWidgets}
								entries={currentUntypedLinks}
								on:change={onChange}
								{address}
							/>
						{/if}

						{#if !correctlyTagged || !incorrectlyTagged}
							<EntryView
								title={`${$i18n.t('Members')}`}
								widgets={taggedWidgets}
								entries={tagged}
								on:change={onChange}
								{address}
							/>
						{:else}
							<EntryView
								title={`${$i18n.t('Typed Members')} (${correctlyTagged.length})`}
								widgets={taggedWidgets}
								entries={tagged.filter((e) => correctlyTagged?.includes(e.entity))}
								on:change={onChange}
								{address}
							/>
							<EntryView
								title={`${$i18n.t('Untyped members')} (${incorrectlyTagged.length})`}
								widgets={taggedWidgets}
								entries={tagged.filter((e) => incorrectlyTagged?.includes(e.entity))}
								on:change={onChange}
								{address}
							/>
						{/if}

						{#if currentBacklinks.length > 0}
							<EntryView
								title={`${$i18n.t('Referred to')} (${currentBacklinks.length})`}
								entries={currentBacklinks}
								on:change={onChange}
								{address}
							/>
						{/if}

						{#if $entityInfo?.t === 'Attribute'}
							<LabelBorder>
								<span slot="header">{$i18n.t('Used')} ({attributesUsed.length})</span>
								<EntryList columns="entity,value" entries={attributesUsed} orderByValue />
							</LabelBorder>
						{/if}
					</div>
				{:else if $error}
					<div class="error">
						{$error}
					</div>
				{/if}
			</div>
		</div>
	{:else}
		<div class="entries">
			<h2>{$i18n.t('Attributes')}</h2>
			<EntryList
				entries={$entity?.attributes || []}
				columns={detail ? 'timestamp, provenance, attribute, value' : 'attribute, value'}
				on:change={onChange}
			/>
			<h2>{$i18n.t('Backlinks')}</h2>
			<EntryList
				entries={$entity?.backlinks || []}
				columns={detail ? 'timestamp, provenance, entity, attribute' : 'entity, attribute'}
				on:change={onChange}
			/>
		</div>
	{/if}
	<div class="footer">
		<IconButton
			name="detail"
			title={$i18n.t('Show as entries') || ''}
			active={showAsEntries}
			on:click={() => (showAsEntries = !showAsEntries)}
		/>
	</div>
	<IconButton
		name="trash"
		outline
		subdued
		color="#dc322f"
		on:click={deleteObject}
		title={$i18n.t('Delete object') || ''}
	/>
</div>

<style lang="scss">
	header h2 {
		margin-bottom: 0;
	}

	.inspect,
	.main-content {
		flex: auto;
		display: flex;
		flex-direction: column;
		gap: 0.5rem;
		min-height: 0;
	}

	.properties {
		flex: auto;
		height: 0; // https://stackoverflow.com/a/14964944
		min-height: 12em;
		overflow-y: auto;
		padding-right: 1rem;
	}

	@media screen and (min-width: 1600px) {
		.inspect.detail {
			.main-content {
				position: relative;
				flex-direction: row;
				justify-content: end;
			}

			&.blob {
				.detail-col {
					width: 33%;
					flex-grow: 0;
				}

				.blob-viewer {
					width: 65%;
					height: 100%;
					position: absolute;
					left: 1%;
					top: 0;
				}
			}
		}
	}

	.main-content .detail-col {
		display: flex;
		flex-direction: column;
		flex-grow: 1;
	}

	.entries {
		flex-grow: 1;
	}

	.footer {
		margin-top: 2rem;
		display: flex;
		justify-content: end;
	}

	.error {
		color: red;
	}
</style>
