import { Component, toChildArray } from 'preact';
import { suspended } from './suspense.js';

// Indexes to linked list nodes (nodes are stored as arrays to save bytes).
const SUSPENDED_COUNT = 0;
const RESOLVED_COUNT = 1;
const NEXT_NODE = 2;

// Having custom inheritance instead of a class here saves a lot of bytes.
export function SuspenseList() {
	this._next = null;
	this._map = null;
}

// Mark one of child's earlier suspensions as resolved.
// Some pending callbacks may become callable due to this
// (e.g. the last suspended descendant gets resolved when
// revealOrder === 'together'). Process those callbacks as well.
const resolve = (list, child, node) => {
	if (++node[RESOLVED_COUNT] === node[SUSPENDED_COUNT]) {
		// The number a child (or any of its descendants) has been suspended
		// matches the number of times it's been resolved. Therefore we
		// mark the child as completely resolved by deleting it from ._map.
		// This is used to figure out when *all* children have been completely
		// resolved when revealOrder is 'together'.
		list._map.delete(child);
	}

	// If revealOrder is falsy then we can do an early exit, as the
	// callbacks won't get queued in the node anyway.
	// If revealOrder is 'together' then also do an early exit
	// if all suspended descendants have not yet been resolved.
	if (
		!list.props.revealOrder ||
		(list.props.revealOrder[0] === 't' && list._map.size)
	) {
		return;
	}

	// Walk the currently suspended children in order, calling their
	// stored callbacks on the way. Stop if we encounter a child that
	// has not been completely resolved yet.
	node = list._next;
	while (node) {
		while (node.length > 3) {
			node.pop()();
		}
		if (node[RESOLVED_COUNT] < node[SUSPENDED_COUNT]) {
			break;
		}
		list._next = node = node[NEXT_NODE];
	}
};

// Things we do here to save some bytes but are not proper JS inheritance:
// - call `new Component()` as the prototype
// - do not set `Suspense.prototype.constructor` to `Suspense`
SuspenseList.prototype = new Component();

SuspenseList.prototype._suspended = function(child) {
	const list = this;
	const delegated = suspended(list._vnode);

	let node = list._map.get(child);
	node[SUSPENDED_COUNT]++;

	return unsuspend => {
		const wrappedUnsuspend = () => {
			if (!list.props.revealOrder) {
				// Special case the undefined (falsy) revealOrder, as there
				// is no need to coordinate a specific order or unsuspends.
				unsuspend();
			} else {
				node.push(unsuspend);
				resolve(list, child, node);
			}
		};
		if (delegated) {
			delegated(wrappedUnsuspend);
		} else {
			wrappedUnsuspend();
		}
	};
};

SuspenseList.prototype.render = function(props) {
	this._next = null;
	this._map = new Map();

	const children = toChildArray(props.children);
	if (props.revealOrder && props.revealOrder[0] === 'b') {
		// If order === 'backwards' (or, well, anything starting with a 'b')
		// then flip the child list around so that the last child will be
		// the first in the linked list.
		children.reverse();
	}
	// Build the linked list. Iterate through the children in reverse order
	// so that `_next` points to the first linked list node to be resolved.
	for (let i = children.length; i--; ) {
		// Create a new linked list node as an array of form:
		// 	[suspended_count, resolved_count, next_node]
		// where suspended_count and resolved_count are numeric counters for
		// keeping track how many times a node has been suspended and resolved.
		//
		// Note that suspended_count starts from 1 instead of 0, so we can block
		// processing callbacks until componentDidMount has been called. In a sense
		// node is suspended at least until componentDidMount gets called!
		//
		// Pending callbacks are added to the end of the node:
		// 	[suspended_count, resolved_count, next_node, callback_0, callback_1, ...]
		this._map.set(children[i], (this._next = [1, 0, this._next]));
	}
	return props.children;
};

SuspenseList.prototype.componentDidUpdate = SuspenseList.prototype.componentDidMount = function() {
	// Iterate through all children after mounting for two reasons:
	// 1. As each node[SUSPENDED_COUNT] starts from 1, this iteration increases
	//    each node[RELEASED_COUNT] by 1, therefore balancing the counters.
	//    The nodes can now be completely consumed from the linked list.
	// 2. Handle nodes that might have gotten resolved between render and
	//    componentDidMount.
	this._map.forEach((node, child) => {
		resolve(this, child, node);
	});
};
