import { $, capture, isolate, Signal, teardown, TeardownHook } from "../signals.js";

export type MapArrayFn<I, O> = (input: I, index: () => number) => O;

export interface MapArrayStateEntry<I, O> {
	/** input */
	i: I;
	/** output */
	o: O;
	/** index */
	s: Signal<number>,
	/** dispose */
	d: TeardownHook,
	/** removed */
	r: boolean,
}

export interface MapArrayUpdate<I, O> {
	/** inclusive start index in new state */
	s: number;
	/** exclusive end index in new state */
	e: number;
	/** previous chunk */
	p: MapArrayStateEntry<I, O>[];
	/** next chunk */
	n: MapArrayStateEntry<I, O>[];
}

export function createMapArrayState<I, O>() {
	const state: MapArrayStateEntry<I, O>[] = [];
	teardown(() => {
		for (let i = 0; i < state.length; i++) {
			state[i].d();
		}
	});
	return state;
}

function createEntry<I, O>(value: I, index: number, fn: MapArrayFn<I, O>): MapArrayStateEntry<I, O> {
	const signal = $(index);
	let output!: O;
	const dispose = isolate(capture, () => {
		output = fn(value, () => signal.value);
	});
	return {
		i: value,
		o: output,
		s: signal,
		d: dispose,
		r: false,
	};
}

export function mapArrayUpdate<I, O>(state: MapArrayStateEntry<I, O>[], rawInput: Iterable<I>, fn: MapArrayFn<I, O>): MapArrayUpdate<I, O> | null {
	const inputs = Array.isArray(rawInput) ? rawInput : Array.from(rawInput);

	let start = 0;
	const maxStart = Math.min(state.length, inputs.length);
	while (start < maxStart && Object.is(inputs[start], state[start].i)) {
		start++;
	}

	if (start === inputs.length && inputs.length === state.length) {
		return null;
	}

	const minEnd = inputs.length - maxStart + start;
	const lenDiff = inputs.length - state.length;
	let end = inputs.length - 1;
	while (end >= minEnd && Object.is(inputs[end], state[end - lenDiff].i)) {
		end--;
	}
	end++;
	const stateEnd = end - lenDiff;

	const nextState: MapArrayStateEntry<I, O>[] = [];
	if ((end - lenDiff - start) === 0) {
		for (let i = start; i < end; i++) {
			nextState[i - start] = createEntry(inputs[i], i, fn);
		}
	} else {
		const indexByValue = new Map<I, number>();
		const nextIndexByIndex: number[] = [];
		for (let i = end - 1; i >= start; i--) {
			const value = inputs[i];
			const next = indexByValue.get(value);
			if (next !== undefined) {
				nextIndexByIndex[i] = next;
			}
			indexByValue.set(value, i);
		}

		for (let i = start; i < stateEnd; i++) {
			const instance = state[i];
			const index = indexByValue.get(instance.i);
			if (index === undefined) {
				instance.d();
				instance.r = true;
			} else {
				nextState[index - start] = instance;
				indexByValue.set(instance.i, nextIndexByIndex[index]);
			}
		}

		for (let i = start; i < end; i++) {
			const old = nextState[i - start];
			if (old) {
				old.s.value = i;
			} else {
				nextState[i - start] = createEntry(inputs[i], i, fn);
			}
		}
	}

	const prevState = state.splice(start, stateEnd - start, ...nextState);

	if (stateEnd !== end) {
		for (let i = end; i < state.length; i++) {
			state[i].s.value = i;
		}
	}

	return {
		s: start,
		e: end,
		p: prevState,
		n: nextState,
	}
}
