// @ts-check

// This script is run within the webview itself
(function () {
	// @ts-ignore
	const vscode = acquireVsCodeApi();

	/**
	 * A drawn line.
	 */
	class Stroke {
		constructor(/** @type {string} */ color, /** @type {Array<[number, number]> | undefined} */ stroke) {
			this.color = color;
			/** @type {Array<[number, number]>} */
			this.stroke = stroke || [];
		}

		addPoint(/** @type {number} */ x, /** @type {number} */ y) {
			this.stroke.push([x, y])
		}
	}

	/**
	 * @param {Uint8Array} initialContent 
	 * @return {Promise<HTMLImageElement>}
	 */
	async function loadImageFromData(initialContent) {
		const blob = new Blob([initialContent], { 'type': 'image/png' });
		const url = URL.createObjectURL(blob);
		try {
			const img = document.createElement('img');
			img.crossOrigin = 'anonymous';
			img.src = url;
			await new Promise((resolve, reject) => {
				img.onload = resolve;
				img.onerror = reject;
			});
			return img;
		} finally {
			URL.revokeObjectURL(url);
		}
	}

	class PawDrawEditor {
		constructor( /** @type {HTMLElement} */ parent) {
			this.ready = false;

			this.editable = false;

			this.drawingColor = 'black';

			/** @type {Array<Stroke>} */
			this.strokes = [];

			/** @type {Stroke | undefined} */
			this.currentStroke = undefined;

			this._initElements(parent);
		}

		addPoint(/** @type {number} */ x, /** @type {number} */ y) {
			if (this.currentStroke) {
				this.currentStroke.addPoint(x, y)
			}
		}

		beginStoke(/** @type {string} */ color) {
			this.currentStroke = new Stroke(color);
			this.strokes.push(this.currentStroke);
		}

		endStroke() {
			const previous = this.currentStroke;
			this.currentStroke = undefined;
			return previous;
		}

		setEditable(editable) {
			this.editable = editable;
			const colorButtons = /** @type {NodeListOf<HTMLButtonElement>} */ (document.querySelectorAll('.drawing-controls button'));
			for (const colorButton of colorButtons) {
				colorButton.disabled = !editable;
			}
		}

		_initElements(/** @type {HTMLElement} */ parent) {
			const colorButtons = /** @type {NodeListOf<HTMLButtonElement>} */ (document.querySelectorAll('.drawing-controls button'));
			for (const colorButton of colorButtons) {
				colorButton.addEventListener('click', e => {
					e.stopPropagation();
					colorButtons.forEach(button => button.classList.remove('active'));
					colorButton.classList.add('active');
					this.drawingColor = colorButton.dataset['color'];
				});
			}

			this.wrapper = document.createElement('div');
			this.wrapper.style.position = 'relative';
			parent.append(this.wrapper);

			this.initialCanvas = document.createElement('canvas');
			this.initialCtx = this.initialCanvas.getContext('2d');
			this.wrapper.append(this.initialCanvas);

			this.drawingCanvas = document.createElement('canvas');
			this.drawingCanvas.style.position = 'absolute';
			this.drawingCanvas.style.top = '0';
			this.drawingCanvas.style.left = '0';
			this.drawingCtx = this.drawingCanvas.getContext('2d');
			this.wrapper.append(this.drawingCanvas);

			let isDrawing = false;

			parent.addEventListener('mousedown', () => {
				if (!this.ready || !this.editable) {
					return;
				}

				this.beginStoke(this.drawingColor);
				this.drawingCtx.strokeStyle = this.drawingColor;

				isDrawing = true;
				document.body.classList.add('isDrawing');
				this.drawingCtx.beginPath();
			});

			document.body.addEventListener('mouseup', async () => {
				if (!isDrawing || !this.ready || !this.editable) {
					return;
				}

				isDrawing = false;
				document.body.classList.remove('isDrawing');
				this.drawingCtx.closePath();

				const edit = this.endStroke();

				if (edit.stroke.length) {
					vscode.postMessage({
						type: 'stroke',
						color: edit.color,
						stroke: edit.stroke,
					});
				}
			});

			parent.addEventListener('mousemove', e => {
				if (!isDrawing || !this.ready || !this.editable) {
					return;
				}
				const rect = this.wrapper.getBoundingClientRect();
				const x = e.clientX - rect.left;
				const y = e.clientY - rect.top;
				this.drawingCtx.lineTo(x, y);
				this.drawingCtx.stroke();
				this.addPoint(x, y);
			});
		}

		_redraw() {
			this.drawingCtx.clearRect(0, 0, this.drawingCanvas.width, this.drawingCanvas.height);
			for (const stroke of this.strokes) {
				this.drawingCtx.strokeStyle = stroke.color;
				this.drawingCtx.beginPath();
				for (const [x, y] of stroke.stroke) {
					this.drawingCtx.lineTo(x, y);
				}
				this.drawingCtx.stroke();
				this.drawingCtx.closePath();
			}
		}

		/**
		 * @param {Uint8Array | undefined} data 
		 * @param {Array<Stroke> | undefined} strokes 
		 */
		async reset(data, strokes = []) {
			if (data) {
				const img = await loadImageFromData(data);
				this.initialCanvas.width = this.drawingCanvas.width = img.naturalWidth;
				this.initialCanvas.height = this.drawingCanvas.height = img.naturalHeight;
				this.initialCtx.drawImage(img, 0, 0);
				this.ready = true;
			}

			this.strokes = strokes;
			this._redraw();
		}

		/**
		 * @param {Array<Stroke> | undefined} strokes 
		 */
		async resetUntitled(strokes = []) {
			const size = 100;
			this.initialCanvas.width = this.drawingCanvas.width = size;
			this.initialCanvas.height = this.drawingCanvas.height = size;

			this.initialCtx.save();
			{
				this.initialCtx.fillStyle = 'white';
				this.initialCtx.fillRect(0, 0, size, size);
			}
			this.initialCtx.restore();

			this.ready = true;

			this.strokes = strokes;
			this._redraw();
		}

		/** @return {Promise<Uint8Array>} */
		async getImageData() {
			const outCanvas = document.createElement('canvas');
			outCanvas.width = this.drawingCanvas.width;
			outCanvas.height = this.drawingCanvas.height;

			const outCtx = outCanvas.getContext('2d');
			outCtx.drawImage(this.initialCanvas, 0, 0);
			outCtx.drawImage(this.drawingCanvas, 0, 0);

			const blob = await new Promise(resolve => {
				outCanvas.toBlob(resolve, 'image/png')
			});

			return new Uint8Array(await blob.arrayBuffer());
		}
	}

	const editor = new PawDrawEditor(document.querySelector('.drawing-canvas'));

	// Handle messages from the extension
	window.addEventListener('message', async e => {
		const { type, body, requestId } = e.data;
		switch (type) {
			case 'init':
				{
					editor.setEditable(body.editable);
					if (body.untitled) {
						await editor.resetUntitled();
						return;
					} else {
						// Load the initial image into the canvas.
						await editor.reset(body.value);
						return;
					}
				}
			case 'update':
				{
					const strokes = body.edits.map(edit => new Stroke(edit.color, edit.stroke));
					await editor.reset(body.content, strokes)
					return;
				}
			case 'getFileData':
				{
					// Get the image data for the canvas and post it back to the extension.
					editor.getImageData().then(data => {
						vscode.postMessage({ type: 'response', requestId, body: Array.from(data) });
					});
					return;
				}
		}
	});

	// Signal to VS Code that the webview is initialized.
	vscode.postMessage({ type: 'ready' });
}());
