import type { Accessor, JSX } from 'solid-js'
import { action, untracked } from '@wovin/core/mobx'
import { Logger } from 'besonders-logger'
import classNames from 'classnames'
import { createEffect, createSignal, For, on, onCleanup, Show, untrack } from 'solid-js'
import { Spinner } from '../components/mini-components'

const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO) // eslint-disable-line unused-imports/no-unused-vars

interface LazyRenderProps<T> {
	items: Accessor<readonly T[]>
	initial?: number
	fallback?: JSX.Element
	reset?: any
	children?: (item: T, isInitial: boolean) => JSX.Element
	allLoaded: Accessor<boolean>
	loadMore: () => Promise<boolean>
} // ? & JSX.HTMLAttributes<HTMLDivElement>

const INITIAL_ITEMS_TO_LOAD = 7
const NEXT_ITEMS_TO_LOAD = 3

export const LazyRender: <T>(
	props: LazyRenderProps<T>,
) => JSX.Element = (props) => {
	const untrackedItems = untrack(() => untracked(() => [...props.items()])) // We observe the items later
	const { initial = INITIAL_ITEMS_TO_LOAD } = props
	DEBUG(`<LazyRender>.create`, { untrackedItems, initial })
	// const [props, otherProps] = splitProps(fullProps, ['items', 'children', 'fallback', 'initial', 'reset'])
	const [loadedItems, setLoadedItems] = createSignal(untrackedItems.slice(0, initial))
	const [skipAnimationFor, setSkipAnimationFor] = createSignal(untrackedItems.slice(0, initial))
	const [allLoadedLocal, setAllLoadedLocal] = createSignal(untrackedItems.length <= initial)
	const [showLoader, setShowLoader] = createSignal(false)
	const allLoaded = () => allLoadedLocal() && (props.allLoaded ? props.allLoaded() : true)

	createEffect(on(() => props.reset, () => {
		DEBUG(`[LazyRender] reset?`, props.reset, { all: allLoaded(), loaded: loadedItems().length, initial })
		if (!allLoaded() && loadedItems().length > initial) {
			LOG(`[LazyRender] reset!`, props.reset, { all: allLoaded(), loaded: loadedItems().length, initial })
			setLoadedItems(props.items().slice(0, initial))
		}
	}, { defer: true }))

	let loaderVisible = false
	let externalLoadMorePending = false
	let loadMoreTimer = null
	const loadMore = action(() => {
		DEBUG(`[loadMore]`, {
			allLoadedLocal: allLoadedLocal(),
			loaded: loadedItems().length,
			all: props.items().length,
			allLoadedExt: props.allLoaded?.(),
		})

		if (allLoadedLocal() && props.allLoaded && !props.allLoaded?.()) {
			DEBUG(`[loadMore] calling external loadMore`, props.loadMore, externalLoadMorePending)
			if (externalLoadMorePending) return
			externalLoadMorePending = true
			void (async () => {
				const foundMore = await props.loadMore()
				DEBUG(`[loadMore] external loadMore done. foundMore? ${foundMore}`)
				externalLoadMorePending = false
				if (foundMore) {
					setTimerToCheckLoader(0) // HACK: defer to wait for items in list
				}
			})()
		} else {
			addChunkToLoadedItems()
			if (loadedItems().length >= props.items().length) {
				// observer.disconnect() // ? Stop observing once all items are loaded - how to reconnect
				setAllLoadedLocal(true)
			}
			if (!allLoaded()) {
				setTimerToCheckLoader()
			}
		}

		function setTimerToCheckLoader(sleep = 250) {
			loadMoreTimer = setTimeout(() => {
				DEBUG(`[loadMore] loadMore timer done. still visible?`, loaderVisible)
				if (!loaderVisible) setShowLoader(false)
				else loadMore()
			}, sleep)
		}
		function addChunkToLoadedItems() {
			setLoadedItems((prev) => {
				// if (prev.length > 20) return prev
				const nextItems = props.items().slice(prev.length, prev.length + NEXT_ITEMS_TO_LOAD)
				DEBUG(`[loadMore] Loading ${nextItems.length} more, previously ${prev.length}`, { prev, nextItems })
				return nextItems.length ? [...prev, ...nextItems] : prev
			})
		}
	})

	const onObserverStateChange = (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => {
		const isVisible = !!entries.find(({ isIntersecting }) => isIntersecting)
		DEBUG(`[LazyRender] observer state change`, isVisible, entries, observer)

		if (!isVisible || allLoaded()) {
			loaderVisible = false
			setShowLoader(false)
			return
		}
		loaderVisible = true
		setShowLoader(true)
		loadMore()
	}
	onCleanup(() => loadMoreTimer && clearTimeout(loadMoreTimer))
	createEffect(on(
		() => props.items(),
		(items) => {
			LOG('[LazyRender] items changed - re-rendering', { loadedItems: loadedItems() })
			const newItems = items.slice(0, loadedItems().length)
			setLoadedItems(newItems)
			setSkipAnimationFor(newItems)
			setAllLoadedLocal(newItems.length >= items.length)
			setShowLoader(allLoaded())
		},
		{ defer: true },
	))

	const observer = new IntersectionObserver(onObserverStateChange, {
		rootMargin: '200px', // Load items when they come within X pixels of the viewport
		root: document.getElementById('main-container'),
	})

	onCleanup(() => {
		observer.disconnect()
	})

	return (
		<>
			<For each={loadedItems()} fallback={props.fallback}>
				{(item) => {
					VERBOSE(`Rendering`, item)
					return props.children(item, skipAnimationFor().includes(item))
					// return <pre style='height: 400px'>{stringify(item)}</pre>
				}}
			</For>
			<Show when={!allLoaded()}>
				<div
					flex='~ justify-center items-center'
					gap-4
					class={classNames(
						showLoader() ? 'opacity-100' : 'opacity-0',
						'duration-700 transition-property-[opacity]',
						// doesn't work well: 'animate-fade-in-up animate-duration-[700ms] animate-ease-out',
					)}
					ref={(elem) => {
						DEBUG(`[LazyRender] observer ref`, elem)
						observer.observe(elem)
					}}
				>
					<Spinner />
					{' '}
					Loading more...
				</div>
			</Show>
		</>
	)
}
