# src/TempoInduction.js

    /**
     * Performs tempo induction by finding clusters of similar inter-onset intervals (IOIs)
     * @class
     */
    export default class TempoInduction {
        /**
         * Find clusters
         * @param {Array} events - the onsets from which the tempo is induced
         * @param {Object} [params={}] - parameters
         * @param {Number} [params.widthTreshold=0.025] - the maximum difference in IOIs which are in the same cluster
         * @param {Number} [params.maxIOI=2.5] - the maximum IOI for inclusion in a cluster
         * @param {Number} [params.minIOI=0.07] - the minimum IOI for inclusion in a cluster
         * @return {{clIntervals: Array, clSizes: Array}} - object with clusters
         */
        static processRhythmicEvents(events, params = {}) {
            const widthTreshold = params.widthTreshold || 0.025,
                  maxIOI = params.maxIOI || 2.5,
                  minIOI = params.minIOI || 0.07,
                  length = events.length;

            let clIntervals = [],
                clSizes = [],
                clCount = 0;

            for (let i = 0; i < length - 1; i++) {
                for(let j = i + 1; j < length; j++) {
                    let ioi = events[j] - events[i];
                    if (ioi < minIOI) {
                        continue
                    }
                    if (ioi > maxIOI) {
                        break;
                    }
                    let k = 0;
                    for ( ; k < clCount; k++) {
                        if (Math.abs(clIntervals[k] - ioi) < widthTreshold) {
                            if ( Math.abs(clIntervals[k + 1] - ioi) < Math.abs(clIntervals[k] - ioi)
                                && k < clCount - 1 ) {
                                k++;
                            }
                            clIntervals[k] = (clIntervals[k] * clSizes[k] + ioi) / (clSizes[k] + 1);
                            clSizes[k]++;
                            break;
                        }
                    }
                    if (k != clCount) continue;
                    clCount++;
                    for ( ; k > 0 && clIntervals[k - 1] > ioi; k--) {
                        clIntervals[k] = clIntervals[k - 1];
                        clSizes[k] = clSizes[k - 1];
                    }
                    clIntervals[k] = ioi;
                    clSizes[k] = 1;
                }
            }
            if (clCount == 0) {
              throw "Fail to find IOIs";
            }
            clIntervals.length = clCount;
            clSizes.length = clCount;
            return {clIntervals, clSizes};
        }
        /**
         * Merge similar intervals
         * @param {Object} clusters - object with clusters
         * @param {Array} clusters.clIntervals - clusters IOIs array
         * @param {Array} clusters.clSizes - clusters sizes array
         * @param {Object} [params={}] - parameters
         * @param {Number} [params.widthTreshold=0.025] - the maximum difference in IOIs which are in the same cluster
         * @return {{clIntervals: Array, clSizes: Array}} - object with clusters
         */
        static mergeClusters(clusters, params = {}) {
            const widthTreshold = params.widthTreshold || 0.025;

            let clIntervals = clusters.clIntervals,
                clSizes = clusters.clSizes;
            let clCount = clIntervals.length;

            for (let i = 0; i < clCount; i++)
                for (let j = i + 1; j < clCount; j++)
                    if (Math.abs(clIntervals[i] - clIntervals[j]) < widthTreshold) {
                        clIntervals[i] =
                            (clIntervals[i] * clSizes[i] +
                            clIntervals[j] * clSizes[j]) /
                            (clSizes[i] + clSizes[j]);
                        clSizes[i] = clSizes[i] + clSizes[j];
                        --clCount;
                        for (let k = j + 1; k <= clCount; k++) {
                            clIntervals[k-1] = clIntervals[k];
                            clSizes[k-1] = clSizes[k];
                        }
                    }
            clIntervals.length = clCount;
            clSizes.length = clCount;
            return {clIntervals, clSizes};
        }
        /**
         * Score intervals
         * @param {Object} clusters - object with clusters
         * @param {Array} clusters.clIntervals - clusters IOIs array
         * @param {Array} clusters.clSizes - clusters sizes array
         * @param {Object} [params={}] - parameters
         * @param {Number} [params.widthTreshold=0.025] - the maximum difference in IOIs which are in the same cluster
         * @param {Number} [params.maxTempos=10] - initial amount of tempo hypotheses
         * @return {{clScores: Array, clScoresIdxs: Array}} - object with intervals scores
         */
        static calculateScore(clusters, params = {}) {
            const widthTreshold = params.widthTreshold || 0.025;
            let maxTempos = params.maxTempos || 10;

            let clIntervals = clusters.clIntervals,
                clSizes = clusters.clSizes,
                clScores = [],
                clScoresIdxs = [];
            let clCount = clIntervals.length;

            for (let i = 0; i < clCount; i++) {
                clScores[i] = 10 * clSizes[i];
                clScoresIdxs[i] = { score: clScores[i], idx: i};
            }

            clScoresIdxs.sort( (a, b) => b.score - a.score );
            if (clScoresIdxs.length > maxTempos) {
              for (let i = maxTempos - 1; i <  clScoresIdxs.length - 1; i++) {
                  if (clScoresIdxs[i].score == clScoresIdxs[i + 1].score) {
                      maxTempos++;
                  } else {
                      break;
                  }
              }
              clScoresIdxs.length = maxTempos;
            }

            clScoresIdxs = clScoresIdxs.map( a => a.idx );

            for (let i = 0; i < clCount; i++) {
                for (let j = i + 1; j < clCount; j++) {
                    let ratio = clIntervals[i] / clIntervals[j];
                    let isFraction = ratio < 1;
                    let d, err;
                    d = isFraction ? Math.round(1 / ratio) : Math.round(ratio);
                    if (d < 2 || d > 8) continue;

                    if (isFraction)
                        err = Math.abs(clIntervals[i] * d - clIntervals[j]);
                    else
                        err = Math.abs(clIntervals[i] - clIntervals[j] * d);
                    let errTreshold = isFraction ? widthTreshold : widthTreshold * d;
                    if (err >= errTreshold) continue;

                    d = d >= 5 ? 1 : 6 - d;
                    clScores[i] += d * clSizes[j];
                    clScores[j] += d * clSizes[i];
                }
            }
            return {clScores, clScoresIdxs};
        }
        /**
         * Get array of tempo hypotheses
         * @param {Object} clusters - object with clusters
         * @param {Array} clusters.clIntervals - clusters IOIs array
         * @param {Array} clusters.clSizes - clusters sizes array
         * @param {Array} clusters.clScores - clusters scores array
         * @param {Array} clusters.clScoresIdxs - clusters scores indexes array
         * @param {Object} [params={}] - parameters
         * @param {Number} [params.widthTreshold=0.025] - the maximum difference in IOIs which are in the same cluster
         * @param {Number} [params.minBeatInterval=0.3] - the minimum inter-beat interval (IBI) (0.30 seconds == 200 BPM)
         * @param {Number} [params.maxBeatInterval=1] - the maximum inter-beat interval (IBI) (1.00 seconds ==  60 BPM)
         * @return {Array} tempoList - tempo hypotheses array
         */
        static createTempoList(clusters, params = {}) {
            const widthTreshold = params.widthTreshold || 0.025,
                minBeatInterval = params.minBeatInterval || 0.3,
                maxBeatInterval = params.maxBeatInterval || 1;
            let clIntervals = clusters.clIntervals,
                clSizes = clusters.clSizes,
                clScores = clusters.clScores,
                clScoresIdxs = clusters.clScoresIdxs,
                tempoList = [];
            let clCount = clIntervals.length;

            for (let i = 0; i < clScoresIdxs.length; i++) {
                let idx = clScoresIdxs[i];
                let newSum = clIntervals[idx] * clScores[idx];
                let newWeight = clScores[idx];
                let err, errTreshold;
                for (let j = 0; j < clCount; j++) {
                    if (j == idx) continue;
                    let ratio = clIntervals[idx] / clIntervals[j];
                    let isFraction = ratio < 1;
                    let sumInc = 0;
                    let d = isFraction ? Math.round(1 / ratio) : Math.round(ratio);
                    if (d < 2 || d > 8) continue;

                    if (isFraction) {
                        err = Math.abs(clIntervals[idx] * d - clIntervals[j]);
                        errTreshold = widthTreshold;
                    } else {
                        err = Math.abs(clIntervals[idx] - d * clIntervals[j]);
                        errTreshold = widthTreshold * d;
                    }
                    if (err >= errTreshold) continue;

                    if (isFraction) {
                        newSum += clIntervals[j] / d * clScores[j];
                    } else {
                        newSum += clIntervals[j] * d * clScores[j];
                    }
                    newWeight += clScores[j];
                }
                let beat = newSum / newWeight;

                while (beat < minBeatInterval) beat *= 2;
                while (beat > maxBeatInterval) beat /= 2;

                tempoList.push(beat);
            }
            return tempoList;
        }
    }