/**
 * the node to rull them all
 *
 * @remarks
 *
 * The root node is unique in the scene. it is the higest parent, just above the [obj](/docs/nodes/obj) nodes.
 *
 * It allows you to control the following scene properties:
 *
 * - the scene background (which can be nothing, a plain color, or a texture)
 * - which camera is used when exporting the scene
 * - the fog
 * - an environment map which would override every material's environment map
 * - a material which would override every other material
 * - the display of an audio icon in the viewer, which is useful when using sound in your scene, and allowing users to turn it on/off
 *
 */
import {TypedBaseManagerNode} from './_Base';
import {BaseObjNodeType} from '../obj/_Base';
import {NodeContext} from '../../poly/NodeContext';
import {ObjNodeChildrenMap} from '../../poly/registers/nodes/Obj';
import {NodeParamsConfig} from '../utils/params/ParamsConfig';
import {BaseNodeType} from '../_Base';
import {HierarchyObjNode} from '../obj/utils/HierarchyController';
import {Constructor, valueof} from '../../../types/GlobalTypes';
import {ROOT_NAME} from '../../scene/utils/ObjectsController';
import {Scene} from 'three';

import {RootAudioController, RootAudioParamConfig} from './utils/Scene/Audio';
import {SceneAutoUpdateParamConfig, SceneAutoUpdateController} from './utils/Scene/AutoUpdate';
import {SceneBackgroundParamConfig, SceneBackgroundController} from './utils/Scene/Background';
import {SceneEnvParamConfig, SceneEnvController} from './utils/Scene/Env';
import {SceneFogParamConfig, SceneFogController} from './utils/Scene/Fog';
import {RootLoadProgressParamConfig, RootLoadProgressController} from './utils/Scene/LoadProgress';
import {SceneMaterialOverrideParamConfig, SceneMaterialOverrideController} from './utils/Scene/MaterialOverride';
import {RootMainCameraController, RootMainCameraParamConfig} from './utils/Scene/RootMainCamera';
import {NodeCreateOptions} from '../utils/hierarchy/ChildrenController';

export const ROOT_NODE_NAME = 'RootNode';
class ObjectsManagerParamsConfig extends RootLoadProgressParamConfig(
	RootAudioParamConfig(
		SceneMaterialOverrideParamConfig(
			SceneEnvParamConfig(
				SceneFogParamConfig(
					RootMainCameraParamConfig(SceneBackgroundParamConfig(SceneAutoUpdateParamConfig(NodeParamsConfig)))
				)
			)
		)
	)
) {}
const ParamsConfig = new ObjectsManagerParamsConfig();

export class RootManagerNode extends TypedBaseManagerNode<ObjectsManagerParamsConfig> {
	override paramsConfig = ParamsConfig;
	static override type() {
		return 'root';
	}

	protected _object: Scene = this._createScene();
	private _queuedNodesById: Map<number, BaseObjNodeType> = new Map();
	// private _expected_geo_nodes: PolyDictionary<GeoObjNode> = {};
	// private _process_queue_start: number = -1;
	readonly audioController: RootAudioController = new RootAudioController(this);
	readonly sceneAutoUpdateController: SceneAutoUpdateController = new SceneAutoUpdateController(this);
	readonly sceneBackgroundController: SceneBackgroundController = new SceneBackgroundController(this);
	readonly sceneEnvController: SceneEnvController = new SceneEnvController(this);
	readonly sceneFogController: SceneFogController = new SceneFogController(this);
	readonly loadProgress: RootLoadProgressController = new RootLoadProgressController(this);
	readonly sceneMaterialOverrideController: SceneMaterialOverrideController = new SceneMaterialOverrideController(
		this as any
	);
	readonly mainCameraController: RootMainCameraController = new RootMainCameraController(this);

	override cook() {
		// the cook method is necessary here,
		// with the .endCook()
		// Without it, the button param of this node would not execute
		// its callback, as this node would not be perceived as cooking done
		this.cookController.endCook();
	}

	protected override _childrenControllerContext = NodeContext.OBJ;
	override initializeNode() {
		// this.children_controller?.init({dependent: false});

		this.params.onParamsCreated('init controllers', () => {
			this.sceneEnvController.addHooks();
			this.sceneBackgroundController.addHooks();
		});

		this.lifecycle.onChildAdd(this._onChildAdd.bind(this));
		this.lifecycle.onChildRemove(this._onChildRemove.bind(this));
	}

	private _createScene() {
		const scene = new Scene();
		scene.name = ROOT_NAME;
		// DO NOT set matrixAutoUpdate to false
		// on the scene, as this has too many side effects
		// and is probably an over optimisation anyway
		scene.matrixAutoUpdate = true;
		return scene;
	}

	get object() {
		return this._object;
	}
	override createNode<S extends keyof ObjNodeChildrenMap>(
		nodeClass: S,
		options?: NodeCreateOptions
	): ObjNodeChildrenMap[S];
	override createNode<K extends valueof<ObjNodeChildrenMap>>(
		nodeClass: Constructor<K>,
		options?: NodeCreateOptions
	): K;
	override createNode<K extends valueof<ObjNodeChildrenMap>>(
		nodeClass: Constructor<K>,
		options?: NodeCreateOptions
	): K {
		return super.createNode(nodeClass, options) as K;
	}
	override children() {
		return super.children() as BaseObjNodeType[];
	}
	override nodesByType<K extends keyof ObjNodeChildrenMap>(type: K): ObjNodeChildrenMap[K][] {
		return super.nodesByType(type) as ObjNodeChildrenMap[K][];
	}

	// multiple_display_flags_allowed() {
	// 	return true;
	// }
	private _updateScene() {
		this.sceneAutoUpdateController.update();
		this.sceneBackgroundController.update();
		this.sceneEnvController.update();
		this.sceneFogController.update();
		this.sceneMaterialOverrideController.update();
	}

	private _addToQueue(node: BaseObjNodeType) {
		const id = node.graphNodeId();
		if (!this._queuedNodesById.has(id)) {
			this._queuedNodesById.set(id, node);
		}
		return node;
	}

	processQueue() {
		this._updateScene();

		const queuedNodesByPath: Map<string, BaseObjNodeType> = new Map();
		const paths: string[] = [];
		this._queuedNodesById.forEach((node, id) => {
			const fullPath = `_____${node.renderOrder}__${node.path()}`;
			paths.push(fullPath);
			queuedNodesByPath.set(fullPath, node);
		});
		this._queuedNodesById.clear();

		// const promises = [];
		for (const path_id of paths) {
			const node = queuedNodesByPath.get(path_id);
			if (node) {
				queuedNodesByPath.delete(path_id);
				this._addToScene(node);
				// promises.push();
			}
		}
		// this._expected_geo_nodes = this._expected_geo_nodes || (await this.expected_loading_geo_nodes_by_id());

		// this._process_queue_start = performance.now();
		// Promise.all(promises).then(() => {
		// 	// Poly.log(`SCENE LOADED '${this.scene.name}`);
		// 	// `SCENE LOADED '${this.scene.name}' in ${performance.now() - this._process_queue_start}`
		// 	// this.scene().performance().print()
		// 	// do the update here if there are no objects to load
		// 	// otherwise an empty scene will have a loader that never gets removed
		// 	// if (Object.keys(this._expected_geo_nodes).length == 0) {
		// 	// 	this.update_on_all_objects_loaded();
		// 	// }
		// });
	}

	private _updateObject(node: BaseObjNodeType) {
		if (!this.scene().loadingController.autoUpdating()) {
			return this._addToQueue(node);
		} else {
			// update 25/01/2022: the light obj nodes, the null obj do not cook when created.
			// this aims to solve this.
			// and only once the scene has loaded
			if (node.isDisplayed() && !node.cookController.isCooking()) {
				node.compute();
			}
			return this._addToScene(node);
		}
	}

	//
	//
	// OBJ PARENTING
	//
	//
	getParentForNode(node: BaseObjNodeType) {
		if (node.attachableToHierarchy()) {
			const node_input = node.io.inputs.input(0);
			if (node_input) {
				return node_input.childrenGroup();
			} else {
				return this._object;
			}
		} else {
			return null;
		}
	}

	private _addToScene(node: BaseObjNodeType): void {
		if (node.attachableToHierarchy()) {
			const parentObject = this.getParentForNode(node);
			if (parentObject) {
				// await node.params.eval_all().then((params_eval_key) => {
				// 	node.compute();
				// });

				if (node.usedInScene()) {
					// I need to query the displayNodeController here,
					// for geo obj whose display_node is a node without inputs.
					// Since that node will not be made dirty, it seems that there is
					// nothing triggering the obj to request it itself.
					// TODO: investigate if it has a performance cost, or if it could be done
					// only when scene loads. Or if the displayNodeController itself could be improved
					// to take care of it itself.
					// node.compute();

					if (node.childrenDisplayController) {
						node.childrenDisplayController.requestDisplayNodeContainer();
					} else {
						node.compute();
					}

					node.addObjectToParent(parentObject);
				} else {
					node.removeObjectFromParent();
					// parent_object.remove(node.object);
				}

				// node.request_display_node();
			} else {
				// node.compute().then(() => {
				// 	// force events and mat to cook and remove the dirty state
				// 	// ensure that pickers are cooked
				// 	// TODO: although there has been cases with two picker and
				// 	// one referencing the other with an expression, and that
				// 	// expression be evaluated before the second was created
				// 	// which led to an error. This should not happen
				// 	node.children_controller.traverse_children((child) => child.setDirty());
				// });
			}
		}
	}

	private _removeFromScene(node: BaseObjNodeType) {
		node.removeObjectFromParent();
	}
	areChildrenCooking(): boolean {
		const children = this.children();
		for (const child of children) {
			if (child.cookController.isCooking() || child.isDisplayNodeCooking()) {
				return true;
			}
		}
		return false;
	}

	// private async expected_loading_geo_nodes_by_id() {
	// 	const geo_nodes = this.nodesByType('geo');
	// 	const node_by_id: PolyDictionary<GeoObjNode> = {};
	// 	for (let geo_node of geo_nodes) {
	// 		const isDisplayed = await geo_node.isDisplayed();
	// 		if (isDisplayed) {
	// 			node_by_id[geo_node.graphNodeId()] = geo_node;
	// 		}
	// 	}
	// 	return node_by_id;
	// }

	addToParentTransform(node: HierarchyObjNode) {
		this._updateObject(node);
	}

	removeFromParentTransform(node: HierarchyObjNode) {
		this._updateObject(node);
	}

	private _onChildAdd(node?: BaseNodeType) {
		if (node) {
			this._updateObject(node as BaseObjNodeType);
		}
	}
	private _onChildRemove(node?: BaseNodeType) {
		if (node) {
			this._removeFromScene(node as BaseObjNodeType);
		}
	}
}
