/**
 * Librairie de fonctions utilitaires mathématiques.
 *
 * @module
 */

/**
 * Convertit un ensemble de pondérations relatives (% : nb réel entre 0 et 100) en pondérations absolues (entier).
 * @param {Array.<number>} ponderationsRelatives - pondérations relatives (% réel entre 0 et 100)
 * @returns {Array.<number>} pondérations absolues
 */
export function convertitPonderationsRelativesEnAbsolues(ponderationsRelatives) {
    const fractions = ponderationsRelatives.map((pr) => {
        const f = float_to_fraction(pr / 100, 0.0001);
        return f;
    });

    //console.log('fractions', fractions);

    // Calcul du LCM des dénominateurs des fractions
    const denominateurs = fractions.map((f) => f.denominator);
    //console.log('denominateurs', denominateurs);

    const ppcm = lcm(denominateurs);
    //console.log('ppcm', ppcm);

    // Normalisation des fractions pour qu'elles aient toutes le LCM comme dénominateur
    const fractionsNormalisees = fractions.map((f) => {
        if (f.denominator == ppcm) return f;
        const k = ppcm / f.denominator;
        return { numerator: f.numerator * k, denominator: f.denominator * k };
    });
    //console.log('fractionsNormalisees', fractionsNormalisees);

    // Ne conserve que les numérateurs
    //TODO factoriser cette étape avec la précédente
    const ponderationsAbsolues = fractionsNormalisees.map((pr) => pr.numerator);
    return ponderationsAbsolues;
}

/**
 * Arrondit un ensemble de pondérations flottantes en répartissant les écarts d'arrondit de façon à garantir la somme totale.
 *
 * @param {Array.<number>} ponderationsFloat - Pondérations, dont la somme devrait idéalement faire exactement le total demandé
 * @param {number} totalAAtteindre - Total à atteindre (ex : 100)
 * @param {number} precision - Nombre de décimales à conserver (ex : 0 pour avoir des entiers)
 * @param {number} indexMinVariable - Index min de la pondération variable à impacter pour garantir que la somme des valeurs arrondies atteigne totalAAtteindre
 * @returns {Array.<number>} - Tableau de pondération arrondies, dont la somme fait exactement le total demandé.
 */
export function arronditPonderations(ponderationsFloat, totalAAtteindre, precision = 0, indexMinVariable = 0) {
    if (!ponderationsFloat) return;

    let ponderationsArrondies = [],
        sommeArrondie = 0,
        sommeArrondiePrecedente = 0,
        indexAImpacter = indexMinVariable;

    const nb = ponderationsFloat.length;
    for (let i = 0; i < nb; i++) {
        const p = ponderationsFloat[i];
        const pArrondie = parseFloat(p.toFixed(precision));

        sommeArrondiePrecedente = sommeArrondie;
        sommeArrondie += pArrondie;

        const pa = Math.max(0, sommeArrondie - sommeArrondiePrecedente);
        ponderationsArrondies.push(pa);

        if (i >= indexMinVariable && pa >= ponderationsArrondies[indexAImpacter]) {
            indexAImpacter = i;
        }
    }

    //console.log(ponderationsFloat, totalAAtteindre, ponderationsArrondies, sommeArrondie);

    // Si la pondération totale arrondie est différente du total à atteindre, on impacte la pondération variable la plus élevée pour rectifier le tir
    if (totalAAtteindre != sommeArrondie) {
        ponderationsArrondies[indexAImpacter] += totalAAtteindre - sommeArrondie;
    }

    return ponderationsArrondies;
}

/**
 * Convertit un nombre flottant en fraction, selon l'algorithme de Stern-Brocot.
 * @param {number} x - Nombre à convertir
 * @param {number} error - Marge d'erreur
 * @returns fraction X/Y sous la forme {numerator: X, denominator: Y}
 * @see https://stackoverflow.com/questions/5124743/algorithm-for-simplifying-decimal-to-fractions
 */
export function float_to_fraction(x, error = 0.001) {
    const n = Math.floor(x);
    x -= n;

    if (x < error) {
        return { numerator: n, denominator: 1 };
    } else if (1 - error < x) {
        return { numerator: n + 1, denominator: 1 };
    }

    //  The lower fraction is 0/1
    let lower_n = 0;
    let lower_d = 1;

    // The upper fraction is 1/1
    let upper_n = 1;
    let upper_d = 1;
    while (true) {
        // The middle fraction is (lower_n + upper_n) / (lower_d + upper_d)
        let middle_n = lower_n + upper_n;
        let middle_d = lower_d + upper_d;
        // If x + error < middle
        if (middle_d * (x + error) < middle_n) {
            // middle is our new upper
            upper_n = middle_n;
            upper_d = middle_d;
            // Else If middle < x - error
        } else if (middle_n < (x - error) * middle_d) {
            // middle is our new lower
            lower_n = middle_n;
            lower_d = middle_d;
            //Else middle is our best fraction
        } else {
            return { numerator: n * middle_d + middle_n, denominator: middle_d };
        }
    }
}

/**
 * Calcule le plus petit multiple commun d'un tableau d'entiers
 *
 * @param  {Array.<number>} numbers - Tableau d'entiers
 * @returns plus petit commun multiple
 */
export function lcm(numbers) {
    const gcd2 = (a, b) => {
        // Greatest common divisor of 2 integers
        if (!b) return b === 0 ? a : NaN;
        return gcd2(b, a % b);
    };

    const lcm2 = (a, b) => {
        // Least common multiple of 2 integers
        return (a * b) / gcd2(a, b);
    };

    // Least common multiple of a list of integers
    let n = 1;
    for (let i = 0; i < numbers.length; ++i) {
        n = lcm2(numbers[i], n);
    }
    return n;
}
