/**
 * Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
 * Node module: @schukai/monster
 *
 * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
 * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
 *
 * For those who do not wish to adhere to the AGPLv3, a commercial license is available.
 * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
 * For more information about purchasing a commercial license, please contact Volker Schukai.
 *
 * SPDX-License-Identifier: AGPL-3.0
 */

import { getGlobal } from "../types/global.mjs";

export { random };

/**
 * this function uses crypt and returns a random number.
 *
 * @param {number} min starting value of the definition set (default is 0)
 * @param {number} max end value of the definition set (default is 1000000000)
 * @return {number}
 * @throws {Error} missing crypt
 * @throws {Error} we cannot generate numbers larger than 53 bits.
 * @throws {Error} the distance is too small to create a random number.

 * @license AGPLv3
 * @since 1.0.0
 * @copyright Volker Schukai
 */
function random(min, max) {
	if (min === undefined) {
		min = 0;
	}
	if (max === undefined) {
		max = MAX;
	}

	if (max < min) {
		throw new Error("max must be greater than min");
	}

	return Math.round(create(min, max));
}

/**
 * @private
 * @type {number}
 */
const MAX = 1000000000;

Math.log2 =
	Math.log2 ||
	function (n) {
		return Math.log(n) / Math.log(2);
	};

/**
 *
 * @param {number} min
 * @param {number} max
 * @return {number}
 * @private
 * @throws {Error} missing crypt
 * @throws {Error} we cannot generate numbers larger than 53 bits.
 * @throws {Error} the distance is too small to create a random number.
 */
function create(min, max) {
	const globalReference = getGlobal();

	const crypt =
		globalReference?.["crypto"] ||
		globalReference?.["msCrypto"] ||
		globalReference?.["crypto"] ||
		undefined;

	if (typeof crypt === "undefined") {
		throw new Error("missing crypt");
	}

	let rval = 0;
	const range = max - min;
	if (range < 2) {
		throw new Error("the distance is too small to create a random number.");
	}

	const bitsNeeded = Math.ceil(Math.log2(range));
	if (bitsNeeded > 53) {
		throw new Error("we cannot generate numbers larger than 53 bits.");
	}
	const bytesNeeded = Math.ceil(bitsNeeded / 8);
	const mask = Math.pow(2, bitsNeeded) - 1;

	const byteArray = new Uint8Array(bytesNeeded);
	crypt.getRandomValues(byteArray);

	let p = (bytesNeeded - 1) * 8;
	for (let i = 0; i < bytesNeeded; i++) {
		rval += byteArray[i] * Math.pow(2, p);
		p -= 8;
	}

	rval = rval & mask;

	if (rval >= range) {
		return create(min, max);
	}

	if (rval < min) {
		rval += min;
	}

	return rval;
}
