
// The following code is from png-to-ico
// (https://github.com/steambap/png-to-ico/blob/main/index.js),
// With minor modifications for TypeScript.

// The reason why the code was copied/pasted instead of being imported is because
// the original package has a dependency on Node, whereas the present
// package can be used in a browser environment.

// Thank you Weilin Shi, the author of png-to-ico!

import { ImageAndMeta } from "../svg/adapter";

export const imagesToIco = (images: ImageAndMeta[]) => {
	const header = getHeader(images.length);
	const headerAndIconDir = [header];
	const imageDataArr: Buffer[] = [];

	let len = header.length;
	let offset = header.length + 16 * images.length;

	images.forEach(img => {
		const dir = getDir(img, offset);
		const bmpInfoHeader = getBmpInfoHeader(img);
		const dib = getDib(img);

		len += dir.length + bmpInfoHeader.length + dib.length;
		const newSize = bmpInfoHeader.length + dib.length;
		offset += newSize;

		dir.writeUInt32LE(newSize, 8);
		headerAndIconDir.push(dir);
		imageDataArr.push(bmpInfoHeader, dib);
	});

	return Buffer.concat(headerAndIconDir.concat(imageDataArr) as unknown as Uint8Array[], len);
}

// https://en.wikipedia.org/wiki/ICO_(file_format)
function getHeader(numOfImages: number) {
	const buf = Buffer.alloc(6);

	buf.writeUInt16LE(0, 0); // Reserved. Must always be 0.
	buf.writeUInt16LE(1, 2); // Specifies image type: 1 for icon (.ICO) image
	buf.writeUInt16LE(numOfImages, 4); // Specifies number of images in the file.

	return buf;
}

function getDir(img: ImageAndMeta, offset: number) {
	const buf = Buffer.alloc(16);
	const bitmap = img //.bitmap;
	const width = bitmap.width >= 256 ? 0 : bitmap.width;
	const height = width;
	const bpp = 32;

	buf.writeUInt8(width, 0); // Specifies image width in pixels.
	buf.writeUInt8(height, 1); // Specifies image height in pixels.
	buf.writeUInt8(0, 2); // Should be 0 if the image does not use a color palette.
	buf.writeUInt8(0, 3); // Reserved. Should be 0.
	buf.writeUInt16LE(1, 4); // Specifies color planes. Should be 0 or 1.
	buf.writeUInt16LE(bpp, 6); // Specifies bits per pixel.
	buf.writeUInt32LE(0, 8); // Specifies the size of the image's data in bytes
	buf.writeUInt32LE(offset, 12); // Specifies the offset of BMP or PNG data from the beginning of the ICO/CUR file

	return buf;
}

// https://en.wikipedia.org/wiki/BMP_file_format
function getBmpInfoHeader(img: ImageAndMeta): Buffer {
	const buf = Buffer.alloc(40);
	const bitmap = img; //.bitmap;
	const width = bitmap.width;
	// https://en.wikipedia.org/wiki/ICO_(file_format)
	// ...Even if the AND mask is not supplied,
	// if the image is in Windows BMP format,
	// the BMP header must still specify a doubled height.
	const height = width * 2;
	const bpp = 32;

	buf.writeUInt32LE(40, 0); // The size of this header (40 bytes)
	buf.writeInt32LE(width, 4); // The bitmap width in pixels (signed integer)
	buf.writeInt32LE(height, 8); // The bitmap height in pixels (signed integer)
	buf.writeUInt16LE(1, 12); // The number of color planes (must be 1)
	buf.writeUInt16LE(bpp, 14); // The number of bits per pixel
	buf.writeUInt32LE(0, 16); // The compression method being used.
	buf.writeUInt32LE(0, 20); // The image size.
	buf.writeInt32LE(0, 24); // The horizontal resolution of the image. (signed integer)
	buf.writeInt32LE(0, 28); // The vertical resolution of the image. (signed integer)
	buf.writeUInt32LE(0, 32); // The number of colors in the color palette, or 0 to default to 2n
	buf.writeUInt32LE(0, 36); // The number of important colors used, or 0 when every color is important; generally ignored.

	return buf;
}

// https://en.wikipedia.org/wiki/BMP_file_format
// Note that the bitmap data starts with the lower left hand corner of the image.
// blue green red alpha in order
function getDib(img: ImageAndMeta) {
	const bitmap = img; //.bitmap;
	const size = bitmap.data.length;
	const width = bitmap.width;
	const height = width;
	const andMapRow = getRowStride(width);
	const andMapSize = andMapRow * height;
	const buf = Buffer.alloc(size + andMapSize);
	// xor map
	for (let y = 0; y < height; y++) {
		for (let x = 0; x < width; x++) {
			const pxColor = getPixelColor(img, x, y);

			const r = (pxColor >> 24) & 255;
			const g = (pxColor >> 16) & 255;
			const b = (pxColor >> 8) & 255;
			const a = pxColor & 255;
			const newColor = b | (g << 8) | (r << 16) | (a << 24);

			const pos = ((height - y - 1) * width + x) * 4;

			buf.writeInt32LE(newColor, pos);
		}
	}

	// and map. It's padded out to 32 bits per line
	for (let y = 0; y < height; y++) {
		for (let x = 0; x < width; x++) {
			const pxColor = getPixelColor(img, x, y);
			// TODO make threshhold configurable
			const alpha = (pxColor & 255) > 0 ? 0 : 1;
			const bitNum = (height - y - 1) * width + x;
			// width per line in multiples of 32 bits
			const width32 =
				width % 32 === 0 ? Math.floor(width / 32) : Math.floor(width / 32) + 1;

			const line = Math.floor(bitNum / width);
			const offset = Math.floor(bitNum % width);
			const bitVal = alpha & 0x00000001;

			const pos = size + line * width32 * 4 + Math.floor(offset / 8);
			const newVal = buf.readUInt8(pos) | (bitVal << (7 - (offset % 8)));
			buf.writeUInt8(newVal, pos);
		}
	}

	return buf;
}

function getRowStride(width: number) {
	if (width % 32 === 0) {
		return width / 8;
	} else {
		return 4 * (Math.floor(width / 32) + 1);
	}
}

function getPixelColor(png: ImageAndMeta, x: number, y: number) {
	let xi = x < 0 ? 0: x;
	let yi = y < 0 ? 0: y;

	if (x >= png.width) xi = png.width - 1;
	if (y >= png.height) yi = png.height - 1;

	const i = (xi < 0 || xi >= png.width) || (yi < 0 || yi >= png.height)
		? -1
		: (png.width * yi + xi) << 2
	;
		
	return png.data.readUInt32BE(i);
}
