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

// Adds lock/unlock menu item for nodes + groups to prevent moving / resizing them

const LOCKED = Symbol();

function lockArray(arr, isLocked) {
	const v = [];

	for (let i = 0; i < 2; i++) {
		v[i] = arr[i];

		Object.defineProperty(arr, i, {
			get() {
				return v[i];
			},
			set(value) {
				if (!isLocked()) {
					v[i] = value;
				}
			},
		});
	}
}

app.registerExtension({
	name: "pysssss.Locking",
	init() {
		function lockGroup(node) {
			node[LOCKED] = true;
		}

		// Add the locked flag to serialization
		const serialize = LGraphGroup.prototype.serialize;
		LGraphGroup.prototype.serialize = function () {
			const o = serialize.apply(this, arguments);
			o.locked = !!this[LOCKED];
			return o;
		};

		// On initial configure lock group if required
		const configure = LGraphGroup.prototype.configure;
		LGraphGroup.prototype.configure = function (o) {
			configure.apply(this, arguments);
			if (o.locked) {
				lockGroup(this);
			}
		};

		// Allow click through locked groups
		const getGroupOnPos = LGraph.prototype.getGroupOnPos;
		LGraph.prototype.getGroupOnPos = function () {
			const r = getGroupOnPos.apply(this, arguments);
			if (r && r[LOCKED] && !new Error().stack.includes("processContextMenu")) return null;
			return r;
		};

		// Add menu options for lock/unlock
		const getGroupMenuOptions = LGraphCanvas.prototype.getGroupMenuOptions;
		LGraphCanvas.prototype.getGroupMenuOptions = function (node) {
			const opts = getGroupMenuOptions.apply(this, arguments);

			opts.unshift(
				node[LOCKED]
					? {
							content: "Unlock",
							callback: () => {
								delete node[LOCKED];
							},
					  }
					: {
							content: "Lock",
							callback: () => lockGroup(node),
					  },
				null
			);

			return opts;
		};
	},
	setup() {
		const drawNodeShape = LGraphCanvas.prototype.drawNodeShape;
		LGraphCanvas.prototype.drawNodeShape = function (node, ctx, size, fgcolor, bgcolor, selected, mouse_over) {
			const res = drawNodeShape.apply(this, arguments);

			if (node[LOCKED]) {
				ctx.fillText("🔒", node.getBounding()[2] - 20, -10);
			}

			return res;
		};
	},
	async beforeRegisterNodeDef(nodeType) {
		const nodesArray = (nodes) => {
			if (nodes) {
				if (nodes instanceof Array) {
					return nodes;
				}
				return [nodes];
			}
			return Object.values(app.canvas.selected_nodes);
		};
		function unlockNode(nodes) {
			nodes = nodesArray(nodes);
			for (const node of nodes) {
				delete node[LOCKED];
			}
			app.graph.setDirtyCanvas(true, false);
		}
		function lockNode(nodes) {
			nodes = nodesArray(nodes);
			for (const node of nodes) {
				if (node[LOCKED]) continue;

				node[LOCKED] = true;
				// Same hack as above
				lockArray(node.pos, () => !!node[LOCKED]);

				// Size is set by both replacing the value and setting individual values
				// So define a new property that can prevent reassignment
				const sz = [node.size[0], node.size[1]];
				Object.defineProperty(node, "size", {
					get() {
						return sz;
					},
					set(value) {
						if (!node[LOCKED]) {
							sz[0] = value[0];
							sz[1] = value[1];
						}
					},
				});
				// And then lock each element if required
				lockArray(sz, () => !!node[LOCKED]);
			}

			app.graph.setDirtyCanvas(true, false);
		}

		// Add menu options for lock/unlock
		const getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
		nodeType.prototype.getExtraMenuOptions = function (_, options) {
			const r = getExtraMenuOptions ? getExtraMenuOptions.apply(this, arguments) : undefined;

			options.splice(
				options.findIndex((o) => o?.content === "Properties") + 1,
				0,
				null,
				this[LOCKED]
					? {
							content: "Unlock",
							callback: () => {
								unlockNode();
							},
					  }
					: {
							content: "Lock",
							callback: () => lockNode(),
					  }
			);

			return r;
		};

		// Add the locked flag to serialization
		const onSerialize = nodeType.prototype.onSerialize;
		nodeType.prototype.onSerialize = function (o) {
			if (onSerialize) {
				onSerialize.apply(this, arguments);
			}
			o.locked = this[LOCKED];
		};

		// On initial configure lock node if required
		const onConfigure = nodeType.prototype.onConfigure;
		nodeType.prototype.onConfigure = function (o) {
			if (onConfigure) {
				onConfigure.apply(this, arguments);
			}
			if (o.locked) {
				lockNode(this);
			}
		};
	},
});