/*!
 * V4Fire Core
 * https://github.com/V4Fire/Core
 *
 * Released under the MIT license
 * https://github.com/V4Fire/Core/blob/master/LICENSE
 */

/**
 * [[include:core/async/modules/timers/README.md]]
 * @packageDocumentation
 */

import SyncPromise from 'core/promise/sync';

import Super, {

	AsyncCb,

	AsyncOptions,
	AsyncCbOptions,
	ClearOptionsId

} from 'core/async/modules/proxy';

import type {

	TimerId,
	IdleCb,

	AsyncWaitOptions,
	AsyncIdleOptions,
	AsyncRequestIdleCallbackOptions

} from 'core/async/interface';

export * from 'core/async/modules/proxy';
export * from 'core/async/interface';

export default class Async<CTX extends object = Async<any>> extends Super<CTX> {
	/**
	 * Wrapper for `globalThis.setImmediate`
	 *
	 * @param cb - callback function
	 * @param [opts] - additional options for the operation
	 */
	setImmediate(cb: Function, opts?: AsyncCbOptions<CTX>): Nullable<TimerId> {
		return this.registerTask({
			...opts,
			name: this.namespaces.immediate,
			obj: cb,
			clearFn: clearImmediate,
			wrapper: setImmediate,
			linkByWrapper: true
		});
	}

	/**
	 * Wrapper for `globalThis.clearImmediate`
	 * @param [id] - operation id (if not specified, then the operation will be applied for all registered tasks)
	 */
	clearImmediate(id?: TimerId): this;

	/**
	 * Clears the specified "setImmediate" timer or a group of timers
	 * @param opts - options for the operation
	 */
	clearImmediate(opts: ClearOptionsId<TimerId>): this;
	clearImmediate(task?: TimerId | ClearOptionsId<TimerId>): this {
		return this.cancelTask(task, this.namespaces.immediate);
	}

	/**
	 * Mutes the specified "setImmediate" timer
	 * @param [id] - operation id (if not specified, then the operation will be applied for all registered tasks)
	 */
	muteImmediate(id?: TimerId): this;

	/**
	 * Mutes the specified "setImmediate" timer or a group of timers
	 * @param opts - options for the operation
	 */
	muteImmediate(opts: ClearOptionsId<TimerId>): this;
	muteImmediate(task?: TimerId | ClearOptionsId<TimerId>): this {
		return this.markTask('muted', task, this.namespaces.immediate);
	}

	/**
	 * Unmutes the specified "setImmediate" timer
	 * @param [id] - operation id (if not defined will be got all handlers)
	 */
	unmuteImmediate(id?: TimerId): this;

	/**
	 * Unmutes the specified "setImmediate" timer or a group of timers
	 *
	 * @param opts - options for the operation
	 */
	unmuteImmediate(opts: ClearOptionsId<TimerId>): this;
	unmuteImmediate(p?: TimerId | ClearOptionsId<TimerId>): this {
		return this.markTask('!muted', p, this.namespaces.immediate);
	}

	/**
	 * Suspends the specified "setImmediate" timer
	 * @param [id] - operation id (if not specified, then the operation will be applied for all registered tasks)
	 */
	suspendImmediate(id?: TimerId): this;

	/**
	 * Suspends the specified "setImmediate" timer or a group of timers
	 * @param opts - options for the operation
	 */
	suspendImmediate(opts: ClearOptionsId<TimerId>): this;
	suspendImmediate(p?: TimerId | ClearOptionsId<TimerId>): this {
		return this.markTask('paused', p, this.namespaces.immediate);
	}

	/**
	 * Unsuspends the specified "setImmediate" timer
	 * @param [id] - operation id (if not specified, then the operation will be applied for all registered tasks)
	 */
	unsuspendImmediate(id?: TimerId): this;

	/**
	 * Unsuspends the specified "setImmediate" timer or a group of timers
	 * @param opts - options for the operation
	 */
	unsuspendImmediate(opts: ClearOptionsId<TimerId>): this;
	unsuspendImmediate(p?: TimerId | ClearOptionsId<TimerId>): this {
		return this.markTask('!paused', p, this.namespaces.immediate);
	}

	/**
	 * Wrapper for `globalThis.setInterval`
	 *
	 * @param cb - callback function
	 * @param timeout - timer value
	 * @param [opts] - additional options for the operation
	 */
	setInterval(cb: Function, timeout: number, opts?: AsyncCbOptions<CTX>): Nullable<TimerId> {
		return this.registerTask({
			...opts,
			name: this.namespaces.interval,
			obj: cb,
			clearFn: clearInterval,
			wrapper: setInterval,
			linkByWrapper: true,
			periodic: true,
			args: [timeout]
		});
	}

	/**
	 * Wrapper for `globalThis.clearInterval`
	 * @param [id] - operation id (if not specified, then the operation will be applied for all registered tasks)
	 */
	clearInterval(id?: TimerId): this;

	/**
	 * Clears the specified "setInterval" timer or a group of timers
	 * @param opts - options for the operation
	 */
	clearInterval(opts: ClearOptionsId<TimerId>): this;
	clearInterval(task?: TimerId | ClearOptionsId<TimerId>): this {
		return this.cancelTask(task, this.namespaces.interval);
	}

	/**
	 * Mutes the specified "setInterval" timer
	 * @param [id] - operation id (if not specified, then the operation will be applied for all registered tasks)
	 */
	muteInterval(id?: TimerId): this;

	/**
	 * Mutes the specified "setInterval" timer or a group of timers
	 * @param opts - options for the operation
	 */
	muteInterval(opts: ClearOptionsId<TimerId>): this;
	muteInterval(task?: TimerId | ClearOptionsId<TimerId>): this {
		return this.markTask('muted', task, this.namespaces.interval);
	}

	/**
	 * Unmutes the specified "setInterval" timer
	 * @param [id] - operation id (if not specified, then the operation will be applied for all registered tasks)
	 */
	unmuteInterval(id?: TimerId): this;

	/**
	 * Unmutes the specified "setInterval" timer or a group of timers
	 * @param opts - options for the operation
	 */
	unmuteInterval(opts: ClearOptionsId<TimerId>): this;
	unmuteInterval(task?: TimerId | ClearOptionsId<TimerId>): this {
		return this.markTask('!muted', task, this.namespaces.interval);
	}

	/**
	 * Suspends the specified "setInterval" timer
	 * @param [id] - operation id (if not specified, then the operation will be applied for all registered tasks)
	 */
	suspendInterval(id?: TimerId): this;

	/**
	 * Suspends the specified "setInterval" timer or a group of timers
	 * @param opts - options for the operation
	 */
	suspendInterval(opts: ClearOptionsId<TimerId>): this;
	suspendInterval(task?: TimerId | ClearOptionsId<TimerId>): this {
		return this.markTask('paused', task, this.namespaces.interval);
	}

	/**
	 * Unsuspends the specified "setImmediate" timer
	 * @param [id] - operation id (if not specified, then the operation will be applied for all registered tasks)
	 */
	unsuspendInterval(id?: TimerId): this;

	/**
	 * Unsuspends the specified "setImmediate" timer or a group of timers
	 * @param opts - options for the operation
	 */
	unsuspendInterval(opts: ClearOptionsId<TimerId>): this;
	unsuspendInterval(task?: TimerId | ClearOptionsId<TimerId>): this {
		return this.markTask('!paused', task, this.namespaces.interval);
	}

	/**
	 * Wrapper for `globalThis.setTimeout`
	 *
	 * @param cb - callback function
	 * @param timeout - timeout value
	 * @param [opts] - additional options for the operation
	 */
	setTimeout(cb: Function, timeout: number, opts?: AsyncCbOptions<CTX>): Nullable<TimerId> {
		return this.registerTask({
			...opts,
			name: this.namespaces.timeout,
			obj: cb,
			clearFn: clearTimeout,
			wrapper: setTimeout,
			linkByWrapper: true,
			args: [timeout]
		});
	}

	/**
	 * Wrapper for `globalThis.clearTimeout`
	 * @param [id] - operation id (if not specified, then the operation will be applied for all registered tasks)
	 */
	clearTimeout(id?: TimerId): this;

	/**
	 * Clears the specified "setTimeout" timer or a group of timers
	 * @param opts - options for the operation
	 */
	clearTimeout(opts: ClearOptionsId<TimerId>): this;
	clearTimeout(task?: TimerId | ClearOptionsId<TimerId>): this {
		return this.cancelTask(task, this.namespaces.timeout);
	}

	/**
	 * Mutes the specified "setTimeout" timer
	 * @param [id] - operation id (if not specified, then the operation will be applied for all registered tasks)
	 */
	muteTimeout(id?: TimerId): this;

	/**
	 * Mutes the specified "setTimeout" timer or a group of timers
	 * @param opts - options for the operation
	 */
	muteTimeout(opts: ClearOptionsId<TimerId>): this;
	muteTimeout(task?: TimerId | ClearOptionsId<TimerId>): this {
		return this.markTask('muted', task, this.namespaces.timeout);
	}

	/**
	 * Unmutes the specified "setTimeout" timer
	 * @param [id] - operation id (if not specified, then the operation will be applied for all registered tasks)
	 */
	unmuteTimeout(id?: TimerId): this;

	/**
	 * Unmutes the specified "setTimeout" timer or a group of timers
	 * @param opts - options for the operation
	 */
	unmuteTimeout(opts: ClearOptionsId<TimerId>): this;
	unmuteTimeout(task?: TimerId | ClearOptionsId<TimerId>): this {
		return this.markTask('!muted', task, this.namespaces.timeout);
	}

	/**
	 * Suspends the specified "setTimeout" timer
	 * @param [id] - operation id (if not specified, then the operation will be applied for all registered tasks)
	 */
	suspendTimeout(id?: TimerId): this;

	/**
	 * Suspends the specified "setTimeout" timer or a group of timers
	 * @param opts - options for the operation
	 */
	suspendTimeout(opts: ClearOptionsId<TimerId>): this;
	suspendTimeout(task?: TimerId | ClearOptionsId<TimerId>): this {
		return this.markTask('paused', task, this.namespaces.timeout);
	}

	/**
	 * Unsuspends the specified "setTimeout" timer
	 * @param [id] - operation id (if not specified, then the operation will be applied for all registered tasks)
	 */
	unsuspendTimeout(id?: TimerId): this;

	/**
	 * Unsuspends the specified "setTimeout" timer or a group of names
	 * @param opts - options for the operation
	 */
	unsuspendTimeout(opts: ClearOptionsId<TimerId>): this;
	unsuspendTimeout(task?: TimerId | ClearOptionsId<TimerId>): this {
		return this.markTask('!paused', task, this.namespaces.timeout);
	}

	/**
	 * Wrapper for `globalThis.requestIdleCallback`
	 *
	 * @param cb - callback function
	 * @param [opts] - additional options for the operation
	 */
	requestIdleCallback<R = unknown>(
		cb: IdleCb<R, CTX>,
		opts?: AsyncRequestIdleCallbackOptions<CTX>
	): Nullable<TimerId> {
		let
			wrapper,
			clearFn;

		if (typeof requestIdleCallback !== 'function') {
			wrapper = (fn) => setTimeout(() => fn({timeRemaining: () => 0}), 50);
			clearFn = clearTimeout;

		} else {
			wrapper = requestIdleCallback;
			clearFn = cancelIdleCallback;
		}

		return this.registerTask({
			...opts && Object.reject(opts, 'timeout'),
			name: this.namespaces.idleCallback,
			obj: cb,
			clearFn,
			wrapper,
			linkByWrapper: true,
			args: opts && Object.select(opts, 'timeout')
		});
	}

	/**
	 * Wrapper for `globalThis.cancelIdleCallback`
	 *
	 * @alias
	 * @param [id] - operation id (if not specified, then the operation will be applied for all registered tasks)
	 */
	cancelIdleCallback(id?: TimerId): this;

	/**
	 * Clears the specified "requestIdleCallback" timer or a group of timers
	 *
	 * @alias
	 * @param opts - options for the operation
	 */
	cancelIdleCallback(opts: ClearOptionsId<TimerId>): this;
	cancelIdleCallback(task?: TimerId | ClearOptionsId<TimerId>): this {
		return this.clearIdleCallback(Object.cast(task));
	}

	/**
	 * Wrapper for `globalThis.cancelIdleCallback`
	 * @param [id] - operation id (if not specified, then the operation will be applied for all registered tasks)
	 */
	clearIdleCallback(id?: TimerId): this;

	/**
	 * Clears the specified "requestIdleCallback" timer or a group of timers
	 * @param opts - options for the operation
	 */
	clearIdleCallback(opts: ClearOptionsId<TimerId>): this;
	clearIdleCallback(task?: TimerId | ClearOptionsId<TimerId>): this {
		return this.cancelTask(task, this.namespaces.idleCallback);
	}

	/**
	 * Mutes the specified "requestIdleCallback" timer
	 * @param [id] - operation id (if not specified, then the operation will be applied for all registered tasks)
	 */
	muteIdleCallback(id?: TimerId): this;

	/**
	 * Mutes the specified "requestIdleCallback" timer or a group of timers
	 * @param opts - options for the operation
	 */
	muteIdleCallback(opts: ClearOptionsId<TimerId>): this;
	muteIdleCallback(task?: TimerId | ClearOptionsId<TimerId>): this {
		return this.markTask('muted', task, this.namespaces.idleCallback);
	}

	/**
	 * Unmutes the specified "requestIdleCallback" timer
	 * @param [id] - operation id (if not specified, then the operation will be applied for all registered tasks)
	 */
	unmuteIdleCallback(id?: TimerId): this;

	/**
	 * Unmutes the specified "requestIdleCallback" timer or a group of timers
	 * @param opts - options for the operation
	 */
	unmuteIdleCallback(opts: ClearOptionsId<TimerId>): this;
	unmuteIdleCallback(task?: TimerId | ClearOptionsId<TimerId>): this {
		return this.markTask('!muted', task, this.namespaces.idleCallback);
	}

	/**
	 * Suspends the specified "requestIdleCallback" timer
	 * @param [id] - operation id (if not specified, then the operation will be applied for all registered tasks)
	 */
	suspendIdleCallback(id?: TimerId): this;

	/**
	 * Suspends the specified "requestIdleCallback" timer or a group of timers
	 * @param opts - options for the operation
	 */
	suspendIdleCallback(opts: ClearOptionsId<TimerId>): this;
	suspendIdleCallback(task?: TimerId | ClearOptionsId<TimerId>): this {
		return this.markTask('paused', task, this.namespaces.idleCallback);
	}

	/**
	 * Unsuspends the specified "requestIdleCallback" timer
	 * @param [id] - operation id (if not specified, then the operation will be applied for all registered tasks)
	 */
	unsuspendIdleCallback(id?: TimerId): this;

	/**
	 * Unsuspends the specified "requestIdleCallback" timer or a group of timers
	 * @param opts - options for the operation
	 */
	unsuspendIdleCallback(opts: ClearOptionsId<TimerId>): this;
	unsuspendIdleCallback(task?: TimerId | ClearOptionsId<TimerId>): this {
		return this.markTask('!paused', task, this.namespaces.idleCallback);
	}

	/**
	 * Returns a promise that will be resolved after the specified timeout
	 *
	 * @param timeout
	 * @param [opts] - additional options for the operation
	 */
	sleep(timeout: number, opts?: AsyncOptions): Promise<void> {
		return new SyncPromise((resolve, reject) => {
			this.setTimeout(resolve, timeout, {
				...opts,
				promise: true,
				onClear: <AsyncCb<CTX>>this.onPromiseClear(resolve, reject),
				onMerge: <AsyncCb<CTX>>this.onPromiseMerge(resolve, reject)
			});
		});
	}

	/**
	 * Returns a promise that will be resolved on the next tick of the event loop
	 * @param [opts] - additional options for the operation
	 */
	nextTick(opts?: AsyncOptions): Promise<void> {
		return new SyncPromise((resolve, reject) => {
			this.setImmediate(resolve, {
				...opts,
				promise: true,
				onClear: <AsyncCb<CTX>>this.onPromiseClear(resolve, reject),
				onMerge: <AsyncCb<CTX>>this.onPromiseMerge(resolve, reject)
			});
		});
	}

	/**
	 * Returns a promise that will be resolved on the process idle
	 * @param [opts] - additional options for the operation
	 */
	idle(opts?: AsyncIdleOptions): Promise<IdleDeadline> {
		return new SyncPromise((resolve, reject) => {
			this.requestIdleCallback(resolve, {
				...opts,
				promise: true,
				onClear: <AsyncCb<CTX>>this.onPromiseClear(resolve, reject),
				onMerge: <AsyncCb<CTX>>this.onPromiseMerge(resolve, reject)
			});
		});
	}

	/**
	 * Returns a promise that will be resolved only when the specified function returns a positive value (== true)
	 *
	 * @param fn
	 * @param [opts] - additional options for the operation
	 */
	wait(fn: Function, opts?: AsyncWaitOptions): Promise<boolean> {
		if (Object.isTruly(fn())) {
			if (opts?.label != null) {
				this.clearPromise(opts);
			}

			return SyncPromise.resolve(true);
		}

		return new SyncPromise((resolve, reject) => {
			let
				// eslint-disable-next-line prefer-const
				id;

			const cb = () => {
				if (Object.isTruly(fn())) {
					resolve(true);
					this.clearPromise(id);
				}
			};

			id = this.setInterval(cb, opts?.delay ?? 15, {
				...opts,
				promise: true,
				onClear: <AsyncCb<CTX>>this.onPromiseClear(resolve, reject),
				onMerge: <AsyncCb<CTX>>this.onPromiseMerge(resolve, reject)
			});
		});
	}
}
