/**
 * External dependencies
 */
import { without } from 'lodash';
import type { ComponentType } from 'react';

/**
 * GeChiUI dependencies
 */
import { Component } from '@gechiui/element';

/**
 * Internal dependencies
 */
import createHigherOrderComponent from '../../utils/create-higher-order-component';

/**
 * We cannot use the `Window['setTimeout']` and `Window['clearTimeout']`
 * types here because those functions include functionality that is not handled
 * by this component, like the ability to pass extra arguments.
 *
 * In the case of this component, we only handle the simplest case where
 * `setTimeout` only accepts a function (not a string) and an optional delay.
 */
type TimeoutProps = {
	setTimeout: ( fn: () => void, delay: number ) => number;
	clearTimeout: ( id: number ) => void;
};

/**
 * A higher-order component used to provide and manage delayed function calls
 * that ought to be bound to a component's lifecycle.
 */
const withSafeTimeout = createHigherOrderComponent< TimeoutProps >(
	< TProps extends TimeoutProps >(
		OriginalComponent: ComponentType< TProps >
	) => {
		return class WrappedComponent extends Component<
			Omit< TProps, keyof TimeoutProps >
		> {
			timeouts: number[];

			constructor( props: Omit< TProps, keyof TimeoutProps > ) {
				super( props );
				this.timeouts = [];
				this.setTimeout = this.setTimeout.bind( this );
				this.clearTimeout = this.clearTimeout.bind( this );
			}

			componentWillUnmount() {
				this.timeouts.forEach( clearTimeout );
			}

			setTimeout( fn: ( ...args: any[] ) => void, delay: number ) {
				const id = setTimeout( () => {
					fn();
					this.clearTimeout( id );
				}, delay );
				this.timeouts.push( id );
				return id;
			}

			clearTimeout( id: number ) {
				clearTimeout( id );
				this.timeouts = without( this.timeouts, id );
			}

			render() {
				const props = {
					...this.props,
					setTimeout: this.setTimeout,
					clearTimeout: this.clearTimeout,
				} as TProps;

				return <OriginalComponent { ...props } />;
			}
		};
	},
	'withSafeTimeout'
);

export default withSafeTimeout;
