/**
 * VMPlayer Visualizer
 *
 * Two-mode audio visualizer. Preferred mode uses the Web Audio API
 * AnalyserNode to read real frequency data from the audio element,
 * yielding an accurate equalizer display that reacts to what the
 * user actually hears. Fallback mode uses CSS keyframe animation
 * for a decorative equalizer that looks alive without touching audio.
 *
 * Mode selection is automatic. Streams that send permissive CORS
 * headers get the real mode. Streams that do not (which is most
 * Shoutcast/Icecast servers by default) get the CSS mode. Either
 * mode is invisible when the player is paused.
 *
 * Attached to window.VMPlayerVisualizer so the main frontend script
 * can instantiate it without a module system.
 *
 * @package VMPlayer
 */

( function () {
    'use strict';

    var FFT_SIZE = 128;
    var MIN_BAR_HEIGHT_PCT = 12;
    var MAX_BAR_HEIGHT_PCT = 100;
    var ZERO_DATA_FRAMES_TO_FALLBACK = 30;

    /**
     * VMPlayerVisualizer
     *
     * @param {HTMLAudioElement} audioElement The audio element to analyze.
     * @param {HTMLElement}      container    The .vmplayer__visualizer node.
     */
    function VMPlayerVisualizer( audioElement, container ) {
        this.audio         = audioElement;
        this.container     = container;
        this.bars          = Array.prototype.slice.call( container.querySelectorAll( '.vmplayer__visualizer-bar' ) );
        this.mode          = 'idle';
        this.rafId         = null;
        this.audioContext  = null;
        this.analyser      = null;
        this.source        = null;
        this.frequencyData = null;
        this.zeroFrames    = 0;
        this.isRunning     = false;
    }

    /**
     * Initialize the visualizer.
     * Called after the user has interacted (usually clicked play)
     * because browsers block AudioContext creation before user gesture.
     *
     * By default this uses the CSS animation path only. The Web Audio
     * path would sound better on CORS-enabled streams but causes silent
     * audio on streams without CORS headers, because calling
     * createMediaElementSource on a cross-origin audio element with no
     * CORS puts the element into a Web Audio graph that the browser
     * silences for security reasons, and once captured the element can
     * never be un-captured. Since most Shoutcast and Icecast streams
     * do not send CORS headers, CSS mode is the safe default.
     *
     * To opt in to the real-time EQ, set window.VMPLAYER_TRY_WEB_AUDIO
     * to true before the visualizer starts. This is intentionally an
     * escape hatch, not a UI setting, because getting it wrong on a
     * user's site produces silent audio and is hard to diagnose.
     */
    VMPlayerVisualizer.prototype.init = function () {
        if ( 'idle' !== this.mode ) {
            return;
        }

        if ( window.VMPLAYER_TRY_WEB_AUDIO && this.tryWebAudio() ) {
            return;
        }

        this.setMode( 'css' );
    };

    /**
     * Attempt to set up the Web Audio API pipeline.
     * Returns true on success. Failure paths: no AudioContext support,
     * MediaElementSource already connected, or CORS blocks the connection.
     *
     * @return {boolean}
     */
    VMPlayerVisualizer.prototype.tryWebAudio = function () {
        var Ctx = window.AudioContext || window.webkitAudioContext;
        if ( ! Ctx ) {
            return false;
        }

        try {
            this.audioContext = new Ctx();
            this.analyser    = this.audioContext.createAnalyser();
            this.analyser.fftSize = FFT_SIZE;
            this.analyser.smoothingTimeConstant = 0.8;

            this.source = this.audioContext.createMediaElementSource( this.audio );
            this.source.connect( this.analyser );
            this.analyser.connect( this.audioContext.destination );

            this.frequencyData = new Uint8Array( this.analyser.frequencyBinCount );
            this.setMode( 'webaudio' );
            return true;
        } catch ( e ) {
            // Common failure: cross-origin without CORS headers, or
            // audio element already connected to another source.
            this.teardownWebAudio();
            return false;
        }
    };

    /**
     * Release Web Audio resources cleanly.
     */
    VMPlayerVisualizer.prototype.teardownWebAudio = function () {
        try {
            if ( this.source ) {
                this.source.disconnect();
            }
            if ( this.analyser ) {
                this.analyser.disconnect();
            }
            if ( this.audioContext && 'closed' !== this.audioContext.state ) {
                this.audioContext.close();
            }
        } catch ( e ) {
            // Ignore cleanup errors; browser will GC eventually.
        }
        this.audioContext  = null;
        this.analyser      = null;
        this.source        = null;
        this.frequencyData = null;
    };

    /**
     * Switch to a specific mode and apply the matching CSS class.
     *
     * @param {string} mode One of "webaudio", "css", "idle".
     */
    VMPlayerVisualizer.prototype.setMode = function ( mode ) {
        this.mode = mode;
        this.container.classList.remove( 'is-css-mode', 'is-webaudio-mode' );
        if ( 'css' === mode ) {
            this.container.classList.add( 'is-css-mode' );
        } else if ( 'webaudio' === mode ) {
            this.container.classList.add( 'is-webaudio-mode' );
        }
    };

    /**
     * Start the visualizer animation loop.
     * Safe to call multiple times.
     */
    VMPlayerVisualizer.prototype.start = function () {
        if ( this.isRunning ) {
            return;
        }
        this.isRunning = true;
        this.container.classList.remove( 'is-paused' );

        // Resume audio context if it was suspended by browser autoplay policy.
        if ( this.audioContext && 'suspended' === this.audioContext.state ) {
            this.audioContext.resume();
        }

        if ( 'webaudio' === this.mode ) {
            this.zeroFrames = 0;
            this.renderFrame();
        }
        // CSS mode animates purely via CSS keyframes so no JS work needed.
    };

    /**
     * Stop the visualizer. Reset all bars to their minimum height.
     */
    VMPlayerVisualizer.prototype.stop = function () {
        this.isRunning = false;
        this.container.classList.add( 'is-paused' );

        if ( this.rafId ) {
            window.cancelAnimationFrame( this.rafId );
            this.rafId = null;
        }

        this.resetBars();
    };

    /**
     * Reset every bar to the minimum height.
     */
    VMPlayerVisualizer.prototype.resetBars = function () {
        var i, len;
        for ( i = 0, len = this.bars.length; i < len; i++ ) {
            this.bars[ i ].style.height = MIN_BAR_HEIGHT_PCT + '%';
        }
    };

    /**
     * Animation loop for Web Audio mode.
     * Reads frequency bins, maps them across the visible bars, and
     * updates each bar's height. If we see too many consecutive zero
     * frames (indicating a CORS-silent connection), we drop into CSS mode.
     */
    VMPlayerVisualizer.prototype.renderFrame = function () {
        if ( ! this.isRunning || 'webaudio' !== this.mode ) {
            return;
        }

        this.analyser.getByteFrequencyData( this.frequencyData );

        // CORS-blocked MediaElementSource returns all-zero data even
        // though audio plays normally. Detect that pattern and give
        // up on Web Audio.
        var maxValue = 0;
        var i;
        for ( i = 0; i < this.frequencyData.length; i++ ) {
            if ( this.frequencyData[ i ] > maxValue ) {
                maxValue = this.frequencyData[ i ];
            }
        }

        if ( 0 === maxValue ) {
            this.zeroFrames++;
            if ( this.zeroFrames >= ZERO_DATA_FRAMES_TO_FALLBACK ) {
                this.teardownWebAudio();
                this.setMode( 'css' );
                this.container.classList.remove( 'is-paused' );
                return;
            }
        } else {
            this.zeroFrames = 0;
        }

        // Map frequency bins to visible bars, then update heights.
        var barCount = this.bars.length;
        var binCount = this.frequencyData.length;
        var binsPerBar = Math.max( 1, Math.floor( binCount / barCount ) );

        for ( i = 0; i < barCount; i++ ) {
            var sum = 0;
            var startBin = i * binsPerBar;
            var endBin = Math.min( startBin + binsPerBar, binCount );
            var actualBins = endBin - startBin;

            for ( var b = startBin; b < endBin; b++ ) {
                sum += this.frequencyData[ b ];
            }

            var avg = actualBins > 0 ? sum / actualBins : 0;
            var pct = ( avg / 255 ) * ( MAX_BAR_HEIGHT_PCT - MIN_BAR_HEIGHT_PCT ) + MIN_BAR_HEIGHT_PCT;
            this.bars[ i ].style.height = pct + '%';
        }

        this.rafId = window.requestAnimationFrame( this.renderFrame.bind( this ) );
    };

    /**
     * Report the current mode. Used by main script for debugging.
     *
     * @return {string}
     */
    VMPlayerVisualizer.prototype.getMode = function () {
        return this.mode;
    };

    /**
     * Fully destroy the visualizer. Called when the player unmounts.
     */
    VMPlayerVisualizer.prototype.destroy = function () {
        this.stop();
        this.teardownWebAudio();
        this.container.classList.remove( 'is-css-mode', 'is-webaudio-mode', 'is-paused' );
        this.mode = 'idle';
    };

    // Expose for the main frontend script.
    window.VMPlayerVisualizer = VMPlayerVisualizer;

} )();
