<script setup lang="ts">
import type { ChatImageAttachment, ChatImageLoader } from './types';

import { computed, onBeforeUnmount, ref, watch } from 'vue';

import { useEasyFlowModal } from '@easyflow-core/popup-ui';

const props = withDefaults(
  defineProps<{
    compact?: boolean;
    imageLoader?: ChatImageLoader;
    items: ChatImageAttachment[];
    removable?: boolean;
    retryable?: boolean;
  }>(),
  {
    compact: false,
    imageLoader: undefined,
    removable: false,
    retryable: false,
  },
);

const emit = defineEmits<{
  remove: [item: ChatImageAttachment];
  retry: [item: ChatImageAttachment];
}>();

const resolvedUrls = ref<Record<string, string>>({});
const failedSources = ref<Set<string>>(new Set());
const previewItem = ref<ChatImageAttachment>();
const ownedUrls = new Set<string>();
let resolutionVersion = 0;

const [ImagePreviewModal, imagePreviewModalApi] = useEasyFlowModal({
  onOpenChange(open) {
    if (!open) previewItem.value = undefined;
  },
});

function sourceOf(item: ChatImageAttachment) {
  return item.previewUrl || '';
}

function resolvedUrl(item: ChatImageAttachment) {
  return resolvedUrls.value[sourceOf(item)] || '';
}

const previewUrl = computed(() =>
  previewItem.value ? resolvedUrl(previewItem.value) : '',
);

function canPreview(item: ChatImageAttachment) {
  const source = sourceOf(item);
  return (
    Boolean(resolvedUrl(item)) &&
    item.status !== 'error' &&
    item.status !== 'uploading' &&
    !failedSources.value.has(source)
  );
}

function openPreview(item: ChatImageAttachment) {
  if (!canPreview(item)) return;
  previewItem.value = item;
  imagePreviewModalApi.open();
}

function closePreview() {
  previewItem.value = undefined;
  imagePreviewModalApi.close();
}

function releaseOwnedUrl(url?: string) {
  if (!url || !ownedUrls.delete(url)) return;
  URL.revokeObjectURL(url);
}

async function resolveImages() {
  const version = ++resolutionVersion;
  const activeSources = new Set(
    props.items.map((item) => sourceOf(item)).filter(Boolean),
  );
  if (previewItem.value && !activeSources.has(sourceOf(previewItem.value))) {
    closePreview();
  }
  const nextUrls = { ...resolvedUrls.value };
  for (const [source, url] of Object.entries(nextUrls)) {
    if (!activeSources.has(source)) {
      releaseOwnedUrl(url);
      delete nextUrls[source];
    }
  }
  resolvedUrls.value = nextUrls;
  failedSources.value = new Set(
    [...failedSources.value].filter((source) => activeSources.has(source)),
  );

  for (const source of activeSources) {
    if (resolvedUrls.value[source]) continue;
    if (!props.imageLoader || /^(?:blob:|data:)/i.test(source)) {
      resolvedUrls.value = { ...resolvedUrls.value, [source]: source };
      continue;
    }
    try {
      const url = await props.imageLoader(source);
      if (version !== resolutionVersion || !activeSources.has(source)) {
        if (url.startsWith('blob:')) URL.revokeObjectURL(url);
        continue;
      }
      if (url.startsWith('blob:')) ownedUrls.add(url);
      resolvedUrls.value = { ...resolvedUrls.value, [source]: url };
    } catch {
      if (version === resolutionVersion) {
        failedSources.value = new Set([source, ...failedSources.value]);
      }
    }
  }
}

watch(
  () =>
    [
      props.items.map((item) => sourceOf(item)).join('|'),
      props.imageLoader,
    ] as const,
  () => void resolveImages(),
  { immediate: true },
);

onBeforeUnmount(() => {
  resolutionVersion += 1;
  for (const url of ownedUrls) URL.revokeObjectURL(url);
  ownedUrls.clear();
});
</script>

<template>
  <div
    v-if="items.length > 0"
    class="chat-image-attachments"
    :class="{ 'is-compact': compact }"
  >
    <div
      v-for="item in items"
      :key="item.uploadId || item.imageRef || item.localId || item.previewUrl"
      class="chat-image-attachments__item"
      :class="`is-${item.status || 'ready'}`"
    >
      <button
        v-if="resolvedUrl(item)"
        type="button"
        class="chat-image-attachments__preview-trigger"
        :disabled="!canPreview(item)"
        :aria-label="`查看完整图片：${item.name}`"
        title="查看完整图片"
        @click="openPreview(item)"
      >
        <img
          class="chat-image-attachments__image"
          :src="resolvedUrl(item)"
          :alt="item.name"
        />
      </button>
      <div
        v-if="item.status === 'uploading'"
        class="chat-image-attachments__state"
        aria-live="polite"
      >
        <span class="chat-image-attachments__spinner"></span>
        上传中
      </div>
      <div
        v-else-if="item.status === 'error'"
        class="chat-image-attachments__state is-error"
        :title="item.error || '上传失败'"
      >
        上传失败
      </div>
      <div
        v-else-if="failedSources.has(sourceOf(item))"
        class="chat-image-attachments__state is-error"
      >
        加载失败
      </div>
      <div
        v-else-if="!resolvedUrl(item)"
        class="chat-image-attachments__state"
        aria-live="polite"
      >
        <span class="chat-image-attachments__spinner"></span>
        加载中
      </div>
      <div class="chat-image-attachments__name" :title="item.name">
        {{ item.name }}
      </div>
      <div class="chat-image-attachments__actions">
        <button
          v-if="retryable && item.status === 'error'"
          type="button"
          class="chat-image-attachments__action"
          aria-label="重新上传"
          title="重新上传"
          @click.stop="emit('retry', item)"
        >
          ↻
        </button>
        <button
          v-if="removable"
          type="button"
          class="chat-image-attachments__action"
          aria-label="移除图片"
          title="移除图片"
          @click.stop="emit('remove', item)"
        >
          ×
        </button>
      </div>
    </div>

    <ImagePreviewModal
      :bordered="false"
      centered
      class="!max-h-[calc(100vh-24px)] !w-fit max-w-[calc(100vw-24px)]"
      close-on-click-modal
      content-class="!p-0 !overflow-hidden"
      destroy-on-close
      :footer="false"
      :fullscreen-button="false"
      :header="false"
    >
      <img
        v-if="previewUrl"
        class="chat-image-preview__image"
        :src="previewUrl"
        :alt="previewItem?.name || '聊天图片'"
      />
    </ImagePreviewModal>
  </div>
</template>

<style scoped>
.chat-image-attachments {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2, 8px);
  align-items: flex-start;
  width: min(100%, 640px);
}

.chat-image-attachments__item {
  position: relative;
  flex: 0 1 auto;
  width: fit-content;
  max-width: 100%;
  overflow: hidden;
  background: var(--el-fill-color-light);
  border: 1px solid var(--el-border-color-lighter);
  border-radius: var(--el-border-radius-base);
}

.chat-image-attachments__preview-trigger {
  display: block;
  width: fit-content;
  max-width: 100%;
  padding: 0;
  overflow: hidden;
  cursor: zoom-in;
  background: hsl(var(--surface-subtle));
  border: 0;
}

.chat-image-attachments__preview-trigger:disabled {
  cursor: default;
}

.chat-image-attachments__preview-trigger:not(:disabled):hover {
  background: hsl(var(--surface-contrast-soft));
}

.chat-image-attachments__preview-trigger:not(:disabled):focus-visible {
  position: relative;
  z-index: 1;
  outline: 2px solid var(--el-color-primary-light-5);
  outline: 2px solid
    color-mix(in srgb, var(--el-color-primary) 48%, transparent);
  outline-offset: -2px;
}

.chat-image-attachments__image {
  display: block;
  width: auto;
  max-width: 160px;
  height: auto;
  max-height: 120px;
}

.chat-image-attachments__name {
  max-width: 160px;
  padding: var(--space-1, 4px) var(--space-2, 8px);
  overflow: hidden;
  font-size: 12px;
  color: var(--el-text-color-secondary);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat-image-attachments__state {
  position: absolute;
  inset: 0 0 25px;
  display: flex;
  gap: var(--space-1, 4px);
  align-items: center;
  justify-content: center;
  font-size: 12px;
  color: var(--el-text-color-regular);
  background: var(--el-bg-color);
  background: color-mix(in srgb, var(--el-bg-color) 84%, transparent);
}

.chat-image-attachments__state.is-error {
  color: var(--el-color-danger);
}

.chat-image-attachments__spinner {
  width: 14px;
  height: 14px;
  border: 2px solid var(--el-border-color);
  border-top-color: var(--el-color-primary);
  border-radius: 50%;
  animation: chat-image-spin 0.8s linear infinite;
}

.chat-image-attachments__actions {
  position: absolute;
  top: var(--space-1, 4px);
  right: var(--space-1, 4px);
  display: flex;
  gap: 2px;
}

.chat-image-attachments__action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  font-size: 17px;
  color: var(--el-text-color-primary);
  cursor: pointer;
  background: var(--el-bg-color);
  background: color-mix(in srgb, var(--el-bg-color) 88%, transparent);
  border: 0;
  border-radius: 50%;
}

.chat-image-attachments__action:hover,
.chat-image-attachments__action:focus-visible {
  color: var(--el-color-primary);
  outline: 2px solid var(--el-color-primary-light-5);
  outline: 2px solid
    color-mix(in srgb, var(--el-color-primary) 36%, transparent);
}

.chat-image-attachments.is-compact .chat-image-attachments__image {
  max-width: 240px;
  max-height: 160px;
}

.chat-image-attachments.is-compact .chat-image-attachments__name {
  display: none;
}

.chat-image-attachments.is-compact .chat-image-attachments__state {
  inset: 0;
}

.chat-image-preview__image {
  display: block;
  width: auto;
  max-width: calc(100vw - var(--space-6, 24px));
  height: auto;
  max-height: calc(100vh - var(--space-6, 24px));
  object-fit: scale-down;
}

@keyframes chat-image-spin {
  to {
    transform: rotate(360deg);
  }
}
</style>
