// CONSTANTS {{{

const commandBarElement = document.getElementById('menu-bar');
const toolBarElement = document.getElementById('tool-bar');
const layerControllersElement = document.getElementById('layer-controllers');
const studioElement = document.getElementById('studio');
const infoBarElement = document.getElementById('info-bar');
const easelElement = document.getElementById('easel');

const dZoom = 0.001;
const dBrushSize = 0.5;
const initialWidth = 800;
const initialHeight = 600;
const maxBrushSize = 500;
const tolerance = 1;
const shapes = ['circle', 'square'];

// }}}

// VARS {{{

let brushShape = 'circle'
let brushSize = 10;
let zoom = 1;

let startX = 0;
let startY = 0;
let endX = 0;
let endY = 0;
let dX = 0;
let dY = 0;
let canvasStartX = 0;
let canvasStartY = 0;
let canvasEndX = 0;
let canvasEndY = 0;
let canvasDX = 0;
let canvasDY = 0;

let isKeyDown = false;
let isMouseDown = false;

let interval;
var startTime;

// }}}

// HELPERS {{{

function disableImageSmoothing(ctx) {
	ctx.imageSmoothingEnabled = false;
	if (ctx.imageSmoothingEnabled !== false) {
		ctx.mozImageSmoothingEnabled = false;
		ctx.webkitImageSmoothingEnabled = false;
		ctx.msImageSmoothingEnabled = false;
	}
};

function hexToRgbArray(hex) {
	const r = parseInt(hex.substring(1, 3), 16);
	const g = parseInt(hex.substring(3, 5), 16);
	const b = parseInt(hex.substring(5, 7), 16);
	return [r, g, b, 255];
}

function colorsMatch(color1, color2, tolerance = 0) {
	return color1[0] === color2[0] && color1[1] === color2[1] && color1[2] === color2[2] && color1[3] === color2[3];
    return Math.abs(color1[0] - color2[0]) <= tolerance &&
           Math.abs(color1[1] - color2[1]) <= tolerance &&
           Math.abs(color1[2] - color2[2]) <= tolerance &&
           Math.abs(color1[3] - color2[3]) <= tolerance; // Include alpha comparison
}

function makeIconElement(htmlString) {
	const parentElement = document.createElement('div');
	parentElement.innerHTML = htmlString;
	const iconElement = parentElement.firstChild;
	return iconElement;
}

function closeRgbArray(color1, color2, tolerance) {
	return Math.abs(color1[0] - color2[0]) <= tolerance && Math.abs(color1[1] - color2[1]) <= tolerance && Math.abs(color1[2] - color2[2]) <= tolerance;	
}

function makeButtonElement({icon, name, func, key}) {
	if (!icon) throw new Error('No icon provided');
	if (!name) throw new Error('No name provided');
	if (!func) throw new Error('No click function provided');
	if (!key) throw new Error('No key provided');

	const button = {};
	button.name = name;
	button.key = key;
	button.icon = icon;
	button.element = document.createElement('div');
	button.element.className = 'button';

	button.element.addEventListener('click', func);

	button.refresh = function() {
		button.element.innerHTML = '';
		const iconElement = makeIconElement(button.icon);
		button.element.appendChild(iconElement);
		button.element.title = button.name;
		if (button.key) {
			const keyHint = document.createElement('span');
			keyHint.className = 'key-hint';
			keyHint.innerHTML = key;
			button.element.appendChild(keyHint);
		}
	}

	button.refresh();

	return button;
}

// }}}

// COLOR {{{

function makeColor(rgb) {
	const color = {};

	color.fromRgba = function(rgba) {
		color.r = rgba.split(',')[0].split('(')[1];
		color.g = rgba.split(',')[1];
		color.b = rgba.split(',')[2];
		color.a = rgba.split(',')[3].split(')')[0];
	}

	color.toRgba = function() {
		return `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`;
	}

	color.fromRgb = function(rgb) {
		color.r = rgb.split(',')[0].split('(')[1];
		color.g = rgb.split(',')[1];
		color.b = rgb.split(',')[2].split(')')[0];
		color.a = 255;
	}

	color.toRgb = function() {
		return `rgb(${color.r}, ${color.g}, ${color.b})`;
	}

	color.fromHex = function(hex) {
		color.r = parseInt(hex.substring(1, 3), 16);
		color.g = parseInt(hex.substring(3, 5), 16);
		color.b = parseInt(hex.substring(5, 7), 16);
		color.a = 255;
	}

	color.toHex = function() {
		color.r = parseInt(rgb[0]);
		color.g = parseInt(rgb[1]);
		color.b = parseInt(rgb[2]);
		return `#${color.r.toString(16)}${color.g.toString(16)}${color.b.toString(16)}`;
	}

	color.fromRgbaArray = function(array) {
		color.r = array[0];
		color.g = array[1];
		color.b = array[2];
		color.a = array[3];
	}

	color.toRgbaArray = function() {
		return [color.r, color.g, color.b, color.a];
	}

	color.fromRgbArray = function(array) {
		color.r = array[0];
		color.g = array[1];
		color.b = array[2];
		color.a = 255;
	}

	color.toRgbArray = function() {
		return [color.r, color.g, color.b];
	}

	color.mixxRgbArray = function(color2RgbArray, t) {
		const color1RgbArray = color.toRgbArray();
		if (color1RgbArray === color2RgbArray) {
			return;
		}
		var newColorRgbArray = color2RgbArray;
		if (!closeRgbArray(color1RgbArray, color2RgbArray, 2)) {
			console.log('mixxing');
			newColorRgbArray = mixbox.lerp(color1RgbArray, color2RgbArray, t);
		}
		color.fromRgbArray(newColorRgbArray);
	}

	color.mixxRgb = function(color2Rgb, t) {
		const result = color2Rgb.match(/rgb\((\d+), (\d+), (\d+)\)/);
		if (result) {
			const color2RgbArray = [result[1], result[2], result[3]];
			color.mixxRgbArray(color2RgbArray, t);
		}
	}

	color.mixxRgbaArray = function(color2RgbaArray, t) {
		const color2a = color2RgbaArray[3];
		if (color2a !== 225) {
			return;
		}
		const color2RgbArray = color2RgbaArray.slice(0, 3);
		color.mixxRgbArray(color2RgbArray, t);
	}

	color.mixxRgba = function(color2Rgba, t) {
		const result = color2Rgba.match(/rgba\((\d+), (\d+), (\d+), (\d+)\)/);
		if (result) {
			const color2RgbaArray = [result[1], result[2], result[3], result[4]];
			color.mixxRgbaArray(color2RgbaArray, t);
		}
	}

	color.mixx = function(color2, t) {
		if (color2.a === 255) {
			color.mixxRgbArray(color2.toRgbArray(), t);
		}
	}

	color.isOpaque = function() {
		return color.a === 255;
	}

	color.match = function(color2) {
		return color.r === color2.r && color.g === color2.g && color.b === color2.b && color.a === color2.a;
	}

	color.copy = function(color2) {
		color.r = color2.r;
		color.g = color2.g;
		color.b = color2.b;
		color.a = color2.a;
	}

	color.fromRgb(rgb);

	return color;
}

const brushColor = makeColor('rgb(0, 0, 0)');
const canvasColor = makeColor('rgb(0, 0, 0)');
const tempColor = makeColor('rgb(0, 0, 0)');

// }}}

// LAYERS {{{

// FACTORY {{{

function makeCanvas({height=600, width=800, background=false}) { // {{{
	const canvas = document.createElement('canvas');
	canvas.style.imageRendering = 'pixelated';
	canvas.ctx = canvas.getContext('2d');
	canvas.background = background;

	canvas.tempCanvas = document.createElement('canvas');
	canvas.tempCtx = canvas.tempCanvas.getContext('2d');

	canvas.saveCanvas = function() {
		canvas.ctx.save();
		canvas.tempCanvas.width = canvas.width;
		canvas.tempCanvas.height = canvas.height;
		canvas.tempCtx.clearRect(0, 0, canvas.width, canvas.height);
		disableImageSmoothing(canvas.tempCtx);
		canvas.tempCtx.drawImage(canvas, 0, 0);
	}

	canvas.clearCanvas = function() {
		if (!canvas.background) {
			canvas.ctx.clearRect(0, 0, canvas.width, canvas.height);
		}
	}

	canvas.restoreCanvas = function(x=0, y=0) {
		canvas.ctx.drawImage(canvas.tempCanvas, x, y);
	}

	canvas.setHeight = function(height) {
		canvas.height = height;
		disableImageSmoothing(canvas.ctx);
	};

	canvas.setWidth = function(width) {
		canvas.width = width;
		disableImageSmoothing(canvas.ctx);
	};

	canvas.resize = function(width, height) {
		canvas.saveCanvas();
		canvas.clearCanvas();
		canvas.width = width;
		canvas.height = height;
		canvas.style.width = width * zoom + 'px';
		canvas.style.height = height * zoom + 'px';
		disableImageSmoothing(canvas.ctx);
		canvas.restoreCanvas();
	}

	canvas.getPositionOnCanvas = function(e) {
		const rect = canvas.getBoundingClientRect();
		return {
			x: Math.round((e.clientX - rect.left) / zoom),
			y: Math.round((e.clientY - rect.top) / zoom),
		};
	}

	canvas.drawPixel = function(x, y, color) {
		if (!canvas.background) {
			canvas.ctx.fillStyle = color;
			canvas.ctx.fillRect(x, y, 1, 1);
		}
	}

	canvas.drawLineWithPixels = function(x1, y1, x2, y2, color) {
		if (!canvas.background) {
			const dx = Math.abs(x2 - x1);
			const dy = Math.abs(y2 - y1);
			const sx = x1 < x2 ? 1 : -1;
			const sy = y1 < y2 ? 1 : -1;
			let err = dx - dy;
			while (true) {
				canvas.drawPixel(x1, y1, color); // Draw each pixel along the line
				if (x1 === x2 && y1 === y2) break;
				const e2 = err * 2;
				if (e2 > -dy) { err -= dy; x1 += sx; }
				if (e2 < dx) { err += dx; y1 += sy; }
			}
		}
	}

	canvas.drawShape = function(x, y, shape, size, color) {
		if (!canvas.background) {
			x = Math.round(x);
			y = Math.round(y);

			if (size === 1) {
				canvas.drawPixel(x, y, color);
				return;
			}
			canvas.ctx.fillStyle = color;

			if (shape === 'square') {
				canvas.ctx.fillRect(x - Math.floor(size / 2), y - Math.floor(size / 2), size, size);
			} else if (shape === 'circle') {
				let radius = Math.floor(size / 2);
				let radiusSquared = radius * radius;

				for (let y1 = -radius; y1 <= radius; y1++) {
					for (let x1 = -radius; x1 <= radius; x1++) {
						// Adjust the condition to avoid the outcrop
						if ((x1 * x1 + y1 * y1) <= radiusSquared - radius) {
							canvas.ctx.fillRect(x + x1, y + y1, 1, 1);
						}
					}
				}
			} else if (shape === 'empty-circle') {
				let radius = Math.floor(size / 2);
				let x1 = radius;
				let y1 = 0;
				let radiusError = 1 - x1;

				while (x1 >= y1) {
					// Draw the 8 octants of the circle
					canvas.ctx.fillRect(x + x1, y + y1, 1, 1);
					canvas.ctx.fillRect(x + y1, y + x1, 1, 1);
					canvas.ctx.fillRect(x - y1, y + x1, 1, 1);
					canvas.ctx.fillRect(x - x1, y + y1, 1, 1);
					canvas.ctx.fillRect(x - x1, y - y1, 1, 1);
					canvas.ctx.fillRect(x - y1, y - x1, 1, 1);
					canvas.ctx.fillRect(x + y1, y - x1, 1, 1);
					canvas.ctx.fillRect(x + x1, y - y1, 1, 1);

					y1++;
					if (radiusError < 0) {
						radiusError += 2 * y1 + 1;
					} else {
						x1--;
						radiusError += 2 * (y1 - x1 + 1);
					}
				}
			}
		}
	}

	canvas.drawLineWithShape = function(x1, y1, x2, y2, shape, size, color) {
		if (!canvas.background) {
			const dx = x2 - x1;
			const dy = y2 - y1;
			const distance = Math.sqrt(dx * dx + dy * dy);
			const steps = Math.ceil(distance / (size / 2));

			for (let i = 0; i <= steps; i++) {
				const x = Math.round(x1 + (dx * i) / steps);
				const y = Math.round(y1 + (dy * i) / steps);
				canvas.drawShape(x, y, shape, size, color);
			}
		}
	}

	canvas.fill = function(color) {
		canvas.ctx.fillStyle = color;
		canvas.ctx.fillRect(0, 0, canvas.width, canvas.height);
	}

	canvas.getRgbaArrayColorAtPixelData = function(x, y, data) {
		const index = (y * canvas.width + x) * 4;
		const color = [data[index], data[index + 1], data[index + 2], data[index + 3]];
		return color;
	}

	canvas.setRgbaArrayColorAtPixelData = function(x, y, color, data) {
		const index = (y * canvas.width + x) * 4;
		data[index] = color[0];
		data[index + 1] = color[1];
		data[index + 2] = color[2];
		data[index + 3] = color[3];
	}

	canvas.floodFill = function(x, y, colorRgbaArray) {
		if (!canvas.background) {
			const imageData = canvas.ctx.getImageData(0, 0, canvas.width, canvas.height);
			const data = imageData.data;

			const targetColor = canvas.getRgbaArrayColorAtPixelData(x, y, data);
			const fillColorArray = colorRgbaArray;

			if (colorsMatch(targetColor, fillColorArray, tolerance)) {
				return;
			}

			const stack = [{x, y}];

			while (stack.length > 0) {
				const {x, y} = stack.pop();
				const currentColor = canvas.getRgbaArrayColorAtPixelData(x, y, data);

				if (colorsMatch(currentColor, targetColor, tolerance)) {
					canvas.setRgbaArrayColorAtPixelData(x, y, fillColorArray, data);

					if (x > 0) stack.push({x: x - 1, y});
					if (x < canvas.width - 1) stack.push({x: x + 1, y});
					if (y > 0) stack.push({x, y: y - 1});
					if (y < canvas.height - 1) stack.push({x, y: y + 1});
				}
			}

			canvas.ctx.putImageData(imageData, 0, 0);
		}
	}


	canvas.getRgbaColorArrayAtPixel = function(x, y) {
		const data = canvas.ctx.getImageData(0, 0, canvas.width, canvas.height).data;
		return canvas.getRgbaArrayColorAtPixelData(x, y, data);
	}

	canvas.getRgbColorAtPixel = function(x, y) {
		const color = canvas.getRgbaColorArrayAtPixel(x, y);
		return `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
	}

	canvas.setColorAtPixel = function(x, y, color) {
		const imageData = canvas.ctx.getImageData(0, 0, canvas.width, canvas.height);
		const data = imageData.data;
		canvas.setRgbaArrayColorAtPixelData(x, y, color, data);
		canvas.ctx.putImageData(new ImageData(data, canvas.width, canvas.height), 0, 0);
	}

	canvas.toDataUrl = function() {
		const dataURL = canvas.toDataURL();
		const dimensions = `${canvas.width}x${canvas.height}`;
		return {dataURL, dimensions};
	}

	canvas.fromDataUrl = function(dataURL, dimensions) {
		const img = new Image();
		img.src = dataURL;
		img.onload = function() {
			canvas.width = dimensions.split('x')[0];
			canvas.height = dimensions.split('x')[1];
			canvas.style.width = canvas.width * zoom + 'px';
			canvas.style.height = canvas.height * zoom + 'px';
			canvas.ctx.drawImage(img, 0, 0);
		}
	}

	canvas.deleteCanvas = function() {
		if (!background) {
			canvas.remove();
		}
	}

	canvas.setWidth(width);
	canvas.setHeight(height);

	return canvas;

} // }}}

function makeLayer({height=600, width=800, background=undefined}) { // {{{
	const layer = {}
	layer.canvas = makeCanvas({height, width, background});
	layer.active = false;
	layer.opacity = 1;
	layer.background = background;

	layer.controllerElement = document.createElement('div');
	layer.controllerElement.className = 'layer-controller';
	layer.controllerElement.innerHTML = '<i class="fa-solid fa-circle-check"></i>';

	layer.controllerElement.addEventListener('click', () => {
		layers.setActive(layer);
	});

	layer.activate = function() {
		layer.active = true;
		layer.controllerElement.classList.add('active');
	}

	layer.deactivate = function() {
		layer.active = false;
		layer.controllerElement.classList.remove('active');
	}

	return layer;
} // }}}

function makeLayers({height=600, width=800, backgroundColor='rgb(255, 255, 255)'}) { // {{{
	const layers = [];
	layers.height = height;
	layers.width = width;

	layers.addButton = document.createElement('div');
	layers.addButton.className = 'layer-add-button';
	layers.addButton.innerHTML = '<i class="fa-solid fa-plus"></i>';
	layers.addButton.addEventListener('click', () => {
		layers.add();
	});

	layers.setHeight = function(height) {
		layers.height = height;
		layers.forEach(layer => layer.canvas.setHeight(height));
		easelElement.style.height = height + 2 + 'px';
	}

	layers.setHeight(height);

	layers.setWidth = function(width) {
		layers.width = width;
		layers.forEach(layer => layer.canvas.setWidth(width));
		easelElement.style.width = width + 2 + 'px';
	}

	layers.setWidth(width);

	layers.resize = function(width, height) {
		layers.height = height;
		layers.width = width;
		easelElement.style.height = height * zoom + 2 + 'px';
		easelElement.style.width = width * zoom + 2 + 'px';
		layers.forEach(layer => layer.canvas.resize(width, height));
		layers[0].canvas.fill(backgroundColor);
	}

	layers.zoom = function(newZoom) {
		easelElement.style.height = layers.height * zoom + 2 + 'px';
		easelElement.style.width = layers.width * zoom + 2 + 'px';
		layers.forEach(layer => {
			layer.canvas.style.height = layers.height * zoom + 'px';
			layer.canvas.style.width = layers.width * zoom + 'px';
		});
	}

	layers.resetPosition = function() {
		const studioRect = studioElement.getBoundingClientRect();
		easelElement.style.left = `${studioRect.left}px`;
		easelElement.style.top = `${studioRect.top}px`;
	}

	layers.refreshControllers = function() {
		layerControllersElement.innerHTML = '';
		layers.forEach(layer => {
			layerControllersElement.appendChild(layer.controllerElement);
		});
		layerControllersElement.appendChild(layers.addButton);
	}

	layers.refreshLayers = function() {
		easelElement.innerHTML = '';
		layers.forEach(layer => {
			easelElement.appendChild(layer.canvas);
		});
	}

	layers.refresh = function() {
		layers.refreshControllers();
		layers.refreshLayers();
	}

	layers.add = function() {
		const layer = makeLayer({
			height: layers.height,
			width: layers.width,
		});
		layers.push(layer);
		layer.activate();
		layers.refresh();
	}

	layers.delete = function(layer) {
		if (!layer.background) {
			layer.canvas.deleteCanvas();
			layers.splice(layers.indexOf(layer), 1);
			layers.refresh();
		}
	}

	layers.deleteAll = function() {
		layers.forEach(function(layer) {
			if (!layer.background) {
				layer.canvas.deleteCanvas();
				layers.splice(layers.indexOf(layer), 1);
			}
		});
	}

	layers.moveUp = function(layer) {
		if (layer.background) return;
		if (layers.indexOf(layer) === layers.length - 1) return;
		const index = layers.indexOf(layer);
		const temp = layers[index + 1];
		layers[index + 1] = layer;
		layers[index] = temp;
		layers.refresh();
	}

	layers.moveDown = function(layer) {
		if (layer.background) return;
		if (layers.indexOf(layer) === 1) return;
		const index = layers.indexOf(layer);
		const temp = layers[index - 1];
		layers[index - 1] = layer;
		layers[index] = temp;
		layers.refresh();
	}

	layers.setActive = function(layer) {
		layers.forEach(layer => layer.deactivate());
		layer.activate();
	}

	layers.getActive = function() {
		return layers.find(layer => layer.active);
	}

	layers.push(makeLayer({height, width, background: true}));
	layers[0].canvas.fill(backgroundColor);

	return layers;
} // }}}

// }}}

const layers = makeLayers({height: initialHeight, width: initialWidth});
layers.add();
layers.setActive(layers[1]);

// }}}

// COLOR PREVIEW {{{

function makeColorPreview() {
	const colorPreview = {}
	colorPreview.element = document.createElement('div');
	colorPreview.element.id = 'color-preview';
	colorPreview.element.className = 'puck';
	colorPreview.element.style.backgroundColor = brushColor.toRgb();
	commandBarElement.appendChild(colorPreview.element);
	colorPreview.update = function() {
		colorPreview.element.style.backgroundColor = brushColor.toRgb();
	}

	return colorPreview;
}

const colorPreview = makeColorPreview();

// }}}

// BRUSH PREVIEW {{{

function makeBrushPreview() {
	const brushPreview = {};
	brushPreview.element = document.createElement('div');
	brushPreview.element.id = 'brush-preview';
	brushPreview.element.style.width = brushSize + 'px';
	brushPreview.element.style.height = brushSize + 'px';
	brushPreview.element.style.position = 'absolute';
	brushPreview.element.style.display = 'none';
	brushPreview.element.style.pointerEvents = 'none';
	brushPreview.element.style.border = '1px solid black';
	brushPreview.element.style.zIndex = '1000';

	document.body.appendChild(brushPreview.element);

	brushPreview.update = function() {
		brushPreview.element.style.width = brushSize * zoom + 'px';
		brushPreview.element.style.height = brushSize * zoom + 'px';
		if (brushShape === 'circle') {
			brushPreview.element.style.borderRadius = '50%';
		} else {
			brushPreview.element.style.borderRadius = '0';
		}
		if (brushSize < 3) {
			brushPreview.element.style.visibility = 'hidden';
		} else {
			brushPreview.element.style.visibility = 'visible';
		}
	}

	brushPreview.setPosition = function(x, y) {
		brushPreview.element.style.left = x - brushSize * zoom / 2 + 'px';
		brushPreview.element.style.top = y - brushSize * zoom / 2 + 'px';
	}

	brushPreview.show = function() {
		brushPreview.element.style.display = 'block';
	}

	brushPreview.hide = function() {
		brushPreview.element.style.display = 'none';
	}

	brushPreview.update();

	return brushPreview;
}

const brushPreview = makeBrushPreview();


// }}}

// COMMANDS {{{

// FACTORY {{{

function makeCommand({name, key, icon, func}) {
	if (!name) throw new Error('No name provided');
	if (!icon) throw new Error('No icon provided');
	if (!func) throw new Error('No click function provided');
	if (!key) throw new Error('No key provided');

	const command = {};
	command.name = name;
	command.key = key;
	command.func = function() {
		func();
		infos.update();
	}
	command.button = makeButtonElement({
		icon: icon,
		name: name,
		key: key,
		func: command.func,
	});
	commandBarElement.appendChild(command.button.element);

	return command
}

function makeCommands() {
	const commands = [];

	commands.add = function({name, key, icon, func}) {
		const command = makeCommand({name, key, icon, func});
		commands.push(command);
	}

	commands.get = function(name) {
		return commands.find(command => command.name === name);
	}

	commands.click = function(name) {
		const command = commands.get(name);
		command.func();
	}

	return commands;
}


// }}}

const commands = makeCommands();

commands.add({ // flip-horizontally {{{
	name: 'flip-horizontally',
	key: 'f',
	icon: '<i class="fa-solid fa-left-right"></i>',
	func: function flipCanvasHorizontally() {
		const canvas = layers.getActive().canvas;
		const ctx = canvas.ctx;
		ctx.save();
		ctx.clearRect(0, 0, canvas.width, canvas.height);
		ctx.scale(-1, 1);
		ctx.translate(-canvas.width, 0);
		canvas.restoreCanvas();
		ctx.restore();
	}
}); // }}}

commands.add({ // flip-vertically {{{
	name: 'flip-vertically',
	key: 'v',
	icon: '<i class="fa-solid fa-up-down"></i>',
	func: function flipCanvasVertically() {
		const canvas = layers.getActive().canvas;
		const ctx = canvas.ctx;
		ctx.save();
		canvas.saveCanvas();
		ctx.clearRect(0, 0, canvas.width, canvas.height);
		ctx.scale(1, -1);
		ctx.translate(0, -canvas.height);
		canvas.restoreCanvas();
		ctx.restore();
	}
}); // }}}

commands.add({ // export {{{
	name: 'export',
	key: 'e',
	icon: '<i class="fa-solid fa-floppy-disk"></i>',
	func: function exportCanvas() {
		const canvas = layers.getActive().canvas;
		const link = document.createElement('a');
		link.download = 'canvas.png';
		link.href = canvas.toDataURL();
		link.click();
	}
}); // }}}

commands.add({ // import {{{
	name: 'import',
	key: 'i',
	icon: '<i class="fa-regular fa-folder-open"></i>',
	func: function importCanvas() {
		const canvas = layers.getActive().canvas;
		const ctx = canvas.ctx;
		const input = document.createElement('input');
		input.type = 'file';
		input.accept = 'image/*';
		input.onchange = (e) => {
			const file = e.target.files[0];
			const reader = new FileReader();
			reader.onload = (e) => {
				const img = new Image();
				img.onload = () => {
					canvas.width = img.width;
					canvas.height = img.height;
					ctx.drawImage(img, 0, 0);
				}
				img.src = e.target.result;
			}
			reader.readAsDataURL(file);
		}
		input.click();
	}
}); // }}}

commands.add({ // clear {{{
	name: 'clear',
	key: 'c',
	icon: '<i class="fa-solid fa-trash-can"></i>',
	func: function clearCanvas() {
		const canvas = layers.getActive().canvas;
		const ctx = canvas.ctx;
		canvas.saveCanvas();
		ctx.clearRect(0, 0, canvas.width, canvas.height);
		ctx.fillStyle = 'white';
		ctx.fillRect(0, 0, canvas.width, canvas.height);
	}
}); // }}}

commands.add({ // change-shape {{{
	name: 'change-shape',
	key: 's',
	icon: `<i class="fa-solid fa-shapes"></i>`,
	func: function changeShape() {
		const currentIndex = shapes.indexOf(brushShape);
		brushShape = shapes[(currentIndex + 1) % shapes.length];
		brushPreview.update();
	}
}); // }}}

commands.add({
	name: 'reset',
	key: 'r',
	icon: '<i class="fa-solid fa-home"></i>',
	func: function resetCanvas() {
		zoom = 1;
		layers.zoom();
		layers.resetPosition();
	}
});

// }}}

// TOOLS {{{

// FACTORY {{{

function makeTool({name, key, icon, mouseDown, mouseMove, mouseUp, mouseDrag, mouseLeave}) {
	if (!name) throw new Error('No name provided');
	if (!key) throw new Error('No key provided');
	if (!icon) throw new Error('No icon provided');

	const tool = {};
	tool.name = name;
	tool.key = key;
	tool.icon = icon;
	tool.mouseDown = mouseDown;
	tool.mouseMove = mouseMove;
	tool.mouseUp = mouseUp;
	tool.mouseDrag = mouseDrag;
	tool.mouseLeave = mouseLeave;
	tool.active = false;

	tool.activate = function() {
		tool.active = true;
		tool.button.element.classList.add('active');
	}

	tool.deactivate = function() {
		tool.active = false;
		tool.button.element.classList.remove('active');
	}

	tool.button = makeButtonElement({
		icon: tool.icon,
		name: tool.name,
		key: tool.key,
		func: function() {
			tools.activate(tool.name);
		}
	});

	toolBarElement.appendChild(tool.button.element);

	return tool;
}

function makeTools() {
	const tools = [];

	tools.prevToolName = 'na';

	tools.add = function({name, key, icon, mouseDown, mouseMove, mouseUp, mouseDrag, mouseLeave}) {
		const tool = makeTool({name, key, icon, mouseDown, mouseMove, mouseUp, mouseDrag, mouseLeave});	
		tools.push(tool);
	}

	tools.get = function(name) {
		return tools.find(tool => tool.name === name);
	}

	tools.getActive = function() {
		return tools.find(tool => tool.active);
	}

	tools.activate = function(name) {
		const tool = tools.get(name);
		if (tool.active) return;
		if (tools.getActive()) {
			tools.prevToolName = tools.getActive().name;
			tools.forEach(tool => tool.deactivate());
		}
		tool.activate();
	}

	tools.restore = function() {
		const tool = tools.get(tools.prevToolName);
		tools.forEach(tool => tool.deactivate());
		tool.activate();
	}

	return tools;
}

// }}}

const tools = makeTools();

tools.add({ // brush {{{
	name: 'brush',
	key: 'b',
	icon: '<i class="fa-solid fa-paintbrush"></i>',
	mouseDown: function(e) {
		const canvas = layers.getActive().canvas;
		if (brushSize === 1) {
			canvas.drawPixel(canvasStartX, canvasStartY, brushColor.toRgb());
		} else {
			canvas.drawShape(canvasStartX, canvasStartY, brushShape, brushSize, brushColor.toRgb());
		}
	},
	mouseMove: function(e) {
		brushPreview.show();
		brushPreview.setPosition(e.clientX, e.clientY);
	},
	mouseDrag: function(e) {
		const canvas = layers.getActive().canvas;
		if (brushSize === 1) {
			canvas.drawLineWithPixels(canvasStartX, canvasStartY, canvasEndX, canvasEndY, brushColor.toRgb());
		} else {
			canvas.drawLineWithShape(canvasStartX, canvasStartY, canvasEndX, canvasEndY, brushShape, brushSize, brushColor.toRgb());
		}
		canvasStartX = canvasEndX;
		canvasStartY = canvasEndY;
	},
	mouseLeave: function(e) {
		brushPreview.hide();
	}
}); // }}}

tools.add({ // content-move {{{
	name: 'content-move',
	key: 'h',
	icon: '<i class="fa-regular fa-hand"></i>',
	mouseDown: function(e) {
		const canvas = layers.getActive().canvas;
		canvas.saveCanvas();
	},
	mouseDrag: function(e) {
		const canvas = layers.getActive().canvas;
		canvas.clearCanvas();
		canvas.restoreCanvas(dX, dY);
	},
}); // }}}

tools.add({ // move {{{
	name: 'move',
	key: 'm',
	icon: '<i class="fa-solid fa-arrows-up-down-left-right"></i>',
	mouseDown: function(e) {
		startX = e.clientX - easelElement.offsetLeft;
		startY = e.clientY - easelElement.offsetTop;
	},
	mouseDrag: function(e) {
		easelElement.style.left = dX + 'px';
		easelElement.style.top = dY + 'px';
	},
}); // }}}

tools.add({ // zoom {{{
	name: 'zoom',
	key: 'z',
	icon: '<i class="fa-solid fa-magnifying-glass"></i>',
	mouseDrag: function(e) {
		zoom += dX * dZoom;
		if (zoom < 0.1) zoom = 0.1;
		layers.zoom();
		brushPreview.update();
		startX = endX;
	}
}); // }}}

tools.add({ // bucket-fill {{{
	name: 'bucket-fill',
	key: 'k',
	icon: '<i class="fa-solid fa-fill"></i>',
	mouseDown: function(e) {
		const canvas = layers.getActive().canvas;
		canvas.floodFill(canvasStartX, canvasStartY, brushColor.toRgbaArray());
	}
}); // }}}

tools.add({ // color-picker {{{
	name: 'color-picker',
	key: 'a',
	icon: '<i class="fa-solid fa-eye-dropper"></i>',
	mouseDown: function(e) {
		const canvas = layers.getActive().canvas;
		const imageData = canvas.ctx.getImageData(canvasStartX, canvasStartY, 1, 1).data;
		const pickedColor = `rgb(${imageData[0]}, ${imageData[1]}, ${imageData[2]})`;
		brushColor.fromRgb(pickedColor);
		colorPreview.update();
	}
}); // }}}

tools.add({ // brush-size {{{
	name: 'brush-size',
	key: 'd',
	icon: '<i class="fa-regular fa-circle-dot"></i>',
	mouseMove: function(e) {
		brushPreview.show();
		brushPreview.setPosition(e.clientX, e.clientY);
	},
	mouseDrag: function(e) {
		brushSize += dX * dBrushSize;
		if (brushSize < 1) brushSize = 1;
		if (brushSize > maxBrushSize) brushSize = maxBrushSize;
		startX = endX;
		brushPreview.update();
	},
	mouseLeave: function(e) {
		brushPreview.hide();
	}
}); // }}}

tools.add({ // resize {{{
	name: 'resize',
	key: 'r',
	icon: '<i class="fa-solid fa-ruler-combined"></i>',
	mouseDrag: function(e) {
		let newWidth = layers.width + dX / zoom;
		let newHeight = layers.height + dY / zoom;
		layers.resize(newWidth, newHeight);
		startX = endX;
		startY = endY;
	}
}); // }}}

tools.add({ // color-mix {{{
	name: 'color-mix',
	key: 'x',
	icon: '<i class="fa-solid fa-mortar-pestle"></i>',
	mouseDown: function(e) {
		tempColor.copy(canvasColor);
		startTime = Date.now();
		interval = setInterval(() => {
			if (!tempColor.match(canvasColor)) {
				startTime = Date.now();
				tempColor.copy(canvasColor);
			}
			if (!canvasColor.isOpaque()) {
				startTime = Date.now();
			} else {
				const elapsedTime = Date.now() - startTime;
				const t = Math.min(1, elapsedTime / 10000);
				brushColor.mixx(canvasColor, t);
				colorPreview.update();
				if (!isMouseDown) {
					clearInterval(interval);
					startTime = Date.now();
				}
			}
		}, 50);
	},
	mouseUp: function(e) {
		clearInterval(interval);
	},
	mouseLeave: function(e) {
		clearInterval(interval);
	}
}); // }}}

// }}}

// PUCKS {{{

// FACTORY {{{

function makePuck({puckColor, key, editable=true}) {
	if (!puckColor) throw new Error('No puck color provided');


	const puck = {}
	puck.element = document.createElement('div');
	puck.element.style.backgroundColor = puckColor;
	puck.element.className = 'puck';

	if (editable) {
		const deleteHandle = document.createElement('div');
		deleteHandle.className = 'delete-handle';
		deleteHandle.innerHTML = '<i class="fa-solid fa-trash-can"></i>';
		puck.element.appendChild(deleteHandle);
		deleteHandle.addEventListener('click', () => {
			puck.element.remove();
		});
	}

	if (key) {
		puck.key = key;
		const keyHint = document.createElement('div');
		keyHint.className = 'key-hint';
		keyHint.innerHTML = key;
		puck.element.appendChild(keyHint);
	}

	puck.element.addEventListener('mousedown', (e) => {
		const startTime = Date.now();
		interval = setInterval(() => {
			const elapsedTime = Date.now() - startTime;
			const t = Math.min(1, elapsedTime / 10000);
			brushColor.mixxRgb(puck.element.style.backgroundColor, t);
			colorPreview.update();
		}, 50);
	});

	puck.element.addEventListener('mouseup', (e) => {
		clearInterval(interval);
	});

	puck.element.addEventListener('mouseleave', (e) => {
		clearInterval(interval);
	});

	puck.keydown = function(e) {
		startTime = Date.now();
		var interval = setInterval(() => {
			const elapsedTime = Date.now() - startTime;
			const t = Math.min(1, elapsedTime / 10000);
			brushColor.mixxRgb(puck.element.style.backgroundColor, t);
			colorPreview.update();
		}, 50);
		function onKeyUp() {
			clearInterval(interval);
			document.removeEventListener('keyup', onKeyUp);
		}
		document.addEventListener('keyup', onKeyUp);
	}

	commandBarElement.appendChild(puck.element);

	return puck;
}

function makePucks() {
	const pucks = [];

	pucks.add = function({puckColor, key, editable}) {
		const puck = makePuck({puckColor, key, editable});
		pucks.push(puck);
	}

	return pucks;
}

// }}}

const pucks = makePucks();


pucks.add({ // black
	puckColor: 'rgb(0, 0, 0)',
	key: '1',
	editable: false,
});

pucks.add({ // white
	puckColor: 'rgb(255, 255, 255)',
	key: '2',
	editable: false,
});

pucks.add({ // red
	puckColor: 'rgb(255, 0, 0)',
	key: '3',
	editable: false,
});

pucks.add({ // blue
	puckColor: 'rgb(0, 0, 255)',
	key: '5',
	editable: false,
});

pucks.add({ // yellow
	puckColor: 'rgb(255, 255, 0)',
	key: '6',
	editable: false,
});

pucks.add({ // cyan
	puckColor: 'rgb(0, 255, 255)',
	key: '7',
	editable: false,
});

pucks.add({ // magenta
	puckColor: 'rgb(255, 0, 255)',
	key: '8',
	editable: false,
});

pucks.add({ // green
	puckColor: 'rgb(0, 255, 0)',
	key: '4',
	editable: false,
});

// }}}

// INFO {{{

// FACTORY {{{

function makeInfo({name, updateFunction}) {
	if (!name) throw new Error('No name provided');
	if (!updateFunction) throw new Error('No update function provided');

	const info = {};
	info.name = name;
	info.updateFunction = updateFunction;

	info.element = document.createElement('span');
	info.element.className = 'info';

	const key = document.createElement('span');
	key.className = 'key';
	key.innerHTML = info.name + ':';

	const value = document.createElement('span');
	value.className = 'value';
	value.innerHTML = '0';

	info.element.appendChild(key);
	info.element.appendChild(value);

	infoBarElement.appendChild(info.element);

	info.update = function() {
		let v = updateFunction();
		if (v === undefined) v = '?';
		value.innerHTML = v;
	}

	return info;
}

function makeInfos() {
	const infos = []
	infos.add = function({name, updateFunction}) {
		const info = makeInfo({name, updateFunction});
		infos.push(info);
	}
	infos.update = function() {
		infos.forEach(function(info){
			info.update();
		});
	}
	return infos;
}

// }}}

const infos = makeInfos();

infos.add({
	name: 'zoom',
	updateFunction: function() {
		var percent = zoom * 100;
		return percent.toFixed(0) + '%';
	}
});

infos.add({
	name: 'brush',
	updateFunction: function() {
		return brushSize;
	}
});


infos.add({
	name: 'x',
	updateFunction: function() {
		return canvasEndX;
	}
});


infos.add({
	name: 'y',
	updateFunction: function() {
		return canvasEndY;
	}
});

infos.add({
	name: 'color',
	updateFunction: function() {
		return brushColor.toRgb();
	}
});

infos.add({
	name: 'width',
	updateFunction: function() {
		return "width";
	}
});

infos.add({
	name: 'height',
	updateFunction: function() {
		return "height";
	}
});

infos.add({
	name: 'shape',
	updateFunction: function() {
		return brushShape;
	}
});

infos.add({
	name: 'tool',
	updateFunction: function() {
		return tools.getActive().name;
	}
});

infos.add({
	name: 'cursor-color',
	updateFunction: function() {
		return canvasColor;
	}
});

// }}}

// MOUSE EVENT LISTENERS {{{

studioElement.addEventListener('mousedown', (e) => {
	const canvas = layers.getActive().canvas;
	isMouseDown = true;
	startX = e.clientX;
	startY = e.clientY;
	canvasStartX = canvas.getPositionOnCanvas(e).x;
	canvasStartY = canvas.getPositionOnCanvas(e).y;
	canvasEndX = canvas.getPositionOnCanvas(e).x;
	canvasEndX = canvas.getPositionOnCanvas(e).y;
	canvasColor.fromRgbaArray(canvas.getRgbaColorArrayAtPixel(canvasStartX, canvasStartY));

	for (var i = 0; i < tools.length; i++) {
		var tool = tools[i];
		if (tool.active) {
			if (tool.mouseDown) {
				tool.mouseDown(e);
			}
		}
	}

	infos.update();

});

studioElement.addEventListener('mousemove', (e) => {
	const canvas = layers.getActive().canvas;
	endX = e.clientX;
	endY = e.clientY;
	dX = endX - startX;
	dY = endY - startY;
	canvasEndX = canvas.getPositionOnCanvas(e).x;
	canvasEndY = canvas.getPositionOnCanvas(e).y;
	canvasDX = canvasEndX - canvasStartX;
	canvasDY = canvasEndY - canvasStartY;
	canvasColor.fromRgbaArray(canvas.getRgbaColorArrayAtPixel(canvasEndX, canvasEndY));

	for (var i = 0; i < tools.length; i++) {
		var tool = tools[i];
		if (tool.active) {
			if (tool.mouseMove) {
				tool.mouseMove(e);
			}
		}
	}

	if (isMouseDown) {
		for (var i = 0; i < tools.length; i++) {
			var tool = tools[i];
			if (tool.active) {
				if (tool.mouseDrag) {
					tool.mouseDrag(e);
				}
			}
		}
	}

	infos.update();

});


studioElement.addEventListener('mouseup', () => {
	isMouseDown = false;

	for (var i = 0; i < tools.length; i++) {
		var tool = tools[i];
		if (tool.active) {
			if (tool.mouseUp) {
				tool.mouseUp();
				break;
			}
		}
	}

	infos.update();
});

studioElement.addEventListener('mouseleave', () => {
	isMouseDown = false;

	for (var i = 0; i < tools.length; i++) {
		var tool = tools[i];
		if (tool.active) {
			if (tool.mouseLeave) {
				tool.mouseLeave();
				break;
			}
		}
	}

	infos.update();
});

// }}}

// KEYBINDINGS {{{

document.addEventListener('keydown', (e) => {
	if (isKeyDown) return;

	tools.forEach(tool => {
		if (tool.key.toLowerCase() === e.key.toLowerCase()) {
			tools.activate(tool.name);
		}
	});

	commands.forEach(command => {
		if (command.key.toLowerCase() === e.key.toLowerCase()) {
			command.func();
		}
	});

	pucks.filter(puck => puck.key !== undefined).forEach(puck => {
		if (puck.key.toLowerCase() === e.key.toLowerCase()) {
			puck.keydown(e);
		}
	});

	isKeyDown = true;

});

document.addEventListener('keyup', (e) => {
	tools.forEach(tool => {
		if (tool.key.toLowerCase() === e.key) {
			tools.restore();
		}
	});

	isKeyDown = false;

});

// }}}

layers.resetPosition();
tools.activate('brush');