
const default_magnification = 1;
const default_tool = get_tool_by_name("Pencil");

const default_canvas_width = 683;
const default_canvas_height = 384;
let my_canvas_width = default_canvas_width;
let my_canvas_height = default_canvas_height;

let aliasing = true;
let transparency = false;
let monochrome = false;

let magnification = default_magnification;
let return_to_magnification = 4;

const canvas = make_canvas();
canvas.classList.add("main-canvas");
const ctx = canvas.ctx;

let palette = [
	"#000000","#787878","#790300","#757A01","#007902","#007778","#0A0078","#7B0077","#767A38","#003637","#286FFE","#083178","#4C00FE","#783B00",
	"#FFFFFF","#BBBBBB","#FF0E00","#FAFF08","#00FF0B","#00FEFF","#3400FE","#FF00FE","#FBFF7A","#00FF7B","#76FEFF","#8270FE","#FF0677","#FF7D36",
];
let polychrome_palette = palette;
let monochrome_palette = make_monochrome_palette();


let stroke_color;
let fill_color;
let stroke_color_k = "foreground"; // enum of "foreground", "background", "ternary"
let fill_color_k = "background"; // enum of "foreground", "background", "ternary"

let selected_tool = default_tool;
let selected_tools = [selected_tool];
let return_to_tools = [selected_tool];
let colors = {
	foreground: "",
	background: "",
	ternary: "",
};

let selection; //the one and only OnCanvasSelection
let textbox; //the one and only OnCanvasTextBox
let helper_layer; //the OnCanvasHelperLayer for the grid and tool previews
let show_grid = false;
let text_tool_font = {
	family: '"Arial"', // should be an exact value detected by Font Detective
	size: 12,
	line_scale: 20 / 12,
	bold: false,
	italic: false,
	underline: false,
	vertical: false,
	color: "",
	background: "",
};

const undos = []; //array of ImageData
const redos = []; //array of ImageData
//const frames = []; //array of {delay: N, undos: [ImageData], redos: [ImageData], image: ImageData}? array of Frames?

let file_name;
let document_file_path;
let saved = true;

let pointer, pointer_start, pointer_previous, pointer_type, pointer_buttons;
let reverse;
let ctrl;
let button;
let pointer_active = false;
let pointer_over_canvas = false;
let update_helper_layer_on_pointermove_active = false;

const $app = $(E("div")).addClass("jspaint").appendTo("body");

const $V = $(E("div")).addClass("vertical").appendTo($app);
const $H = $(E("div")).addClass("horizontal").appendTo($V);

const $canvas_area = $(E("div")).addClass("canvas-area").appendTo($H);
$canvas_area.attr("touch-action", "pan-x pan-y");

const $canvas = $(canvas).appendTo($canvas_area);
$canvas.attr("touch-action", "none");
let canvas_bounding_client_rect = canvas.getBoundingClientRect(); // cached for performance, updated later

const $canvas_handles = $Handles($canvas_area, canvas, {
	outset: 4,
	get_offset_left: ()=> parseFloat($canvas_area.css("padding-left")) + 1,
	get_offset_top: ()=> parseFloat($canvas_area.css("padding-top")) + 1,
	size_only: true,
});

const $top = $(E("div")).addClass("component-area").prependTo($V);
const $bottom = $(E("div")).addClass("component-area").appendTo($V);
const $left = $(E("div")).addClass("component-area").prependTo($H);
const $right = $(E("div")).addClass("component-area").appendTo($H);

const $status_area = $(E("div")).addClass("status-area").appendTo($V);
const $status_text = $(E("div")).addClass("status-text").appendTo($status_area);
const $status_position = $(E("div")).addClass("status-coordinates").appendTo($status_area);
const $status_size = $(E("div")).addClass("status-coordinates").appendTo($status_area);

$status_text.default = () => {
	$status_text.text("For Help, click Help Topics on the Help Menu.");
};
$status_text.default();

// menu bar
let menu_bar_outside_frame = false;
if(frameElement){
	try{
		if(parent.$MenuBar){
			$MenuBar = parent.$MenuBar;
			menu_bar_outside_frame = true;
		}
	// eslint-disable-next-line no-empty
	}catch(e){}
}
const $menu_bar = $MenuBar(menus);
if(menu_bar_outside_frame){
	$menu_bar.insertBefore(frameElement);
}else{
	$menu_bar.prependTo($V);
}

$menu_bar.on("info", (e, info) => {
	$status_text.text(info);
});
$menu_bar.on("default-info", e => {
	$status_text.default();
});

const $extras_menu_button = $menu_bar.get(0).ownerDocument.defaultView.$(".extras-menu-button");
// TODO: DRY with $MenuBar
// if localStorage is not available, the default setting is visible
let extras_menu_should_start_visible = true;
try{
	// if localStorage is available, the default setting is invisible (for now)
	// TODO: refactor shared key string
	extras_menu_should_start_visible = localStorage["jspaint extras menu visible"] == "true"
// eslint-disable-next-line no-empty
}catch(e){}
if(!extras_menu_should_start_visible){
	$extras_menu_button.hide();
}
// </menu bar>

const $toolbox = $ToolBox(tools);
// const $toolbox2 = $ToolBox(extra_tools, true);//.hide();
// Note: a second $ToolBox doesn't work because they use the same tool options (which could be remedied)
// and also the UI isn't designed for multiple vertical components (or horizontal ones)
// If there's to be extra tools, they should probably get a window, with different UI
// so it can display names of the tools, and maybe authors and previews (and not necessarily icons)
const $colorbox = $ColorBox();

$canvas.on("user-resized", (e, _x, _y, width, height) => {
	undoable(0, () => {
		canvas.width = Math.max(1, width);
		canvas.height = Math.max(1, height);
		ctx.disable_image_smoothing();
		
		if(!transparency){
			ctx.fillStyle = colors.background;
			ctx.fillRect(0, 0, canvas.width, canvas.height);
		}

		const previous_imagedata = undos[undos.length-1];
		if(previous_imagedata){
			const temp_canvas = make_canvas(previous_imagedata);
			ctx.drawImage(temp_canvas, 0, 0);
		}

		$canvas_area.trigger("resize");

		storage.set({
			width: canvas.width,
			height: canvas.height,
		}, err => {
			// oh well
		})
	});
});

$G.on("resize", () => { // for browser zoom, and in-app zoom of the canvas
	update_canvas_rect();
	update_disable_aa();
});
$canvas_area.on("scroll", () => {
	update_canvas_rect();
});
$canvas_area.on("resize", () => {
	update_magnified_canvas_size();
});

$("body").on("dragover dragenter", e => {
	const dt = e.originalEvent.dataTransfer;
	const has_files = Array.from(dt.types).includes("Files");
	if(has_files){
		e.preventDefault();
	}
}).on("drop", e => {
	if(e.isDefaultPrevented()){
		return;
	}
	const dt = e.originalEvent.dataTransfer;
	const has_files = Array.from(dt.types).includes("Files");
	if(has_files){
		e.preventDefault();
		if(dt && dt.files && dt.files.length){
			open_from_FileList(dt.files, "dropped");
		}
	}
});

$G.on("keydown", e => {
	if(e.isDefaultPrevented()){
		return;
	}
	// TODO: return if menus/menubar focused or focus in dialog window
	// or maybe there's a better way to do this that works more generally
	// maybe it should only handle the event if document.activeElement is the body or html element?
	// (or $app could have a tabIndex and no focus style and be focused under various conditions,
	// if that turned out to make more sense for some reason)
	if(
		e.target instanceof HTMLInputElement ||
		e.target instanceof HTMLTextAreaElement
	){
		return;
	}

	// TODO: preventDefault in all cases where the event is handled
	// also, ideally check that modifiers *aren't* pressed
	// probably best to use a library at this point!
	
	if(selection){
		const nudge_selection = (delta_x, delta_y) => {
			selection.x += delta_x;
			selection.y += delta_y;
			selection.position();
		};
		switch(e.keyCode){
			case 37: // Left
				nudge_selection(-1, 0);
				e.preventDefault();
				break;
			case 39: // Right
				nudge_selection(+1, 0);
				e.preventDefault();
				break;
			case 40: // Down
				nudge_selection(0, +1);
				e.preventDefault();
				break;
			case 38: // Up
				nudge_selection(0, -1);
				e.preventDefault();
				break;
		}
	}

	if(e.keyCode === 27){ //Escape
		if(selection){
			deselect();
		}else{
			cancel();
		}
		stopSimulatingGestures();
	}else if(e.keyCode === 13){ //Enter
		if(selection){
			deselect();
		}
	}else if(e.keyCode === 115){ //F4
		redo();
	}else if(e.keyCode === 46){ //Delete
		delete_selection();
	}else if(e.keyCode === 107 || e.keyCode === 109){ // Numpad Plus and Minus
		const plus = e.keyCode === 107;
		const minus = e.keyCode === 109;
		const delta = plus - minus; // const delta = +plus++ -minus--; // Δ = ±±±±

		if(selection){
			selection.scale(2 ** delta);
		}else{
			if(selected_tool.name === "Brush"){
				brush_size = Math.max(1, Math.min(brush_size + delta, 500));
			}else if(selected_tool.name === "Eraser/Color Eraser"){
				eraser_size = Math.max(1, Math.min(eraser_size + delta, 500));
			}else if(selected_tool.name === "Airbrush"){
				airbrush_size = Math.max(1, Math.min(airbrush_size + delta, 500));
			}else if(selected_tool.name === "Pencil"){
				pencil_size = Math.max(1, Math.min(pencil_size + delta, 50));
			}else if(selected_tool.name.match(/Line|Curve|Rectangle|Ellipse|Polygon/)){
				stroke_size = Math.max(1, Math.min(stroke_size + delta, 500));
			}

			$G.trigger("option-changed");
			if(button !== undefined){
				selected_tools.forEach((selected_tool)=> {
					tool_go(selected_tool);
				});
			}
		}
		e.preventDefault();
		return;
	}else if(e.ctrlKey || e.metaKey){
		const key = String.fromCharCode(e.keyCode).toUpperCase();
		if(textbox){
			switch(key){
				case "A":
				case "Z":
				case "Y":
				case "I":
				case "B":
				case "U":
					// Don't prevent the default. Allow text editing commands.
					return;
			}
		}
		switch(e.keyCode){
			case 188: // , <
			case 219: // [ {
				rotate(-TAU/4);
				$canvas_area.trigger("resize");
			break;
			case 190: // . >
			case 221: // ] }
				rotate(+TAU/4);
				$canvas_area.trigger("resize");
			break;
		}
		switch(key){
			case "Z":
				e.shiftKey ? redo() : undo();
			break;
			case "Y":
				redo();
			break;
			case "G":
				e.shiftKey ? render_history_as_gif() : toggle_grid();
			break;
			case "F":
				view_bitmap();
			break;
			case "O":
				file_open();
			break;
			case "N":
				e.shiftKey ? clear() : file_new();
			break;
			case "S":
				e.shiftKey ? file_save_as() : file_save();
			break;
			case "A":
				select_all();
			break;
			case "I":
				image_invert();
			break;
			case "E":
				image_attributes();
			break;
			default:
				return; // don't preventDefault
		}
		e.preventDefault();
	}
});
$G.on("cut copy paste", e => {
	if(e.isDefaultPrevented()){
		return;
	}
	if(
		document.activeElement instanceof HTMLInputElement ||
		document.activeElement instanceof HTMLTextAreaElement ||
		!window.getSelection().isCollapsed
	){
		// Don't prevent cutting/copying/pasting within inputs or textareas, or if there's a selection
		return;
	}

	e.preventDefault();
	const cd = e.originalEvent.clipboardData || window.clipboardData;
	if(!cd){ return; }

	if(e.type === "copy" || e.type === "cut"){
		if(selection && selection.canvas){
			const do_sync_clipboard_copy_or_cut = () => {
				// works only for pasting within a jspaint instance
				const data_url = selection.canvas.toDataURL();
				cd.setData("text/x-data-uri; type=image/png", data_url);
				cd.setData("text/uri-list", data_url);
				cd.setData("URL", data_url);
				if(e.type === "cut"){
					delete_selection();
				}
			};
			if (!navigator.clipboard || !navigator.clipboard.write) {
				return do_sync_clipboard_copy_or_cut();
			}
			try {
				if (e.type === "cut") {
					edit_cut();
				} else {
					edit_copy();
				}
			} catch(e) {
				do_sync_clipboard_copy_or_cut();
			}
		}
	}else if(e.type === "paste"){
		for (const item of cd.items) {
			if(item.type.match(/^text\/(?:x-data-uri|uri-list|plain)|URL$/)){
				item.getAsString(text => {
					const uris = get_URIs(text);
					if (uris.length > 0) {
						load_image_from_URI(uris[0], (err, img) => {
							if(err){ return show_resource_load_error_message(); }
							paste(img);
						});
					} else {
						show_error_message("The information on the Clipboard can't be inserted into Paint.");
					}
				});
				break;
			}else if(item.type.match(/^image\//)){
				paste_image_from_file(item.getAsFile());
				break;
			}
		}
	}
});

reset_file();
reset_colors();
reset_canvas_and_history(); // (with newly reset colors)
set_magnification(default_magnification);

storage.get({
	width: default_canvas_width,
	height: default_canvas_height,
}, (err, stored_values) => {
	if(err){return;}
	my_canvas_width = stored_values.width;
	my_canvas_height = stored_values.height;
	canvas.width = Math.max(1, my_canvas_width);
	canvas.height = Math.max(1, my_canvas_height);
	ctx.disable_image_smoothing();
	if(!transparency){
		ctx.fillStyle = colors.background;
		ctx.fillRect(0, 0, canvas.width, canvas.height);
	}
	$canvas_area.trigger("resize");
});

if(window.document_file_path_to_open){
	open_from_file_path(document_file_path_to_open, err => {
		if(err){
			return show_error_message(`Failed to open file ${document_file_path_to_open}`, err);
		}
	});
}

function to_canvas_coords({clientX, clientY}) {
	const rect = canvas_bounding_client_rect;
	const cx = clientX - rect.left;
	const cy = clientY - rect.top;
	return {
		x: ~~(cx / rect.width * canvas.width),
		y: ~~(cy / rect.height * canvas.height),
	};
}

function update_fill_and_stroke_colors_and_lineWidth(selected_tool) {
	ctx.lineWidth = stroke_size;

	const reverse_because_fill_only = selected_tool.$options && selected_tool.$options.fill && !selected_tool.$options.stroke;
	ctx.fillStyle = fill_color =
	ctx.strokeStyle = stroke_color =
		colors[
			(ctrl && colors.ternary && pointer_active) ? "ternary" :
			((reverse ^ reverse_because_fill_only) ? "background" : "foreground")
		];
		
	fill_color_k =
	stroke_color_k =
		ctrl ? "ternary" : ((reverse ^ reverse_because_fill_only) ? "background" : "foreground");
		
	if(selected_tool.shape || selected_tool.shape_colors){
		if(!selected_tool.stroke_only){
			if((reverse ^ reverse_because_fill_only)){
				fill_color_k = "foreground";
				stroke_color_k = "background";
			}else{
				fill_color_k = "background";
				stroke_color_k = "foreground";
			}
		}
		ctx.fillStyle = fill_color = colors[fill_color_k];
		ctx.strokeStyle = stroke_color = colors[stroke_color_k];
	}
}

function tool_go(selected_tool, event_name){
	update_fill_and_stroke_colors_and_lineWidth(selected_tool);

	if(selected_tools.length <= 1){
		if(selected_tool.shape){
			const previous_imagedata = undos[undos.length-1];
			if(previous_imagedata){
				ctx.clearRect(0, 0, canvas.width, canvas.height);
				ctx.putImageData(previous_imagedata, 0, 0);
			}
		}
	}
	if(selected_tool.shape){
		selected_tool.shape(ctx, pointer_start.x, pointer_start.y, pointer.x-pointer_start.x, pointer.y-pointer_start.y);
	}

	if(selected_tool[event_name]){
		selected_tool[event_name](ctx, pointer.x, pointer.y);
	}
	if(selected_tool.paint){
		if(selected_tool.continuous === "space"){
			const ham = brush_shape.match(/diagonal/) ? brosandham_line : bresenham_line;
			ham(pointer_previous.x, pointer_previous.y, pointer.x, pointer.y, (x, y) => {
				selected_tool.paint(ctx, x, y);
			});
		}else{
			selected_tool.paint(ctx, pointer.x, pointer.y);
		}
	}
}
function canvas_pointer_move(e){
	ctrl = e.ctrlKey;
	shift = e.shiftKey;
	pointer = to_canvas_coords(e);
	
	// Quick Undo
	// (Note: pointermove also occurs when the set of buttons pressed changes,
	// except when another event would fire like pointerdown)
	if(pointer_active && e.button != -1){
		// compare buttons other than middle mouse button by using bitwise OR to make that bit of the number the same
		const MMB = 4;
		if(e.pointerType != pointer_type || (e.buttons | MMB) != (pointer_buttons | MMB)){
			pointer_active = false;
			cancel();
			return;
		}
	}

	if(e.shiftKey){
		if(selected_tool.name.match(/Line|Curve/)){
			// snap to eight directions
			const dist = Math.sqrt(
				(pointer.y - pointer_start.y) * (pointer.y - pointer_start.y) +
				(pointer.x - pointer_start.x) * (pointer.x - pointer_start.x)
			);
			const eighth_turn = TAU / 8;
			const angle_0_to_8 = Math.atan2(pointer.y - pointer_start.y, pointer.x - pointer_start.x) / eighth_turn;
			const angle = Math.round(angle_0_to_8) * eighth_turn;
			pointer.x = Math.round(pointer_start.x + Math.cos(angle) * dist);
			pointer.y = Math.round(pointer_start.y + Math.sin(angle) * dist);
		}else if(selected_tool.shape){
			// snap to four diagonals
			const w = Math.abs(pointer.x - pointer_start.x);
			const h = Math.abs(pointer.y - pointer_start.y);
			if(w < h){
				if(pointer.y > pointer_start.y){
					pointer.y = pointer_start.y + w;
				}else{
					pointer.y = pointer_start.y - w;
				}
			}else{
				if(pointer.x > pointer_start.x){
					pointer.x = pointer_start.x + h;
				}else{
					pointer.x = pointer_start.x - h;
				}
			}
		}
	}
	selected_tools.forEach((selected_tool)=> {
		tool_go(selected_tool);
	});
	pointer_previous = pointer;
}
$canvas.on("pointermove", e => {
	pointer = to_canvas_coords(e);
	$status_position.text(`${pointer.x},${pointer.y}`);
});
$canvas.on("pointerenter", e => {
	pointer_over_canvas = true;

	update_helper_layer();

	if (!update_helper_layer_on_pointermove_active) {
		$G.on("pointermove", update_helper_layer);
		update_helper_layer_on_pointermove_active = true;
	}
});
$canvas.on("pointerleave", e => {
	pointer_over_canvas = false;

	$status_position.text("");

	update_helper_layer();
	
	if (!pointer_active && update_helper_layer_on_pointermove_active) {
		$G.off("pointermove", update_helper_layer);
		update_helper_layer_on_pointermove_active = false;
	}
});

$canvas.on("pointerdown", e => {
	update_canvas_rect();

	// Quick Undo when there are multiple pointers (i.e. for touch)
	// see pointermove for other pointer types
	if(pointer_active && (reverse ? (button === 2) : (button === 0))){
		pointer_active = false;
		cancel();
		return;
	}
	
	pointer_active = true;
	pointer_type = e.pointerType;
	pointer_buttons = e.buttons;
	$G.one("pointerup", e => {
		pointer_active = false;
		update_helper_layer();
		
		if (!pointer_over_canvas && update_helper_layer_on_pointermove_active) {
			$G.off("pointermove", update_helper_layer);
			update_helper_layer_on_pointermove_active = false;
		}
	});
	
	if(e.button === 0){
		reverse = false;
	}else if(e.button === 2){
		reverse = true;
	}else{
		return;
	}

	button = e.button;
	ctrl = e.ctrlKey;
	shift = e.shiftKey;
	pointer_start = pointer_previous = pointer = to_canvas_coords(e);

	const pointerdown_action = () => {
	// TODO for multitools: don't register event listeners for each tool
	selected_tools.forEach((selected_tool)=> {

		if(selected_tool.paint || selected_tool.pointerdown){
			tool_go(selected_tool, "pointerdown");
		}

		$G.on("pointermove", canvas_pointer_move);
		let iid;
		if(selected_tool.continuous === "time"){
			iid = setInterval(()=> { tool_go(selected_tool); }, 5);
		}
		$G.one("pointerup", (e, canceling) => {
			button = undefined;
			reverse = false;
			if(canceling){
				selected_tool.cancel && selected_tool.cancel();
			}else{
				pointer = to_canvas_coords(e);
				selected_tool.pointerup && selected_tool.pointerup(ctx, pointer.x, pointer.y);
			}
			if (selected_tools.length === 1) {
				if (selected_tool.deselect) {
					select_tools(return_to_tools);
				}
			}
			$G.off("pointermove", canvas_pointer_move);
			if(iid){
				clearInterval(iid);
			}
		});
	});
	};

	if(isPassive(selected_tools)){
		pointerdown_action();
	}else{
		undoable(pointerdown_action);
	}
	
	update_helper_layer();
});

$canvas_area.on("pointerdown", e => {
	if(e.button === 0){
		if($canvas_area.is(e.target)){
			if(selection){
				deselect();
			}
		}
	}
});

$app
.add($toolbox)
// .add($toolbox2)
.add($colorbox)
.on("mousedown selectstart contextmenu", e => {
	if(e.isDefaultPrevented()){
		return;
	}
	if(
		e.target instanceof HTMLSelectElement ||
		e.target instanceof HTMLTextAreaElement ||
		(e.target instanceof HTMLLabelElement && e.type !== "contextmenu") ||
		(e.target instanceof HTMLInputElement && e.target.type !== "color")
	){
		return;
	}
	if(e.button === 1){
		return; // allow middle-click scrolling
	}
	e.preventDefault();
	// we're just trying to prevent selection
	// but part of the default for mousedown is *deselection*
	// so we have to do that ourselves explicitly
	window.getSelection().removeAllRanges();
});

// Stop drawing (or dragging or whatver) if you Alt+Tab or whatever
$G.on("blur", e => {
	$G.triggerHandler("pointerup");
});
