import { ReadonlySignal, Signal, signal } from "@preact/signals-core";
import { useSignal } from "@preact/signals";
import { Fragment, createElement, ComponentChildren } from "preact";
import { useMemo } from "preact/hooks";

interface ShowProps<T = boolean> {
	when: Signal<T> | ReadonlySignal<T> | (() => T);
	fallback?: ComponentChildren;
	children: ComponentChildren | ((value: NonNullable<T>) => ComponentChildren);
}

const Item = (props: any) => {
	return typeof props.children === "function"
		? props.children(props.v, props.i ? props.i.value : undefined)
		: props.children;
};

Item.displayName = "Item";

export function Show<T = boolean>(
	props: ShowProps<T>
): ComponentChildren | null {
	const value =
		typeof props.when === "function" ? props.when() : props.when.value;
	if (!value) return props.fallback || null;
	return <Item v={value} children={props.children} />;
}

Show.displayName = "Show";

type ForEach<T> =
	| ReadonlyArray<T>
	| Signal<ReadonlyArray<T>>
	| ReadonlySignal<ReadonlyArray<T>>;

interface ForProps<T> {
	each: ForEach<T> | (() => ForEach<T>);
	fallback?: ComponentChildren;
	getKey?: (item: T, index: number) => string | number;
	children: (value: T, index: number) => ComponentChildren;
}

export function For<T>(props: ForProps<T>): ComponentChildren | null {
	const cache = useMemo(() => new Map(), []);
	const list = (typeof props.each === "function" ? props.each() : props.each) as
		| Signal<ReadonlyArray<T>>
		| ReadonlyArray<T>;

	const listValue = list instanceof Signal ? list.value : list;

	if (!listValue.length) return props.fallback || null;

	const removed = new Set(cache.keys());

	const items = listValue.map((value, index) => {
		removed.delete(value);
		let entry = cache.get(value);
		if (!entry) {
			const i = signal(index);
			const key = props.getKey ? props.getKey(value, index) : index;
			const vnode = (
				<Item key={key} v={value} i={i} children={props.children} />
			);
			entry = { vnode, i };
			cache.set(value, entry);
		} else if (entry.i.peek() !== index) {
			// Index changed (e.g. an earlier item was removed/reordered). Push the
			// new index through the per-item signal so the cached vnode is reused
			// and the child re-renders reactively instead of being recreated.
			entry.i.value = index;
		}
		return entry.vnode;
	});

	removed.forEach(value => {
		cache.delete(value);
	});

	return createElement(Fragment, null, items);
}

For.displayName = "For";

export function useLiveSignal<T>(value: T): Signal<T> {
	const s = useSignal(value);
	if (s.peek() !== value) s.value = value;
	return s;
}

export function useSignalRef<T>(value: T): Signal<T> & { current: T } {
	const ref = useSignal(value) as Signal<T> & { current: T };
	if (!("current" in ref))
		Object.defineProperty(ref, "current", refSignalProto);
	return ref;
}
const refSignalProto = {
	configurable: true,
	get(this: Signal) {
		return this.value;
	},
	set(this: Signal, v: any) {
		this.value = v;
	},
};
