import { Component, NewState, Util, Hash } from '@apigo.cc/state'

export const VirtualScroll = (options = {}) => {
	const itemHeights = new Map()
	const groupHeights = new Map()
	let groupItemCount = 1
	const avg = Util.newAvg()
	let padTop = 0, rowGap = 0, topMargin = 0, itemMarginTop = null, itemMarginBottom = null, listInited = false

	const providedItemHeight = options.itemHeight || null;

	return {
		reset: (list, container) => {
			listInited = false; itemHeights.clear(); groupHeights.clear(); avg.clear(); topMargin = 0; itemMarginTop = null; itemMarginBottom = null;
			if (!list?.length) return [];
			const size = list.length; groupItemCount = Math.ceil(Math.sqrt(size)) || 10;
			const style = window.getComputedStyle(container);
			padTop = parseFloat(style.paddingTop) || 0; rowGap = parseFloat(style.rowGap) || 0;
			// Optimization: Give a reasonably large initial buffer instead of just 30 to prevent the
			// "first scroll unresponsiveness" where the user scrolls past the small initial slice
			// before the init() callback has time to map all heights.
			const visibleCount = Math.max(10, Math.ceil((container.clientHeight || 100) / (providedItemHeight || 32)));
			return list.slice(0, Math.min(visibleCount * 3, size));
		},
		init: (list, refreshCallback) => {
			if (listInited) return;
			const size = list.length;
			let defaultHeight = providedItemHeight || avg.get() || 32;
			
			// Optimization: If the first item declares a fixed height, use it as the global baseline.
			// This completely bypasses the need for DOM measurement (node.offsetHeight) in fixed-height scenarios.
			if (size > 0 && typeof list[0] === 'object' && list[0] !== null && list[0]._itemHeight) {
				defaultHeight = list[0]._itemHeight;
			}
			
			avg.add(defaultHeight);
			if (itemMarginTop === null) { itemMarginTop = 0; itemMarginBottom = 0; }

			for (let i = 0; i < size; i++) {
				if (!itemHeights.has(i)) {
					// Fallback to individual item height if specified, otherwise global default
					const ih = (typeof list[i] === 'object' && list[i] !== null && list[i]._itemHeight) ? list[i]._itemHeight : defaultHeight;
					itemHeights.set(i, ih);
				}
			}
			for (let i = 0; i < size; i += groupItemCount) groupHeights.set(i, Math.min(groupItemCount, size - i) * defaultHeight);
			listInited = true; refreshCallback();
		},
		update: (absoluteIndex, node) => {
			if (node.offsetHeight === 0) return;
			if (itemMarginTop === null) {
				const style = window.getComputedStyle(node);
				itemMarginTop = parseFloat(style.marginTop) || 0; itemMarginBottom = parseFloat(style.marginBottom) || 0;
			}
			if (absoluteIndex === 0 && !topMargin) topMargin = itemMarginTop;
			const newHeight = node.offsetHeight + itemMarginTop + itemMarginBottom + rowGap;
			const oldHeight = itemHeights.get(absoluteIndex);
			if (newHeight !== oldHeight) {
				itemHeights.set(absoluteIndex, newHeight); avg.add(newHeight);
				const offset = newHeight - (oldHeight || 0), groupIndex = absoluteIndex - (absoluteIndex % groupItemCount);
				if (groupHeights.has(groupIndex)) groupHeights.set(groupIndex, groupHeights.get(groupIndex) + offset);
			}
		},
		calc: (container, list) => {
			if (!listInited || !list) return null;
			const size = list.length;
			const avgVal = Math.max(16, avg.get() || 32);
			let visibleCount = Math.max(10, Math.ceil((container.clientHeight || 100) / avgVal));
			let prev = padTop + topMargin + rowGap, post = 0, status = 0, listStartIndex = 0, listEndIndex = 0;
			let renderedList = [];
			const scrollTop = container.scrollTop;
			let loopCount = 0;

			for (let i = 0; i < size; i++) {
				if (++loopCount > size * 2) {
					throw new Error(`VirtualScroll.calc infinite loop detected at i=${i}, status=${status}, size=${size}, groupItemCount=${groupItemCount}`);
				}
				if (status === 0) {
					const gh = groupHeights.get(i);
					if (gh && prev + gh <= scrollTop && (i + groupItemCount < size)) {
						prev += gh;
						i += groupItemCount - 1;
					} else {
						const ih = itemHeights.get(i);
						if (prev + ih <= scrollTop && i < size - 1) {
							prev += ih;
						} else {
							status = 1;
							let visibleStartIndex = Math.max(0, i);
							listStartIndex = Math.max(0, visibleStartIndex - visibleCount);
							listEndIndex = Math.min(listStartIndex + visibleCount * 3, size);
							i = listEndIndex - 1;
							renderedList = list.slice(listStartIndex, listEndIndex);
							for (let j = listStartIndex; j < visibleStartIndex; j++) prev -= itemHeights.get(j);
						}
					}
				} else if (status === 1) {
					const gh = groupHeights.get(i);
					if (gh) {
						post += gh;
						i += groupItemCount - 1;
					} else {
						post += itemHeights.get(i);
					}
				}
			}
			// Fix for flex gap inflation: 
			// prevHeight block participates in flex gap, so we subtract one rowGap.
			const finalPrevHeight = Math.max(0, prev - padTop - topMargin - rowGap - (listStartIndex > 0 ? rowGap : 0));
			// post accumulated M * (h + gap). Real space is sum(h) + (M-1)*gap = post - gap.
			// DOM adds one gap before the block, so we need postHeight + gap = post - gap => postHeight = post - 2*gap.
			const finalPostHeight = post > 0 ? Math.max(0, post - 2 * rowGap) : 0;
			return { prevHeight: finalPrevHeight, postHeight: finalPostHeight, renderedList, listStartIndex };
		}
	}
}

Component.register('List', container => {
	container.mode = container.getAttribute('mode') || 'normal'
	container.fast = container.hasAttribute('fast')
	container.collapsible = container.hasAttribute('collapsible')
	
	const padTopEl = container.fast ? container.querySelector('.vs-pad-top') : null
	const padBottomEl = container.fast ? container.querySelector('.vs-pad-bottom') : null

	const defaultSets = {
		idfield: 'id', labelfield: 'label', summaryfield: 'summary',
		groupidfield: 'id', grouplabelfield: 'label', groupsummaryfield: 'summary', groupfield: 'group',
		parentfield: 'parent', groupicon: 'folder', itemicon: 'file'
	}
	container.collapsed = NewState({})
	
	const updateFlatList = () => {
		Util.updateDefaults(container, defaultSets)
		const list = container.state.list || [], flatList = []
		if (container.mode === 'group') {
			const itemMap = {}
			list.forEach(item => (itemMap[item[container.groupfield]] ??= []).push(item));
			(container.state.groups || []).forEach(group => {
				flatList.push({ type: 'group', ...group })
				const items = itemMap[group[container.groupidfield]]
				if (items) items.forEach(item => flatList.push({ type: 'item', ...item }))
			})
		} else if (container.mode === 'tree') {
			const childrenMap = {}
			list.forEach(item => (childrenMap[item[container.parentfield] || ''] ??= []).push(item))
			const traverse = (items, level, parents) => items.forEach(item => {
				const id = item[container.idfield], hasChildren = !!childrenMap[id]?.length
				const isCollapsed = container.collapsed[id]
				flatList.push({ type: 'item', ...item, _level: level, _hasChildren: hasChildren, _parents: parents })
				if (hasChildren && !isCollapsed) traverse(childrenMap[id], level + 1, [...parents, id])
			})
			traverse(childrenMap[''] || [], 0, [])
		} else list.forEach(item => flatList.push({ type: 'item', ...item }))
		container.state._flatList = flatList
	}
	
	container.state.__watch('list', updateFlatList)
	const vs = container.fast ? VirtualScroll() : null
	container.state._renderedList = []

	let refreshing = false
	container.refresh = () => {
		if (!container.fast || refreshing) return
		refreshing = true
		try {
			const res = vs.calc(container, container.state._flatList)
			if (res) {
				if (padTopEl) padTopEl.style.height = `${res.prevHeight}px`
				if (padBottomEl) padBottomEl.style.height = `${res.postHeight}px`

				if (container.state._listStartIndex !== res.listStartIndex) {
					container.state._listStartIndex = res.listStartIndex
				}

				const cur = container.state._renderedList || []
				if (cur.length !== res.renderedList.length || cur[0] !== res.renderedList[0] || cur[cur.length - 1] !== res.renderedList[res.renderedList.length - 1]) {
					container.state._renderedList = res.renderedList
				}
			}
		} finally {
			setTimeout(() => { refreshing = false }, 0)
		}
	}

	container.onItemUpdate = (index, node) => { if (container.fast) vs.update(index + (container.state._listStartIndex || 0), node) }

	container.state.__watch('_flatList', flatList => {
		if (container.fast) {
			if (padTopEl) padTopEl.style.height = '0px'
			if (padBottomEl) padBottomEl.style.height = '0px'
			container.state._listStartIndex = 0
			container.state._renderedList = vs.reset(flatList, container) || []
			setTimeout(() => { if (container.state._flatList === flatList) vs.init(flatList, container.refresh) })
		} else container.state._renderedList = flatList
	})

	container.selectItem = (item, index) => {
		if (container.hasAttribute('auto-select')) container.state.selectedItem = container.state.selectedItem === item[container.idfield] ? null : item[container.idfield]
		container.dispatchEvent(new CustomEvent('itemclick', { bubbles: false, detail: { item, index: index + (container.fast ? (container.state._listStartIndex || 0) : 0) } }))
	}
	container.selectGroup = (item, index) => {
		if (container.hasAttribute('auto-select-group')) container.state.selectedGroup = container.state.selectedGroup === item[container.groupidfield] ? null : item[container.groupidfield]
		container.dispatchEvent(new CustomEvent('groupclick', { bubbles: false, detail: { item, index } }))
	}
	container.toggleCollapse = (item) => { if (container.collapsible && item._hasChildren) { container.collapsed[item[container.idfield]] = !container.collapsed[item[container.idfield]]; updateFlatList(); } }

	updateFlatList()
}, Util.makeDom(/*html*/`
<div class="list-group overflow-auto" onscroll="this.refresh()" style="overflow-anchor:none">
	<div class="vs-pad-top flex-shrink-0" style="height:0px;"></div>
	<template slot-id="item" $each="this.state?._renderedList">
		<div $onupdate="this.onItemUpdate(index, thisNode)" $class="list-group-item list-group-item-action d-inline-flex align-items-center ps-2 pe-2 \${item.type==='group'?(this.state?.selectedGroup===item[this.groupidfield]?'active':''):(this.state?.selectedItem===item[this.idfield]?'active':'')}" $onclick="item.type==='group'?this.selectGroup(item,index):this.selectItem(item,index)">
			<template $if="item.type === 'group'">
				<span $if="this.groupicon" $class="bi bi-\${this.groupicon} text-body"></span>
				<div class="flex-shrink-0 px-1" $text="\${item[this.grouplabelfield]}"></div>
				<div class="text-muted small flex-fill text-end text-truncate" style="width: 0" $text="\${item[this.groupsummaryfield]}"></div>
				<div slot-id="group-actions"></div>
			</template>
			<template $if="item.type === 'item'">
				<div $if="this.mode === 'tree'" $style="width:\${item._level * 16 + (this.collapsible ? 16 : 0)}px; cursor:\${this.collapsible ? 'pointer' : 'default'}" class="text-end text-muted flex-shrink-0" $onclick="event.stopPropagation(); this.toggleCollapse(item)">
					<i $if="this.collapsible && item._hasChildren" $class="bi \${this.collapsed[item[this.idfield]] ? 'bi-caret-right-fill' : 'bi-caret-down-fill'}"></i>
				</div>
				<span $if="this.mode === 'tree'" $class="text-muted bi bi-\${item._hasChildren ? this.groupicon : this.itemicon}"></span>
				<span $if="this.mode !== 'tree' && this.itemicon" class="bi bi-\${this.itemicon} text-body"></span>
				<div class="flex-shrink-0 px-1" $text="\${item[this.labelfield]}"></div>
				<div class="text-muted small flex-fill text-end text-truncate" style="width: 0" $text="\${item[this.summaryfield]}"></div>
				<div slot-id="item-actions"></div>
			</template>
		</div>
	</template>
	<div class="vs-pad-bottom flex-shrink-0" style="height:0px;"></div>
</div>
`))
