<template>
  <div
    ref="containerRef"
    class="virtual-scroller"
    :style="containerStyle"
  >
    <div
      ref="wrapperRef"
      class="virtual-scroller-wrapper"
      :style="wrapperStyle"
    >
      <div
        ref="renderContainerRef"
        class="virtual-scroller-render-container"
        :style="renderContainerStyle"
        @scroll.passive="handleScroll"
        @wheel="handleWheel"
      >
        <div class="virtual-scroller-spacer" :style="{ height: `${totalHeight}px` }"></div>
        <div
          class="virtual-scroller-bottom-placeholder"
          :style="bottomPlaceholderStyle"
        >
          <slot name="bottom-placeholder" />
        </div>
        <div
          v-for="renderItem in visibleItems"
          :key="getItemKey(renderItem.item, renderItem.index)"
          :ref="el => setItemRef(el, renderItem.index)"
          class="virtual-scroller-item"
          :style="getItemStyle(renderItem)"
          :data-index="renderItem.index"
        >
          <slot
            name="default"
            :item="renderItem.item"
            :index="renderItem.index"
          />
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'

const props = defineProps({
  data: {
    type: Array,
    required: false,
    default: () => []
  },
  items: {
    type: Array,
    required: false,
    default: () => []
  },
  itemKey: {
    type: [String, Function],
    default: 'id'
  },
  keyField: {
    type: String,
    default: 'id'
  },
  estimatedHeight: {
    type: Number,
    default: 100
  },
  buffer: {
    type: Number,
    default: 3
  },
  bufferSize: {
    type: Number,
    default: 3
  },
  height: {
    type: [String, Number],
    default: '100%'
  },
  width: {
    type: [String, Number],
    default: '100%'
  },
  renderMode: {
    type: String,
    default: 'default',
    validator: (value) => ['default', 'top'].includes(value)
  },
  direction: {
    type: String,
    default: 'reverse',
    validator: (value) => ['normal', 'reverse'].includes(value)
  },
  bottomPlaceholderHeight: {
    type: Number,
    default: 350
  }
})

const computedData = computed(() => {
  return props.data.length > 0 ? props.data : props.items
})

const computedItemKey = computed(() => {
  if (typeof props.itemKey === 'function') return props.itemKey
  if (props.itemKey !== 'id') return props.itemKey
  return props.keyField
})

const computedBuffer = computed(() => {
  return props.buffer !== 3 ? props.buffer : props.bufferSize
})

const emit = defineEmits(['scroll', 'scroll-start', 'scroll-end', 'visible-change'])

const containerRef = ref(null)
const wrapperRef = ref(null)
const renderContainerRef = ref(null)
const itemRefs = new Map()
const itemHeights = ref(new Map())
const resizeObserver = ref(null)
const scrollTop = ref(0)
const isScrolling = ref(false)
const scrollTimeout = ref(null)
const isInitialized = ref(false)
const pendingScrollToBottom = ref(false)
const previousDataLength = ref(0)

const containerStyle = computed(() => {
  return {
    height: '100%',
    width: '100%',
    position: 'relative'
  }
})

const totalHeight = computed(() => {
  let height = 0
  const len = computedData.value.length
  
  for (let i = 0; i < len; i++) {
    const cachedHeight = itemHeights.value.get(i)
    height += cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
  }
  
  height += props.bottomPlaceholderHeight
  
  return height
})

const getItemPosition = (index) => {
  let offset = 0
  
  for (let i = 0; i < index; i++) {
    const cachedHeight = itemHeights.value.get(i)
    offset += cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
  }
  
  const height = itemHeights.value.get(index) ?? props.estimatedHeight
  
  return { offset, height }
}

const containerHeight = computed(() => {
  if (!renderContainerRef.value) return 0
  return renderContainerRef.value.clientHeight
})

const visibleRange = computed(() => {
  if (!renderContainerRef.value || computedData.value.length === 0) {
    return { start: 0, end: 0, offset: 0 }
  }
  
  const viewportHeight = containerHeight.value
  const currentScrollTop = scrollTop.value
  const bufferCount = computedBuffer.value
  
  let startIndex = 0
  let endIndex = computedData.value.length - 1
  let startOffset = 0
  
  let offset = 0
  for (let i = 0; i < computedData.value.length; i++) {
    const cachedHeight = itemHeights.value.get(i)
    const height = cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
    
    if (offset + height > currentScrollTop) {
      startIndex = Math.max(0, i - bufferCount)
      break
    }
    
    offset += height
  }
  
  startOffset = 0
  for (let i = 0; i < startIndex; i++) {
    const cachedHeight = itemHeights.value.get(i)
    startOffset += cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
  }
  
  offset = startOffset
  endIndex = startIndex
  for (let i = startIndex; i < computedData.value.length; i++) {
    const cachedHeight = itemHeights.value.get(i)
    const height = cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
    
    offset += height
    
    if (offset > currentScrollTop + viewportHeight) {
      endIndex = Math.min(computedData.value.length - 1, i + bufferCount)
      break
    }
    
    endIndex = i
  }
  
  return { start: Math.min(startIndex, endIndex), end: Math.max(startIndex, endIndex), offset: startOffset }
})

const visibleItems = computed(() => {
  const { start, end, offset } = visibleRange.value
  const items = []
  
  let currentOffset = offset
  
  for (let i = start; i <= end && i < computedData.value.length; i++) {
    const cachedHeight = itemHeights.value.get(i)
    const height = cachedHeight !== undefined ? cachedHeight : props.estimatedHeight
    
    items.push({
      item: computedData.value[i],
      index: i,
      offset: currentOffset + props.bottomPlaceholderHeight,
      height
    })
    
    currentOffset += height
  }
  
  return items
})

const wrapperStyle = computed(() => ({
  direction: 'rtl',
  height: '100%',
  position: 'relative',
  scrollbarWidth: 'auto',
  overflow: 'hidden',
  transform: 'rotate(180deg)',
  width: '100%'
}))

const renderContainerStyle = computed(() => ({
  direction: 'ltr',
  display: 'flex',
  flexDirection: 'column',
  justifyContent: 'flex-end',
  bottom: 0,
  left: 0,
  overflowX: 'hidden',
  overflowY: 'auto',
  position: 'absolute',
  right: 0,
  top: 0,
  width: '100%'
}))

const bottomPlaceholderStyle = computed(() => ({
  position: 'absolute',
  left: 0,
  right: 0,
  top: 0,
  width: '100%',
  height: `${props.bottomPlaceholderHeight}px`,
  transform: `translateY(0px)`,
  zIndex: 1
}))

const getItemKey = (item, index) => {
  const keyField = computedItemKey.value
  if (typeof keyField === 'function') {
    return keyField(item, index)
  }
  if (typeof keyField === 'string' && item && typeof item === 'object') {
    return item[keyField] ?? index
  }
  return index
}

const getItemStyle = (renderItem) => {
  return {
    position: 'absolute',
    left: 0,
    right: 0,
    top: 0,
    width: '100%',
    transform: `translateY(${renderItem.offset}px)`,
    willChange: 'transform'
  }
}

const setItemRef = (el, index) => {
  if (el) {
    itemRefs.set(index, el)
  } else {
    itemRefs.delete(index)
  }
}

const measureItem = (index, element) => {
  if (!element) return
  
  const firstChild = element.firstElementChild
  const targetElement = firstChild || element
  
  const height = Math.ceil(targetElement.offsetHeight)
  
  if (height > 0) {
    const cachedHeight = itemHeights.value.get(index)
    if (cachedHeight !== height) {
      const newHeights = new Map(itemHeights.value)
      newHeights.set(index, height)
      itemHeights.value = newHeights
    }
  }
}

const setupResizeObserver = () => {
  if (resizeObserver.value) {
    resizeObserver.value.disconnect()
  }
  
  resizeObserver.value = new ResizeObserver((entries) => {
    for (const entry of entries) {
      const index = parseInt(entry.target.dataset.index, 10)
      if (!isNaN(index)) {
        measureItem(index, entry.target)
      }
    }
  })
}

const handleWheel = (event) => {
  if (!renderContainerRef.value) return
  
  const { deltaY } = event
  const el = renderContainerRef.value
  
  el.scrollBy({
    top: -deltaY,
    behavior: 'instant'
  })
  
  event.preventDefault()
}

const handleScroll = (event) => {
  const target = event.target
  scrollTop.value = target.scrollTop
  isScrolling.value = true
  
  if (scrollTimeout.value) {
    clearTimeout(scrollTimeout.value)
  }
  
  scrollTimeout.value = setTimeout(() => {
    isScrolling.value = false
  }, 150)
  
  const st = target.scrollTop
  const scrollHeight = target.scrollHeight
  const clientHeight = target.clientHeight
  
  const distanceToContainerTop = st
  const distanceToContainerBottom = scrollHeight - st - clientHeight
  
  const distanceToPageTop = distanceToContainerBottom
  const distanceToPageBottom = distanceToContainerTop
  const isAtPageTop = distanceToPageTop <= 0
  const isAtPageBottom = distanceToPageBottom <= 0
  
  emit('scroll', {
    target,
    scrollTop: st,
    scrollHeight,
    clientHeight,
    distanceToPageTop,
    distanceToPageBottom,
    isAtPageTop,
    isAtPageBottom
  })
  
  if (isAtPageTop) {
    emit('scroll-start')
  }
  
  if (isAtPageBottom) {
    emit('scroll-end')
  }
}

const scrollToIndex = (index, behavior = 'auto') => {
  if (!renderContainerRef.value || index < 0 || index >= computedData.value.length) return
  
  const position = getItemPosition(index)
  
  renderContainerRef.value.scrollTo({
    top: position.offset,
    behavior
  })
}

const scrollToBottom = (behavior = 'smooth') => {
  if (!renderContainerRef.value) {
    pendingScrollToBottom.value = true
    return
  }
  
  requestAnimationFrame(() => {
    if (!renderContainerRef.value) return
    
    renderContainerRef.value.scrollTo({
      top: 0,
      behavior
    })
  })
}

const scrollToTop = (behavior = 'smooth') => {
  if (!renderContainerRef.value) return
  
  requestAnimationFrame(() => {
    if (!renderContainerRef.value) return
    
    const scrollHeight = renderContainerRef.value.scrollHeight
    renderContainerRef.value.scrollTo({
      top: scrollHeight,
      behavior
    })
  })
}

const getScrollElement = () => renderContainerRef.value

const getVisibleIndices = () => {
  const { start, end } = visibleRange.value
  return Array.from({ length: end - start + 1 }, (_, i) => start + i)
}

const resetMeasurements = () => {
  itemHeights.value = new Map()
  itemRefs.clear()
}

const isAtPageBottom = () => {
  if (!renderContainerRef.value) return false
  const { scrollTop } = renderContainerRef.value
  return scrollTop <= 0
}

const isAtPageTop = () => {
  if (!renderContainerRef.value) return false
  const { scrollTop, scrollHeight, clientHeight } = renderContainerRef.value
  return scrollHeight - scrollTop - clientHeight <= 0
}

const observeVisibleItems = () => {
  if (!resizeObserver.value) return
  
  resizeObserver.value.disconnect()
  
  for (const [index, element] of itemRefs) {
    if (element) {
      resizeObserver.value.observe(element)
    }
  }
}

watch(() => computedData.value, (newData, oldData) => {
  const oldLength = oldData?.length || 0
  const newLength = newData.length
  
  if (newLength !== oldLength) {
    const newHeights = new Map()
    
    const minLen = Math.min(oldLength, newLength)
    for (let i = 0; i < minLen; i++) {
      if (itemHeights.value.has(i)) {
        newHeights.set(i, itemHeights.value.get(i))
      }
    }
    
    itemHeights.value = newHeights
    
    nextTick(() => {
      observeVisibleItems()
    })
  }
  
  previousDataLength.value = newLength
}, { deep: false })

watch(visibleItems, (newItems) => {
  nextTick(() => {
    observeVisibleItems()
    cleanupExtraItems(newItems)
  })
  if (newItems.length > 0) {
    const firstItem = newItems[0]
    const lastItem = newItems[newItems.length - 1]
    emit('visible-change', firstItem.index, lastItem.index)
  }
}, { deep: true })

const cleanupExtraItems = (visibleItems) => {
  if (!itemRefs.size || !visibleItems.length) return
  
  const visibleIndices = new Set(visibleItems.map(item => item.index))
  const existingIndices = Array.from(itemRefs.keys()).sort((a, b) => a - b)
  
  const toRemove = []
  let lastValidIndex = -1
  
  for (const index of existingIndices) {
    if (visibleIndices.has(index)) {
      if (lastValidIndex !== -1 && index !== lastValidIndex + 1) {
        for (let i = lastValidIndex + 1; i < index; i++) {
          if (itemRefs.has(i)) {
            toRemove.push(i)
          }
        }
      }
      lastValidIndex = index
    } else {
      toRemove.push(index)
    }
  }
  
  for (const index of toRemove) {
    const element = itemRefs.get(index)
    if (element && element.parentNode) {
      element.parentNode.removeChild(element)
    }
    itemRefs.delete(index)
  }
  
  if (toRemove.length > 0) {
    console.log(`清理了 ${toRemove.length} 个多余项目:`, toRemove)
  }
}

onMounted(() => {
  setupResizeObserver()
  isInitialized.value = true
  previousDataLength.value = computedData.value.length
  
  nextTick(() => {
    if (pendingScrollToBottom.value) {
      pendingScrollToBottom.value = false
      scrollToBottom()
    }
    
    observeVisibleItems()
    cleanupExtraItems(visibleItems.value)
  })
})

onBeforeUnmount(() => {
  if (resizeObserver.value) {
    resizeObserver.value.disconnect()
  }
  
  if (scrollTimeout.value) {
    clearTimeout(scrollTimeout.value)
  }
  
  itemRefs.clear()
})

defineExpose({
  scrollToIndex,
  scrollToItem: scrollToIndex,
  scrollToBottom,
  scrollToTop,
  getScrollElement,
  getVisibleIndices,
  resetMeasurements,
  containerRef,
  isAtPageBottom,
  isAtPageTop
})
</script>

<style lang="less" scoped>
.virtual-scroller {
  -webkit-overflow-scrolling: touch;
  
  &::-webkit-scrollbar {
    display: none;
    width: 6px;
    height: 6px;
  }
  
  &::-webkit-scrollbar-track {
    background: transparent;
  }
  
  &::-webkit-scrollbar-thumb {
    background: rgba(0, 0, 0, 0.2);
    border-radius: 3px;
    
    &:hover {
      background: rgba(0, 0, 0, 0.3);
    }
  }
  
  .virtual-scroller-wrapper {
    contain: content;
    
  }
  
  .virtual-scroller-spacer {
    flex-shrink: 0;
    width: 100%;
  }
  
  .virtual-scroller-placeholder {
    width: 100%;
  }
  
  .virtual-scroller-render-container {
    contain: layout style;
      
    &::-webkit-scrollbar {
      display: none;
      width: 6px;
      height: 6px;
    }
  }
  
  .virtual-scroller-item {
    contain: layout style;
    backface-visibility: hidden;
    perspective: 1000px;
  }
  
  .virtual-scroller-bottom-placeholder {
    contain: layout style;
  }
}
</style>
