// Based on bspatch by Colin Percival (2003-2005)
// Zig port by Yoav Givati (2024)
// TypeScript port for SparkBun — uses gzip instead of zstd

const MAGIC = "SBDIFF10";

function offtin(buf: Uint8Array, offset: number): bigint {
	let y = 0n;
	for (let i = 0; i < 7; i++) {
		y |= BigInt(buf[offset + i]!) << BigInt(i * 8);
	}
	y |= BigInt(buf[offset + 7]! & 0x7f) << 56n;
	if (buf[offset + 7]! & 0x80) {
		y = -y;
	}
	return y;
}

export function applyPatch(oldData: Uint8Array, patchData: Uint8Array): Uint8Array {
	const header = patchData.slice(0, 32);
	const magic = new TextDecoder().decode(header.slice(0, 8));
	if (magic !== MAGIC) {
		throw new Error(`Invalid patch magic: ${magic} (expected ${MAGIC})`);
	}

	const controlLen = Number(offtin(header, 8));
	const diffLen = Number(offtin(header, 16));
	const newSize = Number(offtin(header, 24));

	if (controlLen < 0 || diffLen < 0 || newSize < 0) {
		throw new Error("Corrupt patch: negative header length");
	}

	const controlStart = 32;
	const diffStart = controlStart + controlLen;
	const extraStart = diffStart + diffLen;

	if (diffStart > patchData.length || extraStart > patchData.length) {
		throw new Error("Corrupt patch: header lengths exceed patch size");
	}

	const controlBlock = Bun.gunzipSync(patchData.slice(controlStart, diffStart));
	const diffBlock = Bun.gunzipSync(patchData.slice(diffStart, extraStart));
	const extraBlock = Bun.gunzipSync(patchData.slice(extraStart));

	const newData = new Uint8Array(newSize);

	let controlPos = 0;
	let diffPos = 0;
	let extraPos = 0;
	let oldPos = 0;
	let newPos = 0;

	while (controlPos < controlBlock.length) {
		const readDiffBy = Number(offtin(controlBlock, controlPos));
		controlPos += 8;
		const readExtraBy = Number(offtin(controlBlock, controlPos));
		controlPos += 8;
		const seekBy = Number(offtin(controlBlock, controlPos));
		controlPos += 8;

		if (newPos + readDiffBy > newSize) {
			throw new Error("Corrupt patch: diff would write past end of file");
		}

		for (let i = 0; i < readDiffBy; i++) {
			newData[newPos + i] = (oldData[oldPos + i]! + diffBlock[diffPos + i]!) & 0xff;
		}
		diffPos += readDiffBy;
		newPos += readDiffBy;
		oldPos += readDiffBy;

		if (readExtraBy > 0) {
			newData.set(extraBlock.slice(extraPos, extraPos + readExtraBy), newPos);
			extraPos += readExtraBy;
			newPos += readExtraBy;
		}

		oldPos += seekBy;
	}

	return newData;
}

if (import.meta.main) {
	const args = process.argv.slice(2);
	if (args.length !== 3) {
		console.error("Usage: bspatch <oldfile> <newfile> <patchfile>");
		process.exit(1);
	}
	const [oldPath, newPath, patchPath] = args as [string, string, string];
	const oldData = new Uint8Array(await Bun.file(oldPath).arrayBuffer());
	const patchData = new Uint8Array(await Bun.file(patchPath).arrayBuffer());
	const newData = applyPatch(oldData, patchData);
	await Bun.write(newPath, newData);
	const sizeMB = (newData.length / (1024 * 1024)).toFixed(2);
	console.error(`Completed - New file: ${sizeMB} MB`);
}
