<script setup lang="ts">
import type {
  ChatDocumentLoader,
  ChatImageLoader,
  ChatTimelineItem as ChatTimelineItemType,
  ChatTimelineMessageItem,
  ChatTimelineToolApprovalPayload,
} from './types';

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

import ChatTimelineItem from './ChatTimelineItem.vue';

const props = defineProps<{
  approvalLoading?: boolean;
  copyable?: (item: ChatTimelineMessageItem) => boolean;
  copyAction?: (item: ChatTimelineMessageItem) => boolean | Promise<boolean>;
  documentLoader?: ChatDocumentLoader;
  emptyText?: string;
  imageLoader?: ChatImageLoader;
  items: ChatTimelineItemType[];
  regenerable?: (item: ChatTimelineMessageItem) => boolean;
  regenerateDisabled?: boolean;
  variantLoading?: (item: ChatTimelineMessageItem) => boolean;
}>();

const emit = defineEmits<{
  approve: [payload: ChatTimelineToolApprovalPayload];
  copyMessage: [item: ChatTimelineMessageItem];
  regenerateMessage: [item: ChatTimelineMessageItem];
  reject: [payload: ChatTimelineToolApprovalPayload];
  selectNextVariant: [item: ChatTimelineMessageItem];
  selectPreviousVariant: [item: ChatTimelineMessageItem];
}>();

const containerRef = ref<HTMLElement>();
const isPinnedToBottom = ref(true);
const suppressNextAutoScroll = ref(false);
let preservedScrollTop: number | undefined;

const bottomThreshold = 24;
let scrollFrame = 0;
const assistantActionAnchorIds = computed(() => {
  const latestAssistantByRound = new Map<string, string>();
  for (const item of props.items) {
    if (item.type === 'message' && item.role === 'assistant' && item.roundId) {
      latestAssistantByRound.set(item.roundId, item.id);
    }
  }
  return new Set(latestAssistantByRound.values());
});

function isNearBottom(container: HTMLElement) {
  return (
    container.scrollHeight - container.scrollTop - container.clientHeight <=
    bottomThreshold
  );
}

function updatePinnedState() {
  const container = containerRef.value;
  if (!container) {
    return;
  }
  isPinnedToBottom.value = isNearBottom(container);
}

function scrollToBottom() {
  if (scrollFrame) {
    cancelAnimationFrame(scrollFrame);
  }
  scrollFrame = requestAnimationFrame(() => {
    const container = containerRef.value;
    if (!container) {
      return;
    }
    container.scrollTop = container.scrollHeight;
    updatePinnedState();
  });
}

function handleTimelineScroll() {
  updatePinnedState();
}

function handleThinkingToggle() {
  preservedScrollTop = containerRef.value?.scrollTop;
  suppressNextAutoScroll.value = true;
}

function canCopyMessage(item: ChatTimelineItemType) {
  return item.type === 'message' && (props.copyable?.(item) ?? false);
}

function canRegenerateMessage(item: ChatTimelineItemType) {
  return item.type === 'message' && (props.regenerable?.(item) ?? false);
}

function isAssistantActionAnchor(item: ChatTimelineItemType) {
  return (
    item.type === 'message' &&
    item.role === 'assistant' &&
    Boolean(item.roundId) &&
    assistantActionAnchorIds.value.has(item.id)
  );
}

function isVariantLoading(item: ChatTimelineItemType) {
  return item.type === 'message' && (props.variantLoading?.(item) ?? false);
}

onBeforeUnmount(() => {
  if (scrollFrame) {
    cancelAnimationFrame(scrollFrame);
  }
});

watch(
  () => props.items,
  async () => {
    if (suppressNextAutoScroll.value) {
      suppressNextAutoScroll.value = false;
      await nextTick();
      if (preservedScrollTop !== undefined && containerRef.value) {
        containerRef.value.scrollTop = preservedScrollTop;
      }
      preservedScrollTop = undefined;
      updatePinnedState();
      return;
    }
    if (isPinnedToBottom.value) {
      scrollToBottom();
    }
  },
  { deep: true, immediate: true },
);
</script>

<template>
  <div
    ref="containerRef"
    class="chat-timeline"
    @scroll.passive="handleTimelineScroll"
  >
    <div v-if="items.length === 0" class="chat-timeline__empty">
      <div class="chat-timeline__empty-icon" aria-hidden="true"></div>
      <div class="chat-timeline__empty-text">
        {{ emptyText || '开始对话' }}
      </div>
    </div>
    <template v-else>
      <ChatTimelineItem
        v-for="item in items"
        :key="item.id"
        :assistant-actions-visible="isAssistantActionAnchor(item)"
        :item="item"
        :document-loader="documentLoader"
        :image-loader="imageLoader"
        :approval-loading="approvalLoading"
        :copy-action="copyAction"
        :copyable="canCopyMessage(item)"
        :regenerable="canRegenerateMessage(item)"
        :regenerate-disabled="regenerateDisabled"
        :variant-loading="isVariantLoading(item)"
        @approve="emit('approve', $event)"
        @copy-message="emit('copyMessage', $event)"
        @regenerate-message="emit('regenerateMessage', $event)"
        @reject="emit('reject', $event)"
        @select-next-variant="emit('selectNextVariant', $event)"
        @select-previous-variant="emit('selectPreviousVariant', $event)"
        @thinking-toggle="handleThinkingToggle"
      />
    </template>
  </div>
</template>

<style scoped>
.chat-timeline {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: 12px;
  min-height: 0;
  padding: 16px;
  overflow: auto;
}

.chat-timeline__empty-icon {
  width: 72px;
  height: 72px;
  margin: 0 auto;
  background: var(--el-color-primary-light-9);
  border: 1px solid var(--el-color-primary-light-7);
  border-radius: 8px;
}

.chat-timeline__empty {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: 12px;
  align-items: center;
  justify-content: center;
  min-height: 180px;
  color: var(--el-text-color-secondary);
}

.chat-timeline__empty-text {
  font-size: 13px;
  line-height: 20px;
}
</style>
