import {PolyDictionary} from '../../../types/GlobalTypes';
import {PolyScene} from '../PolyScene';
import {RootManagerNode, ROOT_NODE_NAME} from '../../nodes/manager/Root';
import {stringMatchMask} from '../../../core/String';
import {BaseNodeType} from '../../nodes/_Base';
import {NodeContext} from '../../poly/NodeContext';
import {NodeChildrenMapByContext} from '../../poly/registers/nodes/All';
import {CoreGraphNodeId} from '../../../core/graph/CoreGraph';
import {NodeCreateOptions} from '../../nodes/utils/hierarchy/ChildrenController';

type NodeByNodeId = Map<CoreGraphNodeId, BaseNodeType>;
type NodeMapByType<NC extends NodeContext> = Map<keyof NodeChildrenMapByContext[NC], NodeByNodeId>;
export class NodesController {
	constructor(private scene: PolyScene) {}

	_root!: RootManagerNode;
	_nodeContextSignatures: PolyDictionary<boolean> = {};
	_instanciatedNodesByContextAndType: Map<NodeContext, NodeMapByType<NodeContext>> = new Map();

	createRoot(options?: NodeCreateOptions) {
		this._root = new RootManagerNode(this.scene, ROOT_NODE_NAME, options);
		this._root.initializeBaseAndNode();
		this._root.params.init();
	}

	root() {
		return this._root;
	}
	private _traverseNode(parent: BaseNodeType, callback: (node: BaseNodeType) => void) {
		const nodes = parent.children();
		if (!nodes || nodes.length == 0) {
			return;
		}

		for (const node of nodes) {
			if (node.childrenController) {
				this._traverseNode(node, callback);
			}

			callback(node);
		}
	}
	traverseNodes(callback: (node: BaseNodeType) => void) {
		this._traverseNode(this._root, callback);
	}

	// objectsFromMask(mask: string): Object3D[] {
	// 	const masks = mask.split(' ');
	// 	const child_nodes = this.root.children() as BaseObjNodeType[];
	// 	const objects: Object3D[] = [];
	// 	for (let child_node of child_nodes) {
	// 		if (CoreString.matchesOneMask(child_node.name, masks)) {
	// 			if (child_node.object) {
	// 				objects.push(child_node.object);
	// 			}
	// 		}
	// 	}
	// 	return objects;
	// }
	clear() {
		const children = this.root().children();
		for (const child of children) {
			this.root().childrenController?.removeNode(child);
		}
		// return children.forEach(child=> {
		// 	return this.root().removeNode(child);
		// });
	}

	node(path: string) {
		if (path === '/') {
			return this.root();
		} else {
			return this.root().node(path);
		}
	}
	allNodes() {
		let nodes: BaseNodeType[] = [this.root()];
		let current_parents: BaseNodeType[] = [this.root()];
		let cmptr = 0;
		while (current_parents.length > 0 && cmptr < 10) {
			const children = current_parents
				.map((current_parent) => {
					if (current_parent.childrenAllowed()) {
						return current_parent.children();
					} else {
						return [];
					}
				})
				.flat();
			nodes = nodes.concat(children);
			current_parents = children;
			cmptr += 1;
		}
		return nodes.flat();
	}
	nodesFromMask(mask: string) {
		const nodes = this.allNodes();
		const matching_nodes: BaseNodeType[] = [];
		for (const node of nodes) {
			const path = node.path();
			if (stringMatchMask(path, mask)) {
				matching_nodes.push(node);
			}
		}
		return matching_nodes;
	}

	resetNodeContextSignatures() {
		this._nodeContextSignatures = {};
	}
	registerNodeContextSignature(node: BaseNodeType) {
		if (node.childrenAllowed() && node.childrenController) {
			this._nodeContextSignatures[node.childrenController.nodeContextSignature()] = true;
		}
	}
	nodeContextSignatures() {
		return Object.keys(this._nodeContextSignatures)
			.sort()
			.map((s) => s.toLowerCase());
	}

	addToInstanciatedNode<NC extends NodeContext>(node: BaseNodeType) {
		const context = node.context() as NC;
		const nodeType = node.type() as keyof NodeChildrenMapByContext[NC];

		let mapForContext: NodeMapByType<NC> | undefined = this._instanciatedNodesByContextAndType.get(context);
		if (!mapForContext) {
			mapForContext = new Map() as NodeMapByType<NC>;
			this._instanciatedNodesByContextAndType.set(context, mapForContext as any);
		}
		let mapForType = mapForContext.get(nodeType);
		if (!mapForType) {
			mapForType = new Map() as NodeByNodeId;
			mapForContext.set(nodeType, mapForType);
		}
		mapForType.set(node.graphNodeId(), node);

		// this._instanciated_nodes_by_context_and_type[context] =
		// 	this._instanciated_nodes_by_context_and_type[context] || {};
		// this._instanciated_nodes_by_context_and_type[context][node_type] =
		// 	this._instanciated_nodes_by_context_and_type[context][node_type] || {};
		// this._instanciated_nodes_by_context_and_type[context][node_type][node.graphNodeId()] = node;
	}

	removeFromInstanciatedNode<NC extends NodeContext>(node: BaseNodeType) {
		const context = node.context() as NC;
		const nodeType = node.type() as keyof NodeChildrenMapByContext[NC];
		const mapForContext: NodeMapByType<NC> | undefined = this._instanciatedNodesByContextAndType.get(context);
		if (!mapForContext) {
			return;
		}
		const mapForType = mapForContext.get(nodeType);
		if (!mapForType) {
			return;
		}
		mapForType.delete(node.graphNodeId());
		// delete this._instanciated_nodes_by_context_and_type[context][node_type][node.graphNodeId()];
	}
	nodesByType(type: string): BaseNodeType[] {
		const list: BaseNodeType[] = [];

		this._traverseNode(this.scene.root(), (node) => {
			if (node.type() == type) {
				list.push(node);
			}
		});

		return list;
	}

	nodesByContextAndType<T extends keyof NodeChildrenMapByContext[NodeContext.ANIM]>(
		context: NodeContext.ANIM,
		node_type: T
	): NodeChildrenMapByContext[NodeContext.ANIM][T][];
	nodesByContextAndType<T extends keyof NodeChildrenMapByContext[NodeContext.AUDIO]>(
		context: NodeContext.AUDIO,
		node_type: T
	): NodeChildrenMapByContext[NodeContext.AUDIO][T][];
	nodesByContextAndType<T extends keyof NodeChildrenMapByContext[NodeContext.COP]>(
		context: NodeContext.COP,
		node_type: T
	): NodeChildrenMapByContext[NodeContext.COP][T][];
	nodesByContextAndType<T extends keyof NodeChildrenMapByContext[NodeContext.EVENT]>(
		context: NodeContext.EVENT,
		node_type: T
	): NodeChildrenMapByContext[NodeContext.EVENT][T][];
	nodesByContextAndType<T extends keyof NodeChildrenMapByContext[NodeContext.GL]>(
		context: NodeContext.GL,
		node_type: T
	): NodeChildrenMapByContext[NodeContext.GL][T][];
	nodesByContextAndType<T extends keyof NodeChildrenMapByContext[NodeContext.JS]>(
		context: NodeContext.JS,
		node_type: T
	): NodeChildrenMapByContext[NodeContext.JS][T][];
	nodesByContextAndType<T extends keyof NodeChildrenMapByContext[NodeContext.MAT]>(
		context: NodeContext.MAT,
		node_type: T
	): NodeChildrenMapByContext[NodeContext.MAT][T][];
	nodesByContextAndType<T extends keyof NodeChildrenMapByContext[NodeContext.OBJ]>(
		context: NodeContext.OBJ,
		node_type: T
	): NodeChildrenMapByContext[NodeContext.OBJ][T][];
	nodesByContextAndType<T extends keyof NodeChildrenMapByContext[NodeContext.POST]>(
		context: NodeContext.POST,
		node_type: T
	): NodeChildrenMapByContext[NodeContext.POST][T][];
	nodesByContextAndType<T extends keyof NodeChildrenMapByContext[NodeContext.ROP]>(
		context: NodeContext.ROP,
		node_type: T
	): NodeChildrenMapByContext[NodeContext.ROP][T][];
	nodesByContextAndType<T extends keyof NodeChildrenMapByContext[NodeContext.SOP]>(
		context: NodeContext.SOP,
		node_type: T
	): NodeChildrenMapByContext[NodeContext.SOP][T][];
	nodesByContextAndType<NC extends NodeContext>(context: NC, nodeType: string) {
		const nodes: BaseNodeType[] = [];
		const mapForContext: NodeMapByType<NC> | undefined = this._instanciatedNodesByContextAndType.get(context);
		if (mapForContext) {
			const mapForType = mapForContext.get(nodeType as any);
			if (mapForType) {
				mapForType.forEach((node) => {
					nodes.push(node);
				});
			}
		}
		return nodes;
	}

	hasNodesByContextAndType<NC extends NodeContext>(context: NC, nodeType: string) {
		const mapForContext: NodeMapByType<NC> | undefined = this._instanciatedNodesByContextAndType.get(context);
		if (!mapForContext) {
			return false;
		}
		const mapForType = mapForContext.get(nodeType as any);
		if (!mapForType) {
			return false;
		}
		return mapForType.size != 0;
	}
}
