<script lang="ts">
	import { createEventDispatcher, getContext } from 'svelte';

	import HashBadge from './HashBadge.svelte';
	import UpObjectLabel from './UpObjectLabel.svelte';
	import UpLink from './UpLink.svelte';
	import Icon from '../utils/Icon.svelte';
	import { readable, type Readable, writable } from 'svelte/store';
	import { notify, UpNotification } from '$lib/notifications';
	import IconButton from '../utils/IconButton.svelte';
	import { vaultInfo } from '$lib/util/info';
	import type { BrowseContext } from '$lib/util/browse';
	import { Query, type UpObject } from '@upnd/upend';
	import type { ADDRESS_TYPE, EntityInfo } from '@upnd/upend/types';
	import { useEntity } from '$lib/entity';
	import { i18n } from '$lib/i18n';
	import api from '$lib/api';
	import { ATTR_IN, ATTR_KEY, ATTR_LABEL, HIER_ROOT_ADDR } from '@upnd/upend/constants';
	import { selected } from '../EntitySelect.svelte';
	import { Any } from '@upnd/upend/query';
	import type { AddressComponents } from '@upnd/upend/wasm';
	import Editable from '$lib/components/utils/Editable.svelte';
	import type { WidgetChange } from '$lib/types/base';
	import type { SelectorValue } from '$lib/components/utils/Selector.svelte';

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

	export let address: string;
	export let labels: string[] | undefined = undefined;
	export let link = false;
	export let banner = false;
	export let resolve: boolean = !(labels || []).length || banner;
	export let backpath = 0;
	export let select = true;
	export let plain = false;

	let entity: Readable<UpObject | undefined> = readable(undefined);
	let entityInfo: Readable<EntityInfo | AddressComponents | undefined> = writable(undefined);
	$: if (resolve) ({ entity, entityInfo } = useEntity(address));
	$: if (!resolve)
		entityInfo = readable(undefined as undefined | AddressComponents, (set) => {
			api.addressToComponents(address).then((info) => {
				set(info);
			});
		});

	let hasFile = false;
	$: {
		if ($entityInfo?.t == 'Hash' && banner) {
			fetch(api.getRaw(address), {
				method: 'HEAD'
			}).then((response) => {
				hasFile = response.ok;
			});
		}
	}

	// Identification
	let inferredIds: string[] = [];
	$: inferredIds = $entity?.identify() || [];
	let addressIds: string[] = [];
	$: resolving = inferredIds.concat(labels || []).length == 0 && !$entity;

	$: fetchAddressLabels(address);

	async function fetchAddressLabels(address: string) {
		addressIds = [];
		await Promise.all(
			(['Hash', 'Uuid', 'Attribute', 'Url'] as ADDRESS_TYPE[]).map(async (t) => {
				if ((await api.getAddress(t)) == address) {
					addressIds.push(`∈ ${t}`);
				}
			})
		);
		addressIds = addressIds;
	}

	let displayLabel = address;
	$: {
		const allLabels = inferredIds.concat(addressIds).concat(labels || []);
		displayLabel = Array.from(new Set(allLabels)).join(' | ');

		if (!displayLabel && $entityInfo?.t === 'Attribute') {
			displayLabel = `${$entityInfo.c}`;
		}
		displayLabel = displayLabel || address;
	}

	$: dispatch('resolved', inferredIds);

	// Resolved backpath
	let resolvedBackpath: string[] = [];
	$: if (backpath) resolveBackpath();

	async function resolveBackpath() {
		resolvedBackpath = [];
		let levels = 0;
		let current = address;
		while (levels < backpath && current !== HIER_ROOT_ADDR) {
			const parent = await api.query(Query.matches(`@${current}`, ATTR_IN, Any));
			if (parent.entries.length) {
				current = parent.entries[0].value.c as string;
				const label = await api.query(Query.matches(`@${current}`, ATTR_LABEL, Any));
				if (label.entries.length) {
					resolvedBackpath = [label.entries[0].value.c as string, ...resolvedBackpath];
				}
			}
			levels++;
		}
	}

	// Navigation highlights
	const context = getContext('browse') as BrowseContext | undefined;
	const index = context?.index || readable(0);
	const addresses = context?.addresses || readable([]);

	// Native open
	function nativeOpen() {
		notify.emit(
			'notification',
			new UpNotification(
				$i18n.t('Opening {{identity}} in a default native application...', {
					identity: inferredIds[0] || address
				})
			)
		);
		api
			.nativeOpen(address)
			.then(async (response) => {
				if (!response.ok) {
					throw new Error(`${response.statusText} - ${await response.text()}`);
				}
				const rawWarning = response.headers.get('warning');
				if (rawWarning) {
					const warningText = rawWarning?.split(' ').slice(2).join(' ');
					notify.emit('notification', new UpNotification(warningText, 'warning'));
				}
			})
			.catch((err) => {
				notify.emit(
					'notification',
					new UpNotification(
						$i18n.t('Failed to open in native application! ({{err}})', { err }),
						'error'
					)
				);
			});
	}

	function onLabelEdit(ev: CustomEvent<SelectorValue>) {
		if (ev.detail.t == 'String') {
			dispatch('change', { type: 'upsert', attribute: ATTR_LABEL, value: ev.detail });
		}
	}

	let background: string | undefined;
	$: background = $entity?.get('COVER')?.toString();

	let resultBackground = background;
	let imageBackground: string | undefined = undefined;
	$: {
		if (background) {
			imageBackground = `url(${api.getRaw(background)})`;
			resultBackground = 'transparent';
		} else {
			resultBackground = background;
			imageBackground = undefined;
		}
		resultBackground ||= 'var(--background-lighter)';
	}
</script>

<div
	class="upobject"
	class:left-active={address == $addresses[$index - 1]}
	class:right-active={address == $addresses[$index + 1]}
	class:selected={select && $selected.includes(address)}
	class:plain
	style="--background: {resultBackground}"
	class:image-background={Boolean(imageBackground)}
>
	<div
		class="address"
		class:identified={inferredIds.length || addressIds.length || labels?.length}
		class:banner
		class:show-type={$entityInfo?.t === 'Url' && !addressIds.length}
	>
		{#if imageBackground}
			<div class="image-gradient"></div>
			<div class="image-background" style="background-image: {imageBackground}"></div>
		{/if}
		<HashBadge {address} />
		<div class="label" class:resolving title={displayLabel}>
			<Editable
				value={{ t: 'String', c: displayLabel }}
				editable={banner}
				types={['String']}
				on:edit={onLabelEdit}
			>
				<div class="label-inner">
					{#if banner && hasFile}
						<UpObjectLabel label={displayLabel} backpath={resolvedBackpath} />
					{:else if link}
						<UpLink to={{ entity: address }}>
							<UpObjectLabel label={displayLabel} backpath={resolvedBackpath} />
						</UpLink>
					{:else}
						<UpObjectLabel label={displayLabel} backpath={resolvedBackpath} />
					{/if}
				</div>
			</Editable>
			{#if $entity?.get(ATTR_KEY) && !$entity?.get(ATTR_KEY)?.toString()?.startsWith('TYPE_')}
				<div class="key">{$entity.get(ATTR_KEY)}</div>
			{/if}
			<div class="secondary">
				<div class="type">
					{$entityInfo?.t}
					{#if $entityInfo?.t === 'Url' || $entityInfo?.t === 'Attribute'}
						&mdash; {$entityInfo.c}
					{/if}
				</div>
			</div>
		</div>
		{#if banner}
			{#if $entityInfo?.t === 'Attribute'}
				<div class="icon">
					<UpLink to={{ surfaceAttribute: $entityInfo.c }} title={$i18n.t('Open on surface') || ''}>
						<Icon name="cross" />
					</UpLink>
				</div>
			{/if}
			{#if $entityInfo?.t == 'Hash'}
				<div
					class="icon"
					title={hasFile ? $i18n.t('Download as file') : $i18n.t('File not present in vault')}
				>
					<a
						class="link-button"
						class:disabled={!hasFile}
						href="{api.apiUrl}/raw/{address}"
						download={inferredIds[0]}
					>
						<Icon name="download" />
					</a>
				</div>
				{#if $vaultInfo?.desktop && hasFile}
					<div class="icon">
						<IconButton
							name="window-alt"
							on:click={nativeOpen}
							title={$i18n.t('Open in default application...') || ''}
						/>
					</div>
				{/if}
			{/if}
		{/if}
	</div>
</div>

<style lang="scss">
	@use '$lib/styles/colors';

	.upobject {
		height: 100%;
		display: flex;

		align-items: stretch;
		justify-content: stretch;
		border-radius: 4px;

		&.left-active {
			background: linear-gradient(90deg, var(--color-active) 0%, transparent 100%);
			padding: 2px 0 2px 2px;
		}

		&.right-active {
			background: linear-gradient(90deg, transparent 0%, var(--color-active) 100%);
			padding: 2px 2px 2px 0;
		}

		&.plain .address {
			border: none;
			background: none;
			padding: 0;
		}
	}

	.address {
		position: relative;
		flex-grow: 1;
		min-width: 0;

		display: flex;
		align-items: center;

		padding: 0.1em 0.25em;

		font-family: var(--monospace-font);
		line-break: anywhere;

		background: var(--background);
		border: 0.1em solid var(--foreground-lighter);
		border-radius: 0.2em;

		&.banner {
			border: 0.12em solid var(--foreground);
			padding: 0.5em 0.25em;
		}

		&.identified {
			font-family: var(--default-font);
			font-size: 0.95em;
			line-break: auto;
		}

		.label {
			display: flex;
			flex-wrap: wrap;
			align-items: baseline;
			margin-left: 0.25em;
		}

		.label-inner {
			max-width: 100%;
		}

		&.banner .label {
			flex-direction: column;
			gap: 0.1em;
		}

		.secondary {
			font-size: 0.66em;
			display: none;
			opacity: 0.8;
		}

		.key {
			font-family: var(--monospace-font);
			color: colors.$yellow;
			opacity: 0.8;

			&:before {
				content: '⌘';
				margin-right: 0.1em;
			}
		}

		&.banner .key {
			font-size: 0.66em;
		}

		&:not(.banner) .key {
			flex-grow: 1;
			text-align: right;
		}

		&.show-type .secondary,
		&.banner .secondary {
			display: unset;
		}

		.image-gradient {
			position: absolute;
			top: 0;
			left: 0;
			right: 0;
			bottom: 0;
			background: linear-gradient(90deg, rgba(0, 0, 0, 0.66) 16%, var(--background) 66%);
			z-index: -1;
		}

		.image-background {
			position: absolute;
			top: 0;
			left: 0;
			right: 0;
			bottom: 0;
			z-index: -2;

			background-size: cover;
			background-position: center;
			filter: brightness(0.8);
		}
	}

	.label {
		flex-grow: 1;
		min-width: 0;

		:global(a) {
			text-decoration: none;
		}
	}

	.icon {
		margin: 0 0.1em;
	}

	.resolving {
		opacity: 0.7;
	}

	.link-button {
		opacity: 0.66;
		transition:
			opacity 0.2s,
			color 0.2s;

		&:hover {
			opacity: 1;
			color: var(--active-color, var(--primary));
		}
	}

	.upobject {
		transition:
			margin 0.2s ease,
			box-shadow 0.2s ease;
	}

	.selected {
		margin: 0.12rem;
		box-shadow: 0 0 0.1rem 0.11rem colors.$red;
	}

	.disabled {
		pointer-events: none;
		opacity: 0.7;
	}
</style>
