import { app } from "../../../scripts/app.js";
import { ComfyWidgets } from "../../../scripts/widgets.js";

const REROUTE_PRIMITIVE = "ReroutePrimitive|pysssss";
const MULTI_PRIMITIVE = "MultiPrimitive|pysssss";
const LAST_TYPE = Symbol("LastType");

app.registerExtension({
	name: "pysssss.ReroutePrimitive",
	init() {
		// On graph configure, fire onGraphConfigured to create widgets
		const graphConfigure = LGraph.prototype.configure;
		LGraph.prototype.configure = function () {
			const r = graphConfigure.apply(this, arguments);
			for (const n of app.graph._nodes) {
				if (n.type === REROUTE_PRIMITIVE) {
					n.onGraphConfigured();
				}
			}

			return r;
		};

		// Hide this node as it is no longer supported
		const getNodeTypesCategories = LiteGraph.getNodeTypesCategories;
		LiteGraph.getNodeTypesCategories = function() {
			return getNodeTypesCategories.apply(this, arguments).filter(c => !c.startsWith("__hidden__"));
		}

		const graphToPrompt = app.graphToPrompt;
		app.graphToPrompt = async function () {
			const res = await graphToPrompt.apply(this, arguments);

			const multiOutputs = [];
			for (const nodeId in res.output) {
				const output = res.output[nodeId];
				if (output.class_type === MULTI_PRIMITIVE) {
					multiOutputs.push({ id: nodeId, inputs: output.inputs });
				}
			}

			function permute(outputs) {
				function generatePermutations(inputs, currentIndex, currentPermutation, result) {
					if (currentIndex === inputs.length) {
						result.push({ ...currentPermutation });
						return;
					}

					const input = inputs[currentIndex];

					for (const k in input) {
						currentPermutation[currentIndex] = input[k];
						generatePermutations(inputs, currentIndex + 1, currentPermutation, result);
					}
				}

				const inputs = outputs.map((output) => output.inputs);
				const result = [];
				const current = new Array(inputs.length);

				generatePermutations(inputs, 0, current, result);

				return outputs.map((output, index) => ({
					...output,
					inputs: result.reduce((p, permutation) => {
						const count = Object.keys(p).length;
						p["value" + (count || "")] = permutation[index];
						return p;
					}, {}),
				}));
			}

			const permutations = permute(multiOutputs);
			for (let i = 0; i < permutations.length; i++) {
				res.output[multiOutputs[i].id].inputs = permutations[i].inputs;
			}

			return res;
		};
	},
	async beforeRegisterNodeDef(nodeType, nodeData, app) {
		function addOutputHandler() {
			// Finds the first non reroute output node down the chain
			nodeType.prototype.getFirstReroutedOutput = function (slot) {
				if (nodeData.name === MULTI_PRIMITIVE) {
					slot = 0;
				}
				const links = this.outputs[slot].links;
				if (!links) return null;

				const search = [];
				for (const l of links) {
					const link = app.graph.links[l];
					if (!link) continue;

					const node = app.graph.getNodeById(link.target_id);
					if (node.type !== REROUTE_PRIMITIVE && node.type !== MULTI_PRIMITIVE) {
						return { node, link };
					}
					search.push({ node, link });
				}

				for (const { link, node } of search) {
					const r = node.getFirstReroutedOutput(link.target_slot);
					if (r) {
						return r;
					}
				}
			};
		}

		if (nodeData.name === REROUTE_PRIMITIVE) {
			const configure = nodeType.prototype.configure || LGraphNode.prototype.configure;
			const onConnectionsChange = nodeType.prototype.onConnectionsChange;
			const onAdded = nodeType.prototype.onAdded;

			nodeType.title_mode = LiteGraph.NO_TITLE;

			function hasAnyInput(node) {
				for (const input of node.inputs) {
					if (input.link) {
						return true;
					}
				}
				return false;
			}

			// Remove input text
			nodeType.prototype.onAdded = function () {
				onAdded?.apply(this, arguments);
				this.inputs[0].label = "";
				this.outputs[0].label = "value";
				this.setSize(this.computeSize());
			};

			// Restore any widgets
			nodeType.prototype.onGraphConfigured = function () {
				if (hasAnyInput(this)) return;

				const outputNode = this.getFirstReroutedOutput(0);
				if (outputNode) {
					this.checkPrimitiveWidget(outputNode);
				}
			};

			// Check if we need to create (or remove) a widget on the node
			nodeType.prototype.checkPrimitiveWidget = function ({ node, link }) {
				let widgetType = link.type;
				let targetLabel = widgetType;
				const input = node.inputs[link.target_slot];
				if (input.widget?.config?.[0] instanceof Array) {
					targetLabel = input.widget.name;
					widgetType = "COMBO";
				}

				if (widgetType in ComfyWidgets) {
					if (!this.widgets?.length) {
						let v;
						if (this.widgets_values?.length) {
							v = this.widgets_values[0];
						}
						let config = [link.type, {}];
						if (input.widget?.config) {
							config = input.widget.config;
						}
						const { widget } = ComfyWidgets[widgetType](this, "value", config, app);
						if (v !== undefined && (!this[LAST_TYPE] || this[LAST_TYPE] === widgetType)) {
							widget.value = v;
						}
						this[LAST_TYPE] = widgetType;
					}
				} else if (this.widgets) {
					this.widgets.length = 0;
				}

				return targetLabel;
			};

			// Finds all input nodes from the current reroute
			nodeType.prototype.getReroutedInputs = function (slot) {
				let nodes = [{ node: this }];
				let node = this;
				while (node?.type === REROUTE_PRIMITIVE) {
					const input = node.inputs[slot];
					if (input.link) {
						const link = app.graph.links[input.link];
						node = app.graph.getNodeById(link.origin_id);
						slot = link.origin_slot;
						nodes.push({
							node,
							link,
						});
					} else {
						node = null;
					}
				}

				return nodes;
			};

			addOutputHandler();

			// Update the type of all reroutes in a chain
			nodeType.prototype.changeRerouteType = function (slot, type, label) {
				const color = LGraphCanvas.link_type_colors[type];
				const output = this.outputs[slot];
				this.inputs[slot].label = " ";
				output.label = label || (type === "*" ? "value" : type);
				output.type = type;

				// Process all linked outputs
				for (const linkId of output.links || []) {
					const link = app.graph.links[linkId];
					if (!link) continue;
					link.color = color;
					const node = app.graph.getNodeById(link.target_id);
					if (node.changeRerouteType) {
						// Recursively update reroutes
						node.changeRerouteType(link.target_slot, type, label);
					} else {
						// Validate links to 'real' nodes
						const theirType = node.inputs[link.target_slot].type;
						if (theirType !== type && theirType !== "*") {
							node.disconnectInput(link.target_slot);
						}
					}
				}

				if (this.inputs[slot].link) {
					const link = app.graph.links[this.inputs[slot].link];
					if (link) link.color = color;
				}
			};

			// Override configure so we can flag that we are configuring to avoid link validation breaking
			let configuring = false;
			nodeType.prototype.configure = function () {
				configuring = true;
				const r = configure?.apply(this, arguments);
				configuring = false;

				return r;
			};

			Object.defineProperty(nodeType, "title_mode", {
				get() {
					return app.canvas.current_node?.widgets?.length ? LiteGraph.NORMAL_TITLE : LiteGraph.NO_TITLE;
				},
			});

			nodeType.prototype.onConnectionsChange = function (type, _, connected, link_info) {
				// If configuring treat everything as OK as links may not be set by litegraph yet
				if (configuring) return;

				const isInput = type === LiteGraph.INPUT;
				const slot = isInput ? link_info.target_slot : link_info.origin_slot;

				let targetLabel = null;
				let targetNode = null;
				let targetType = "*";
				let targetSlot = slot;

				const inputPath = this.getReroutedInputs(slot);
				const rootInput = inputPath[inputPath.length - 1];
				const outputNode = this.getFirstReroutedOutput(slot);
				if (rootInput.node.type === REROUTE_PRIMITIVE) {
					// Our input node is a reroute, so see if we have an output
					if (outputNode) {
						targetType = outputNode.link.type;
					} else if (rootInput.node.widgets) {
						rootInput.node.widgets.length = 0;
					}
					targetNode = rootInput;
					targetSlot = rootInput.link?.target_slot ?? slot;
				} else {
					// We have a real input, so we want to use that type
					targetNode = inputPath[inputPath.length - 2];
					targetType = rootInput.node.outputs[rootInput.link.origin_slot].type;
					targetSlot = rootInput.link.target_slot;
				}

				if (this.widgets && inputPath.length > 1) {
					// We have an input node so remove our widget
					this.widgets.length = 0;
				}

				if (outputNode && rootInput.node.checkPrimitiveWidget) {
					// We have an output, check if we need to create a widget
					targetLabel = rootInput.node.checkPrimitiveWidget(outputNode);
				}

				// Trigger an update of the type to all child nodes
				targetNode.node.changeRerouteType(targetSlot, targetType, targetLabel);

				return onConnectionsChange?.apply(this, arguments);
			};

			// When collapsed fix the size to just the dot
			const computeSize = nodeType.prototype.computeSize || LGraphNode.prototype.computeSize;
			nodeType.prototype.computeSize = function () {
				const r = computeSize.apply(this, arguments);
				if (this.flags?.collapsed) {
					return [1, 25];
				} else if (this.widgets?.length) {
					return r;
				} else {
					let w = 75;
					if (this.outputs?.[0]?.label) {
						const t = LiteGraph.NODE_TEXT_SIZE * this.outputs[0].label.length * 0.6 + 30;
						if (t > w) {
							w = t;
						}
					}
					return [w, r[1]];
				}
			};

			// On collapse shrink the node to just a dot
			const collapse = nodeType.prototype.collapse || LGraphNode.prototype.collapse;
			nodeType.prototype.collapse = function () {
				collapse.apply(this, arguments);
				this.setSize(this.computeSize());
				requestAnimationFrame(() => {
					this.setDirtyCanvas(true, true);
				});
			};

			// Shift the bounding area up slightly as LiteGraph miscalculates it for collapsed nodes
			nodeType.prototype.onBounding = function (area) {
				if (this.flags?.collapsed) {
					area[1] -= 15;
				}
			};
		} else if (nodeData.name === MULTI_PRIMITIVE) {
			addOutputHandler();
			nodeType.prototype.onConnectionsChange = function (type, _, connected, link_info) {
				for (let i = 0; i < this.inputs.length - 1; i++) {
					if (!this.inputs[i].link) {
						this.removeInput(i--);
					}
				}
				if (this.inputs[this.inputs.length - 1].link) {
					this.addInput("v" + +new Date(), this.inputs[0].type).label = "value";
				}
			};
		}
	},
});