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

const MAGIC = "SBDIFF10";

function offtout(x: bigint, buf: Uint8Array, offset: number): void {
	let y: bigint;
	if (x < 0n) {
		y = (-x) | (1n << 63n);
	} else {
		y = x;
	}
	for (let i = 0; i < 8; i++) {
		buf[offset + i] = Number((y >> BigInt(i * 8)) & 0xffn);
	}
}

function matchlen(oldData: Uint8Array, oldStart: number, newData: Uint8Array, newStart: number): number {
	const maxLen = Math.min(oldData.length - oldStart, newData.length - newStart);
	let i = 0;
	while (i < maxLen && oldData[oldStart + i] === newData[newStart + i]) {
		i++;
	}
	return i;
}

function search(
	suffixArray: Int32Array,
	oldData: Uint8Array,
	newData: Uint8Array,
	newStart: number,
	lo: number,
	hi: number,
	bestPos: { value: number },
): number {
	if (hi - lo < 2) {
		const loMatch = matchlen(oldData, suffixArray[lo]!, newData, newStart);
		const hiMatch = matchlen(oldData, suffixArray[hi]!, newData, newStart);
		if (loMatch > hiMatch) {
			bestPos.value = suffixArray[lo]!;
			return loMatch;
		}
		bestPos.value = suffixArray[hi]!;
		return hiMatch;
	}

	const mid = lo + ((hi - lo) >>> 1);
	const midPos = suffixArray[mid]!;
	const compareLen = Math.min(oldData.length - midPos, newData.length - newStart);

	let cmp = 0;
	for (let i = 0; i < compareLen; i++) {
		if (oldData[midPos + i]! < newData[newStart + i]!) { cmp = -1; break; }
		if (oldData[midPos + i]! > newData[newStart + i]!) { cmp = 1; break; }
	}

	if (cmp < 0) {
		return search(suffixArray, oldData, newData, newStart, mid, hi, bestPos);
	}
	return search(suffixArray, oldData, newData, newStart, lo, mid, bestPos);
}

function qsufsort(data: Uint8Array): Int32Array {
	const n = data.length;
	const sa = new Int32Array(n + 1);
	for (let i = 0; i <= n; i++) sa[i] = i;
	sa.sort((a, b) => {
		const la = n - a;
		const lb = n - b;
		const len = la < lb ? la : lb;
		for (let i = 0; i < len; i++) {
			if (data[a + i] !== data[b + i]) return data[a + i]! - data[b + i]!;
		}
		return la - lb;
	});
	return sa;
}

export function createPatch(oldData: Uint8Array, newData: Uint8Array): Uint8Array {
	const oldSize = oldData.length;
	const newSize = newData.length;

	const suffixArray = qsufsort(oldData);

	const controlEntries: bigint[] = [];
	const diffBytes: number[] = [];
	const extraBytes: number[] = [];

	let scan = 0;
	let len = 0;
	let lastScan = 0;
	let lastPos = 0;
	let lastOffset = 0;
	let pos = 0;
	const bestPos = { value: 0 };

	while (scan < newSize) {
		let oldscore = 0;
		scan += len;

		let scsc = scan;
		while (scan < newSize) {
			len = search(suffixArray, oldData, newData, scan, 0, oldSize, bestPos);
			pos = bestPos.value;

			while (scsc < scan + len) {
				if (scsc + lastOffset < oldSize && oldData[scsc + lastOffset] === newData[scsc]) {
					oldscore++;
				}
				scsc++;
			}

			if (len === oldscore && len !== 0) break;
			if (len > oldscore + 8) break;
			if (scan + lastOffset < oldSize && oldData[scan + lastOffset] === newData[scan]) {
				oldscore--;
			}
			scan++;
		}

		if (len !== oldscore || scan >= newSize) {
			let s = 0;
			let sf = 0;
			let lenf = 0;

			for (let i = 0; lastScan + i < scan && lastPos + i < oldSize; i++) {
				if (oldData[lastPos + i] === newData[lastScan + i]) s++;
				if (s * 2 - i > sf * 2 - lenf) {
					sf = s;
					lenf = i + 1;
				}
			}

			let lenb = 0;
			if (scan < newSize) {
				s = 0;
				let sb = 0;
				for (let i = 1; scan >= lastScan + i && pos >= i; i++) {
					if (oldData[pos - i] === newData[scan - i]) s++;
					if (s * 2 - i > sb * 2 - lenb) {
						sb = s;
						lenb = i;
					}
				}
			}

			if (lastScan + lenf > scan - lenb) {
				const overlap = (lastScan + lenf) - (scan - lenb);
				s = 0;
				let ss = 0;
				let lens = 0;
				for (let i = 0; i < overlap; i++) {
					if (newData[lastScan + lenf - overlap + i] === oldData[lastPos + lenf - overlap + i]) s++;
					if (newData[scan - lenb + i] === oldData[pos - lenb + i]) s--;
					if (s > ss) {
						ss = s;
						lens = i + 1;
					}
				}
				lenf += lens - overlap;
				lenb -= lens;
			}

			for (let i = 0; i < lenf; i++) {
				diffBytes.push((newData[lastScan + i]! - oldData[lastPos + i]! + 256) & 0xff);
			}

			const extraLen = (scan - lenb) - (lastScan + lenf);
			for (let i = 0; i < extraLen; i++) {
				extraBytes.push(newData[lastScan + lenf + i]!);
			}

			controlEntries.push(BigInt(lenf), BigInt(extraLen), BigInt((pos - lenb) - (lastPos + lenf)));

			lastScan = scan - lenb;
			lastPos = pos - lenb;
			lastOffset = pos - scan;
		}
	}

	const controlBuf = new Uint8Array(controlEntries.length * 8);
	for (let i = 0; i < controlEntries.length; i++) {
		offtout(controlEntries[i]!, controlBuf, i * 8);
	}

	const controlCompressed = Bun.gzipSync(controlBuf);
	const diffCompressed = Bun.gzipSync(new Uint8Array(diffBytes));
	const extraCompressed = Bun.gzipSync(new Uint8Array(extraBytes));

	const patchSize = 32 + controlCompressed.length + diffCompressed.length + extraCompressed.length;
	const patch = new Uint8Array(patchSize);

	const encoder = new TextEncoder();
	patch.set(encoder.encode(MAGIC), 0);
	offtout(BigInt(controlCompressed.length), patch, 8);
	offtout(BigInt(diffCompressed.length), patch, 16);
	offtout(BigInt(newSize), patch, 24);

	let offset = 32;
	patch.set(controlCompressed, offset);
	offset += controlCompressed.length;
	patch.set(diffCompressed, offset);
	offset += diffCompressed.length;
	patch.set(extraCompressed, offset);

	return patch;
}

if (import.meta.main) {
	const args = process.argv.slice(2);
	if (args.length !== 3) {
		console.error("Usage: bsdiff <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 newData = new Uint8Array(await Bun.file(newPath).arrayBuffer());
	const patch = createPatch(oldData, newData);
	await Bun.write(patchPath, patch);
	const patchKB = (patch.length / 1024).toFixed(2);
	const ratio = ((patch.length / newData.length) * 100).toFixed(1);
	console.error(`Completed - Patch: ${patchKB} KB (${ratio}% of new size)`);
}
