<template>
	<view
		class="cl-waterfall"
		:class="[pt.className]"
		:style="{
			padding: `0 ${props.gutter / 2}rpx`
		}"
	>
		<view
			class="cl-waterfall__column"
			v-for="(column, columnIndex) in columns"
			:key="columnIndex"
			:style="{
				margin: `0 ${props.gutter / 2}rpx`
			}"
		>
			<view class="cl-waterfall__column-inner">
				<view
					v-for="(item, index) in column"
					:key="`${columnIndex}-${index}-${item[props.nodeKey]}`"
					class="cl-waterfall__item"
					:class="{
						'is-virtual': item.isVirtual
					}"
				>
					<slot name="item" :item="item" :index="index"></slot>
				</view>
			</view>
		</view>
	</view>
</template>

<script setup lang="ts">
import { assign, isNull } from "@/cool";
import { computed, getCurrentInstance, nextTick, onMounted, ref, watch } from "vue";
import { parsePt } from "@/cool";

defineOptions({
	name: "cl-waterfall"
});

// 定义插槽类型，item插槽接收item和index参数
defineSlots<{
	item(props: { item: UTSJSONObject; index: number }): any;
}>();

// 组件属性定义
const props = defineProps({
	// 透传属性
	pt: {
		type: Object,
		default: () => ({})
	},
	// 瀑布流列数，默认为2列
	column: {
		type: Number,
		default: 2
	},
	// 列间距，单位为rpx，默认为12
	gutter: {
		type: Number,
		default: 12
	},
	// 数据项的唯一标识字段名，默认为"id"
	nodeKey: {
		type: String,
		default: "id"
	}
});

// 获取当前组件实例的代理对象
const { proxy } = getCurrentInstance()!;

type PassThrough = {
	className?: string;
};

const pt = computed(() => parsePt<PassThrough>(props.pt));

// 存储每列的当前高度，用于计算最短列
const heights = ref<number[]>([]);

// 存储瀑布流数据，二维数组，每个子数组代表一列
const columns = ref<UTSJSONObject[][]>([]);

/**
 * 获取各列的当前高度
 * 通过uni.createSelectorQuery查询DOM元素的实际高度
 * @returns Promise<> 返回Promise对象
 */
async function getHeight(): Promise<void> {
	// 等待DOM更新完成
	await nextTick();

	return new Promise((resolve) => {
		// 创建选择器查询，获取所有列容器的边界信息
		uni.createSelectorQuery()
			.in(proxy)
			.selectAll(".cl-waterfall__column-inner")
			.boundingClientRect()
			.exec((rect) => {
				const nodes = rect[0] as NodeInfo[];

				if (!isNull(nodes)) {
					// 提取每列的高度信息，如果获取失败则默认为0
					heights.value = nodes.map((e) => e.height ?? 0);
				}

				resolve();
			});
	});
}

/**
 * 向瀑布流添加新数据
 * 使用虚拟定位技术计算每个项目的高度，然后分配到最短的列
 * @param data 要添加的数据数组
 */
async function append(data: UTSJSONObject[]) {
	// 首先获取当前各列高度
	await getHeight();

	// 将新数据作为虚拟项目添加到第一列，用于计算高度
	columns.value[0].push(
		...data.map((e) => {
			return {
				...e,
				isVirtual: true // 标记为虚拟项目，会在CSS中隐藏
			} as UTSJSONObject;
		})
	);

	// 等待DOM更新
	await nextTick();

	// 延迟300ms后计算虚拟项目的高度并重新分配
	setTimeout(() => {
		uni.createSelectorQuery()
			.in(proxy)
			.selectAll(".is-virtual")
			.boundingClientRect()
			.exec((rect) => {
				// 遍历每个虚拟项目
				(rect[0] as NodeInfo[]).forEach((e, i) => {
					// 找到当前高度最小的列
					const min = Math.min(...heights.value);
					const index = heights.value.indexOf(min);

					// 将实际数据添加到最短列
					columns.value[index].push(data[i]);

					// 更新该列的高度
					heights.value[index] += e.height ?? 0;

					// 清除第一列中的虚拟项目（临时用于计算高度的项目）
					columns.value[0] = columns.value[0].filter((e) => e.isVirtual != true);
				});
			});
	}, 300);
}

/**
 * 根据ID移除指定项目
 * @param id 要移除的项目ID
 */
function remove(id: string | number) {
	columns.value.forEach((column, columnIndex) => {
		// 过滤掉指定ID的项目
		columns.value[columnIndex] = column.filter((e) => e[props.nodeKey] != id);
	});
}

/**
 * 根据ID更新指定项目的数据
 * @param id 要更新的项目ID
 * @param data 新的数据对象
 */
function update(id: string | number, data: UTSJSONObject) {
	columns.value.forEach((column) => {
		column.forEach((e) => {
			// 找到指定ID的项目并更新数据
			if (e[props.nodeKey] == id) {
				assign(e, data);
			}
		});
	});
}

/**
 * 清空瀑布流数据
 * 重新初始化列数组
 */
function clear() {
	columns.value = [];

	// 根据列数创建空的列数组
	for (let i = 0; i < props.column; i++) {
		columns.value.push([]);
	}
}

// 组件挂载时的初始化逻辑
onMounted(() => {
	// 监听列数变化，当列数改变时重新初始化
	watch(
		computed(() => props.column),
		() => {
			clear(); // 清空现有数据
			getHeight(); // 重新获取高度
		},
		{
			immediate: true // 立即执行一次
		}
	);
});

defineExpose({
	append,
	remove,
	update,
	clear
});
</script>

<style lang="scss" scoped>
.cl-waterfall {
	@apply flex flex-row w-full relative;

	&__column {
		flex: 1;
	}

	&__item {
		&.is-virtual {
			@apply absolute top-0 w-full;
			left: -100%;
			opacity: 0;
		}
	}
}
</style>
