
/**
 * Provides a simplified access point to the frequency bins in the form of a visualization update listener.
 */
export default class Visualizer {
    #source;
    #stream;
    #analyzing = false;
    #updateListeners = [];
    #audioCtx;
    #analyzer;
    #buffer;
    #lastUpdate;
    #fftSize;

    /**
     * @callback Visualizer~visualizerUpdateListener
     * @param {number} delta elapsed time since last update.
     * @param {Uint8Array} bins the bins with varying frequency values. 
     * @param {number} sigBin The bin with the greatest amplitude.
     */

    /**
     * 
     * @param {MediaSource|HTMLMediaElement} stream a media source to analyze.
     * @param {number} [fftSize = 1024] the size of the fft window.
     */
    constructor(stream, fftSize = 1024) {
        this.#fftSize = fftSize;
        this.#stream = stream;
        this.#analyzing = false;
        this.#updateListeners = [];
        this.#lastUpdate = null;
    }

    #initialize() {
        this.#audioCtx = new window.AudioContext();
        if (this.#stream instanceof HTMLMediaElement) {
            this.#source = this.#audioCtx.createMediaElementSource(this.#stream);
        } else {
            this.#source = this.#audioCtx.createMediaStreamSource(this.#stream);
        }
        this.#analyzer = this.#audioCtx.createAnalyser();
        this.#analyzer.fftSize = this.fftSize;
        this.#buffer = new Uint8Array(this.#analyzer.frequencyBinCount);
        this.#source.connect(this.#analyzer);
        this.#analyzer.connect(this.#audioCtx.destination);
    }

    /**
     * Begins analyzing and sending out update pings.
     */
    analyze() {
        if (this.#analyzing) {
            return;
        }
        if (!this.#audioCtx) this.#initialize();
        this.#analyzing = true;
        let self = this; // since calling from requestAnimationFrame means "this" is no longer set to produced object.
        const update = (timestamp) => {
            if (!self.#analyzing) return;

            if (!self.#lastUpdate) {
                self.#lastUpdate = timestamp;
            }
            let delta = timestamp - self.#lastUpdate;
            self.#analyzer.getByteFrequencyData(self.#buffer);

            self.#updateListeners.forEach(listener => {
                listener(delta, self.#buffer, self.#buffer.indexOf(Math.max(self.#buffer)));
            });
            requestAnimationFrame(update);
        };
        requestAnimationFrame(update);
    }

    /**
     * Stops the analysis. Listeners will stop receiving bins.
     */
    stop() {
        this.#analyzing = false;
    }

    /**
     * 
     * @param {Visualizer~visualizerUpdateListener} listener the visualizer update listener to be registered.
     * @returns {boolean} true if and only if the listener was successfully added.
     */
    addUpdateListener(listener) {
        if (this.#updateListeners.includes(listener));
        this.#updateListeners.push(listener);
        return true;
    }

    /**
     * 
     * @param {Visualizer~visualizerUpdateListener} listener the visualizer update listener to remove.
     * @returns {boolean} true if and only if the removal of the listener was a success.
     */
    removeUpdateListener(listener) {
        const removeIndex = this.#updateListeners.indexOf(listener);
        if (removeIndex < 0) return false;

        this.#updateListeners.splice(removeIndex, 1);
        return true;
    }


    /**
     * @returns {number} The number of bins based on the size of the FFT window.
     */
    get numberOfBins() {
        return this.#fftSize / 2;
    }

    /**
     * 
     * @returns {number} The fft window size.
     */
    get fftSize() {
        return this.#fftSize;
    }
}