import {PolyScene} from '../../engine/scene/PolyScene';
import {CoreGraphNode} from './CoreGraphNode';

export type CoreGraphNodeId = number;

export class CoreGraph {
	private _next_id: CoreGraphNodeId = 0;
	private _scene: PolyScene | undefined;
	private _successors: Map<CoreGraphNodeId, Set<CoreGraphNodeId>> = new Map();
	private _predecessors: Map<CoreGraphNodeId, Set<CoreGraphNodeId>> = new Map();
	private _nodes_by_id: Map<number, CoreGraphNode> = new Map();
	private _nodesCount = 0;

	private _debugging = false;
	private _addedNodesDuringDebugging: Map<CoreGraphNodeId, CoreGraphNode> = new Map();
	startDebugging() {
		this._debugging = true;
		console.log('CoreGraph.startDebugging', this._next_id);
	}
	stopDebugging() {
		this._debugging = false;
		console.log('CoreGraph.stopDebugging', this._next_id);
	}
	printDebug() {
		this._addedNodesDuringDebugging.forEach((node, nodeId) => {
			console.log(nodeId, node, node.graphPredecessors(), node.graphSuccessors());
		});
	}

	set_scene(scene: PolyScene) {
		this._scene = scene;
	}
	scene() {
		return this._scene;
	}

	next_id(): CoreGraphNodeId {
		this._next_id += 1;
		return this._next_id;
	}

	nodes_from_ids(ids: number[]) {
		const nodes: CoreGraphNode[] = [];
		for (let id of ids) {
			const node = this.node_from_id(id);
			if (node) {
				nodes.push(node);
			}
		}
		return nodes;
	}
	node_from_id(id: number): CoreGraphNode | undefined {
		return this._nodes_by_id.get(id);
	}
	has_node(node: CoreGraphNode): boolean {
		return this._nodes_by_id.get(node.graphNodeId()) != null;
	}
	add_node(node: CoreGraphNode) {
		this._nodes_by_id.set(node.graphNodeId(), node);
		this._nodesCount += 1;
		if (this._debugging) {
			this._addedNodesDuringDebugging.set(node.graphNodeId(), node);
		}
	}
	remove_node(node: CoreGraphNode) {
		this._nodes_by_id.delete(node.graphNodeId());
		this._successors.delete(node.graphNodeId());
		this._predecessors.delete(node.graphNodeId());
		this._nodesCount -= 1;

		if (this._debugging) {
			this._addedNodesDuringDebugging.delete(node.graphNodeId());
		}
	}
	nodesCount() {
		return this._nodesCount;
	}
	connect(src: CoreGraphNode, dest: CoreGraphNode, check_if_graph_may_have_cycle = true): boolean {
		const src_id = src.graphNodeId();
		const dest_id = dest.graphNodeId();

		if (this.has_node(src) && this.has_node(dest)) {
			// this._graph.setEdge(src_id, dest_id);

			// if check_if_graph_may_have_cycle is passed as false, that means we never check.
			// this can be useful when we know that the connection will not create a cycle,
			// such as when connecting params or inputs to a node
			if (check_if_graph_may_have_cycle) {
				const scene_loading = this._scene ? this._scene.loadingController.isLoading() : true;
				check_if_graph_may_have_cycle = !scene_loading;
			}
			let graph_would_have_cycle = false;
			if (check_if_graph_may_have_cycle) {
				// graph_has_cycle = !alg.isAcyclic(this._graph);
				graph_would_have_cycle = this._has_predecessor(src_id, dest_id);
			}

			if (graph_would_have_cycle) {
				// this._graph.removeEdge(src_id, dest_id);
				return false;
			} else {
				this._create_connection(src_id, dest_id);
				src.dirtyController.clear_successors_cache_with_predecessors();

				return true;
			}
		} else {
			console.warn(`attempt to connect non existing node ${src_id} or ${dest_id}`);
			return false;
		}
	}

	disconnect(src: CoreGraphNode, dest: CoreGraphNode) {
		// const src_id_s = src.graphNodeId();
		// const dest_id_s = dest.graphNodeId();
		// this._graph.removeEdge(src_id_s, dest_id_s);
		this._remove_connection(src.graphNodeId(), dest.graphNodeId());

		src.dirtyController.clear_successors_cache_with_predecessors();
	}
	disconnect_predecessors(node: CoreGraphNode) {
		const predecessors = this.predecessors(node);
		for (let predecessor of predecessors) {
			this.disconnect(predecessor, node);
		}
	}
	disconnect_successors(node: CoreGraphNode) {
		const successors = this.successors(node);
		for (let successor of successors) {
			this.disconnect(node, successor);
		}
	}

	predecessor_ids(id: CoreGraphNodeId): CoreGraphNodeId[] {
		const map = this._predecessors.get(id);
		if (map) {
			const ids: CoreGraphNodeId[] = [];
			map.forEach((bool, id) => {
				ids.push(id);
			});
			return ids;
		}
		return [];
	}
	predecessors(node: CoreGraphNode) {
		const ids = this.predecessor_ids(node.graphNodeId());
		return this.nodes_from_ids(ids);
	}
	successor_ids(id: CoreGraphNodeId): CoreGraphNodeId[] {
		const map = this._successors.get(id);
		if (map) {
			const ids: CoreGraphNodeId[] = [];
			map.forEach((bool, id) => {
				ids.push(id);
			});
			return ids;
		}
		return [];
	}
	successors(node: CoreGraphNode): CoreGraphNode[] {
		const ids = this.successor_ids(node.graphNodeId()) || [];
		return this.nodes_from_ids(ids);
	}
	all_predecessor_ids(node: CoreGraphNode): CoreGraphNodeId[] {
		return this.all_next_ids(node, 'predecessor_ids');
	}
	all_successor_ids(node: CoreGraphNode): CoreGraphNodeId[] {
		return this.all_next_ids(node, 'successor_ids');
	}
	all_predecessors(node: CoreGraphNode): CoreGraphNode[] {
		const ids = this.all_predecessor_ids(node);
		return this.nodes_from_ids(ids);
	}
	all_successors(node: CoreGraphNode): CoreGraphNode[] {
		const ids = this.all_successor_ids(node);
		return this.nodes_from_ids(ids);
	}
	private _create_connection(src_id: CoreGraphNodeId, dest_id: CoreGraphNodeId) {
		// set successors
		let node_successors = this._successors.get(src_id);
		if (!node_successors) {
			node_successors = new Set();
			this._successors.set(src_id, node_successors);
		}
		if (node_successors.has(dest_id)) {
			return;
		}
		node_successors.add(dest_id);

		// set predecessors
		let node_predecessors = this._predecessors.get(dest_id);
		if (!node_predecessors) {
			node_predecessors = new Set();
			this._predecessors.set(dest_id, node_predecessors);
		}
		node_predecessors.add(src_id);
	}
	private _remove_connection(src_id: CoreGraphNodeId, dest_id: CoreGraphNodeId) {
		// remove successors
		let node_successors = this._successors.get(src_id);
		if (node_successors) {
			node_successors.delete(dest_id);
			if (node_successors.size == 0) {
				this._successors.delete(src_id);
			}
		}
		// remove predecessors
		let node_predecessors = this._predecessors.get(dest_id);
		if (node_predecessors) {
			node_predecessors.delete(src_id);
			if (node_predecessors.size == 0) {
				this._predecessors.delete(dest_id);
			}
		}
	}

	private all_next_ids(node: CoreGraphNode, method: 'successor_ids' | 'predecessor_ids'): CoreGraphNodeId[] {
		const ids_by_id: Map<CoreGraphNodeId, boolean> = new Map();
		const ids: CoreGraphNodeId[] = [];
		let next_ids = this[method](node.graphNodeId());

		while (next_ids.length > 0) {
			const next_next_ids = [];
			for (let next_id of next_ids) {
				for (let next_next_id of this[method](next_id)) {
					next_next_ids.push(next_next_id);
				}
			}

			for (let id of next_ids) {
				ids_by_id.set(id, true);
			}
			for (let id of next_next_ids) {
				next_ids.push(id);
			}
			next_ids = next_next_ids;
		}
		ids_by_id.forEach((bool, id) => {
			ids.push(id);
		});
		return ids;
	}

	private _has_predecessor(src_id: CoreGraphNodeId, dest_id: CoreGraphNodeId): boolean {
		const ids = this.predecessor_ids(src_id);
		if (ids) {
			if (ids.includes(dest_id)) {
				return true;
			} else {
				for (let id of ids) {
					return this._has_predecessor(id, dest_id);
				}
			}
		}

		return false;
	}
}
