import { app } from "../../scripts/app.js";
import { ComfyDialog, $el } from "../../scripts/ui.js";
import { ComfyApp } from "../../scripts/app.js";
import { api } from "../../scripts/api.js"
import { ClipspaceDialog } from "./clipspace.js";

// Helper function to convert a data URL to a Blob object
function dataURLToBlob(dataURL) {
	const parts = dataURL.split(';base64,');
	const contentType = parts[0].split(':')[1];
	const byteString = atob(parts[1]);
	const arrayBuffer = new ArrayBuffer(byteString.length);
	const uint8Array = new Uint8Array(arrayBuffer);
	for (let i = 0; i < byteString.length; i++) {
		uint8Array[i] = byteString.charCodeAt(i);
	}
	return new Blob([arrayBuffer], { type: contentType });
}

function loadedImageToBlob(image) {
	const canvas = document.createElement('canvas');

	canvas.width = image.width;
	canvas.height = image.height;

	const ctx = canvas.getContext('2d');

	ctx.drawImage(image, 0, 0);

	const dataURL = canvas.toDataURL('image/png', 1);
	const blob = dataURLToBlob(dataURL);

	return blob;
}

function loadImage(imagePath) {
	return new Promise((resolve, reject) => {
		const image = new Image();

		image.onload = function() {
			resolve(image);
		};

		image.src = imagePath;
	});
}

async function uploadMask(filepath, formData) {
	await api.fetchApi('/upload/mask', {
		method: 'POST',
		body: formData
	}).then(response => {}).catch(error => {
		console.error('Error:', error);
	});

	ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']] = new Image();
	ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src = api.apiURL("/view?" + new URLSearchParams(filepath).toString() + app.getPreviewFormatParam() + app.getRandParam());

	if(ComfyApp.clipspace.images)
		ComfyApp.clipspace.images[ComfyApp.clipspace['selectedIndex']] = filepath;

	ClipspaceDialog.invalidatePreview();
}

function prepare_mask(image, maskCanvas, maskCtx, maskColor) {
	// paste mask data into alpha channel
	maskCtx.drawImage(image, 0, 0, maskCanvas.width, maskCanvas.height);
	const maskData = maskCtx.getImageData(0, 0, maskCanvas.width, maskCanvas.height);

	// invert mask
	for (let i = 0; i < maskData.data.length; i += 4) {
		if(maskData.data[i+3] == 255)
			maskData.data[i+3] = 0;
		else
			maskData.data[i+3] = 255;

		maskData.data[i] = maskColor.r;
		maskData.data[i+1] = maskColor.g;
		maskData.data[i+2] = maskColor.b;
	}

	maskCtx.globalCompositeOperation = 'source-over';
	maskCtx.putImageData(maskData, 0, 0);
}

class MaskEditorDialog extends ComfyDialog {
	static instance = null;

	static getInstance() {
		if(!MaskEditorDialog.instance) {
			MaskEditorDialog.instance = new MaskEditorDialog(app);
		}

		return MaskEditorDialog.instance;
	}

	is_layout_created =  false;

	constructor() {
		super();
		this.element = $el("div.comfy-modal", { parent: document.body }, 
			[ $el("div.comfy-modal-content", 
				[...this.createButtons()]),
			]);
	}

	createButtons() {
		return [];
	}

	createButton(name, callback) {
		var button = document.createElement("button");
		button.style.pointerEvents = "auto";
		button.innerText = name;
		button.addEventListener("click", callback);
		return button;
	}

	createLeftButton(name, callback) {
		var button = this.createButton(name, callback);
		button.style.cssFloat = "left";
		button.style.marginRight = "4px";
		return button;
	}

	createRightButton(name, callback) {
		var button = this.createButton(name, callback);
		button.style.cssFloat = "right";
		button.style.marginLeft = "4px";
		return button;
	}

	createLeftSlider(self, name, callback) {
		const divElement = document.createElement('div');
		divElement.id = "maskeditor-slider";
		divElement.style.cssFloat = "left";
		divElement.style.fontFamily = "sans-serif";
		divElement.style.marginRight = "4px";
		divElement.style.color = "var(--input-text)";
		divElement.style.backgroundColor = "var(--comfy-input-bg)";
		divElement.style.borderRadius = "8px";
		divElement.style.borderColor = "var(--border-color)";
		divElement.style.borderStyle = "solid";
		divElement.style.fontSize = "15px";
		divElement.style.height = "21px";
		divElement.style.padding = "1px 6px";
		divElement.style.display = "flex";
		divElement.style.position = "relative";
		divElement.style.top = "2px";
		divElement.style.pointerEvents = "auto";
		self.brush_slider_input = document.createElement('input');
		self.brush_slider_input.setAttribute('type', 'range');
		self.brush_slider_input.setAttribute('min', '1');
		self.brush_slider_input.setAttribute('max', '100');
		self.brush_slider_input.setAttribute('value', '10');
		const labelElement = document.createElement("label");
		labelElement.textContent = name;

		divElement.appendChild(labelElement);
		divElement.appendChild(self.brush_slider_input);

		self.brush_slider_input.addEventListener("change", callback);

		return divElement;
	}

	createOpacitySlider(self, name, callback) {
		const divElement = document.createElement('div');
		divElement.id = "maskeditor-opacity-slider";
		divElement.style.cssFloat = "left";
		divElement.style.fontFamily = "sans-serif";
		divElement.style.marginRight = "4px";
		divElement.style.color = "var(--input-text)";
		divElement.style.backgroundColor = "var(--comfy-input-bg)";
		divElement.style.borderRadius = "8px";
		divElement.style.borderColor = "var(--border-color)";
		divElement.style.borderStyle = "solid";
		divElement.style.fontSize = "15px";
		divElement.style.height = "21px";
		divElement.style.padding = "1px 6px";
		divElement.style.display = "flex";
		divElement.style.position = "relative";
		divElement.style.top = "2px";
		divElement.style.pointerEvents = "auto";
		self.opacity_slider_input = document.createElement('input');
		self.opacity_slider_input.setAttribute('type', 'range');
		self.opacity_slider_input.setAttribute('min', '0.1');
		self.opacity_slider_input.setAttribute('max', '1.0');
		self.opacity_slider_input.setAttribute('step', '0.01')
		self.opacity_slider_input.setAttribute('value', '0.7');
		const labelElement = document.createElement("label");
		labelElement.textContent = name;

		divElement.appendChild(labelElement);
		divElement.appendChild(self.opacity_slider_input);

		self.opacity_slider_input.addEventListener("input", callback);

		return divElement;
	}

	setlayout(imgCanvas, maskCanvas) {
		const self = this;

		// If it is specified as relative, using it only as a hidden placeholder for padding is recommended
		// to prevent anomalies where it exceeds a certain size and goes outside of the window.
		var bottom_panel = document.createElement("div");
		bottom_panel.style.position = "absolute";
		bottom_panel.style.bottom = "0px";
		bottom_panel.style.left = "20px";
		bottom_panel.style.right = "20px";
		bottom_panel.style.height = "50px";
		bottom_panel.style.pointerEvents = "none";

		var brush = document.createElement("div");
		brush.id = "brush";
		brush.style.backgroundColor = "transparent";
		brush.style.outline = "1px dashed black";
		brush.style.boxShadow = "0 0 0 1px white";
		brush.style.borderRadius = "50%";
		brush.style.MozBorderRadius = "50%";
		brush.style.WebkitBorderRadius = "50%";
		brush.style.position = "absolute";
		brush.style.zIndex = 8889;
		brush.style.pointerEvents = "none";
		this.brush = brush;
		this.element.appendChild(imgCanvas);
		this.element.appendChild(maskCanvas);
		this.element.appendChild(bottom_panel);
		document.body.appendChild(brush);

		var clearButton = this.createLeftButton("Clear", () => {
			self.maskCtx.clearRect(0, 0, self.maskCanvas.width, self.maskCanvas.height);
		});
		
		this.brush_size_slider = this.createLeftSlider(self, "Thickness", (event) => {
			self.brush_size = event.target.value;
			self.updateBrushPreview(self, null, null);
		});

		this.brush_opacity_slider = this.createOpacitySlider(self, "Opacity", (event) => {
			self.brush_opacity = event.target.value;
			if (self.brush_color_mode !== "negative") {
			    self.maskCanvas.style.opacity = self.brush_opacity;
			}
		});

		this.colorButton = this.createLeftButton(this.getColorButtonText(), () => {
			if (self.brush_color_mode === "black") {
				self.brush_color_mode = "white";
			}
			else if (self.brush_color_mode === "white") {
				self.brush_color_mode = "negative";
			}
			else {
				self.brush_color_mode = "black";
			}

			self.updateWhenBrushColorModeChanged();
		});

		var cancelButton = this.createRightButton("Cancel", () => {
			document.removeEventListener("mouseup", MaskEditorDialog.handleMouseUp);
			document.removeEventListener("keydown", MaskEditorDialog.handleKeyDown);
			self.close();
		});

		this.saveButton = this.createRightButton("Save", () => {
			document.removeEventListener("mouseup", MaskEditorDialog.handleMouseUp);
			document.removeEventListener("keydown", MaskEditorDialog.handleKeyDown);
				self.save();
			});

		this.element.appendChild(imgCanvas);
		this.element.appendChild(maskCanvas);
		this.element.appendChild(bottom_panel);

		bottom_panel.appendChild(clearButton);
		bottom_panel.appendChild(this.saveButton);
		bottom_panel.appendChild(cancelButton);
		bottom_panel.appendChild(this.brush_size_slider);
		bottom_panel.appendChild(this.brush_opacity_slider);
		bottom_panel.appendChild(this.colorButton);

		imgCanvas.style.position = "absolute";
		maskCanvas.style.position = "absolute";

		imgCanvas.style.top = "200";
		imgCanvas.style.left = "0";

		maskCanvas.style.top = imgCanvas.style.top;
		maskCanvas.style.left = imgCanvas.style.left;

		const maskCanvasStyle = this.getMaskCanvasStyle();
		maskCanvas.style.mixBlendMode = maskCanvasStyle.mixBlendMode;
		maskCanvas.style.opacity = maskCanvasStyle.opacity;
	}

	async show() {
		this.zoom_ratio = 1.0;
		this.pan_x = 0;
		this.pan_y = 0;

		if(!this.is_layout_created) {
			// layout
			const imgCanvas = document.createElement('canvas');
			const maskCanvas = document.createElement('canvas');

			imgCanvas.id = "imageCanvas";
			maskCanvas.id = "maskCanvas";

			this.setlayout(imgCanvas, maskCanvas);

			// prepare content
			this.imgCanvas = imgCanvas;
			this.maskCanvas = maskCanvas;
			this.maskCtx = maskCanvas.getContext('2d', {willReadFrequently: true });

			this.setEventHandler(maskCanvas);

			this.is_layout_created = true;

			// replacement of onClose hook since close is not real close
			const self = this;
			const observer = new MutationObserver(function(mutations) {
			mutations.forEach(function(mutation) {
					if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
						if(self.last_display_style && self.last_display_style != 'none' && self.element.style.display == 'none') {
							document.removeEventListener("mouseup", MaskEditorDialog.handleMouseUp);
							self.brush.style.display = "none";
							ComfyApp.onClipspaceEditorClosed();
						}

						self.last_display_style = self.element.style.display;
					}
				});
			});

			const config = { attributes: true };
			observer.observe(this.element, config);
		}

		// The keydown event needs to be reconfigured when closing the dialog as it gets removed.
		document.addEventListener('keydown', MaskEditorDialog.handleKeyDown);

		if(ComfyApp.clipspace_return_node) {
			this.saveButton.innerText = "Save to node";
		}
		else {
			this.saveButton.innerText = "Save";
		}
		this.saveButton.disabled = false;

		this.element.style.display = "block";
		this.element.style.width = "85%";
		this.element.style.margin = "0 7.5%";
		this.element.style.height = "100vh";
		this.element.style.top = "50%";
		this.element.style.left = "42%";
		this.element.style.zIndex = 8888; // NOTE: alert dialog must be high priority.

		await this.setImages(this.imgCanvas);

		this.is_visible = true;
	}

	isOpened() {
		return this.element.style.display == "block";
	}

	invalidateCanvas(orig_image, mask_image) {
		this.imgCanvas.width = orig_image.width;
		this.imgCanvas.height = orig_image.height;

		this.maskCanvas.width = orig_image.width;
		this.maskCanvas.height = orig_image.height;

		let imgCtx = this.imgCanvas.getContext('2d', {willReadFrequently: true });
		let maskCtx = this.maskCanvas.getContext('2d', {willReadFrequently: true });

		imgCtx.drawImage(orig_image, 0, 0, orig_image.width, orig_image.height);
		prepare_mask(mask_image, this.maskCanvas, maskCtx, this.getMaskColor());
	}

	async setImages(imgCanvas) {
		let self = this;

		const imgCtx = imgCanvas.getContext('2d', {willReadFrequently: true });
		const maskCtx = this.maskCtx;
		const maskCanvas = this.maskCanvas;

		imgCtx.clearRect(0,0,this.imgCanvas.width,this.imgCanvas.height);
		maskCtx.clearRect(0,0,this.maskCanvas.width,this.maskCanvas.height);

		// image load
		const filepath = ComfyApp.clipspace.images;

		const alpha_url = new URL(ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src)
		alpha_url.searchParams.delete('channel');
		alpha_url.searchParams.delete('preview');
		alpha_url.searchParams.set('channel', 'a');
		let mask_image = await loadImage(alpha_url);

		// original image load
		const rgb_url = new URL(ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src);
		rgb_url.searchParams.delete('channel');
		rgb_url.searchParams.set('channel', 'rgb');
		this.image = new Image();
		this.image.onload = function() {
			maskCanvas.width = self.image.width;
			maskCanvas.height = self.image.height;

			self.invalidateCanvas(self.image, mask_image);
			self.initializeCanvasPanZoom();
		};
		this.image.src = rgb_url;
	}

	initializeCanvasPanZoom() {
		// set initialize
		let drawWidth = this.image.width;
		let drawHeight = this.image.height;

		let width = this.element.clientWidth;
		let height = this.element.clientHeight;

		if (this.image.width > width) {
			drawWidth = width;
			drawHeight = (drawWidth / this.image.width) * this.image.height;
		}

		if (drawHeight > height) {
			drawHeight = height;
			drawWidth = (drawHeight / this.image.height) * this.image.width;
		}

		this.zoom_ratio = drawWidth/this.image.width;

		const canvasX = (width - drawWidth) / 2;
		const canvasY = (height - drawHeight) / 2;
		this.pan_x = canvasX;
		this.pan_y = canvasY;

		this.invalidatePanZoom();
	}


	invalidatePanZoom() {
		let raw_width = this.image.width * this.zoom_ratio;
		let raw_height = this.image.height * this.zoom_ratio;

		if(this.pan_x + raw_width < 10) {
			this.pan_x = 10 - raw_width;
		}

		if(this.pan_y + raw_height < 10) {
			this.pan_y = 10 - raw_height;
		}

		let width = `${raw_width}px`;
		let height = `${raw_height}px`;

		let left = `${this.pan_x}px`;
		let top = `${this.pan_y}px`;

		this.maskCanvas.style.width = width;
		this.maskCanvas.style.height = height;
		this.maskCanvas.style.left = left;
		this.maskCanvas.style.top = top;

		this.imgCanvas.style.width = width;
		this.imgCanvas.style.height = height;
		this.imgCanvas.style.left = left;
		this.imgCanvas.style.top = top;
	}


	setEventHandler(maskCanvas) {
		const self = this;

		if(!this.handler_registered) {
			maskCanvas.addEventListener("contextmenu", (event) => {
				event.preventDefault();
			});

			this.element.addEventListener('wheel', (event) => this.handleWheelEvent(self,event));
			this.element.addEventListener('pointermove', (event) => this.pointMoveEvent(self,event));
			this.element.addEventListener('touchmove', (event) => this.pointMoveEvent(self,event));

			this.element.addEventListener('dragstart', (event) => {
				if(event.ctrlKey) {
					event.preventDefault();
				}
			});

			maskCanvas.addEventListener('pointerdown', (event) => this.handlePointerDown(self,event));
			maskCanvas.addEventListener('pointermove', (event) => this.draw_move(self,event));
			maskCanvas.addEventListener('touchmove', (event) => this.draw_move(self,event));
			maskCanvas.addEventListener('pointerover', (event) => { this.brush.style.display = "block"; });
			maskCanvas.addEventListener('pointerleave', (event) => { this.brush.style.display = "none"; });

			document.addEventListener('pointerup', MaskEditorDialog.handlePointerUp);

			this.handler_registered = true;
		}
	}

	getMaskCanvasStyle() {
		if (this.brush_color_mode === "negative") {
			return {
				mixBlendMode: "difference",
				opacity: "1",
			};
		}
		else {
			return {
				mixBlendMode: "initial",
				opacity: this.brush_opacity,
			};
		}
	}

	getMaskColor() {
		if (this.brush_color_mode === "black") {
			return { r: 0, g: 0, b: 0 };
		}
		if (this.brush_color_mode === "white") {
			return { r: 255, g: 255, b: 255 };
		}
		if (this.brush_color_mode === "negative") {
			// negative effect only works with white color
			return { r: 255, g: 255, b: 255 };
		}

		return { r: 0, g: 0, b: 0 };
	}

	getMaskFillStyle() {
		const maskColor = this.getMaskColor();

		return "rgb(" + maskColor.r + "," + maskColor.g + "," + maskColor.b + ")";
	}

	getColorButtonText() {
		let colorCaption = "unknown";

		if (this.brush_color_mode === "black") {
			colorCaption = "black";
		}
		else if (this.brush_color_mode === "white") {
			colorCaption = "white";
		}
		else if (this.brush_color_mode === "negative") {
			colorCaption = "negative";
		}

		return "Color: " + colorCaption;
	}

	updateWhenBrushColorModeChanged() {
		this.colorButton.innerText = this.getColorButtonText();

		// update mask canvas css styles

		const maskCanvasStyle = this.getMaskCanvasStyle();
		this.maskCanvas.style.mixBlendMode = maskCanvasStyle.mixBlendMode;
		this.maskCanvas.style.opacity = maskCanvasStyle.opacity;

		// update mask canvas rgb colors

		const maskColor = this.getMaskColor();

		const maskData = this.maskCtx.getImageData(0, 0, this.maskCanvas.width, this.maskCanvas.height);
		
		for (let i = 0; i < maskData.data.length; i += 4) {
			maskData.data[i] = maskColor.r;
			maskData.data[i+1] = maskColor.g;
			maskData.data[i+2] = maskColor.b;
		}
	
		this.maskCtx.putImageData(maskData, 0, 0);
	}

	brush_opacity = 0.7;
	brush_size = 10;
	brush_color_mode = "black";
	drawing_mode = false;
	lastx = -1;
	lasty = -1;
	lasttime = 0;

	static handleKeyDown(event) {
		const self = MaskEditorDialog.instance;
		if (event.key === ']') {
			self.brush_size = Math.min(self.brush_size+2, 100);
			self.brush_slider_input.value = self.brush_size;
		} else if (event.key === '[') {
			self.brush_size = Math.max(self.brush_size-2, 1);
			self.brush_slider_input.value = self.brush_size;
		} else if(event.key === 'Enter') {
			self.save();
		}

		self.updateBrushPreview(self);
	}

	static handlePointerUp(event) {
		event.preventDefault();

		this.mousedown_x = null;
		this.mousedown_y = null;

		MaskEditorDialog.instance.drawing_mode = false;
	}

	updateBrushPreview(self) {
		const brush = self.brush;

		var centerX = self.cursorX;
		var centerY = self.cursorY;

		brush.style.width = self.brush_size * 2 * this.zoom_ratio + "px";
		brush.style.height = self.brush_size * 2 * this.zoom_ratio + "px";
		brush.style.left = (centerX - self.brush_size * this.zoom_ratio) + "px";
		brush.style.top = (centerY - self.brush_size * this.zoom_ratio) + "px";
	}

	handleWheelEvent(self, event) {
		event.preventDefault();

		if(event.ctrlKey) {
			// zoom canvas
			if(event.deltaY < 0) {
				this.zoom_ratio = Math.min(10.0, this.zoom_ratio+0.2);
			}
			else {
				this.zoom_ratio = Math.max(0.2, this.zoom_ratio-0.2);
			}

			this.invalidatePanZoom();
		}
		else {
			// adjust brush size
			if(event.deltaY < 0)
				this.brush_size = Math.min(this.brush_size+2, 100);
			else
				this.brush_size = Math.max(this.brush_size-2, 1);

			this.brush_slider_input.value = this.brush_size;

			this.updateBrushPreview(this);
		}
	}

	pointMoveEvent(self, event) {
		this.cursorX = event.pageX;
		this.cursorY = event.pageY;

		self.updateBrushPreview(self);

		if(event.ctrlKey) {
			event.preventDefault();
			self.pan_move(self, event);
		}

		let left_button_down = window.TouchEvent && event instanceof TouchEvent || event.buttons == 1;

		if(event.shiftKey && left_button_down) {
			self.drawing_mode = false;

			const y = event.clientY;
			let delta = (self.zoom_lasty - y)*0.005;
			self.zoom_ratio = Math.max(Math.min(10.0, self.last_zoom_ratio - delta), 0.2);

			this.invalidatePanZoom();
			return;
		}
	}

	pan_move(self, event) {
		if(event.buttons == 1) {
			if(this.mousedown_x) {
				let deltaX = this.mousedown_x - event.clientX;
				let deltaY = this.mousedown_y - event.clientY;

				self.pan_x = this.mousedown_pan_x - deltaX;
				self.pan_y = this.mousedown_pan_y - deltaY;

				self.invalidatePanZoom();
			}
		}
	}

	draw_move(self, event) {
		if(event.ctrlKey || event.shiftKey) {
			return;
		}

		event.preventDefault();

		this.cursorX = event.pageX;
		this.cursorY = event.pageY;

		self.updateBrushPreview(self);

		let left_button_down = window.TouchEvent && event instanceof TouchEvent || event.buttons == 1;
		let right_button_down = [2, 5, 32].includes(event.buttons);

		if (!event.altKey && left_button_down) {
			var diff = performance.now() - self.lasttime;

			const maskRect = self.maskCanvas.getBoundingClientRect();

			var x = event.offsetX;
			var y = event.offsetY

			if(event.offsetX == null) {
				x = event.targetTouches[0].clientX - maskRect.left;
			}

			if(event.offsetY == null) {
				y = event.targetTouches[0].clientY - maskRect.top;
			}

			x /= self.zoom_ratio;
			y /= self.zoom_ratio;

			var brush_size = this.brush_size;
			if(event instanceof PointerEvent && event.pointerType == 'pen') {
				brush_size *= event.pressure;
				this.last_pressure = event.pressure;
			}
			else if(window.TouchEvent && event instanceof TouchEvent && diff < 20){
				// The firing interval of PointerEvents in Pen is unreliable, so it is supplemented by TouchEvents.
				brush_size *= this.last_pressure;
			}
			else {
				brush_size = this.brush_size;
			}

			if(diff > 20 && !this.drawing_mode)
				requestAnimationFrame(() => {
					self.maskCtx.beginPath();
					self.maskCtx.fillStyle = this.getMaskFillStyle();
					self.maskCtx.globalCompositeOperation = "source-over";
					self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false);
					self.maskCtx.fill();
					self.lastx = x;
					self.lasty = y;
				});
			else
				requestAnimationFrame(() => {
					self.maskCtx.beginPath();
					self.maskCtx.fillStyle = this.getMaskFillStyle();
					self.maskCtx.globalCompositeOperation = "source-over";

					var dx = x - self.lastx;
					var dy = y - self.lasty;

					var distance = Math.sqrt(dx * dx + dy * dy);
					var directionX = dx / distance;
					var directionY = dy / distance;

					for (var i = 0; i < distance; i+=5) {
						var px = self.lastx + (directionX * i);
						var py = self.lasty + (directionY * i);
						self.maskCtx.arc(px, py, brush_size, 0, Math.PI * 2, false);
						self.maskCtx.fill();
					}
					self.lastx = x;
					self.lasty = y;
				});

			self.lasttime = performance.now();
		}
		else if((event.altKey && left_button_down) || right_button_down) {
			const maskRect = self.maskCanvas.getBoundingClientRect();
			const x = (event.offsetX || event.targetTouches[0].clientX - maskRect.left) / self.zoom_ratio;
			const y = (event.offsetY || event.targetTouches[0].clientY - maskRect.top) / self.zoom_ratio;

			var brush_size = this.brush_size;
			if(event instanceof PointerEvent && event.pointerType == 'pen') {
				brush_size *= event.pressure;
				this.last_pressure = event.pressure;
			}
			else if(window.TouchEvent && event instanceof TouchEvent && diff < 20){
				brush_size *= this.last_pressure;
			}
			else {
				brush_size = this.brush_size;
			}

			if(diff > 20 && !drawing_mode) // cannot tracking drawing_mode for touch event
				requestAnimationFrame(() => {
					self.maskCtx.beginPath();
					self.maskCtx.globalCompositeOperation = "destination-out";
					self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false);
					self.maskCtx.fill();
					self.lastx = x;
					self.lasty = y;
				});
			else
				requestAnimationFrame(() => {
					self.maskCtx.beginPath();
					self.maskCtx.globalCompositeOperation = "destination-out";
					
					var dx = x - self.lastx;
					var dy = y - self.lasty;

					var distance = Math.sqrt(dx * dx + dy * dy);
					var directionX = dx / distance;
					var directionY = dy / distance;

					for (var i = 0; i < distance; i+=5) {
						var px = self.lastx + (directionX * i);
						var py = self.lasty + (directionY * i);
						self.maskCtx.arc(px, py, brush_size, 0, Math.PI * 2, false);
						self.maskCtx.fill();
					}
					self.lastx = x;
					self.lasty = y;
				});

				self.lasttime = performance.now();
		}
	}

	handlePointerDown(self, event) {
		if(event.ctrlKey) {
			if (event.buttons == 1) {
				this.mousedown_x = event.clientX;
				this.mousedown_y = event.clientY;

				this.mousedown_pan_x = this.pan_x;
				this.mousedown_pan_y = this.pan_y;
			}
			return;
		}

		var brush_size = this.brush_size;
		if(event instanceof PointerEvent && event.pointerType == 'pen') {
			brush_size *= event.pressure;
			this.last_pressure = event.pressure;
		}

		if ([0, 2, 5].includes(event.button)) {
			self.drawing_mode = true;

			event.preventDefault();

			if(event.shiftKey) {
				self.zoom_lasty = event.clientY;
				self.last_zoom_ratio = self.zoom_ratio;
				return;
			}

			const maskRect = self.maskCanvas.getBoundingClientRect();
			const x = (event.offsetX || event.targetTouches[0].clientX - maskRect.left) / self.zoom_ratio;
			const y = (event.offsetY || event.targetTouches[0].clientY - maskRect.top) / self.zoom_ratio;

			self.maskCtx.beginPath();
			if (!event.altKey && event.button == 0) {
				self.maskCtx.fillStyle = this.getMaskFillStyle();
				self.maskCtx.globalCompositeOperation = "source-over";
			} else {
				self.maskCtx.globalCompositeOperation = "destination-out";
			}
			self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false);
			self.maskCtx.fill();
			self.lastx = x;
			self.lasty = y;
			self.lasttime = performance.now();
		}
	}

	async save() {
		const backupCanvas = document.createElement('canvas');
		const backupCtx = backupCanvas.getContext('2d', {willReadFrequently:true});
		backupCanvas.width = this.image.width;
		backupCanvas.height = this.image.height;

		backupCtx.clearRect(0,0, backupCanvas.width, backupCanvas.height);
		backupCtx.drawImage(this.maskCanvas,
			0, 0, this.maskCanvas.width, this.maskCanvas.height,
			0, 0, backupCanvas.width, backupCanvas.height);

		// paste mask data into alpha channel
		const backupData = backupCtx.getImageData(0, 0, backupCanvas.width, backupCanvas.height);

		// refine mask image
		for (let i = 0; i < backupData.data.length; i += 4) {
			if(backupData.data[i+3] == 255)
				backupData.data[i+3] = 0;
			else
				backupData.data[i+3] = 255;

			backupData.data[i] = 0;
			backupData.data[i+1] = 0;
			backupData.data[i+2] = 0;
		}

		backupCtx.globalCompositeOperation = 'source-over';
		backupCtx.putImageData(backupData, 0, 0);

		const formData = new FormData();
		const filename = "clipspace-mask-" + performance.now() + ".png";

		const item =
			{
				"filename": filename,
				"subfolder": "clipspace",
				"type": "input",
			};

		if(ComfyApp.clipspace.images)
			ComfyApp.clipspace.images[0] = item;

		if(ComfyApp.clipspace.widgets) {
			const index = ComfyApp.clipspace.widgets.findIndex(obj => obj.name === 'image');

			if(index >= 0)
				ComfyApp.clipspace.widgets[index].value = item;
		}

		const dataURL = backupCanvas.toDataURL();
		const blob = dataURLToBlob(dataURL);

		let original_url = new URL(this.image.src);

		const original_ref = { filename: original_url.searchParams.get('filename') };

		let original_subfolder = original_url.searchParams.get("subfolder");
		if(original_subfolder)
			original_ref.subfolder = original_subfolder;

		let original_type = original_url.searchParams.get("type");
		if(original_type)
			original_ref.type = original_type;

		formData.append('image', blob, filename);
		formData.append('original_ref', JSON.stringify(original_ref));
		formData.append('type', "input");
		formData.append('subfolder', "clipspace");

		this.saveButton.innerText = "Saving...";
		this.saveButton.disabled = true;
		await uploadMask(item, formData);
		ComfyApp.onClipspaceEditorSave();
		this.close();
	}
}

app.registerExtension({
	name: "Comfy.MaskEditor",
	init(app) {
		ComfyApp.open_maskeditor =
			function () {
				const dlg = MaskEditorDialog.getInstance();
				if(!dlg.isOpened()) {
					dlg.show();
				}
			};

		const context_predicate = () => ComfyApp.clipspace && ComfyApp.clipspace.imgs && ComfyApp.clipspace.imgs.length > 0
		ClipspaceDialog.registerButton("MaskEditor", context_predicate, ComfyApp.open_maskeditor);
	}
});