/*
	MIT License http://www.opensource.org/licenses/mit-license.php
	Author Alexander Akait @alexander-akait
*/

"use strict";

/** @typedef {import("../Hash")} Hash */
/** @typedef {import("../../../declarations/WebpackOptions").HashDigest} Encoding */

/** @typedef {"26" | "32" | "36" | "49" | "52" | "58" | "62"} Base */

/* cSpell:disable */

/** @type {Record<Base, string>} */
const ENCODE_TABLE = Object.freeze({
	26: "abcdefghijklmnopqrstuvwxyz",
	32: "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567",
	36: "0123456789abcdefghijklmnopqrstuvwxyz",
	49: "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ",
	52: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
	58: "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz",
	62: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
});

/* cSpell:enable */

const ZERO = BigInt("0");
const EIGHT = BigInt("8");
const FF = BigInt("0xff");

/**
 * It encodes octet arrays by doing long divisions on all significant digits in the array, creating a representation of that number in the new base.
 * Then for every leading zero in the input (not significant as a number) it will encode as a single leader character.
 * This is the first in the alphabet and will decode as 8 bits. The other characters depend upon the base.
 * For example, a base58 alphabet packs roughly 5.858 bits per character.
 * This means the encoded string 000f (using a base16, 0-f alphabet) will actually decode to 4 bytes unlike a canonical hex encoding which uniformly packs 4 bits into each character.
 * While unusual, this does mean that no padding is required, and it works for bases like 43.
 * @param {Buffer} buffer buffer
 * @param {Base} base base
 * @returns {string} encoded buffer
 */
const encode = (buffer, base) => {
	if (buffer.length === 0) return "";
	const alphabet = ENCODE_TABLE[base];
	const bigIntBase = BigInt(alphabet.length);
	// Leading zero bytes are not significant in the BigInt below, so they would be
	// lost; Base58Check-style, re-emit each as one leading alphabet[0] char. Keeps
	// the encoding reversible and its length tracking the input (prefix-stable slices).
	let zeros = 0;
	while (zeros < buffer.length && buffer[zeros] === 0) zeros++;
	// Convert buffer to BigInt efficiently using bitwise operations
	let value = ZERO;
	for (let i = 0; i < buffer.length; i++) {
		value = (value << EIGHT) | BigInt(buffer[i]);
	}
	// Convert to baseX string efficiently using array
	/** @type {string[]} */
	const digits = [];
	while (value > ZERO) {
		const remainder = Number(value % bigIntBase);
		digits.push(alphabet[remainder]);
		value /= bigIntBase;
	}
	// Pushed last so they land first after the reverse below.
	for (let i = 0; i < zeros; i++) digits.push(alphabet[0]);
	return digits.reverse().join("");
};

/**
 * Returns buffer.
 * @param {string} data string
 * @param {Base} base base
 * @returns {Buffer} buffer
 */
const decode = (data, base) => {
	if (data.length === 0) return Buffer.from("");
	const alphabet = ENCODE_TABLE[base];
	const bigIntBase = BigInt(alphabet.length);
	// Leading alphabet[0] chars decode back to leading zero bytes (inverse of encode).
	const zeroChar = alphabet[0];
	let zeros = 0;
	while (zeros < data.length && data[zeros] === zeroChar) zeros++;
	// Convert the baseX string to a BigInt value
	let value = ZERO;
	for (let i = 0; i < data.length; i++) {
		const digit = alphabet.indexOf(data[i]);
		if (digit === -1) {
			throw new Error(`Invalid character at position ${i}: ${data[i]}`);
		}
		value = value * bigIntBase + BigInt(digit);
	}
	// Significant byte count of the numeric part (excludes the leading zeros above)
	let temp = value;
	let numLength = 0;
	while (temp > ZERO) {
		temp >>= EIGHT;
		numLength++;
	}
	// Create buffer and fill the numeric part from right to left, leaving the
	// leading `zeros` bytes as 0.
	const buffer = Buffer.alloc(zeros + numLength);
	for (let i = zeros + numLength - 1; i >= zeros; i--) {
		buffer[i] = Number(value & FF);
		value >>= EIGHT;
	}
	return buffer;
};

// Compatibility with the old hash libraries, they can return different structures, so let's stringify them firstly

/**
 * Returns a string representation.
 * @param {string | { toString: (radix: number) => string }} value value
 * @param {string} encoding encoding
 * @returns {string} string
 */
const toString = (value, encoding) =>
	typeof value === "string"
		? value
		: Buffer.from(value.toString(16), "hex").toString(
				/** @type {NodeJS.BufferEncoding} */
				(encoding)
			);

/**
 * Returns buffer.
 * @param {Buffer | { toString: (radix: number) => string }} value value
 * @returns {Buffer} buffer
 */
const toBuffer = (value) =>
	Buffer.isBuffer(value) ? value : Buffer.from(value.toString(16), "hex");

let isBase64URLSupported = false;

try {
	isBase64URLSupported = Boolean(Buffer.from("", "base64url"));
} catch (_err) {
	// Nothing
}

/**
 * Processes the provided hash.
 * @param {Hash} hash hash
 * @param {string | Buffer} data data
 * @param {Encoding=} encoding encoding of the return value
 * @returns {void}
 */
const update = (hash, data, encoding) => {
	if (encoding === "base64url" && !isBase64URLSupported) {
		const base64String = /** @type {string} */ (data)
			.replace(/-/g, "+")
			.replace(/_/g, "/");
		const buf = Buffer.from(base64String, "base64");
		hash.update(buf);
		return;
	} else if (
		typeof data === "string" &&
		encoding &&
		typeof ENCODE_TABLE[/** @type {Base} */ (encoding.slice(4))] !== "undefined"
	) {
		const buf = decode(data, /** @type {Base} */ (encoding.slice(4)));
		hash.update(buf);
		return;
	}

	if (encoding) {
		hash.update(/** @type {string} */ (data), encoding);
	} else {
		hash.update(data);
	}
};

/**
 * Returns digest.
 * @overload
 * @param {Hash} hash hash
 * @returns {Buffer} digest
 */
/**
 * Returns digest.
 * @overload
 * @param {Hash} hash hash
 * @param {undefined} encoding encoding of the return value
 * @param {boolean=} isSafe true when we await right types from digest(), otherwise false
 * @returns {Buffer} digest
 */
/**
 * Returns digest.
 * @overload
 * @param {Hash} hash hash
 * @param {Encoding} encoding encoding of the return value
 * @param {boolean=} isSafe true when we await right types from digest(), otherwise false
 * @returns {string} digest
 */
/**
 * Returns digest.
 * @param {Hash} hash hash
 * @param {Encoding=} encoding encoding of the return value
 * @param {boolean=} isSafe true when we await right types from digest(), otherwise false
 * @returns {string | Buffer} digest
 */
const digest = (hash, encoding, isSafe) => {
	if (typeof encoding === "undefined") {
		return isSafe ? hash.digest() : toBuffer(hash.digest());
	}

	if (encoding === "base64url" && !isBase64URLSupported) {
		const digest = isSafe
			? hash.digest("base64")
			: toString(hash.digest("base64"), "base64");

		return digest.replace(/\+/g, "-").replace(/\//g, "_").replace(/[=]+$/, "");
	} else if (
		typeof ENCODE_TABLE[/** @type {Base} */ (encoding.slice(4))] !== "undefined"
	) {
		const buf = isSafe ? hash.digest() : toBuffer(hash.digest());

		return encode(
			buf,
			/** @type {Base} */
			(encoding.slice(4))
		);
	}

	return isSafe
		? hash.digest(encoding)
		: toString(hash.digest(encoding), encoding);
};

module.exports.decode = decode;
module.exports.digest = digest;
module.exports.encode = encode;
module.exports.update = update;
