import {BaseNodeType} from '../_Base';
import {Poly} from '../../Poly';
import {CorePerformance} from '../../../core/performance/CorePerformance';
import {NodeCookPerformanceformanceController} from './cook/PerformanceController';
import {ContainerMap} from '../../containers/utils/ContainerMap';
import {NodeContext} from '../../poly/NodeContext';
import {ContainableMap} from '../../containers/utils/ContainableMap';
import {touchNodeRef} from '../../../core/reactivity/NodeReactivity';
import {NodeInputsController} from './io/InputsController';

enum ErrorType {
	FROM_INPUTS = 'node inputs error',
	INTERNAL = 'node internal error',
}

export type OnCookCompleteHook = () => void;

export class NodeCookController<NC extends NodeContext> {
	private _corePerformance: CorePerformance;
	private _cooking: boolean = false;
	private _cookingDirtyTimestamp: number | undefined;
	private _performanceController: NodeCookPerformanceformanceController = new NodeCookPerformanceformanceController(
		this
	);
	private _inputContainers: Array<ContainerMap[NC] | null> = [];
	private _inputContents: ContainableMap[NC][] = [];
	private _EMPTY_ARRAY: ContainableMap[NC][] = [];

	constructor(private node: BaseNodeType) {
		this._corePerformance = this.node.scene().performance;
	}
	performanceRecordStarted() {
		return this._corePerformance.started();
	}
	dispose() {
		this._clearHooks();
	}

	// Disallowing inputs evaluation is important for switch nodes (such as SOP and COP)
	// that should not evaluate all inputs, but only a single one, depending on a param value
	// currently only for switch SOP and COP
	private _inputsEvaluationRequired: boolean = true;
	disallowInputsEvaluation() {
		this._inputsEvaluationRequired = false;
	}

	isCooking(): boolean {
		return this._cooking === true;
	}

	private _startCookIfNoErrors(inputContents: ContainableMap[NC][]) {
		if (this.node.states.error.active() || this.node.disposed() == true) {
			this.endCook();
		} else {
			try {
				this._performanceController.recordCookStart();
				// make sure we treat rejected promise
				// if cook is async
				const promise = this.node.cook(inputContents);
				if (promise != null) {
					promise.catch((e: any) => {
						this._onError(e, ErrorType.INTERNAL, false);
					});
				}
			} catch (e) {
				this._onError(e, ErrorType.INTERNAL);
			}
		}
	}

	async cookMain() {
		if (this.isCooking()) {
			return;
		}
		if (this.node.disposed() == true) {
			return;
		}

		this._initCookingState();
		this.node.states.error.clear();
		this.node.scene().cookController.addNode(this.node);

		try {
			// we need to try/catch inputs fetching,
			// as some nodes like the sop/normalsHelper
			// currently fail when being cloned
			// const inputContents: ContainableMap[NC][] | undefined = this._inputsEvaluationRequired
			// 	? await this._evaluateInputs()
			// 	: undefined;
			this._inputContents.length = 0;
			if (this._inputsEvaluationRequired) {
				await this._evaluateInputs(this._inputContents);
			}

			if (this.node.params.paramsEvalRequired()) {
				await this._evaluateParams();
			}
			this._startCookIfNoErrors(this._inputContents);
		} catch (e) {
			this._onError(e, ErrorType.FROM_INPUTS);
		}
	}
	private _onError(e: unknown, errorType: ErrorType, alwaysSet: boolean = true) {
		if (alwaysSet || !this.node.states.error.active()) {
			const processedError = this.node.processError(e);
			this.node.states.error.set(`${errorType}: '${processedError}'.`);
			Poly.warn(e);
		}
		this.endCook();
	}

	async cookMainWithoutInputs() {
		this.node.scene().cookController.addNode(this.node);
		if (this.isCooking()) {
			// TODO:
			// this seems to happen because when we flush the cooker queue,
			// some graph nodes will trigger more updates, which will then make dependent nodes
			// dirty again
			Poly.warn('cook_main_without_inputs already cooking', this.node.path());
			return;
		}
		this._initCookingState();
		this.node.states.error.clear();

		if (this.node.params.paramsEvalRequired()) {
			await this._evaluateParams();
		}
		this._startCookIfNoErrors(this._EMPTY_ARRAY);
	}

	endCook(/*message?: string | null*/) {
		this._finalizeCookPerformance();

		const dirtyTimestamp = this.node.dirtyController.dirtyTimestamp();
		const timestampUnchangedSinceCookStarted =
			dirtyTimestamp == null || dirtyTimestamp === this._cookingDirtyTimestamp;
		if (
			timestampUnchangedSinceCookStarted ||
			this.node.flags?.bypass?.active() /* || this._scenePlayingAndNodeAlreadyCookedForCurrentFrame()*/
		) {
			this.node.removeDirtyState();
			this._terminateCookProcess();
		} else {
			if (this.node.disposed() == true) {
				return;
			}
			Poly.log('COOK AGAIN', dirtyTimestamp, this._cookingDirtyTimestamp, this.node.path());
			this._cooking = false;
			this.cookMain();
		}
	}
	// private _lastFrameCooked: number | undefined;
	// private _scenePlayingAndNodeAlreadyCookedForCurrentFrame() {
	// 	if (this.node.scene().timeController.playing()) {
	// 		return this._lastFrameCooked == this.node.scene().frame();
	// 	}
	// 	return false;
	// }
	private _initCookingState() {
		this._cooking = true;
		// this._lastFrameCooked = this.node.scene().frame();
		this._cookingDirtyTimestamp = this.node.dirtyController.dirtyTimestamp();
	}
	private _terminateCookProcess() {
		if (this.isCooking() || this.node.flags?.bypass?.active()) {
			this._cooking = false;

			// setTimeout(this.node.containerController.notifyRequesters.bind(this.node.containerController), 0);
			this.node.containerController.notifyRequesters();
			this._runOnCookCompleteHooks();
			if (this.node.disposed() == true) {
				return;
			}
			touchNodeRef(this.node.path());
		}
	}

	private async _evaluateInputs(inputContents: ContainableMap[NC][]): Promise<ContainableMap[NC][]> {
		this._performanceController.recordInputsStart();

		const ioOnputs: NodeInputsController<NC> = this.node.io.inputs;

		// const inputContainers: (ContainerMap[NC] | null)[] = this._inputsEvaluationRequired
		// 	? ioOnputs.isGraphNodeDirty()
		// 		? await ioOnputs.evalRequiredInputs()
		// 		: ioOnputs.containersWithoutEvaluation()
		// 	: [];

		this._inputContainers.length = 0;
		if (this._inputsEvaluationRequired) {
			if (ioOnputs.isGraphNodeDirty()) {
				await ioOnputs.evalRequiredInputs(this._inputContainers);
			} else {
				ioOnputs.containersWithoutEvaluation(this._inputContainers);
			}
		}

		const inputs = ioOnputs.inputs();
		// const inputContents: ContainableMap[NC][] = [];
		let inputContainer: ContainerMap[NC] | null;
		for (let i = 0; i < inputs.length; i++) {
			inputContainer = this._inputContainers[i];
			if (inputContainer) {
				if (ioOnputs.cloneRequired(i)) {
					inputContents[i] = inputContainer.coreContentCloned() as ContainableMap[NC];
				} else {
					inputContents[i] = inputContainer.coreContent() as ContainableMap[NC];
				}
			}
		}
		this._performanceController.recordInputsEnd();
		return inputContents;
	}
	private async _evaluateParams() {
		this._performanceController.recordParamsStart();
		await this.node.params.evalAll();
		this._performanceController.recordParamsEnd();
	}

	//
	//
	// PERFORMANCE
	//
	//
	cooksCount(): number {
		return this._performanceController.cooksCount();
	}
	cookTime(): number {
		return this._performanceController.data().cookTime;
	}

	private _finalizeCookPerformance() {
		if (!this._corePerformance.started()) {
			return;
		}
		this._performanceController.recordCookEnd();

		this._corePerformance.recordNodeCookData(this.node, this._performanceController.data());
	}

	//
	//
	// HOOK
	//
	//
	private _onCookCompleteHookNames: string[] | undefined;
	private _onCookCompleteHooks: OnCookCompleteHook[] | undefined;
	registerOnCookEnd(callbackName: string, callback: OnCookCompleteHook) {
		this._onCookCompleteHookNames = this._onCookCompleteHookNames || [];
		this._onCookCompleteHooks = this._onCookCompleteHooks || [];
		this._onCookCompleteHookNames.push(callbackName);
		this._onCookCompleteHooks.push(callback);
	}
	private _clearHooks() {
		if (!this._onCookCompleteHookNames || !this._onCookCompleteHooks) {
			return;
		}
		for (const hookName of this._onCookCompleteHookNames) {
			this.deregisterOnCookEnd(hookName);
		}
	}
	deregisterOnCookEnd(callbackName: string) {
		if (!this._onCookCompleteHookNames || !this._onCookCompleteHooks) {
			return;
		}
		const index = this._onCookCompleteHookNames?.indexOf(callbackName);
		this._onCookCompleteHookNames.splice(index, 1);
		this._onCookCompleteHooks.splice(index, 1);
		if (this._onCookCompleteHookNames.length == 0) {
			this._onCookCompleteHookNames = undefined;
		}
		if (this._onCookCompleteHooks.length == 0) {
			this._onCookCompleteHooks = undefined;
		}
	}
	private _runOnCookCompleteHooks() {
		if (this._onCookCompleteHooks) {
			const hooks = [...this._onCookCompleteHooks];
			for (let hook of hooks) {
				hook();
			}
		}
	}
	onCookEndCallbackNames() {
		return this._onCookCompleteHookNames;
	}
}
