import {addToSetAtEntry} from '../../../../../../core/MapUtils';
import {setToArray} from '../../../../../../core/SetUtils';
import {sanitizeName} from '../../../../../../core/String';
import {NodeContext} from '../../../../../poly/NodeContext';
import {ActorBuilderNode} from '../../../../../scene/utils/ActorsManager';
// import {ActorSopNode} from '../../../../sop/Actor';
import {BaseJsConnectionPoint, JsConnectionPointType} from '../../../../utils/io/connections/Js';
import {BaseJsNodeType} from '../../../_Base';

type ConnectionPointCallback = (connectionPoint: BaseJsConnectionPoint) => boolean;

export function nodeMethodName(node: BaseJsNodeType, outputName?: string): string {
	const functionNode = node.functionNode();
	if (functionNode == null) {
		return node.name();
	}
	const pathWithoutParentPath = node.path().replace(functionNode.path(), '');
	const sanitizedPath = sanitizeName(pathWithoutParentPath);
	const pathWithoutForwardSlash = sanitizedPath.substring(1);
	// we remove the underscore inside the name,
	// so that we can find the method and highlight it in the editor,
	// without assuming that what's after the underscore is the name of the output
	const pathWithoutUnderscore = pathWithoutForwardSlash.replace(/_/g, '');
	const sanitized = pathWithoutUnderscore;
	if (outputName) {
		return `${sanitized}_${outputName}`;
	} else {
		return sanitized;
	}
}
interface MethodNameData {
	outputName: string;
	nodeName: string;
	methodNameWithoutOutputName: string;
}
export function methodNameData(methodName: string): MethodNameData {
	const elements = methodName.split('_');
	if (elements.length > 1) {
		const outputName = elements[elements.length - 1];
		const nodeName = elements[elements.length - 2];
		elements.pop();
		const methodNameWithoutOutputName = elements.join('_');
		return {outputName, nodeName, methodNameWithoutOutputName};
	} else {
		const outputName = JsConnectionPointType.TRIGGER;
		const nodeName = elements[elements.length - 2];
		const methodNameWithoutOutputName = methodName;
		return {outputName, nodeName, methodNameWithoutOutputName};
	}
}

function isTriggeringNode(node: BaseJsNodeType): boolean {
	return node.isTriggering() && _hasTriggerOutputConnected(node);
	// if(!node.isTriggering()){
	// 	return false
	// }
	// const cp = node.io.outputs.namedOutputConnectionPointsByName(TRIGGER_CONNECTION_NAME);
	// return cp != null && cp.type() == JsConnectionPointType.TRIGGER;
}
function _hasTriggerOutputConnected(node: BaseJsNodeType): boolean {
	const outputConnectionPoints = node.io.outputs.namedOutputConnectionPoints();
	let triggerOutputIndices: number[] = [];
	let i = 0;
	if (outputConnectionPoints) {
		for (const outputConnectionPoint of outputConnectionPoints) {
			if (outputConnectionPoint.type() == JsConnectionPointType.TRIGGER) {
				triggerOutputIndices.push(i);
			}
			i++;
		}
	}
	for (const triggerOutputIndex of triggerOutputIndices) {
		const triggerConnections = node.io.connections.outputConnectionsByOutputIndex(triggerOutputIndex);
		if (triggerConnections != null && triggerConnections.size > 0) {
			return true;
		}
	}
	return false;
}

// function isTriggerableNode(node: BaseJsNodeType): boolean {
// 	const cp = node.io.inputs.namedInputConnectionPointsByName(TRIGGER_CONNECTION_NAME);
// 	return cp != null && cp.type() == JsConnectionPointType.TRIGGER;
// }
function evalChildren(
	parent: ActorBuilderNode,
	nodes: Set<BaseJsNodeType>,
	testFunction: (node: BaseJsNodeType) => boolean
): void {
	nodes.clear();
	parent.childrenController?.traverseChildren((child) => {
		if (child.context() == NodeContext.JS) {
			const jsChild = child as BaseJsNodeType;
			if (testFunction(jsChild)) {
				nodes.add(jsChild);
			}
		}
	});
}
export function findTriggeringNodes(parent: ActorBuilderNode): Set<BaseJsNodeType> {
	const nodes: Set<BaseJsNodeType> = new Set();
	evalChildren(parent, nodes, isTriggeringNode);
	return nodes;
}
// export function findTriggeringNodesNonTriggerable(parent: ActorJsSopNode): Set<BaseJsNodeType> {
// 	const nodes: Set<BaseJsNodeType> = new Set();
// 	const _func = (node: BaseJsNodeType) => isTriggeringNode(node) && !isTriggerableNode(node);
// 	evalChildren(parent, nodes, _func);
// 	return nodes;
// }
// export function findTriggerableNodes(triggeringNodes:Set<BaseJsNodeType> ):Set<BaseJsNodeType>{
// 	const nodes:Set<BaseJsNodeType> = new Set()

// 	triggeringNodes.forEach(triggeringNode=>{

// 	})

// 	return nodes
// }
// export function findTriggerableNodes(parent: ActorJsSopNode, nodes: Set<BaseJsNodeType>) {
// 	evalChildren(parent, nodes, isTriggerableNode);
// }

export function groupNodesByType(nodes: Set<BaseJsNodeType>, nodesByType: Map<string, Set<BaseJsNodeType>>) {
	nodesByType.clear();
	nodes.forEach((node) => {
		addToSetAtEntry(nodesByType, node.type(), node);
	});
}

export function getOutputIndices(node: BaseJsNodeType, callback: ConnectionPointCallback) {
	let triggerOutputIndices: number[] = [];
	const outputConnectionPoints = node.io.outputs.namedOutputConnectionPoints();
	if (outputConnectionPoints) {
		let i = 0;
		for (const outputConnectionPoint of outputConnectionPoints) {
			if (callback(outputConnectionPoint) == true) {
				triggerOutputIndices.push(i);
			}
			i++;
		}
	}
	return triggerOutputIndices;
}
export function getInputIndices(node: BaseJsNodeType, callback: ConnectionPointCallback) {
	let triggerInputIndices: number[] = [];
	const inputConnectionPoints = node.io.inputs.namedInputConnectionPoints();
	if (inputConnectionPoints) {
		let i = 0;
		for (const inputConnectionPoint of inputConnectionPoints) {
			if (callback(inputConnectionPoint) == true) {
				triggerInputIndices.push(i);
			}
			i++;
		}
	}
	return triggerInputIndices;
}
interface GetConnectedOutputNodesOptions {
	node: BaseJsNodeType;
	triggerOutputIndices: number[];
	triggerableNodes: Set<BaseJsNodeType>;
	recursive: boolean;
}

export function getConnectedOutputNodes(options: GetConnectedOutputNodesOptions) {
	const {node, triggerOutputIndices, triggerableNodes, recursive} = options;
	for (const triggerOutputIndex of triggerOutputIndices) {
		const triggerConnections = node.io.connections.outputConnectionsByOutputIndex(triggerOutputIndex);
		if (triggerConnections) {
			triggerConnections.forEach((triggerConnection) => {
				triggerableNodes.add(triggerConnection.nodeDest());
				if (recursive) {
					connectedTriggerableNodes({
						triggeringNodes: new Set([triggerConnection.nodeDest()]),
						triggerableNodes,
						recursive,
					});
				}
			});
		}
	}
}

interface ConnectedTriggerableNodesOptions {
	triggeringNodes: Set<BaseJsNodeType>;
	triggerableNodes: Set<BaseJsNodeType>;
	recursive: boolean;
}
export function connectedTriggerableNodes(options: ConnectedTriggerableNodesOptions) {
	const {triggeringNodes, triggerableNodes, recursive} = options;
	triggeringNodes.forEach((node) => {
		const triggerOutputIndices = getOutputIndices(node, (c) => c.type() == JsConnectionPointType.TRIGGER);
		// get output connection points with type trigger
		// let triggerOutputIndices: number[] = [];
		// const outputConnectionPoints = node.io.outputs.namedOutputConnectionPoints();
		// let i = 0;
		// for (let outputConnectionPoint of outputConnectionPoints) {
		// 	if (outputConnectionPoint.type() == JsConnectionPointType.TRIGGER) {
		// 		triggerOutputIndices.push(i);
		// 	}
		// 	i++;
		// }
		getConnectedOutputNodes({node, triggerOutputIndices, triggerableNodes, recursive});
		// get connected nodes
		// for (let triggerOutputIndex of triggerOutputIndices) {
		// 	const triggerConnections = node.io.connections.outputConnectionsByOutputIndex(triggerOutputIndex);
		// 	if (triggerConnections) {
		// 		triggerConnections.forEach((triggerConnection) => {
		// 			triggerableNodes.add(triggerConnection.node_dest);
		// 			if (recursive) {
		// 				connectedTriggerableNodes({
		// 					triggeringNodes: new Set([triggerConnection.node_dest]),
		// 					triggerableNodes,
		// 					recursive,
		// 				});
		// 			}
		// 		});
		// 	}
		// }
	});
}

const _nonTriggerInputNodes: Set<BaseJsNodeType> = new Set();
const _nonTriggerInputIndices: number[] = [];
function _inputNodesFromConnectionWithCallback(
	node: BaseJsNodeType,
	callback: ConnectionPointCallback,
	target: BaseJsNodeType[]
) {
	_nonTriggerInputNodes.clear();
	_nonTriggerInputIndices.length = 0;
	const inputConnectionPoints = node.io.inputs.namedInputConnectionPoints();
	if (inputConnectionPoints) {
		let i = 0;
		for (const outputConnectionPoint of inputConnectionPoints) {
			if (callback(outputConnectionPoint)) {
				_nonTriggerInputIndices.push(i);
			}
			i++;
		}
	}
	for (const nonTriggerInputIndex of _nonTriggerInputIndices) {
		const connection = node.io.connections.inputConnection(nonTriggerInputIndex);
		if (connection) {
			_nonTriggerInputNodes.add(connection.nodeSrc());
		}
	}
	return setToArray(_nonTriggerInputNodes, target);
}
export function inputNodesExceptTrigger(node: BaseJsNodeType, target: BaseJsNodeType[]) {
	// return inputNodesFromConnectionWithCallback(node, (c) => c.type() != JsConnectionPointType.TRIGGER);
	// update:
	// I initially thought we should not take into account node when only connected via a trigger connection,
	// but that fails with the onObjectAttributeUpdate,
	// if it is only connected via its trigger, as its inputs are then not parsed,
	// which is very much needed when using a different object as input
	return _inputNodesFromConnectionWithCallback(node, (c) => true, target);
}

export function triggerInputIndex(triggeringNode: BaseJsNodeType, triggeredNode: BaseJsNodeType): number | null {
	const triggerOutputIndices = getOutputIndices(triggeringNode, (c) => c.type() == JsConnectionPointType.TRIGGER);

	let index: number | null = null;
	for (const triggerOutputIndex of triggerOutputIndices) {
		const triggerConnections = triggeringNode.io.connections.outputConnectionsByOutputIndex(triggerOutputIndex);
		if (triggerConnections) {
			triggerConnections.forEach((triggerConnection) => {
				if (triggerConnection.nodeDest() == triggeredNode) {
					index = triggerConnection.inputIndex();
				}
			});
		}
	}
	return index;
}

const _triggerableNodesSet = new Set<BaseJsNodeType>();
const _triggerableNodes: BaseJsNodeType[] = [];
export function triggerableMethodCalls(triggeringNode: BaseJsNodeType) {
	_triggerableNodesSet.clear();
	connectedTriggerableNodes({
		triggeringNodes: new Set([triggeringNode]),
		triggerableNodes: _triggerableNodesSet,
		recursive: false,
	});
	setToArray(_triggerableNodesSet, _triggerableNodes);

	const methodCalls: string[] = [];
	for (const triggerableNode of _triggerableNodes) {
		const methodName = nodeMethodName(triggerableNode);
		const argIndex = triggerInputIndex(triggeringNode, triggerableNode);
		// argIndex is used to highlight the connection
		const methodCall = `this.${methodName}(${argIndex});`;
		methodCalls.push(methodCall);
	}
	return methodCalls.join('\n');
	// const triggerableMethodCalls = triggerableMethodNames
	// 	.map((methodName) => `this.${methodName}(${})`)
	// 	.join('\n');
}
