import { app } from "../../../scripts/app.js";
import { $el } from "../../../scripts/ui.js";

let setting, guide_setting, guide_config;
const id = "pysssss.SnapToGrid";
const guide_id = id + ".Guide";
const guide_config_default = {
	lines: {
		enabled: false,
		fillStyle: "rgba(255, 0, 0, 0.5)",
	},
	block: {
		enabled: false,
		fillStyle: "rgba(0, 0, 255, 0.5)",
	},
}

/** Wraps the provided function call to set/reset shiftDown when setting is enabled. */
function wrapCallInSettingCheck(fn) {
	if (setting?.value) {
		const shift = app.shiftDown;
		app.shiftDown = true;
		const r = fn();
		app.shiftDown = shift;
		return r;
	}
	return fn();
}

const ext = {
	name: id,
	init() {
		if (localStorage.getItem(guide_id) === null) {
			localStorage.setItem(guide_id, JSON.stringify(guide_config_default));
		}
		guide_config = JSON.parse(localStorage.getItem(guide_id));

		setting = app.ui.settings.addSetting({
			id,
			name: "🐍 Always snap to grid",
			defaultValue: false,
			type: "boolean",
			onChange(value) {
				app.canvas.align_to_grid = value;
			},
		});

		guide_setting = app.ui.settings.addSetting({
			id: id + ".Guide",
			name: "🐍 Display drag-and-drop guides",
			type: (name, setter, value) => {
				return $el("tr", [
					$el("td", [
						$el("label", {
							for: id.replaceAll(".", "-"),
							textContent: name,
						}),
					]),
					$el("td", [
						$el(
							"label",
							{
								textContent: "Lines: ",
								style: {
									display: "inline-block",
								},
							},
							[
								$el("input", {
									id: id.replaceAll(".", "-") + "-line-text",
									type: "text",
									value: guide_config.lines.fillStyle,
									onchange: (event) => {
										guide_config.lines.fillStyle = event.target.value;
										localStorage.setItem(guide_id, JSON.stringify(guide_config));
									}
								}),
								$el("input", {
									id: id.replaceAll(".", "-") + "-line-checkbox",
									type: "checkbox",
									checked: guide_config.lines.enabled,
									onchange: (event) => {
										guide_config.lines.enabled = !!event.target.checked;
										localStorage.setItem(guide_id, JSON.stringify(guide_config));
									},
								}),
							]
						),
						$el(
							"label",
							{
								textContent: "Block: ",
								style: {
									display: "inline-block",
								},
							},
							[
								$el("input", {
									id: id.replaceAll(".", "-") + "-block-text",
									type: "text",
									value: guide_config.block.fillStyle,
									onchange: (event) => {
										guide_config.block.fillStyle = event.target.value;
										localStorage.setItem(guide_id, JSON.stringify(guide_config));
									}
								}),
								$el("input", {
									id: id.replaceAll(".", "-") + '-block-checkbox',
									type: "checkbox",
									checked: guide_config.block.enabled,
									onchange: (event) => {
										guide_config.block.enabled = !!event.target.checked;
										localStorage.setItem(guide_id, JSON.stringify(guide_config));
									},
								}),
							]
						),
					]),
				]);
			}
		});

		// We need to register our hooks after the core snap to grid extension runs
		// Do this from the graph configure function so we still get onNodeAdded calls
		const configure = LGraph.prototype.configure;
		LGraph.prototype.configure = function () {
			// Override drawNode to draw the drop position
			const drawNode = LGraphCanvas.prototype.drawNode;
			LGraphCanvas.prototype.drawNode = function () {
				wrapCallInSettingCheck(() => drawNode.apply(this, arguments));
			};

			// Override node added to add a resize handler to force grid alignment
			const onNodeAdded = app.graph.onNodeAdded;
			app.graph.onNodeAdded = function (node) {
				const r = onNodeAdded?.apply(this, arguments);
				const onResize = node.onResize;
				node.onResize = function () {
					wrapCallInSettingCheck(() => onResize?.apply(this, arguments));
				};
				return r;
			};


			const groupMove = LGraphGroup.prototype.move;
			LGraphGroup.prototype.move = function(deltax, deltay, ignore_nodes) {
				wrapCallInSettingCheck(() => groupMove.apply(this, arguments));
			}

			const canvasDrawGroups = LGraphCanvas.prototype.drawGroups;
			LGraphCanvas.prototype.drawGroups = function (canvas, ctx) {
				wrapCallInSettingCheck(() => canvasDrawGroups.apply(this, arguments));
			}

			const canvasOnGroupAdd = LGraphCanvas.onGroupAdd;
			LGraphCanvas.onGroupAdd = function() {
				wrapCallInSettingCheck(() => canvasOnGroupAdd.apply(this, arguments));
			}

			return configure.apply(this, arguments);
		};

		// Override drag-and-drop behavior to show orthogonal guide lines around selected node(s) and preview of where the node(s) will be placed
		const origDrawNode = LGraphCanvas.prototype.drawNode
		LGraphCanvas.prototype.drawNode = function (node, ctx) {
			const enabled = guide_config.lines.enabled || guide_config.block.enabled;
			if (enabled && app.shiftDown && this.node_dragged && node.id in this.selected_nodes) {
				// discretize the canvas into grid
				let x = LiteGraph.CANVAS_GRID_SIZE * Math.round(node.pos[0] / LiteGraph.CANVAS_GRID_SIZE);
				let y = LiteGraph.CANVAS_GRID_SIZE * Math.round(node.pos[1] / LiteGraph.CANVAS_GRID_SIZE);

				// calculate the width and height of the node
				// (also need to shift the y position of the node, depending on whether the title is visible)
				x -= node.pos[0];
				y -= node.pos[1];
				let w, h;
				if (node.flags.collapsed) {
					w = node._collapsed_width;
					h = LiteGraph.NODE_TITLE_HEIGHT;
					y -= LiteGraph.NODE_TITLE_HEIGHT;
				} else {
					w = node.size[0];
					h = node.size[1];
					let titleMode = node.constructor.title_mode;
					if (titleMode !== LiteGraph.TRANSPARENT_TITLE && titleMode !== LiteGraph.NO_TITLE) {
						h += LiteGraph.NODE_TITLE_HEIGHT;
						y -= LiteGraph.NODE_TITLE_HEIGHT;
					}
				}

				// save the original fill style
				const f = ctx.fillStyle;

				// draw preview for drag-and-drop (rectangle to show where the node will be placed)
				if (guide_config.block.enabled) {
					ctx.fillStyle = guide_config.block.fillStyle;
					ctx.fillRect(x, y, w, h);
				}

				// add guide lines around node (arbitrarily long enough to span most workflows)
				if (guide_config.lines.enabled) {
					const xd = 10000;
					const yd = 10000;
					const thickness = 3;
					ctx.fillStyle = guide_config.lines.fillStyle;
					ctx.fillRect(x - xd, y, 2*xd, thickness);
					ctx.fillRect(x, y - yd, thickness, 2*yd);
					ctx.fillRect(x - xd, y + h, 2*xd, thickness);
					ctx.fillRect(x + w, y - yd, thickness, 2*yd);
				}

				// restore the original fill style
				ctx.fillStyle = f;
			}

			return origDrawNode.apply(this, arguments);
		};
	},
};

app.registerExtension(ext);