/**
 * Internal dependencies
 */
import type { Hooks, StoreKey } from './types';

export type RunHook = (
	hookName: string,
	...args: unknown[]
) => undefined | unknown;

/**
 * Returns a function which, when invoked, will execute all callbacks
 * registered to a hook of the specified type, optionally returning the final
 * value of the call chain.
 *
 * @param hooks          Hooks instance.
 * @param storeKey
 * @param returnFirstArg Whether each hook callback is expected to return its first argument.
 * @param async          Whether the hook callback should be run asynchronously
 *
 * @return Function that runs hook callbacks.
 */
function createRunHook(
	hooks: Hooks,
	storeKey: StoreKey,
	returnFirstArg: boolean,
	async: boolean
): RunHook {
	return function runHook( hookName, ...args ) {
		const hooksStore = hooks[ storeKey ];

		if ( ! hooksStore[ hookName ] ) {
			hooksStore[ hookName ] = {
				handlers: [],
				runs: 0,
			};
		}

		hooksStore[ hookName ].runs++;

		const handlers = hooksStore[ hookName ].handlers;

		// The following code is stripped from production builds.
		if ( 'production' !== process.env.NODE_ENV ) {
			// Handle any 'all' hooks registered.
			if ( 'hookAdded' !== hookName && hooksStore.all ) {
				handlers.push( ...hooksStore.all.handlers );
			}
		}

		if ( ! handlers || ! handlers.length ) {
			return returnFirstArg ? args[ 0 ] : undefined;
		}

		const hookInfo = {
			name: hookName,
			currentIndex: 0,
		};

		async function asyncRunner() {
			try {
				hooksStore.__current.add( hookInfo );
				let result = returnFirstArg ? args[ 0 ] : undefined;
				while ( hookInfo.currentIndex < handlers.length ) {
					const handler = handlers[ hookInfo.currentIndex ];
					result = await handler.callback.apply( null, args );
					if ( returnFirstArg ) {
						args[ 0 ] = result;
					}
					hookInfo.currentIndex++;
				}
				return returnFirstArg ? result : undefined;
			} finally {
				hooksStore.__current.delete( hookInfo );
			}
		}

		function syncRunner() {
			try {
				hooksStore.__current.add( hookInfo );
				let result = returnFirstArg ? args[ 0 ] : undefined;
				while ( hookInfo.currentIndex < handlers.length ) {
					const handler = handlers[ hookInfo.currentIndex ];
					result = handler.callback.apply( null, args );
					if ( returnFirstArg ) {
						args[ 0 ] = result;
					}
					hookInfo.currentIndex++;
				}
				return returnFirstArg ? result : undefined;
			} finally {
				hooksStore.__current.delete( hookInfo );
			}
		}

		return ( async ? asyncRunner : syncRunner )();
	};
}

export default createRunHook;
