export function encodeBase32UpperCase(bytes: Uint8Array): string {
	return encodeBase32_internal(bytes, base32UpperCaseAlphabet, EncodingPadding.Include);
}

export function encodeBase32UpperCaseNoPadding(bytes: Uint8Array): string {
	return encodeBase32_internal(bytes, base32UpperCaseAlphabet, EncodingPadding.None);
}

export function encodeBase32LowerCase(bytes: Uint8Array): string {
	return encodeBase32_internal(bytes, base32LowerCaseAlphabet, EncodingPadding.Include);
}

export function encodeBase32LowerCaseNoPadding(bytes: Uint8Array): string {
	return encodeBase32_internal(bytes, base32LowerCaseAlphabet, EncodingPadding.None);
}

/** Replaced: Use encodeBase32UpperCase() instead. */
export function encodeBase32(bytes: Uint8Array): string {
	return encodeBase32UpperCase(bytes);
}

/** Replaced: Use encodeBase32UpperCaseNoPadding() instead. */
export function encodeBase32NoPadding(bytes: Uint8Array): string {
	return encodeBase32UpperCaseNoPadding(bytes);
}

function encodeBase32_internal(
	bytes: Uint8Array,
	alphabet: string,
	padding: EncodingPadding
): string {
	let result = '';
	for (let i = 0; i < bytes.byteLength; i += 5) {
		let buffer = 0n;
		let bufferBitSize = 0;
		for (let j = 0; j < 5 && i + j < bytes.byteLength; j++) {
			buffer = (buffer << 8n) | BigInt(bytes[i + j]);
			bufferBitSize += 8;
		}
		if (bufferBitSize % 5 !== 0) {
			buffer = buffer << BigInt(5 - (bufferBitSize % 5));
			bufferBitSize += 5 - (bufferBitSize % 5);
		}
		for (let j = 0; j < 8; j++) {
			if (bufferBitSize >= 5) {
				result += alphabet[Number((buffer >> BigInt(bufferBitSize - 5)) & 0x1fn)];
				bufferBitSize -= 5;
			} else if (bufferBitSize > 0) {
				result += alphabet[Number((buffer << BigInt(6 - bufferBitSize)) & 0x3fn)];
				bufferBitSize = 0;
			} else if (padding === EncodingPadding.Include) {
				result += '=';
			}
		}
	}
	return result;
}

export function decodeBase32(encoded: string): Uint8Array {
	return decodeBase32_internal(encoded, base32DecodeMap, DecodingPadding.Required);
}

export function decodeBase32IgnorePadding(encoded: string): Uint8Array {
	return decodeBase32_internal(encoded, base32DecodeMap, DecodingPadding.Ignore);
}

function decodeBase32_internal(
	encoded: string,
	decodeMap: Record<string, number>,
	padding: DecodingPadding
): Uint8Array {
	const result = new Uint8Array(Math.ceil(encoded.length / 8) * 5);
	let totalBytes = 0;
	for (let i = 0; i < encoded.length; i += 8) {
		let chunk = 0n;
		let bitsRead = 0;
		for (let j = 0; j < 8; j++) {
			if (padding === DecodingPadding.Required) {
				if (encoded[i + j] === '=') {
					continue;
				}
				if (i + j >= encoded.length) {
					throw new Error('Invalid padding');
				}
			}
			if (padding === DecodingPadding.Ignore) {
				if (i + j >= encoded.length || encoded[i + j] === '=') {
					continue;
				}
			}
			if (j > 0 && encoded[i + j - 1] === '=') {
				throw new Error('Invalid padding');
			}
			if (!(encoded[i + j] in decodeMap)) {
				throw new Error('Invalid character');
			}
			chunk |= BigInt(decodeMap[encoded[i + j]]) << BigInt((7 - j) * 5);
			bitsRead += 5;
		}
		if (bitsRead < 40) {
			let unused: bigint;
			if (bitsRead === 10) {
				unused = chunk & 0xffffffffn;
			} else if (bitsRead === 20) {
				unused = chunk & 0xffffffn;
			} else if (bitsRead === 25) {
				unused = chunk & 0xffffn;
			} else if (bitsRead === 35) {
				unused = chunk & 0xffn;
			} else {
				throw new Error('Invalid padding');
			}
			if (unused !== 0n) {
				throw new Error('Invalid padding');
			}
		}
		const byteLength = Math.floor(bitsRead / 8);
		for (let i = 0; i < byteLength; i++) {
			result[totalBytes] = Number((chunk >> BigInt(32 - i * 8)) & 0xffn);
			totalBytes++;
		}
	}
	return result.slice(0, totalBytes);
}

const base32UpperCaseAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
const base32LowerCaseAlphabet = 'abcdefghijklmnopqrstuvwxyz234567';

const base32DecodeMap = {
	A: 0,
	B: 1,
	C: 2,
	D: 3,
	E: 4,
	F: 5,
	G: 6,
	H: 7,
	I: 8,
	J: 9,
	K: 10,
	L: 11,
	M: 12,
	N: 13,
	O: 14,
	P: 15,
	Q: 16,
	R: 17,
	S: 18,
	T: 19,
	U: 20,
	V: 21,
	W: 22,
	X: 23,
	Y: 24,
	Z: 25,
	a: 0,
	b: 1,
	c: 2,
	d: 3,
	e: 4,
	f: 5,
	g: 6,
	h: 7,
	i: 8,
	j: 9,
	k: 10,
	l: 11,
	m: 12,
	n: 13,
	o: 14,
	p: 15,
	q: 16,
	r: 17,
	s: 18,
	t: 19,
	u: 20,
	v: 21,
	w: 22,
	x: 23,
	y: 24,
	z: 25,
	'2': 26,
	'3': 27,
	'4': 28,
	'5': 29,
	'6': 30,
	'7': 31,
};

enum EncodingPadding {
	Include = 0,
	None = 1,
}

enum DecodingPadding {
	Required = 0,
	Ignore = 1,
}
