// @ts-check
/* global $canvas_area, $status_position, $status_size, main_canvas, main_ctx, selected_colors, tool_transparent_mode, transparency */
import { Handles } from "./Handles.js";
import { OnCanvasObject } from "./OnCanvasObject.js";
import { get_tool_by_id, make_or_update_undoable, undoable, update_helper_layer } from "./functions.js";
import { $G, get_icon_for_tool, get_rgba_from_color, make_canvas, make_css_cursor, to_canvas_coords } from "./helpers.js";
import { replace_colors_with_swatch } from "./image-manipulation.js";
import { TOOL_SELECT } from "./tools.js";

class OnCanvasSelection extends OnCanvasObject {
	/**
	 * @param {number} x
	 * @param {number} y
	 * @param {number} width
	 * @param {number} height
	 * @param {HTMLImageElement | HTMLCanvasElement | ImageData=} image_source
	 */
	constructor(x, y, width, height, image_source) {
		super(x, y, width, height, true);

		this.$el.addClass("selection");
		let last_tool_transparent_mode = tool_transparent_mode;
		let last_background_color = selected_colors.background;
		this._on_option_changed = () => {
			if (!this.source_canvas) {
				return;
			}
			if (last_tool_transparent_mode !== tool_transparent_mode ||
				last_background_color !== selected_colors.background) {
				last_tool_transparent_mode = tool_transparent_mode;
				last_background_color = selected_colors.background;
				this.update_tool_transparent_mode();
			}
		};
		$G.on("option-changed", this._on_option_changed);

		this.instantiate(image_source);
	}
	position() {
		super.position(true);
		update_helper_layer(); // @TODO: under-grid specific helper layer?
	}
	/**
	 * @param {HTMLImageElement | HTMLCanvasElement | ImageData=} image_source
	 */
	instantiate(image_source) {
		this.$el.css({
			cursor: make_css_cursor("move", [8, 8], "move"),
			touchAction: "none",
		});
		this.position();

		const instantiate = () => {
			if (image_source) {
				// (this applies when pasting a selection)
				// NOTE: need to create a Canvas because something about imgs makes dragging not work with magnification
				// (width vs naturalWidth?)
				// and at least apply_image_transformation needs it to be a canvas now (and the property name says canvas anyways)
				this.source_canvas = make_canvas(image_source);
				// @TODO: is this width/height code needed? probably not! wouldn't it clear the canvas anyways?
				// but maybe we should assert in some way that the widths are the same, or resize the selection?
				if (this.source_canvas.width !== this.width) {
					this.source_canvas.width = this.width;
				}
				if (this.source_canvas.height !== this.height) {
					this.source_canvas.height = this.height;
				}
				this.canvas = make_canvas(this.source_canvas);
			} else {
				this.source_canvas = make_canvas(this.width, this.height);
				this.source_canvas.ctx.drawImage(main_canvas, this.x, this.y, this.width, this.height, 0, 0, this.width, this.height);
				this.canvas = make_canvas(this.source_canvas);
				this.cut_out_background();
			}
			this.$el.append(this.canvas);
			this.handles = new Handles({
				$handles_container: this.$el,
				$object_container: $canvas_area,
				outset: 2,
				get_rect: () => ({ x: this.x, y: this.y, width: this.width, height: this.height }),
				set_rect: ({ x, y, width, height }) => {
					undoable({
						name: "Resize Selection",
						icon: get_icon_for_tool(get_tool_by_id(TOOL_SELECT)),
						soft: true,
					}, () => {
						this.x = x;
						this.y = y;
						this.width = width;
						this.height = height;
						this.position();
						this.resize();
					});
				},
				get_ghost_offset_left: () => parseFloat($canvas_area.css("padding-left")) + 1,
				get_ghost_offset_top: () => parseFloat($canvas_area.css("padding-top")) + 1,
			});
			let mox, moy;
			const pointermove = (e) => {
				make_or_update_undoable({
					// XXX: Localization hazard: logic based on English action names
					match: (history_node) =>
						(e.shiftKey && /^(Smear|Stamp|Move) Selection$/.test(history_node.name)) ||
						(!e.shiftKey && /^Move Selection$/.test(history_node.name)),
					name: e.shiftKey ? "Smear Selection" : "Move Selection",
					update_name: true,
					icon: get_icon_for_tool(get_tool_by_id(TOOL_SELECT)),
					soft: true,
				}, () => {
					const m = to_canvas_coords(e);
					this.x = Math.max(Math.min(m.x - mox, main_canvas.width), -this.width);
					this.y = Math.max(Math.min(m.y - moy, main_canvas.height), -this.height);
					this.position();
					if (e.shiftKey) {
						// Smear selection
						this.draw();
					}
				});
			};
			this.canvas_pointerdown = (e) => {
				e.preventDefault();
				const rect = this.canvas.getBoundingClientRect();
				const cx = e.clientX - rect.left;
				const cy = e.clientY - rect.top;
				mox = ~~(cx / rect.width * this.canvas.width);
				moy = ~~(cy / rect.height * this.canvas.height);
				$G.on("pointermove", pointermove);
				this.dragging = true;
				update_helper_layer(); // for thumbnail, which draws textbox outline if it's not being dragged
				$G.one("pointerup", () => {
					$G.off("pointermove", pointermove);
					this.dragging = false;
					update_helper_layer(); // for thumbnail, which draws selection outline if it's not being dragged
				});
				if (e.shiftKey) {
					// Stamp or start to smear selection
					undoable({
						name: "Stamp Selection",
						icon: get_icon_for_tool(get_tool_by_id(TOOL_SELECT)),
						soft: true,
					}, () => {
						this.draw();
					});
				} else if (e.ctrlKey) { // @TODO: how should this work for macOS? where ctrl+click = secondary click?
					// Stamp selection
					undoable({
						name: "Stamp Selection",
						icon: get_icon_for_tool(get_tool_by_id(TOOL_SELECT)),
						soft: true,
					}, () => {
						this.draw();
					});
				}
			};
			$(this.canvas).on("pointerdown", this.canvas_pointerdown);
			$canvas_area.trigger("resize"); // could use "update" event instead if this is just to hide the main canvas handles
			$status_position.text("");
			$status_size.text("");
		};

		instantiate();
	}
	cut_out_background() {
		const cutout = this.canvas;
		// doc/this or canvas/cutout, either of those pairs would result in variable names of equal length which is nice :)
		const canvasImageData = main_ctx.getImageData(this.x, this.y, this.width, this.height);
		const cutoutImageData = cutout.ctx.getImageData(0, 0, this.width, this.height);
		// cutoutImageData is initialized with the shape to be cut out (whether rectangular or polygonal)
		// and should end up as the cut out image data for the selection
		// canvasImageData is initially the portion of image data on the canvas,
		// and should end up as... the portion of image data on the canvas that it should end up as.
		// @TODO: could simplify by making the later (shared) condition just if (colored_cutout)
		// but might change how it works anyways so whatever
		// if (!transparency) { // now if !transparency or if tool_transparent_mode
		// this is mainly in order to support patterns as the background color
		// NOTE: must come before cutout canvas is modified
		const colored_cutout = make_canvas(cutout);
		replace_colors_with_swatch(colored_cutout.ctx, selected_colors.background, this.x, this.y);
		// const colored_cutout_image_data = colored_cutout.ctx.getImageData(0, 0, this.width, this.height);
		// }
		for (let i = 0; i < cutoutImageData.data.length; i += 4) {
			const in_cutout = cutoutImageData.data[i + 3] > 0;
			if (in_cutout) {
				cutoutImageData.data[i + 0] = canvasImageData.data[i + 0];
				cutoutImageData.data[i + 1] = canvasImageData.data[i + 1];
				cutoutImageData.data[i + 2] = canvasImageData.data[i + 2];
				cutoutImageData.data[i + 3] = canvasImageData.data[i + 3];
				canvasImageData.data[i + 0] = 0;
				canvasImageData.data[i + 1] = 0;
				canvasImageData.data[i + 2] = 0;
				canvasImageData.data[i + 3] = 0;
			} else {
				cutoutImageData.data[i + 0] = 0;
				cutoutImageData.data[i + 1] = 0;
				cutoutImageData.data[i + 2] = 0;
				cutoutImageData.data[i + 3] = 0;
			}
		}
		main_ctx.putImageData(canvasImageData, this.x, this.y);
		cutout.ctx.putImageData(cutoutImageData, 0, 0);
		this.update_tool_transparent_mode();
		// NOTE: in case you want to use the tool_transparent_mode
		// in a document with transparency (for an operation in an area where there's a local background color)
		// (and since currently switching to the opaque document mode makes the image opaque)
		// (and it would be complicated to make it update the canvas when switching tool options (as opposed to just the selection))
		// I'm having it use the tool_transparent_mode option here, so you could at least choose beforehand
		// (and this might actually give you more options, although it could be confusingly inconsistent)
		// @FIXME: yeah, this is confusing; if you have both transparency modes on and you try to clear an area to transparency, it doesn't work
		// and there's no indication that you should try the other selection transparency mode,
		// and even if you do, if you do it after creating a selection, it still won't work,
		// because you will have already *not cut out* the selection from the canvas
		if (!transparency || tool_transparent_mode) {
			main_ctx.drawImage(colored_cutout, this.x, this.y);
		}

		$G.triggerHandler("session-update"); // autosave
		update_helper_layer();
	}
	update_tool_transparent_mode() {
		const sourceImageData = this.source_canvas.ctx.getImageData(0, 0, this.width, this.height);
		const cutoutImageData = this.canvas.ctx.createImageData(this.width, this.height);
		const background_color_rgba = get_rgba_from_color(selected_colors.background);
		// NOTE: In b&w mode, mspaint treats the transparency color as white,
		// regardless of the pattern selected, even if the selected background color is pure black.
		// We allow any kind of image data while in our "b&w mode".
		// Our b&w mode is essentially 'patterns in the palette'.
		const match_threshold = 1; // 1 is just enough for a workaround for Brave browser's farbling: https://github.com/1j01/jspaint/issues/184
		for (let i = 0; i < cutoutImageData.data.length; i += 4) {
			let in_cutout = sourceImageData.data[i + 3] > 1;
			if (tool_transparent_mode) {
				// @FIXME: work with transparent selected background color
				// (support treating partially transparent background colors as transparency)
				if (
					Math.abs(sourceImageData.data[i + 0] - background_color_rgba[0]) <= match_threshold &&
					Math.abs(sourceImageData.data[i + 1] - background_color_rgba[1]) <= match_threshold &&
					Math.abs(sourceImageData.data[i + 2] - background_color_rgba[2]) <= match_threshold &&
					Math.abs(sourceImageData.data[i + 3] - background_color_rgba[3]) <= match_threshold
				) {
					in_cutout = false;
				}
			}
			if (in_cutout) {
				cutoutImageData.data[i + 0] = sourceImageData.data[i + 0];
				cutoutImageData.data[i + 1] = sourceImageData.data[i + 1];
				cutoutImageData.data[i + 2] = sourceImageData.data[i + 2];
				cutoutImageData.data[i + 3] = sourceImageData.data[i + 3];
			} else {
				// cutoutImageData.data[i+0] = 0;
				// cutoutImageData.data[i+1] = 0;
				// cutoutImageData.data[i+2] = 0;
				// cutoutImageData.data[i+3] = 0;
			}
		}
		this.canvas.ctx.putImageData(cutoutImageData, 0, 0);

		update_helper_layer();
	}
	// @TODO: should Image > Invert apply to this.source_canvas or to this.canvas (replacing this.source_canvas with the result)?
	/**
	 * @param {PixelCanvas} new_source_canvas
	 */
	replace_source_canvas(new_source_canvas) {
		this.source_canvas = new_source_canvas;
		const new_canvas = make_canvas(new_source_canvas);
		$(this.canvas).replaceWith(new_canvas);
		this.canvas = new_canvas;
		const center_x = this.x + this.width / 2;
		const center_y = this.y + this.height / 2;
		const new_width = new_canvas.width;
		const new_height = new_canvas.height;
		// NOTE: flooring the coordinates to integers avoids blurring
		// but it introduces "inching", where the selection can move along by pixels if you rotate it repeatedly
		// could introduce an "error offset" just to avoid this but that seems overkill
		// and then that would be weird hidden behavior, probably not worth it
		// Math.round() might make it do it on fewer occasions(?),
		// but then it goes down *and* to the right, 2 directions vs One Direction
		// and Math.ceil() is the worst of both worlds
		this.x = ~~(center_x - new_width / 2);
		this.y = ~~(center_y - new_height / 2);
		this.width = new_width;
		this.height = new_height;
		this.position();
		$(this.canvas).on("pointerdown", this.canvas_pointerdown);
		this.$el.triggerHandler("resize"); //?
		this.update_tool_transparent_mode();
	}
	resize() {
		const new_source_canvas = make_canvas(this.width, this.height);
		new_source_canvas.ctx.drawImage(this.source_canvas, 0, 0, this.width, this.height);
		this.replace_source_canvas(new_source_canvas);
	}
	scale(factor) {
		const new_width = Math.max(1, this.width * factor);
		const new_height = Math.max(1, this.height * factor);
		const new_source_canvas = make_canvas(new_width, new_height);
		new_source_canvas.ctx.drawImage(this.source_canvas, 0, 0, new_source_canvas.width, new_source_canvas.height);
		this.replace_source_canvas(new_source_canvas);
	}
	draw() {
		try {
			main_ctx.drawImage(this.canvas, this.x, this.y);
		} catch (_error) {
			// ignore
		}
	}
	destroy() {
		super.destroy();
		$G.off("option-changed", this._on_option_changed);
		update_helper_layer(); // @TODO: under-grid specific helper layer?
	}
}

export { OnCanvasSelection };

