<script lang="ts" setup>
import { reactive, ref, computed, unref, onUpdated, watchEffect } from 'vue';

const props = defineProps({
  /**列表高度 */
  height: {
    type: Number,
    default: 300,
  },
  /**列表项高度 */
  itemHeight: {
    type: Number,
    default: 30,
  },
  /**数据 */
  data: {
    type: Array,
    default: () => [],
  },
  /**预先兜底缓存数量 */
  cache: {
    type: Number,
    default: 2,
  },
  /**是否动态加载 */
  dynamic: {
    type: Boolean,
    default: false,
  },
});

const state = reactive<any>({
  start: 0,
  end: 10,
  scrollOffset: 0,
  cacheData: [],
});
const virtualListRef = ref();

const getWrapperStyle = computed(() => {
  const { height } = props;
  return {
    height: `${height}px`,
  };
});

const getInnerStyle = computed(() => {
  return {
    height: `${unref(getTotalHeight)}px`,
    width: '100%',
  };
});

const getListStyle = computed(() => {
  return {
    willChange: 'transform',
    transform: `translateY(${state.scrollOffset}px)`,
  };
});

// 数据数量
const total = computed(() => {
  return props.data.length;
});

// 总体高度
const getTotalHeight = computed(() => {
  if (!props.dynamic) return unref(total) * props.itemHeight;
  return getCurrentTop(unref(total));
});

// 当前屏幕显示的数量
const clientCount = computed(() => {
  return Math.ceil(props.height / props.itemHeight);
});

// 当前屏幕显示的数据
const clientData = computed(() => {
  return props.data.slice(state.start, state.end);
});

const onScroll = (e: any) => {
  const { scrollTop } = e.target;
  if (state.scrollOffset === scrollTop) return;
  const { cache, dynamic, itemHeight } = props;
  const cacheCount = Math.max(1, cache);

  let startIndex = dynamic
    ? getStartIndex(scrollTop)
    : Math.floor(scrollTop / itemHeight);

  const endIndex = Math.max(
    0,
    Math.min(unref(total), startIndex + unref(clientCount) + cacheCount)
  );

  if (startIndex > cacheCount) {
    startIndex = startIndex - cacheCount;
  }

  // 偏移量
  const offset = dynamic
    ? getCurrentTop(startIndex)
    : scrollTop - (scrollTop % itemHeight);

  Object.assign(state, {
    start: startIndex,
    end: endIndex,
    scrollOffset: offset,
  });
};

// 二分法去查找对应的index
const getStartIndex = (scrollTop = 0): number => {
  let low = 0;
  let high = state.cacheData.length - 1;
  while (low <= high) {
    const middle = low + Math.floor((high - low) / 2);
    const middleTopValue = getCurrentTop(middle);
    const middleBottomValue = getCurrentTop(middle + 1);

    if (middleTopValue <= scrollTop && scrollTop <= middleBottomValue) {
      return middle;
    } else if (middleBottomValue < scrollTop) {
      low = middle + 1;
    } else if (middleBottomValue > scrollTop) {
      high = middle - 1;
    }
  }
  return Math.min(
    unref(total) - unref(clientCount),
    Math.floor(scrollTop / props.itemHeight)
  );
};

const getCurrentTop = (index: number) => {
  const lastIndex = state.cacheData.length - 1;

  if (Object.hasOwn(state.cacheData, index)) {
    return state.cacheData[index].top;
  } else if (Object.hasOwn(state.cacheData, index - 1)) {
    return state.cacheData[index - 1].bottom;
  } else if (index > lastIndex) {
    return (
      state.cacheData[lastIndex].bottom +
      Math.max(0, index - state.cacheData[lastIndex].index) * props.itemHeight
    );
  } else {
    return index * props.itemHeight;
  }
};

onUpdated(() => {
  if (!props.dynamic) return;
  const childrenList = virtualListRef.value.children || [];
  [...childrenList].forEach((node: any, index: number) => {
    const height = node.getBoundingClientRect().height;
    const currentIndex = state.start + index;
    if (state.cacheData[currentIndex].height === height) return;

    state.cacheData[currentIndex].height = height;
    state.cacheData[currentIndex].top = getCurrentTop(currentIndex);
    state.cacheData[currentIndex].bottom =
      state.cacheData[currentIndex].top + state.cacheData[currentIndex].height;
  });
});

watchEffect(() => {
  clientData.value.forEach((_, index) => {
    const currentIndex = state.start + index;
    if (Object.hasOwn(state.cacheData, currentIndex)) return;
    state.cacheData[currentIndex] = {
      top: currentIndex * props.itemHeight,
      height: props.itemHeight,
      bottom: (currentIndex + 1) * props.itemHeight,
      index: currentIndex,
    };
  });
});
</script>

<template>
  <div
    class="virtual-list-wrapper"
    ref="wrapperRef"
    :style="getWrapperStyle"
    @scroll="onScroll"
  >
    <div class="virtual-list-inner" ref="innerRef" :style="getInnerStyle">
      <div class="virtual-list" :style="getListStyle" ref="virtualListRef">
        <div v-for="(item, index) in clientData" :key="index + state.start">
          <slot name="default" :item="item"></slot>
        </div>
      </div>
    </div>
  </div>
</template>

<style lang="css" scoped>
.virtual-list-wrapper {
  position: relative;
  overflow-y: auto;
}
</style>
