// @ts-check

import { ComfyButton } from "../components/button.js";
import { prop, getStorageValue, setStorageValue } from "../../utils.js";
import { $el } from "../../ui.js";
import { api } from "../../api.js";
import { ComfyPopup } from "../components/popup.js";
import { createSpinner } from "../spinner.js";
import { ComfyWorkflow, trimJsonExt } from "../../workflows.js";
import { ComfyAsyncDialog } from "../components/asyncDialog.js";

export class ComfyWorkflowsMenu {
	#first = true;
	element = $el("div.comfyui-workflows");

	get open() {
		return this.popup.open;
	}

	set open(open) {
		this.popup.open = open;
	}

	/**
	 * @param {import("../../app.js").ComfyApp} app
	 */
	constructor(app) {
		this.app = app;
		this.#bindEvents();

		const classList = {
			"comfyui-workflows-button": true,
			"comfyui-button": true,
			unsaved: getStorageValue("Comfy.PreviousWorkflowUnsaved") === "true",
			running: false,
		};
		this.buttonProgress = $el("div.comfyui-workflows-button-progress");
		this.workflowLabel = $el("span.comfyui-workflows-label", "");
		this.button = new ComfyButton({
			content: $el("div.comfyui-workflows-button-inner", [$el("i.mdi.mdi-graph"), this.workflowLabel, this.buttonProgress]),
			icon: "chevron-down",
			classList,
		});

		this.element.append(this.button.element);

		this.popup = new ComfyPopup({ target: this.element, classList: "comfyui-workflows-popup" });
		this.content = new ComfyWorkflowsContent(app, this.popup);
		this.popup.children = [this.content.element];
		this.popup.addEventListener("change", () => {
			this.button.icon = "chevron-" + (this.popup.open ? "up" : "down");
		});
		this.button.withPopup(this.popup);

		this.unsaved = prop(this, "unsaved", classList.unsaved, (v) => {
			classList.unsaved = v;
			this.button.classList = classList;
			setStorageValue("Comfy.PreviousWorkflowUnsaved", v);
		});
	}

	#updateProgress = () => {
		const prompt = this.app.workflowManager.activePrompt;
		let percent = 0;
		if (this.app.workflowManager.activeWorkflow === prompt?.workflow) {
			const total = Object.values(prompt.nodes);
			const done = total.filter(Boolean);
			percent = (done.length / total.length) * 100;
		}
		this.buttonProgress.style.width = percent + "%";
	};

	#updateActive = () => {
		const active = this.app.workflowManager.activeWorkflow;
		this.button.tooltip = active.path;
		this.workflowLabel.textContent = active.name;
		this.unsaved = active.unsaved;

		if (this.#first) {
			this.#first = false;
			this.content.load();
		}

		this.#updateProgress();
	};

	#bindEvents() {
		this.app.workflowManager.addEventListener("changeWorkflow", this.#updateActive);
		this.app.workflowManager.addEventListener("rename", this.#updateActive);
		this.app.workflowManager.addEventListener("delete", this.#updateActive);

		this.app.workflowManager.addEventListener("save", () => {
			this.unsaved = this.app.workflowManager.activeWorkflow.unsaved;
		});

		this.app.workflowManager.addEventListener("execute", (e) => {
			this.#updateProgress();
		});

		api.addEventListener("graphChanged", () => {
			this.unsaved = true;
		});
	}

	#getMenuOptions(callback) {
		const menu = [];
		const directories = new Map();
		for (const workflow of this.app.workflowManager.workflows || []) {
			const path = workflow.pathParts;
			if (!path) continue;
			let parent = menu;
			let currentPath = "";
			for (let i = 0; i < path.length - 1; i++) {
				currentPath += "/" + path[i];
				let newParent = directories.get(currentPath);
				if (!newParent) {
					newParent = {
						title: path[i],
						has_submenu: true,
						submenu: {
							options: [],
						},
					};
					parent.push(newParent);
					newParent = newParent.submenu.options;
					directories.set(currentPath, newParent);
				}
				parent = newParent;
			}
			parent.push({
				title: trimJsonExt(path[path.length - 1]),
				callback: () => callback(workflow),
			});
		}
		return menu;
	}

	#getFavoriteMenuOptions(callback) {
		const menu = [];
		for (const workflow of this.app.workflowManager.workflows || []) {
			if (workflow.isFavorite) {
				menu.push({
					title: "⭐ " + workflow.name,
					callback: () => callback(workflow),
				});
			}
		}
		return menu;
	}

	/**
	 * @param {import("../../app.js").ComfyApp} app
	 */
	registerExtension(app) {
		const self = this;
		app.registerExtension({
			name: "Comfy.Workflows",
			async beforeRegisterNodeDef(nodeType) {
				function getImageWidget(node) {
					const inputs = { ...node.constructor?.nodeData?.input?.required, ...node.constructor?.nodeData?.input?.optional };
					for (const input in inputs) {
						if (inputs[input][0] === "IMAGEUPLOAD") {
							const imageWidget = node.widgets.find((w) => w.name === (inputs[input]?.[1]?.widget ?? "image"));
							if (imageWidget) return imageWidget;
						}
					}
				}

				function setWidgetImage(node, widget, img) {
					const url = new URL(img.src);
					const filename = url.searchParams.get("filename");
					const subfolder = url.searchParams.get("subfolder");
					const type = url.searchParams.get("type");
					const imageId = `${subfolder ? subfolder + "/" : ""}${filename} [${type}]`;
					widget.value = imageId;
					node.imgs = [img];
					app.graph.setDirtyCanvas(true, true);
				}

				/**
				 * @param {HTMLImageElement} img
				 * @param {ComfyWorkflow} workflow
				 */
				async function sendToWorkflow(img, workflow) {
					await workflow.load();
					let options = [];
					const nodes = app.graph.computeExecutionOrder(false);
					for (const node of nodes) {
						const widget = getImageWidget(node);
						if (widget == null) continue;

						if (node.title?.toLowerCase().includes("input")) {
							options = [{ widget, node }];
							break;
						} else {
							options.push({ widget, node });
						}
					}

					if (!options.length) {
						alert("No image nodes have been found in this workflow!");
						return;
					} else if (options.length > 1) {
						const dialog = new WidgetSelectionDialog(options);
						const res = await dialog.show(app);
						if (!res) return;
						options = [res];
					}

					setWidgetImage(options[0].node, options[0].widget, img);
				}

				const getExtraMenuOptions = nodeType.prototype["getExtraMenuOptions"];
				nodeType.prototype["getExtraMenuOptions"] = function (_, options) {
					const r = getExtraMenuOptions?.apply?.(this, arguments);

					if (app.ui.settings.getSettingValue("Comfy.UseNewMenu", false) === true) {
						const t = /** @type { {imageIndex?: number, overIndex?: number, imgs: string[]} } */ /** @type {any} */ (this);
						let img;
						if (t.imageIndex != null) {
							// An image is selected so select that
							img = t.imgs?.[t.imageIndex];
						} else if (t.overIndex != null) {
							// No image is selected but one is hovered
							img = t.img?.s[t.overIndex];
						}

						if (img) {
							let pos = options.findIndex((o) => o.content === "Save Image");
							if (pos === -1) {
								pos = 0;
							} else {
								pos++;
							}

							options.splice(pos, 0, {
								content: "Send to workflow",
								has_submenu: true,
								submenu: {
									options: [
										{
											callback: () => sendToWorkflow(img, app.workflowManager.activeWorkflow),
											title: "[Current workflow]",
										},
										...self.#getFavoriteMenuOptions(sendToWorkflow.bind(null, img)),
										null,
										...self.#getMenuOptions(sendToWorkflow.bind(null, img)),
									],
								},
							});
						}
					}

					return r;
				};
			},
		});
	}
}

export class ComfyWorkflowsContent {
	element = $el("div.comfyui-workflows-panel");
	treeState = {};
	treeFiles = {};
	/** @type { Map<ComfyWorkflow, WorkflowElement> } */
	openFiles = new Map();
	/** @type {WorkflowElement} */
	activeElement = null;

	/**
	 * @param {import("../../app.js").ComfyApp} app
	 * @param {ComfyPopup} popup
	 */
	constructor(app, popup) {
		this.app = app;
		this.popup = popup;
		this.actions = $el("div.comfyui-workflows-actions", [
			new ComfyButton({
				content: "Default",
				icon: "file-code",
				iconSize: 18,
				classList: "comfyui-button primary",
				tooltip: "Load default workflow",
				action: () => {
					popup.open = false;
					app.loadGraphData();
					app.resetView();
				},
			}).element,
			new ComfyButton({
				content: "Browse",
				icon: "folder",
				iconSize: 18,
				tooltip: "Browse for an image or exported workflow",
				action: () => {
					popup.open = false;
					app.ui.loadFile();
				},
			}).element,
			new ComfyButton({
				content: "Blank",
				icon: "plus-thick",
				iconSize: 18,
				tooltip: "Create a new blank workflow",
				action: () => {
					app.workflowManager.setWorkflow(null);
					app.clean();
					app.graph.clear();
					app.workflowManager.activeWorkflow.track();
					popup.open = false;
				},
			}).element,
		]);

		this.spinner = createSpinner();
		this.element.replaceChildren(this.actions, this.spinner);

		this.popup.addEventListener("open", () => this.load());
		this.popup.addEventListener("close", () => this.element.replaceChildren(this.actions, this.spinner));

		this.app.workflowManager.addEventListener("favorite", (e) => {
			const workflow = e["detail"];
			const button = this.treeFiles[workflow.path]?.primary;
			if (!button) return; // Can happen when a workflow is renamed
			button.icon = this.#getFavoriteIcon(workflow);
			button.overIcon = this.#getFavoriteOverIcon(workflow);
			this.updateFavorites();
		});

		for (const e of ["save", "open", "close", "changeWorkflow"]) {
			// TODO: dont be lazy and just update the specific element
			app.workflowManager.addEventListener(e, () => this.updateOpen());
		}
		this.app.workflowManager.addEventListener("rename", () => this.load());
		this.app.workflowManager.addEventListener("execute", (e) => this.#updateActive());
	}

	async load() {
		await this.app.workflowManager.loadWorkflows();
		this.updateTree();
		this.updateFavorites();
		this.updateOpen();
		this.element.replaceChildren(this.actions, this.openElement, this.favoritesElement, this.treeElement);
	}

	updateOpen() {
		const current = this.openElement;
		this.openFiles.clear();

		this.openElement = $el("div.comfyui-workflows-open", [
			$el("h3", "Open"),
			...this.app.workflowManager.openWorkflows.map((w) => {
				const wrapper = new WorkflowElement(this, w, {
					primary: { element: $el("i.mdi.mdi-18px.mdi-progress-pencil") },
					buttons: [
						this.#getRenameButton(w),
						new ComfyButton({
							icon: "close",
							iconSize: 18,
							classList: "comfyui-button comfyui-workflows-file-action",
							tooltip: "Close workflow",
							action: (e) => {
								e.stopImmediatePropagation();
								this.app.workflowManager.closeWorkflow(w);
							},
						}),
					],
				});
				if (w.unsaved) {
					wrapper.element.classList.add("unsaved");
				}
				if(w === this.app.workflowManager.activeWorkflow) {
					wrapper.element.classList.add("active");
				}

				this.openFiles.set(w, wrapper);
				return wrapper.element;
			}),
		]);

		this.#updateActive();
		current?.replaceWith(this.openElement);
	}

	updateFavorites() {
		const current = this.favoritesElement;
		const favorites = [...this.app.workflowManager.workflows.filter((w) => w.isFavorite)];

		this.favoritesElement = $el("div.comfyui-workflows-favorites", [
			$el("h3", "Favorites"),
			...favorites
				.map((w) => {
					return this.#getWorkflowElement(w).element;
				})
				.filter(Boolean),
		]);

		current?.replaceWith(this.favoritesElement);
	}

	filterTree() {
		if (!this.filterText) {
			this.treeRoot.classList.remove("filtered");
			// Unfilter whole tree
			for (const item of Object.values(this.treeFiles)) {
				item.element.parentElement.style.removeProperty("display");
				this.showTreeParents(item.element.parentElement);
			}
			return;
		}
		this.treeRoot.classList.add("filtered");
		const searchTerms = this.filterText.toLocaleLowerCase().split(" ");
		for (const item of Object.values(this.treeFiles)) {
			const parts = item.workflow.pathParts;
			let termIndex = 0;
			let valid = false;
			for (const part of parts) {
				let currentIndex = 0;
				do {
					currentIndex = part.indexOf(searchTerms[termIndex], currentIndex);
					if (currentIndex > -1) currentIndex += searchTerms[termIndex].length;
				} while (currentIndex !== -1 && ++termIndex < searchTerms.length);

				if (termIndex >= searchTerms.length) {
					valid = true;
					break;
				}
			}
			if (valid) {
				item.element.parentElement.style.removeProperty("display");
				this.showTreeParents(item.element.parentElement);
			} else {
				item.element.parentElement.style.display = "none";
				this.hideTreeParents(item.element.parentElement);
			}
		}
	}

	hideTreeParents(element) {
		// Hide all parents if no children are visible
		if (element.parentElement?.classList.contains("comfyui-workflows-tree") === false) {
			for (let i = 1; i < element.parentElement.children.length; i++) {
				const c = element.parentElement.children[i];
				if (c.style.display !== "none") {
					return;
				}
			}
			element.parentElement.style.display = "none";
			this.hideTreeParents(element.parentElement);
		}
	}

	showTreeParents(element) {
		if (element.parentElement?.classList.contains("comfyui-workflows-tree") === false) {
			element.parentElement.style.removeProperty("display");
			this.showTreeParents(element.parentElement);
		}
	}

	updateTree() {
		const current = this.treeElement;
		const nodes = {};
		let typingTimeout;

		this.treeFiles = {};
		this.treeRoot = $el("ul.comfyui-workflows-tree");
		this.treeElement = $el("section", [
			$el("header", [
				$el("h3", "Browse"),
				$el("div.comfy-ui-workflows-search", [
					$el("i.mdi.mdi-18px.mdi-magnify"),
					$el("input", {
						placeholder: "Search",
						value: this.filterText ?? "",
						oninput: (e) => {
							this.filterText = e.target["value"]?.trim();
							clearTimeout(typingTimeout);
							typingTimeout = setTimeout(() => this.filterTree(), 250);
						},
					}),
				]),
			]),
			this.treeRoot,
		]);

		for (const workflow of this.app.workflowManager.workflows) {
			if (!workflow.pathParts) continue;

			let currentPath = "";
			let currentRoot = this.treeRoot;

			for (let i = 0; i < workflow.pathParts.length; i++) {
				currentPath += (currentPath ? "\\" : "") + workflow.pathParts[i];
				const parentNode = nodes[currentPath] ?? this.#createNode(currentPath, workflow, i, currentRoot);

				nodes[currentPath] = parentNode;
				currentRoot = parentNode;
			}
		}

		current?.replaceWith(this.treeElement);
		this.filterTree();
	}

	#expandNode(el, workflow, thisPath, i) {
		const expanded = !el.classList.toggle("closed");
		if (expanded) {
			let c = "";
			for (let j = 0; j <= i; j++) {
				c += (c ? "\\" : "") + workflow.pathParts[j];
				this.treeState[c] = true;
			}
		} else {
			let c = thisPath;
			for (let j = i + 1; j < workflow.pathParts.length; j++) {
				c += (c ? "\\" : "") + workflow.pathParts[j];
				delete this.treeState[c];
			}
			delete this.treeState[thisPath];
		}
	}

	#updateActive() {
		this.#removeActive();

		const active = this.app.workflowManager.activePrompt;
		if (!active?.workflow) return;

		const open = this.openFiles.get(active.workflow);
		if (!open) return;

		this.activeElement = open;

		const total = Object.values(active.nodes);
		const done = total.filter(Boolean);
		const percent = done.length / total.length;
		open.element.classList.add("running");
		open.element.style.setProperty("--progress", percent * 100 + "%");
		open.primary.element.classList.remove("mdi-progress-pencil");
		open.primary.element.classList.add("mdi-play");
	}

	#removeActive() {
		if (!this.activeElement) return;
		this.activeElement.element.classList.remove("running");
		this.activeElement.element.style.removeProperty("--progress");
		this.activeElement.primary.element.classList.add("mdi-progress-pencil");
		this.activeElement.primary.element.classList.remove("mdi-play");
	}

	/** @param {ComfyWorkflow} workflow */
	#getFavoriteIcon(workflow) {
		return workflow.isFavorite ? "star" : "file-outline";
	}

	/** @param {ComfyWorkflow} workflow */
	#getFavoriteOverIcon(workflow) {
		return workflow.isFavorite ? "star-off" : "star-outline";
	}

	/** @param {ComfyWorkflow} workflow */
	#getFavoriteTooltip(workflow) {
		return workflow.isFavorite ? "Remove this workflow from your favorites" : "Add this workflow to your favorites";
	}

	/** @param {ComfyWorkflow} workflow */
	#getFavoriteButton(workflow, primary) {
		return new ComfyButton({
			icon: this.#getFavoriteIcon(workflow),
			overIcon: this.#getFavoriteOverIcon(workflow),
			iconSize: 18,
			classList: "comfyui-button comfyui-workflows-file-action-favorite" + (primary ? " comfyui-workflows-file-action-primary" : ""),
			tooltip: this.#getFavoriteTooltip(workflow),
			action: (e) => {
				e.stopImmediatePropagation();
				workflow.favorite(!workflow.isFavorite);
			},
		});
	}

	/** @param {ComfyWorkflow} workflow */
	#getDeleteButton(workflow) {
		const deleteButton = new ComfyButton({
			icon: "delete",
			tooltip: "Delete this workflow",
			classList: "comfyui-button comfyui-workflows-file-action",
			iconSize: 18,
			action: async (e, btn) => {
				e.stopImmediatePropagation();

				if (btn.icon === "delete-empty") {
					btn.enabled = false;
					await workflow.delete();
					await this.load();
				} else {
					btn.icon = "delete-empty";
					btn.element.style.background = "red";
				}
			},
		});
		deleteButton.element.addEventListener("mouseleave", () => {
			deleteButton.icon = "delete";
			deleteButton.element.style.removeProperty("background");
		});
		return deleteButton;
	}

	/** @param {ComfyWorkflow} workflow */
	#getInsertButton(workflow) {
		return new ComfyButton({
			icon: "file-move-outline",
			iconSize: 18,
			tooltip: "Insert this workflow into the current workflow",
			classList: "comfyui-button comfyui-workflows-file-action",
			action: (e) => {
				if (!this.app.shiftDown) {
					this.popup.open = false;
				}
				e.stopImmediatePropagation();
				if (!this.app.shiftDown) {
					this.popup.open = false;
				}
				workflow.insert();
			},
		});
	}

	/** @param {ComfyWorkflow} workflow */
	#getRenameButton(workflow) {
		return new ComfyButton({
			icon: "pencil",
			tooltip: workflow.path ? "Rename this workflow" : "This workflow can't be renamed as it hasn't been saved.",
			classList: "comfyui-button comfyui-workflows-file-action",
			iconSize: 18,
			enabled: !!workflow.path,
			action: async (e) => {
				e.stopImmediatePropagation();
				const newName = prompt("Enter new name", workflow.path);
				if (newName) {
					await workflow.rename(newName);
				}
			},
		});
	}

	/** @param {ComfyWorkflow} workflow */
	#getWorkflowElement(workflow) {
		return new WorkflowElement(this, workflow, {
			primary: this.#getFavoriteButton(workflow, true),
			buttons: [this.#getInsertButton(workflow), this.#getRenameButton(workflow), this.#getDeleteButton(workflow)],
		});
	}

	/** @param {ComfyWorkflow} workflow */
	#createLeafNode(workflow) {
		const fileNode = this.#getWorkflowElement(workflow);
		this.treeFiles[workflow.path] = fileNode;
		return fileNode;
	}

	#createNode(currentPath, workflow, i, currentRoot) {
		const part = workflow.pathParts[i];

		const parentNode = $el("ul" + (this.treeState[currentPath] ? "" : ".closed"), {
			$: (el) => {
				el.onclick = (e) => {
					this.#expandNode(el, workflow, currentPath, i);
					e.stopImmediatePropagation();
				};
			},
		});
		currentRoot.append(parentNode);

		// Create a node for the current part and an inner UL for its children if it isnt a leaf node
		const leaf = i === workflow.pathParts.length - 1;
		let nodeElement;
		if (leaf) {
			nodeElement = this.#createLeafNode(workflow).element;
		} else {
			nodeElement = $el("li", [$el("i.mdi.mdi-18px.mdi-folder"), $el("span", part)]);
		}
		parentNode.append(nodeElement);
		return parentNode;
	}
}

class WorkflowElement {
	/**
	 * @param { ComfyWorkflowsContent } parent
	 * @param { ComfyWorkflow } workflow
	 */
	constructor(parent, workflow, { tagName = "li", primary, buttons }) {
		this.parent = parent;
		this.workflow = workflow;
		this.primary = primary;
		this.buttons = buttons;

		this.element = $el(
			tagName + ".comfyui-workflows-tree-file",
			{
				onclick: () => {
					workflow.load();
					this.parent.popup.open = false;
				},
				title: this.workflow.path,
			},
			[this.primary?.element, $el("span", workflow.name), ...buttons.map((b) => b.element)]
		);
	}
}

class WidgetSelectionDialog extends ComfyAsyncDialog {
	#options;

	/**
	 * @param {Array<{widget: {name: string}, node: {pos: [number, number], title: string, id: string, type: string}}>} options
	 */
	constructor(options) {
		super();
		this.#options = options;
	}

	show(app) {
		this.element.classList.add("comfy-widget-selection-dialog");
		return super.show(
			$el("div", [
				$el("h2", "Select image target"),
				$el(
					"p",
					"This workflow has multiple image loader nodes, you can rename a node to include 'input' in the title for it to be automatically selected, or select one below."
				),
				$el(
					"section",
					this.#options.map((opt) => {
						return $el("div.comfy-widget-selection-item", [
							$el("span", { dataset: { id: opt.node.id } }, `${opt.node.title ?? opt.node.type} ${opt.widget.name}`),
							$el(
								"button.comfyui-button",
								{
									onclick: () => {
										app.canvas.ds.offset[0] = -opt.node.pos[0] + 50;
										app.canvas.ds.offset[1] = -opt.node.pos[1] + 50;
										app.canvas.selectNode(opt.node);
										app.graph.setDirtyCanvas(true, true);
									},
								},
								"Show"
							),
							$el(
								"button.comfyui-button.primary",
								{
									onclick: () => {
										this.close(opt);
									},
								},
								"Select"
							),
						]);
					})
				),
			])
		);
	}
}