import React, {useMemo, useState} from "react";

/**
 * Asynchronous data state.
 */
interface AsyncState<T>
{
	/**
	 * Determine if we are waiting for the promise result or not.
	 */
	pending: boolean;
	/**
	 * The promise which is retrieved (or has retrieved) data.
	 */
	promise: Promise<T>;
	/**
	 * Error thrown by the promise.
	 */
	error: Error;
	/**
	 * The promise result.
	 */
	data: T;
}

/**
 * A promise production function.
 */
export type PromiseFn<T> = () => Promise<T>;

/**
 * React hook for promise result retrieval.
 * @param promise The promise or a function that produces a promise.
 * @param deps When one of the `deps` change, it will wait for the promise again.
 */
export function useAsync<T>(promise: Promise<T>|PromiseFn<T>, deps: any[] = []): AsyncState<T>
{
	// Get the actual promise from the function if there is one.
	if ((promise as PromiseFn<T>)?.call)
		promise = (promise as PromiseFn<T>)();
	else
		promise = Promise.race([promise as Promise<T>]);

	// The async state.
	const [state, setState] = useState<AsyncState<T>>({
		pending: true,
		promise: promise,
		error: undefined,
		data: undefined,
	});

	/**
	 * Partial update of an async state.
	 * @param stateUpdate A partial update object.
	 */
	const updateState = (stateUpdate: Partial<AsyncState<T>>) => {
		// Copy the original state and apply the partial state update.
		setState(Object.assign({}, state, stateUpdate));
	};

	// Reconfigure the promise when any deps have changed.
	useMemo(() => {
		(promise as Promise<T>).then((result) => {
			// When there is a result, disable pending state and set retrieved data, without error.
			updateState({
				pending: false,
				error: undefined,
				data: result,
			})
		}).catch((error) => {
			// An error happened, disable pending state, reset data, and set the error.
			updateState({
				pending: false,
				error: error,
				data: undefined,
			})
		});

		// Promise is ready: reset the state to pending with the configured promise, without data and error.
		updateState({
			pending: true,
			promise: promise as Promise<T>,
			error: undefined,
			data: undefined,
		});
	}, deps);

	return state; // Return the current async state.
}

/**
 * Wait for the promise to be fulfilled to render the children.
 * @param async The async state.
 * @param children Renderer function of the children, takes promised data as argument.
 * @param fallback Content shown when the promise is not fulfilled yet.
 * @constructor
 */
export function Await<T>({ async, children, fallback }: {
	async: AsyncState<T>;
	children: (async: T) => React.ReactElement;
	fallback?: React.ReactElement;
})
{
	// Still waiting for the promised data, showing fallback content.
	if (async.pending) return fallback ?? <></>;
	// An error happened, throwing it.
	if (async.error) throw async.error;

	// Promise is fulfilled, rendering the children with result data.
	return children(async.data);
}
