/**
 * imports an audio file
 *
 *
 */
import {TypedAudioNode} from './_Base';
import {NodeParamsConfig, ParamConfig} from '../utils/params/ParamsConfig';
import {AudioBuilder} from '../../../core/audio/AudioBuilder';
import {Player} from 'tone/build/esm/source/buffer/Player';
import {CoreLoaderAudio} from '../../../core/loader/Audio';
import {isBooleanTrue} from '../../../core/Type';
import {BaseNodeType} from '../_Base';
import {Poly} from '../../Poly';
import {
	AudioPlayerCallbacksManager,
	OnBeforePlayCallback,
	OnPlaySuccessCallback,
	OnPlayErrorCallback,
	OnStopCallback,
	PlayerCallbacks,
	PlayerEventName,
	CallbackByEventName,
} from './../../../core/audio/PlayerCallbacksManager';
import {AudioType} from '../../poly/registers/nodes/types/Audio';
import {EXTENSIONS_BY_NODE_TYPE_BY_CONTEXT} from '../../../core/loader/FileExtensionRegister';
import {NodeContext} from '../../poly/NodeContext';

const EPSILON = 1e-6;

const LOOP_OPTIONS = {
	cook: false,
	callback: (node: BaseNodeType) => {
		FileAudioNode.PARAM_CALLBACK_updateLoop(node as FileAudioNode);
	},
};

class FileAudioParamsConfig extends NodeParamsConfig {
	/** @param url to fetch the audio file from */
	url = ParamConfig.STRING('', {
		fileBrowse: {extensions: EXTENSIONS_BY_NODE_TYPE_BY_CONTEXT[NodeContext.AUDIO][AudioType.FILE]},
	});
	/** @param auto start */
	autostart = ParamConfig.BOOLEAN(1);

	/** @param duration */
	duration = ParamConfig.FLOAT(-1, {
		cook: false,
		editable: false,
	});
	/** @param display currentTime param */
	updateCurrentTimeParam = ParamConfig.BOOLEAN(0, {
		cook: false,
		callback: (node: BaseNodeType) => {
			FileAudioNode.PARAM_CALLBACK_updateUpdateCurrentTimeParam(node as FileAudioNode);
		},
	});
	/** @param currentTime */
	currentTime = ParamConfig.FLOAT(0, {
		visibleIf: {updateCurrentTimeParam: 1},
		range: [0, 100],
		editable: false,
		cook: false,
	});

	/** @param loop */
	loop = ParamConfig.BOOLEAN(1, {
		...LOOP_OPTIONS,
	});
	/** @param useLoopRange */
	// useLoopRange = ParamConfig.BOOLEAN(0, {
	// 	visibleIf: {loop: 1},
	// 	...LOOP_OPTIONS,
	// });
	/** @param loop Range */
	// loopRange = ParamConfig.VECTOR2([-1, -1], {
	// 	visibleIf: {loop: 1, useLoopRange: 1},
	// 	...LOOP_OPTIONS,
	// });

	/** @param play the audio */
	play = ParamConfig.BUTTON(null, {
		callback: (node: BaseNodeType) => {
			FileAudioNode.PARAM_CALLBACK_play(node as FileAudioNode);
		},
		hidden: true,
	});
	/** @param stop the audio */
	pause = ParamConfig.BUTTON(null, {
		callback: (node: BaseNodeType) => {
			FileAudioNode.PARAM_CALLBACK_pause(node as FileAudioNode);
		},
		hidden: true,
	});
	/** @param restart the audio */
	restart = ParamConfig.BUTTON(null, {
		callback: (node: BaseNodeType) => {
			FileAudioNode.PARAM_CALLBACK_restart(node as FileAudioNode);
		},
		hidden: true,
	});
	/** @param seek 10 seconds back */
	seekM10 = ParamConfig.BUTTON(null, {
		callback: (node: BaseNodeType) => {
			FileAudioNode.PARAM_CALLBACK_seekOffset(node as FileAudioNode, -10);
		},
		hidden: true,
	});
	/** @param seek 5 seconds back */
	seekM5 = ParamConfig.BUTTON(null, {
		callback: (node: BaseNodeType) => {
			FileAudioNode.PARAM_CALLBACK_seekOffset(node as FileAudioNode, -5);
		},
		hidden: true,
	});
	/** @param seek 5 seconds forward */
	seekP5 = ParamConfig.BUTTON(null, {
		callback: (node: BaseNodeType) => {
			FileAudioNode.PARAM_CALLBACK_seekOffset(node as FileAudioNode, +5);
		},
		hidden: true,
	});
	/** @param seek 10 seconds forward */
	seekP10 = ParamConfig.BUTTON(null, {
		callback: (node: BaseNodeType) => {
			FileAudioNode.PARAM_CALLBACK_seekOffset(node as FileAudioNode, +10);
		},
		hidden: true,
	});
}
const ParamsConfig = new FileAudioParamsConfig();

export class FileAudioNode extends TypedAudioNode<FileAudioParamsConfig> {
	override paramsConfig = ParamsConfig;
	static override type() {
		return AudioType.FILE;
	}

	override initializeNode() {
		this.io.inputs.setCount(0);
	}

	override dispose(): void {
		super.dispose();
		if (this._player) {
			this.pause();
			this._player.dispose();
		}
		Poly.blobs.clearBlobsForNode(this);
	}

	private _startedAt: number | undefined;
	private _stoppedAt: number = 0;

	override async cook(inputContents: AudioBuilder[]) {
		await this._loadUrl();
		this._updateOnTickHook();
		if (this._player) {
			const audioBuilder = new AudioBuilder();
			audioBuilder.setSource(this._player);
			this.setAudioBuilder(audioBuilder);
		} else {
			this.cookController.endCook();
		}
	}
	private _player: Player | undefined;
	private async _loadUrl(): Promise<Player | void> {
		try {
			const loader = new CoreLoaderAudio(this.pv.url, this);
			const buffer: AudioBuffer = await loader.load();

			return new Promise((resolve) => {
				if (this._player) {
					this._player.dispose();
				}

				const player = new Player({
					url: buffer,
					loop: isBooleanTrue(this.pv.loop),
					volume: 0,
					// no onload event if a buffer is provided instead of a url
					// onload: () => {
					// 	resolve(player);
					// },
					// make sure to have the param loop set to false for the onstop callbacks to be run.
					onstop: () => {
						this._runOnStop(player);
					},
				});
				this._player = player;

				this._reset();
				this.p.duration.set(buffer.duration);
				// this.p.loopRange.set([0, buffer.duration]);
				if (isBooleanTrue(this.pv.autostart)) {
					this.play();
				}
				AudioPlayerCallbacksManager.registerPlayer(this._player);
				resolve(this._player);
			});
		} catch (err) {
			this.states.error.set(`failed to load url '${this.pv.url}'`);
			return;
		}
	}
	async play() {
		if (!this._player) {
			return;
		}
		// const offset = this._stoppedAt;
		// using pv.currentTime is useful when reloading the page
		// and still starting from where we were
		const offset = isBooleanTrue(this.pv.updateCurrentTimeParam) ? this.pv.currentTime : this._currentTime();
		// always add 2 * EPSILON in the offset, which will not be noticeable
		// but helps avoiding errors when restarting
		const sanitizedOffset = Math.max(offset + 2 * EPSILON, 0);
		try {
			this._runOnBeforePlay(this._player, sanitizedOffset);
			this._player.start(0, sanitizedOffset);
			this._runOnPlaySuccess(this._player);
		} catch (err) {
			console.error(err);
			this._runOnPlayError(this._player, err);
		}
		this._startedAt = this._player.now() - this._stoppedAt;
		this._stoppedAt = 0;
	}
	async pause() {
		if (!this._player) {
			console.warn('no player');
			return;
		}

		if (this._player.state != 'started') {
			console.warn(`player state is not "started", but "${this._player.state}"`);
			return;
		}
		if (this._startedAt != null) {
			const elapsed = this._player.now() - this._startedAt;
			this._stoppedAt = elapsed;
		}
		this._player.stop();
	}
	private _reset() {
		this._stoppedAt = 0;
		this._startedAt = undefined;
		this.p.currentTime.set(0);
	}
	async restart() {
		if (!this._player) {
			return;
		}
		this._player.seek(0, 0);
		this._reset();
		this.play();
	}
	seekOffset(offset: number) {
		if (!this._player) {
			return;
		}
		if (this._startedAt == null) {
			return;
		}
		const currentTime = this._currentTime();
		this._startedAt -= offset;
		this._player.seek(currentTime + offset);
	}

	/*
	 * UPDATE CURRENT TIME PARAM
	 */
	static PARAM_CALLBACK_updateUpdateCurrentTimeParam(node: FileAudioNode) {
		node._updateCurrentTimeParam();
		node._updateOnTickHook();
	}

	private _updateCurrentTimeParam() {
		if (!this._player) {
			return;
		}
		const currentTime = this._currentTime();
		this.p.currentTime.set(currentTime);
	}
	private _currentTime() {
		if (this._stoppedAt) {
			return this._stoppedAt;
		}
		if (this._player && this._startedAt != null) {
			let current = this._player.now() - this._startedAt;
			const duration = this.pv.duration;
			if (current > duration) {
				current -= duration;
			}
			return current;
		}
		return 0;
	}

	/*
	 * LOOP
	 */
	static PARAM_CALLBACK_updateLoop(node: FileAudioNode) {
		node._updateLoop();
	}
	private _updateLoop() {
		if (!this._player) {
			return;
		}
		this._player.loop = this.pv.loop;
		// if (this._player.loop) {
		// 	if (isBooleanTrue(this.pv.useLoopRange)) {
		// 		this._player.setLoopPoints(this.pv.loopRange.x, this.pv.loopRange.y);
		// 	}
		// }
	}
	/*
	 * REGISTER TICK CALLBACK
	 */
	private _updateOnTickHook() {
		if (isBooleanTrue(this.pv.updateCurrentTimeParam)) {
			this._registerOnTickHook();
		} else {
			this._unRegisterOnTickHook();
		}
	}
	private async _registerOnTickHook() {
		if (this.scene().registeredBeforeTickCallbacks().has(this._tickCallbackName())) {
			return;
		}
		this.scene().registerOnBeforeTick(this._tickCallbackName(), this._updateCurrentTimeParam.bind(this));
	}
	private async _unRegisterOnTickHook() {
		this.scene().unRegisterOnBeforeTick(this._tickCallbackName());
	}
	private _tickCallbackName() {
		return `audio/File-${this.graphNodeId()}`;
	}

	/*
	 * STATIC CALLBACKS
	 */
	static PARAM_CALLBACK_seekOffset(node: FileAudioNode, offset: number) {
		node.seekOffset(offset);
	}
	static PARAM_CALLBACK_play(node: FileAudioNode) {
		node.play();
	}
	static PARAM_CALLBACK_pause(node: FileAudioNode) {
		node.pause();
	}
	static PARAM_CALLBACK_restart(node: FileAudioNode) {
		node.restart();
	}

	/*
	 * HOOKS
	 */

	private _playerCallbacks: PlayerCallbacks = {};
	// onBeforePlay
	onBeforePlay(callback: OnBeforePlayCallback) {
		this._on('onBeforePlay', callback);
	}
	private _runOnBeforePlay(player: Player, offset: number) {
		this._playerCallbacks.onBeforePlay?.forEach((callback) => callback(offset));
		AudioPlayerCallbacksManager.runOnBeforePlayCallbacks(player, offset);
	}

	// onPlaySuccess
	onPlaySuccess(callback: OnPlaySuccessCallback) {
		this._on('onPlaySuccess', callback);
	}
	private _runOnPlaySuccess(player: Player) {
		this._playerCallbacks.onPlaySuccess?.forEach((callback) => callback());
		AudioPlayerCallbacksManager.runOnPlaySuccessCallbacks(player);
	}

	// onPlayError
	onPlayError(callback: OnPlayErrorCallback) {
		this._on('onPlayError', callback);
	}
	private _runOnPlayError(player: Player, err: unknown) {
		this._playerCallbacks.onPlayError?.forEach((callback) => callback(err));
		AudioPlayerCallbacksManager.runOnPlayErrorCallbacks(player, err);
	}

	// onStop
	onStop(callback: OnStopCallback) {
		this._on('onStop', callback);
	}
	removeOnStop(callback: OnStopCallback) {
		this._removeCallback('onStop', callback);
	}
	private _runOnStop(player: Player) {
		this._playerCallbacks.onStop?.forEach((callback) => callback());
		AudioPlayerCallbacksManager.runOnStopCallbacks(player);
	}
	// generic
	private _on<E extends PlayerEventName>(eventName: E, callback: CallbackByEventName[E]) {
		this._playerCallbacks[eventName] = this._playerCallbacks[eventName] || (new Set() as any);
		this._playerCallbacks[eventName]?.add(callback as any);
	}
	private _removeCallback<E extends PlayerEventName>(eventName: E, callback: CallbackByEventName[E]) {
		this._playerCallbacks[eventName]?.delete(callback as any);
	}
}
