import {BaseNodeType} from '../../_Base';
import {BaseParamType} from '../../../params/_Base';
import {ParamOptions} from '../../../params/utils/OptionsController';
import {CoreGraphNode} from '../../../../core/graph/CoreGraphNode';
import {FloatParam} from '../../../params/Float';
import {ParamType} from '../../../poly/ParamType';
import {ParamTemplate} from './ParamsConfig';
import {ParamConstructorMap} from '../../../params/types/ParamConstructorMap';
import {ParamConstructorByType} from '../../../params/types/ParamConstructorByType';
import {ParamInitValuesTypeMap} from '../../../params/types/ParamInitValuesTypeMap';
import {ParamValuesTypeMap} from '../../../params/types/ParamValuesTypeMap';
import {NodeEvent} from '../../../poly/NodeEvent';
import {ParamInitValueSerializedTypeMap} from '../../../params/types/ParamInitValueSerializedTypeMap';
import {Poly} from '../../../Poly';
import {ParamInitData} from '../io/IOController';
import {PolyDictionary} from '../../../../types/GlobalTypes';
import {arrayCopy} from '../../../../core/ArrayUtils';

const NODE_SIMPLE_NAME = 'params';

export type OnSceneLoadHook = () => void;
type PostCreateParamsHook = () => void;

export interface ParamOptionToAdd<T extends ParamType> {
	name: string;
	type: T;
	initValue: ParamInitValueSerializedTypeMap[T];
	rawInput: ParamInitValueSerializedTypeMap[T];
	options?: ParamOptions;
}
export interface ParamsUpdateOptions {
	namesToDelete?: string[];
	toAdd?: ParamOptionToAdd<ParamType>[];
}
const _tmpAll: BaseParamType[] = [];

export class ParamsController {
	private _param_create_mode: boolean = false;
	private _params_created: boolean = false;
	private _paramsByName: Map<string, BaseParamType> = new Map();
	// caches
	private _paramsList: BaseParamType[] = [];
	private _paramNames: string[] = [];
	private _non_spare_params: BaseParamType[] = [];
	private _spare_params: BaseParamType[] = [];
	private _non_spare_param_names: string[] = [];
	private _spare_param_names: string[] = [];

	private _params_node: CoreGraphNode | undefined;
	// private _params_eval_key: string;
	private _params_added_since_last_params_eval: boolean = false;
	// private _current_param_folder_name: string | undefined;

	// hooks
	private _post_create_params_hook_names: string[] | undefined;
	private _post_create_params_hooks: PostCreateParamsHook[] | undefined;
	private _on_scene_load_hooks: OnSceneLoadHook[] | undefined;
	private _on_scene_load_hook_names: string[] | undefined;

	constructor(public readonly node: BaseNodeType) {}

	dispose() {
		if (this._params_node) {
			this._params_node.dispose();
		}

		// dispose params
		arrayCopy(this.all, _tmpAll);
		for (const param of _tmpAll) {
			param.dispose();
		}

		// hooks
		this._post_create_params_hook_names = undefined;
		this._post_create_params_hooks = undefined;
		this._on_scene_load_hooks = undefined;
		this._on_scene_load_hook_names = undefined;
	}

	private initDependencyNode() {
		if (!this._params_node) {
			// TODO: consider not having a params_node for nodes which have no parameters
			this._params_node = new CoreGraphNode(this.node.scene(), NODE_SIMPLE_NAME);
			// this._params_node.setScene(this.node.scene);
			this.node.addGraphInput(this._params_node, false);
		}
	}

	init() {
		this.initDependencyNode();
		// this.reset_params()
		this._param_create_mode = true;

		this._initFromParamsConfig();
		this.node.createParams();
		this._postCreateParams();
	}
	private _postCreateParams() {
		this._updateCaches();
		// this._create_params_ui_data_dependencies();
		this._initParamAccessors();
		this._param_create_mode = false;
		this._params_created = true;

		this._runPostCreateParamsHooks();

		// This was to debug a weird bug where I was adding nodes to the list
		// of params, from the DependenciesController
		// this._params_list.push = (...items: BaseParamType[]) => {
		// 	if (items[0] && !items[0].compute) {
		// 		Poly.warn('adding params', items);
		// 	}
		// 	for (let i of items) {
		// 		this._params_list[this._params_list.length] = i;
		// 	}
		// 	return 0;
		// };
	}
	postCreateSpareParams() {
		this._updateCaches();
		this._initParamAccessors();
		// param.emit(ParamEvent.DELETED);
		this.node.scene().referencesController.notifyParamsUpdated(this.node);
		this.node.emit(NodeEvent.PARAMS_UPDATED);
	}
	updateParams(options: ParamsUpdateOptions) {
		let hasCreatedAParam = false;
		let hasDeletedAParam = false;
		const addedParams: BaseParamType[] = [];
		if (options.namesToDelete) {
			for (const param_name of options.namesToDelete) {
				if (this.has(param_name)) {
					this._deleteParam(param_name);
					hasDeletedAParam = true;
				}
			}
		}
		if (options.toAdd) {
			for (const paramData of options.toAdd) {
				const param = this.addParam(paramData.type, paramData.name, paramData.initValue, paramData.options);
				if (param) {
					if (paramData.rawInput != null) {
						param.set(paramData.rawInput as never);
					}
					hasCreatedAParam = true;
					addedParams.push(param);
				}
			}
		}

		if (hasDeletedAParam || hasCreatedAParam) {
			this.postCreateSpareParams();
		}
		for (const param of addedParams) {
			this.node.scene().missingExpressionReferencesController.checkForMissingParamReferences(param);
		}
	}

	private _initFromParamsConfig() {
		const paramsConfig = this.node.paramsConfig as PolyDictionary<ParamTemplate<ParamType>>;
		let init_values_used = false;
		if (paramsConfig) {
			const paramsInitValueOverrides = this.node.createOptions?.paramsInitValueOverrides;
			const names = Object.keys(paramsConfig);
			for (const name of names) {
				const config = paramsConfig[name];
				let init_value: ParamInitData<ParamType> | undefined;
				if (paramsInitValueOverrides) {
					init_value = paramsInitValueOverrides[name];
					init_values_used = true;
				}
				this.addParam(config.type, name, config.init_value, config.options, init_value);
			}
		}
		// this set dirty may not be necessary, but when starting a scene with a spotlight
		// with a non default t (ie: [2,2,0]), it would not be positionned correctly and would require
		// a cook
		if (init_values_used) {
			this.node.setDirty();
		}
		if (this.node.createOptions) {
			this.node.createOptions.paramsInitValueOverrides = undefined;
		}
	}
	private _initParamAccessors() {
		let currentNamesInAccessor = Object.getOwnPropertyNames(this.node.pv);
		this._removeUnneededAccessors(currentNamesInAccessor);
		// update var after having removed accessors
		currentNamesInAccessor = Object.getOwnPropertyNames(this.node.pv);
		const all = this.all;
		for (const param of all) {
			const isSpare: boolean = param.options.isSpare();

			const paramNotYetInAccessors = !currentNamesInAccessor.includes(param.name());

			try {
				// this currently fails
				// when copy/pasting the js/getObjectAttribute node
				if (paramNotYetInAccessors || isSpare) {
					Object.defineProperty(this.node.pv, param.name(), {
						get: () => {
							return param.value;
						},
						// only spare parameters can be removed
						configurable: isSpare,
					});
					Object.defineProperty(this.node.p, param.name(), {
						get: () => {
							return param;
						},
						configurable: isSpare,
					});
				}
			} catch (err) {}
		}
	}
	private _removeUnneededAccessors(current_names_in_accessor: string[]) {
		const current_param_names = this._paramNames;
		const names_to_remove = [];
		for (const current_name_in_accessor of current_names_in_accessor) {
			if (!current_param_names.includes(current_name_in_accessor)) {
				names_to_remove.push(current_name_in_accessor);
			}
		}

		for (const name_to_remove of names_to_remove) {
			Object.defineProperty(this.node.pv, name_to_remove, {
				get: () => {
					return undefined;
				},
				configurable: true,
			});
			Object.defineProperty(this.node.p, name_to_remove, {
				get: () => {
					return undefined;
				},
				configurable: true,
			});
		}
	}

	get params_node() {
		return this._params_node;
	}
	get all() {
		return this._paramsList;
	}
	get non_spare() {
		return this._non_spare_params;
	}
	get spare() {
		return this._spare_params;
	}
	get names(): string[] {
		return this._paramNames;
	}
	get non_spare_names(): string[] {
		return this._non_spare_param_names;
	}
	get spare_names(): string[] {
		return this._spare_param_names;
	}

	private set_with_type<T extends ParamType>(paramName: string, value: ParamInitValuesTypeMap[T], type: T) {
		const param = this.paramWithType(paramName, type);
		if (param) {
			param.set(value as never);
		} else {
			Poly.warn(`param ${paramName} not found with type ${type}`);
		}
	}
	set_float(paramName: string, value: ParamInitValuesTypeMap[ParamType.FLOAT]) {
		this.set_with_type(paramName, value, ParamType.FLOAT);
	}
	set_vector3(paramName: string, value: ParamInitValuesTypeMap[ParamType.VECTOR3]) {
		this.set_with_type(paramName, value, ParamType.VECTOR3);
	}

	has_param(paramName: string) {
		return this._paramsByName.has(paramName);
	}
	has(paramName: string) {
		return this.has_param(paramName);
	}
	get(paramName: string) {
		return this.param(paramName);
	}
	paramWithType<T extends ParamType>(param_name: string, type: T): ParamConstructorMap[T] | undefined {
		const param = this.param(param_name);
		if (param && param.type() == type) {
			return param as ParamConstructorMap[T];
		}
	}
	getFloat(param_name: string): FloatParam {
		return this.paramWithType(param_name, ParamType.FLOAT) as FloatParam;
	}
	// get_operator_path(param_name: string): OperatorPathParam {
	// 	return this.paramWithType(param_name, ParamType.OPERATOR_PATH) as OperatorPathParam;
	// }
	value(paramName: string) {
		return this.param(paramName)?.value;
	}
	valueWithType<T extends ParamType>(paramName: string, type: T): ParamValuesTypeMap[T] {
		return this.paramWithType(paramName, type)?.value as ParamValuesTypeMap[T];
		// const param = this.param(name);
		// if (param && param.type() == type) {
		// 	return param.value();
		// }
	}
	boolean(paramName: string) {
		return this.valueWithType(paramName, ParamType.BOOLEAN);
	}
	float(paramName: string) {
		return this.valueWithType(paramName, ParamType.FLOAT);
	}
	integer(paramName: string) {
		return this.valueWithType(paramName, ParamType.INTEGER);
	}
	string(paramName: string) {
		return this.valueWithType(paramName, ParamType.STRING);
	}
	vector2(paramName: string) {
		return this.valueWithType(paramName, ParamType.VECTOR2);
	}
	vector3(paramName: string) {
		return this.valueWithType(paramName, ParamType.VECTOR3);
	}
	color(paramName: string) {
		return this.valueWithType(paramName, ParamType.COLOR);
	}

	param(paramName: string) {
		const p = this._paramsByName.get(paramName);
		if (p != null) {
			return p;
		} else {
			Poly.warn(
				`tried to access param '${paramName}' in node ${this.node.path()}, but existing params are: ${
					this.names
				} on node ${this.node.path()}`
			);
			return null;
		}
	}
	// param_cache_name(param_name: string) {
	// 	return `_param_${param_name}`;
	// }

	// delete_params(param_names: string[]) {
	// 	for (let param_name of param_names) {
	// 		this.delete_param(param_name);
	// 	}

	// }
	// call update_params instead
	private _deleteParam(paramName: string) {
		const param = this._paramsByName.get(paramName);
		if (param) {
			if (this._params_node) {
				this._params_node.removeGraphInput(param);
			}
			param._setupNodeDependencies(null);
			this._paramsByName.delete(paramName);
			if (param.isMultiple() && param.components) {
				for (const component of param.components) {
					const childName = component.name();
					this._paramsByName.delete(childName);
				}
			}
			param.dispose();
			param.scene().graphNodesController.notifyParamPathChanged(param);

			// const name_index = this._param_names.indexOf(param_name)
			// if(name_index >= 0){
			// 	this._param_names.splice(name_index, 1)
			// }
			// param.emit(ParamEvent.DELETED);
		} else {
			throw new Error(`param '${paramName}' does not exist on node ${this.node.path()}`);
		}
	}

	addParam<T extends ParamType>(
		type: T,
		paramName: string,
		defaultValue: ParamInitValuesTypeMap[T],
		options: ParamOptions = {},
		initData?: ParamInitData<T>
	): ParamConstructorMap[T] | undefined {
		const isSpare = options['spare'] || false;
		if (this._param_create_mode === false && !isSpare) {
			Poly.warn(
				`node ${this.node.path()} (${this.node.type()}) param '${paramName}' cannot be created outside of createParams`
			);
		}
		if (this.node.scene() == null) {
			Poly.warn(`node ${this.node.path()} (${this.node.type()}) has no scene assigned`);
		}

		const constructor = ParamConstructorByType[type];
		if (constructor != null) {
			const existing_param = this._paramsByName.get(paramName);
			if (existing_param) {
				if (isSpare) {
					// delete the old one, otherwise the gl nodes when saved will attempt to set the value
					// of a param with the potentially wrong type
					if (existing_param.type() != type) {
						this._deleteParam(existing_param.name());
					}
				} else {
					// check that the param is spare, so that the ones generated by gl nodes are not generating an exception
					Poly.warn(`a param named ${paramName} already exists`, this.node);
				}
			}
			const param: ParamConstructorMap[T] = new constructor(this.node.scene(), this.node, {
				serializerClass: this.node.scene().paramSerializerClass(),
			});
			param.options.set(options);

			param.setName(paramName);
			param.setInitValue(defaultValue as never);
			param.initComponents();

			// set param value
			// and overriden options
			if (initData == null) {
				param.set(defaultValue as never);
			} else {
				// If is_expression_for_entities is true, we need to call param.set with default_value first, such as for attribCreate.
				// Otherwise, as it would fail if the attribute was a vector
				// since that attribute would have .value equal to {x: undefined, y: undefined, z:undefined}
				if (param.options.isExpressionForEntities()) {
					param.set(defaultValue as never);
				}

				if (initData.raw_input != null) {
					param.set(initData.raw_input as never);
				} else {
					if (initData.simple_data != null) {
						param.set(initData.simple_data as never);
					} else {
						if (initData.complex_data != null) {
							const raw_input = initData.complex_data.raw_input;
							if (raw_input) {
								param.set(raw_input as never);
							} else {
								param.set(defaultValue as never);
							}
							const overriden_options = initData.complex_data.overriden_options;
							if (overriden_options != null) {
								const keys = Object.keys(overriden_options);
								for (const key of keys) {
									param.options.setOption(key as keyof ParamOptions, overriden_options[key]);
								}
							}
						}
					}
				}
			}
			param.postOptionsInitialize();
			param._setupNodeDependencies(this.node);

			this._paramsByName.set(param.name(), param as BaseParamType);

			// we add the components, so that we can access them with expressions like ch('ty')
			if (param.isMultiple() && param.components) {
				for (const component of param.components) {
					this._paramsByName.set(component.name(), component as BaseParamType);
				}
			}

			this._params_added_since_last_params_eval = true;

			param.scene().graphNodesController.notifyParamPathChanged(param);
			return param;
		}
	}

	private _updateCaches() {
		this._paramsList.splice(0, this._paramsList.length);
		this._paramsByName.forEach((param) => {
			this._paramsList.push(param);
		});
		this._paramNames = this._paramsList.map((param) => param.name());
		this._non_spare_params = this._paramsList.filter((p) => !p.options.isSpare());
		this._spare_params = this._paramsList.filter((p) => p.options.isSpare());
		this._non_spare_param_names = this._non_spare_params.map((p) => p.name());
		this._spare_param_names = this._spare_params.map((p) => p.name());
	}

	async _evalParam(param: BaseParamType) {
		if (param.isDirty()) {
			await param.compute();
			if (param.states.error.active() && param.disposed() == false) {
				this.node.states.error.set(`param '${param.name()}' error: ${param.states.error.message()}`);
			}
		}
	}

	private _promises: Promise<void>[] = [];
	async evalParams(params: BaseParamType[]) {
		let dirtyParamsCount = 0;
		for (const param of params) {
			if (param.isDirty()) {
				dirtyParamsCount += 1;
			}
		}
		this._promises.length = dirtyParamsCount;
		let i = 0;
		for (const param of params) {
			if (param.isDirty()) {
				this._promises[i] = this._evalParam(param);
				i += 1;
			}
		}

		await Promise.all(this._promises);

		if (this.node.states.error.active()) {
			this.node._setContainer(null);
		}
	}

	paramsEvalRequired(): boolean {
		return this._params_node != null && (this._params_node.isDirty() || this._params_added_since_last_params_eval);
	}
	async evalAll() {
		if (this.paramsEvalRequired()) {
			await this.evalParams(this._paramsList);

			this._params_node?.removeDirtyState();
			this._params_added_since_last_params_eval = false;
		}
	}

	//
	//
	// HOOKS
	//
	//
	onParamsCreated(hook_name: string, hook: PostCreateParamsHook) {
		if (this._params_created) {
			hook();
		} else {
			if (this._post_create_params_hook_names && this._post_create_params_hook_names.includes(hook_name)) {
				Poly.error(`hook name ${hook_name} already exists`);
				return;
			}
			this._post_create_params_hook_names = this._post_create_params_hook_names || [];
			this._post_create_params_hook_names.push(hook_name);
			this._post_create_params_hooks = this._post_create_params_hooks || [];
			this._post_create_params_hooks.push(hook);
		}
	}
	addOnSceneLoadHook(param_name: string, method: OnSceneLoadHook) {
		this._on_scene_load_hook_names = this._on_scene_load_hook_names || [];
		this._on_scene_load_hooks = this._on_scene_load_hooks || [];

		if (!this._on_scene_load_hook_names.includes(param_name)) {
			this._on_scene_load_hook_names.push(param_name);
			this._on_scene_load_hooks.push(method);
		} else {
			Poly.warn(`hook with name ${param_name} already exists`, this.node);
		}
	}
	private _runPostCreateParamsHooks() {
		if (this._post_create_params_hooks) {
			for (const hook of this._post_create_params_hooks) {
				hook();
			}
		}
	}
	runOnSceneLoadHooks() {
		if (this._on_scene_load_hooks) {
			for (const hook of this._on_scene_load_hooks) {
				hook();
			}
		}
	}
}
