import libheif from "libheif-js";

const supportedMIMETypes = ["image/png", "image/jpeg"];

type ResultType = "Blob" | "ImageData";

const utils = {
	blobToDataURL: function (blob: Blob): Promise<string> {
		return new Promise((resolve, reject) => {
			let reader = new FileReader();
			reader.onerror = function () {
				reject("ERR_DOM Error on converting blob to data URL");
			};
			reader.onload = (e) => {
				resolve((reader as any).result);
			};
			reader.readAsDataURL(blob);
		});
	},

	dataURItoBlob: function (dataURI: string): Blob | string {
		try {
			var byteString = atob(dataURI.split(",")[1]);
			var mimeString = dataURI.split(",")[0].split(":")[1].split(";")[0];
			var ab = new ArrayBuffer(byteString.length);
			var ia = new Uint8Array(ab);
			for (var i = 0; i < byteString.length; i++) {
				ia[i] = byteString.charCodeAt(i);
			}
			var blob = new Blob([ab], { type: mimeString });
			return blob;
		} catch (e) {
			return "ERR_DOM Error on converting data URI to blob " + e &&
				e.toString
				? e.toString()
				: e;
		}
	},

	imageDataToBlob: function ({
		imageData,
		toType = "image/png",
		quality = 0.92,
	}: {
		imageData: ImageData;
		toType?: string;
		quality?: number;
	}): Promise<Blob> {
		// normalize quality
		if (quality > 1 || quality < 0) {
			quality = 0.92;
		}
		// normalize MIME type
		if (supportedMIMETypes.indexOf(toType) === -1) {
			toType = "image/png";
		}
		return new Promise((resolve, reject) => {
			let canvas: HTMLCanvasElement | null = null;

			try {
				canvas = document.createElement("canvas");
			} catch (e) {}

			if (!canvas) {
				return reject(
					"ERR_CANVAS Error on converting imagedata to blob: Could not create canvas element"
				);
			}

			canvas.width = imageData.width;
			canvas.height = imageData.height;
			const ctx = canvas.getContext("2d");
			if (!ctx) {
				return reject(
					"ERR_CANVAS Error on converting imagedata to blob: Could not get canvas context"
				);
			}
			ctx.putImageData(imageData, 0, 0);
			canvas.toBlob(
				(blob) => {
					if (!blob) {
						return reject(
							"ERR_CANVAS Error on converting imagedata to blob: Could not get blob from canvas"
						);
					}
					return resolve(blob);
				},
				toType,
				quality
			);
		});
	},

	otherImageType: function (buffer: ArrayBuffer) {
		/**
		 * Some confusion might arise when passing a regular image
		 * like jpeg/png and getting "format is not supported"
		 * so to solve this, we should detect if the image is
		 * already browser-readable.
		 */
		const arr = new Uint8Array(buffer).subarray(0, 4);
		let header = "";
		for (let i = 0; i < arr.length; i++) {
			header = header + arr[i].toString(16);
		}
		switch (header) {
			case "89504e47":
				return "image/png";
			case "ffd8ffe0":
			case "ffd8ffe1":
			case "ffd8ffe2":
			case "ffd8ffe3":
			case "ffd8ffe8":
				return "image/jpeg";
			default:
				return false;
		}
	},

	error: function (message: string) {
		/**
		 * Error coding system:
		 *
		 * UNKNOWN = 0
		 * USER errors = 1
		 * LIBHEIF errors = 2
		 * GIF errors = 3
		 * DOM errors = 4
		 * CANVAS errors = 5
		 *
		 */

		let code = 0;

		if (!message) {
			message = "ERR_UNKNOWN";
		} else if (typeof message !== "string") {
			if ((message as any).toString) {
				message = (message as any).toString();
			} else {
				message = JSON.stringify(message);
			}
		}

		const headers = [
			"ERR_USER",
			"ERR_LIBHEIF",
			"ERR_GIF",
			"ERR_DOM",
			"ERR_CANVAS",
		];
		for (let index = 0; index < headers.length; index++) {
			const header = headers[index];
			if (message.indexOf(header) === 0) {
				code = index + 1;
			}
		}
		return {
			code,
			message,
		};
	},
};

function decodeBuffer(buffer: ArrayBuffer): Promise<ImageData> {
	return new Promise((resolve, reject) => {
		try {
			const decoder = new libheif.HeifDecoder();
			let imagesArr = decoder.decode(buffer);
			if (!imagesArr || !imagesArr.length) {
				throw "ERR_LIBHEIF format not supported";
			}
			imagesArr = imagesArr.filter((x) => {
				let valid = true;
				try {
					/*
					sometimes the heic container is valid
					yet the images themselves are corrupt
					*/
					x.get_height();
				} catch (e) {
					valid = false;
				}
				return valid;
			});
			if (!imagesArr.length) {
				throw "ERR_LIBHEIF Heic doesn't contain valid images";
			}

			processSingleImage(imagesArr[0])
			.then((imageData) => {
				return resolve(imageData as ImageData);
			})
			.catch((e) => {
				return reject(e && e.toString ? e.toString() : e);
			});
		} catch (e) {
			return reject(e && e.toString ? e.toString() : e);
		}
	});
}

function processSingleImage(image: libheif.DecodeResult): Promise<ImageData> {
	return new Promise((resolve, reject) => {
		const w = image.get_width();
		const h = image.get_height();
		const whiteImage = new ImageData(w, h);
		for (let i = 0; i < w * h; i++) {
			whiteImage.data[i * 4 + 3] = 255;
		}
		image.display(whiteImage, (imageData: ImageData) => {
			if (!imageData) {
				return reject(
					"ERR_LIBHEIF Error while processing single image and generating image data, could not ensure image"
				);
			}
			return resolve(imageData);
		});
	});
}

function heic2any({
	blob,
	toType = "image/png",
	quality = 0.92
}: {
	blob: Blob;
	toType?: string;
	quality?: number;
}): Promise<Blob | Blob[]> {
	return new Promise(
		(
			resolve,
			reject: (reason: { code: number; message: string }) => void
		) => {
			if (!(blob instanceof Blob)) {
				utils.error(`ERR_USER library only accepts BLOBs as input`);
			}
			if (typeof quality !== "number") {
				utils.error(
					`ERR_USER "quality" parameter should be of type "number"`
				);
			}
			const reader = new FileReader();
			reader.onload = (e) => {
				const buffer = (e as any).target.result;
				const otherImageType = utils.otherImageType(buffer);
				if (otherImageType) {
					return reject(
						utils.error(
							`ERR_USER Image is already browser readable: ${otherImageType}`
						)
					);
				}
				decodeBuffer(buffer)
					.then((imageData) => {
						return utils.imageDataToBlob({
							imageData,
							toType,
							quality,
						});
					})
					.then((blob) => {
						resolve(blob);
					})
					.catch((e) => {
						reject(utils.error(e));
					});
			};
			reader.readAsArrayBuffer(blob);
		}
	);
}

export default heic2any;