/**
 * Interpolates between two objects, obj1 and obj2, based on a given factor
 * If a property is a number and exists in both objects, it interpolates the values
 * The result includes all properties from both objects
 * Non-numeric properties and properties not shared between objects are copied as is
 *
 * @param {object} obj1 The first object
 * @param {object} obj2 The second object
 * @param {number} factor The interpolation factor (0 to 1).
 * @returns {object} A new object with interpolated number values and other properties from both obj1 and obj2
 */
function interpolateObject(obj1, obj2, factor) {
	const result = {};

	for (const key in obj1) {
		if (Object.hasOwn(obj1, key)) {
			if (typeof obj1[key] === "number" && Object.hasOwn(obj2, key) && typeof obj2[key] === "number") {
				result[key] = interpolate(obj1[key], obj2[key], factor);
			} else if (typeof obj1[key] === "object" && Object.hasOwn(obj2, key) && typeof obj2[key] === "object") {
				result[key] = interpolateObject(obj1[key], obj2[key], factor);
			} else {
				result[key] = obj1[key];
			}
		}
	}

	for (const key in obj2) {
		if (Object.hasOwn(obj2, key) && !Object.hasOwn(result, key)) {
			result[key] = obj2[key];
		}
	}
	return result;
}

window.interpolateObject = interpolateObject;

/**
 * Linearly interpolates between two values based on a given factor
 *
 * @param {number} value1
 * @param {number} value2
 * @param {number} factor Interpolation factor (0 to 1)
 * @returns {number}
 */
function interpolate(value1, value2, factor) {
	return value1 + (value2 - value1) * factor;
}
window.interpolate = interpolate;

function lerp(percent, start, end) {
	return Math.clamp(start + (end - start) * percent, start, end);
}
window.lerp = lerp;

function inverseLerp(value, start, end) {
	return Math.clamp((value - start) / (end - start), 0, 1);
}
window.inverseLerp = inverseLerp;

function formatDecimals(value, decimalPlaces) {
	return Number(Math.round(parseFloat(value + "e" + decimalPlaces)) + "e-" + decimalPlaces);
}
window.formatDecimals = formatDecimals;

function nCr(n, r) {
	// https://stackoverflow.com/questions/11809502/which-is-better-way-to-calculate-ncr
	if (r > n - r) {
		// because C(n, r) == C(n, n - r)
		r = n - r;
	}

	let ans = 1;
	for (let i = 1; i <= r; ++i) {
		ans *= n - r + i;
		ans /= i;
	}

	return ans;
}
window.nCr = nCr;

/**
 * Checks if x is equal or higher than min and lower or equal to max
 *
 * @param {number} x
 * @param {any} min
 * @param {any} max
 * @returns {boolean}
 */
function between(x, min, max) {
	return typeof x === "number" && x >= min && x <= max;
}
window.between = between;

/**
 * This function takes a value, and weights it by exponential curve.
 *
 * Value should be between 0.0 and 1.0 (use normalise to get a percentage of a max).
 *
 * An exponent of 1.0 returns 1 every time.
 *
 * Exponents between 1.0 and 2.0 return a curve favoring higher results (closer to 1)
 *
 * An exponent of 2.0 will return a flat line distribution, and is identical to random()
 *
 * Exponents greater than 2.0 return a curve favoring lower results (closer to 0), reaching to 0 at infinity.
 *
 * For example, see:
 * https://www.desmos.com/calculator/87hhrjfixi
 *
 * @param {number} value Value to be weighted
 * @param {number} exp Exponent used to generate the curve
 * @returns {number} value weighted against exponential curve
 */
function expCurve(value, exp) {
	return value ** exp / value;
}
window.expCurve = expCurve;

/**
 * This function creates a random float 0.0-1.0, weighted by exponential curve.
 *
 * A value of 1.0 returns 1 every time.
 *
 * Values between 1.0 and 2.0 return a curve favoring higher results (closer to 1)
 *
 * A value of 2.0 will return a flat line distribution, and is identical to random()
 *
 * Values greater than 2.0 return a curve favoring lower results (closer to 0), reaching to 0 at infinity.
 *
 * For example, see:
 * https://www.desmos.com/calculator/87hhrjfixi
 *
 * @param {number} exp Exponent used to generate the curve
 * @returns {number} random number weighted against exponential curve
 */
function randomExp(exp) {
	return expCurve(State.random(), exp);
}
window.randomExp = randomExp;

/**
 * Normalises value to a decimal number 0.0-1.0, a percentage of the range specified in min and max.
 *
 * @param {number} value The value to be normalised
 * @param {number} max The highest value of the range
 * @param {number} min The lowest value of the range, default 0
 * @returns {number} Normalised value
 */
function normalise(value, max, min = 0) {
	const denominator = max - min;
	if (denominator === 0) {
		Errors.report("[normalise]: min and max params must be different.", { value, max, min });
		return 0;
	}
	if (denominator < 0) {
		Errors.report("[normalise]: max param must be greater than min param.", { value, max, min });
		return 0;
	}
	return Math.clamp((value - min) / denominator, 0, 1);
}
window.normalise = normalise;

/**
 * Returns a rounded number, with number of decimals based on the second parameter
 *
 * @param {number} number
 * @param {number} decimals
 * @returns {number} new number
 */
function round(number, decimals) {
	const multiplier = 10 ** decimals;
	return Math.round(number * multiplier) / multiplier;
}
window.round = round;

/**
 * Categorises a value into a specified number of categories based on its position within a given range.
 * The function automatically handles both ascending (min < max) and descending (min > max) ranges.
 *
 * @param {number} value The value to be categorized.
 * @param {number} min The start of the range.
 * @param {number} max The end of the range.
 * @param {number} parts The number of categories into which the range should be divided.
 * @returns {number} Returns the category index, ranging from 0 to parts-1.
 * Examples:
 *  categorise(15, 10, 20, 4);
 *  Result: 2
 *  Divides the range 10-20 into 4 parts, and 15 falls into the third part. (First part is 0)
 *
 *  categorise(15, 20, 10, 4);
 *  Result: 1
 *  Divides the range 20-10 into 4 parts, and 15 falls into the second part.
 *
 *  categorise(5, 0, 10, 5);
 *  Result: 0
 *  Divides the range 0-10 into 5 parts, and 5 falls right on the border of the first and second part but is rounded down.
 *
 *  categorise(18, 20, 10, 5);
 *  Result: 3
 *  Divides the range 20-10 into 5 parts, and 18 falls into the fourth part.
 */
function categorise(value, min, max, parts) {
	const normalised = normalise(value, Math.max(min, max), Math.min(min, max));
	const category = Math.floor(normalised * parts);
	return Math.clamp(min > max ? parts - 1 - category : category, 0, parts - 1);
}
window.categorise = categorise;

/**
 * Generates a random number within a specified range around a given base number.
 *
 * @param {number} num The base number.
 * @param {number} min The minimum offset subtracted from the base number.
 * @param {number} max The maximum offset added to the base number.
 * @param rngInstance
 * @returns {number} A random number between `num - min` and `num + max`.
 */
function boundedRandom(num, min, max = min, rngInstance) {
	const result = rngInstance ? rngInstance.randomFloat(num - min, num + max) : randomFloat(num - min, num + max);
	return round(result, 2);
}
window.boundedRandom = boundedRandom;

/**
 * Generates a random integer based on chance and max value
 *
 * @param {number} chance Value between 0 and 1
 * @param {number} max Integer
 */
function calculateBinomial(chance, max) {
	let result = 0;
	for (let i = 0; i < max; i++) {
		if (State.random() < chance) {
			result++;
		}
	}
	return result;
}
window.calculateBinomial = calculateBinomial;