/**
 * VMPlayer Pop-Out Window Controller
 *
 * Runs inside the standalone popout window. Owns its own audio element
 * and metadata polling. Communicates with the opener (main player)
 * via postMessage so the two stay in sync: play state, volume, mute,
 * current station.
 *
 * Also handles the window controls: dock (return to main player),
 * minimize (switch to compact bar), close.
 *
 * @package VMPlayer
 */

( function () {
    'use strict';

    var config = window.vmplayerPopoutConfig || {};

    /* ==================================================================
     * VMPlayerPopout class
     * =============================================================== */

    function VMPlayerPopout( container ) {
        this.container            = container;
        this.audio                = null;
        this.currentStationIndex  = 0;
        this.isPlaying            = false;
        this.isMuted              = false;
        this.volume               = 1.0;
        this.metadataTimer        = null;
        this.clockTimer           = null;
        this.visualizer           = null;
        this.hasStartedPlayback   = false;
        this.isCompactMode        = false;
        this.parentWindow         = null;
        this.dom                  = {};
        this.kebabMenuEl          = null;
        this.kebabBackdropEl      = null;
    }

    VMPlayerPopout.prototype.init = function () {
        if ( ! Array.isArray( config.stations ) || 0 === config.stations.length ) {
            return;
        }

        this.currentStationIndex = parseInt( config.activeStationIndex, 10 ) || 0;

        this.cacheDom();
        this.loadPersistedState();
        // If loadPersistedState restored a station index different
        // from the one rendered in the popout HTML, refresh feature
        // flags from that station's config so LIVE detection and
        // listener count reflect the correct settings.
        this.applyStationFeatureFlags();
        this.createAudio();
        this.bindControls();
        this.bindWindowControls();
        this.bindMessageListener();
        this.startClock();
        this.renderCurrentStation();
        this.applyVolumeToSlider();
        this.applyMuteState();
        this.startVisualizer();

        // Subscribe to admin station changes so the popout updates
        // in place when a station is added, removed, or reordered in
        // a different tab. Playing audio continues uninterrupted.
        this.subscribeToStationSync();

        // Fetch metadata immediately.
        this.fetchMetadata();
        this.startMetadataPolling();

        // Announce readiness to opener so it can send init state.
        this.parentWindow = window.opener;
        this.postToParent( 'vmplayer:ready', {
            stationIndex: this.currentStationIndex
        } );

        // Warn parent when we close so it can resume playback if wanted.
        window.addEventListener( 'beforeunload', this.handleBeforeUnload.bind( this ) );

        // Auto-resume playback if the listener had audio playing in
        // this tab session before a refresh. Shares the sessionStorage
        // key with the main frontend so a popout opened from a main
        // player picks up the same intent state.
        this.attemptAutoResume();
    };

    /* ------------------------------------------------------------------
     * DOM cache
     * --------------------------------------------------------------- */

    VMPlayerPopout.prototype.cacheDom = function () {
        var c = this.container;
        this.dom = {
            onair:         c.querySelector( '.vmplayer__onair' ),
            time:          c.querySelector( '[data-role="time"]' ),
            date:          c.querySelector( '[data-role="date"]' ),
            chromeLogo:    c.querySelector( '[data-role="chrome-logo"]' ),
            chromeName:    c.querySelector( '[data-role="chrome-name"]' ),
            stationCounter: c.querySelector( '[data-role="station-counter"]' ),
            artwork:       c.querySelector( '[data-role="artwork"]' ),
            artworkImg:    c.querySelector( '[data-role="artwork-img"]' ),
            trackTitle:    c.querySelector( '[data-role="track-title"]' ),
            artist:        c.querySelector( '[data-role="artist"]' ),
            genre:         c.querySelector( '[data-role="genre"]' ),
            bitrate:       c.querySelector( '[data-role="bitrate"]' ),
            samplerate:    c.querySelector( '[data-role="samplerate"]' ),
            channels:      c.querySelector( '[data-role="channels"]' ),
            description:   c.querySelector( '[data-role="description"]' ),
            sepBitrate:    c.querySelector( '[data-role="sep-bitrate"]' ),
            sepSampleRate: c.querySelector( '[data-role="sep-samplerate"]' ),
            sepChannels:   c.querySelector( '[data-role="sep-channels"]' ),
            sepListeners:  c.querySelector( '[data-role="sep-listeners"]' ),
            liveBadge:         c.querySelector( '[data-role="live-badge"]' ),
            listenerCount:     c.querySelector( '[data-role="listener-count"]' ),
            listenerCountValue:c.querySelector( '[data-role="listener-count-value"]' ),
            social:        c.querySelector( '[data-role="social"]' ),
            visualizer:    c.querySelector( '[data-role="visualizer"]' ),
            muteToggle:    c.querySelector( '[data-role="mute-toggle"]' ),
            volumeSlider:  c.querySelector( '[data-role="volume-slider"]' ),
            volUp:         c.querySelector( '[data-role="vol-up"]' ),
            prev:          c.querySelector( '[data-role="prev"]' ),
            playToggle:    c.querySelector( '[data-role="play-toggle"]' ),
            next:          c.querySelector( '[data-role="next"]' ),
            kebab:         c.querySelector( '[data-role="kebab"]' ),
            dock:          c.querySelector( '[data-role="dock"]' ),
            minimize:      c.querySelector( '[data-role="minimize"]' ),
            close:         c.querySelector( '[data-role="close"]' ),
            compactBar:    c.querySelector( '[data-role="compact-bar"]' ),
            compactTitle:  c.querySelector( '[data-role="compact-title"]' ),
            compactPlay:   c.querySelector( '[data-role="compact-play-toggle"]' ),
            compactVolume: c.querySelector( '[data-role="compact-volume"]' ),
            restore:       c.querySelector( '[data-role="restore"]' ),
            formatBadges:  c.querySelectorAll( '[data-format]' )
        };
        // Feature toggles read from container data attributes. Mirror
        // the main-player behavior so the popout treats LIVE detection
        // and listener count identically.
        this.showListenerCount = c.getAttribute( 'data-show-listener-count' ) === '1';
        this.detectLive        = c.getAttribute( 'data-detect-live' ) === '1';
    };

    /* ------------------------------------------------------------------
     * State persistence (shared with main player via localStorage)
     * --------------------------------------------------------------- */

    VMPlayerPopout.prototype.loadPersistedState = function () {
        try {
            var v = localStorage.getItem( 'vmplayer_volume' );
            if ( null !== v ) {
                this.volume = Math.max( 0, Math.min( 1, parseFloat( v ) ) );
            }
            var m = localStorage.getItem( 'vmplayer_muted' );
            if ( 'true' === m ) {
                this.isMuted = true;
            }
        } catch ( e ) {}
    };

    VMPlayerPopout.prototype.persistVolume = function () {
        try { localStorage.setItem( 'vmplayer_volume', String( this.volume ) ); } catch ( e ) {}
    };
    VMPlayerPopout.prototype.persistMuted = function () {
        try { localStorage.setItem( 'vmplayer_muted', this.isMuted ? 'true' : 'false' ); } catch ( e ) {}
    };
    VMPlayerPopout.prototype.persistStationIndex = function () {
        try { localStorage.setItem( 'vmplayer_station_index', String( this.currentStationIndex ) ); } catch ( e ) {}
    };

    /* ------------------------------------------------------------------
     * Audio
     * --------------------------------------------------------------- */

    VMPlayerPopout.prototype.createAudio = function () {
        this.audio = document.createElement( 'audio' );
        this.audio.preload = 'none';
        // See main frontend note on why crossOrigin is not set.
        this.audio.volume = this.volume;
        this.audio.muted = this.isMuted;

        this.audio.addEventListener( 'playing', this.onAudioPlaying.bind( this ) );
        this.audio.addEventListener( 'pause',   this.onAudioPaused.bind( this ) );
        this.audio.addEventListener( 'error',   this.onAudioError.bind( this ) );

        this.setAudioSource();
    };

    VMPlayerPopout.prototype.setAudioSource = function () {
        var station = this.getCurrentStation();
        if ( station ) {
            this.audio.src = station.stream_url;
        }
    };

    VMPlayerPopout.prototype.onAudioPlaying = function () {
        this.isPlaying = true;
        this.setPlayButtonState( 'playing' );
        this.setOnAir( true );
        if ( this.visualizer ) { this.visualizer.start(); }
        this.postToParent( 'vmplayer:state', this.getState() );
    };

    VMPlayerPopout.prototype.onAudioPaused = function () {
        this.isPlaying = false;
        this.setPlayButtonState( 'paused' );
        this.setOnAir( false );
        if ( this.visualizer ) { this.visualizer.stop(); }
        this.postToParent( 'vmplayer:state', this.getState() );
    };

    VMPlayerPopout.prototype.onAudioError = function () {
        this.isPlaying = false;
        this.setPlayButtonState( 'paused' );
        this.setOnAir( false );
        if ( this.visualizer ) { this.visualizer.stop(); }
    };

    /* ------------------------------------------------------------------
     * Playback controls
     * --------------------------------------------------------------- */

    VMPlayerPopout.prototype.togglePlay = function () {
        if ( this.isPlaying ) { this.pause(); } else { this.play(); }
    };

    VMPlayerPopout.prototype.play = function () {
        if ( ! this.audio.src ) { this.setAudioSource(); }
        if ( this.audio.paused && this.hasStartedPlayback ) {
            this.audio.load();
            this.setAudioSource();
        }
        this.hasStartedPlayback = true;

        if ( config.enableVisualizer && this.visualizer && 'idle' === this.visualizer.getMode() ) {
            this.visualizer.init();
        }

        var self = this;
        var p = this.audio.play();
        if ( p && 'function' === typeof p.catch ) {
            p.then( function () {
                self.persistPlaybackIntent( true );
            } ).catch( function () {} );
        }
    };

    VMPlayerPopout.prototype.pause = function () {
        this.audio.pause();
        this.persistPlaybackIntent( false );
    };

    /**
     * Read the playback intent flag. See main-frontend counterpart
     * for rationale. Uses the same sessionStorage key so a popout
     * opened from a main player inherits the same intent state.
     */
    VMPlayerPopout.prototype.loadPlaybackIntent = function () {
        try {
            return 'true' === sessionStorage.getItem( 'vmplayer_was_playing' );
        } catch ( e ) {
            return false;
        }
    };

    VMPlayerPopout.prototype.persistPlaybackIntent = function ( wasPlaying ) {
        try {
            sessionStorage.setItem( 'vmplayer_was_playing', wasPlaying ? 'true' : 'false' );
        } catch ( e ) {}
    };

    VMPlayerPopout.prototype.attemptAutoResume = function () {
        if ( ! this.loadPlaybackIntent() ) {
            return;
        }
        var self = this;
        setTimeout( function () {
            self.play();
        }, 250 );
    };

    VMPlayerPopout.prototype.setPlayButtonState = function ( state ) {
        if ( this.dom.playToggle ) {
            this.dom.playToggle.setAttribute( 'data-state', state );
        }
        if ( this.dom.compactPlay ) {
            this.dom.compactPlay.setAttribute( 'data-state', state );
        }
    };

    /* ------------------------------------------------------------------
     * Volume and mute
     * --------------------------------------------------------------- */

    VMPlayerPopout.prototype.setVolume = function ( value ) {
        var vol = Math.max( 0, Math.min( 1, parseFloat( value ) ) );
        if ( isNaN( vol ) ) { return; }
        this.volume = vol;
        this.audio.volume = vol;

        if ( vol > 0 && this.isMuted ) {
            this.isMuted = false;
            this.audio.muted = false;
            this.applyMuteState();
            this.persistMuted();
        }

        this.applyVolumeToSlider();
        this.persistVolume();
        this.postToParent( 'vmplayer:state', this.getState() );
    };

    VMPlayerPopout.prototype.applyVolumeToSlider = function () {
        var pct = Math.round( this.volume * 100 );
        if ( this.dom.volumeSlider ) {
            this.dom.volumeSlider.value = pct;
            this.dom.volumeSlider.style.setProperty( '--vmp-vol', pct + '%' );
        }
        if ( this.dom.compactVolume ) {
            this.dom.compactVolume.value = pct;
            this.dom.compactVolume.style.setProperty( '--vmp-vol', pct + '%' );
        }
    };

    VMPlayerPopout.prototype.toggleMute = function () {
        this.isMuted = ! this.isMuted;
        this.audio.muted = this.isMuted;
        this.applyMuteState();
        this.persistMuted();
        this.postToParent( 'vmplayer:state', this.getState() );
    };

    VMPlayerPopout.prototype.applyMuteState = function () {
        if ( this.dom.muteToggle ) {
            this.dom.muteToggle.setAttribute( 'data-muted', this.isMuted ? 'true' : 'false' );
        }
    };

    /* ------------------------------------------------------------------
     * Station cycling
     * --------------------------------------------------------------- */

    VMPlayerPopout.prototype.getCurrentStation = function () {
        return config.stations[ this.currentStationIndex ] || null;
    };

    VMPlayerPopout.prototype.nextStation = function () {
        var total = config.stations.length;
        if ( total <= 1 ) { return; }
        this.loadStation( ( this.currentStationIndex + 1 ) % total );
    };

    VMPlayerPopout.prototype.previousStation = function () {
        var total = config.stations.length;
        if ( total <= 1 ) { return; }
        var idx = this.currentStationIndex - 1;
        if ( idx < 0 ) { idx = total - 1; }
        this.loadStation( idx );
    };

    VMPlayerPopout.prototype.loadStation = function ( index ) {
        if ( index < 0 || index >= config.stations.length ) { return; }
        var wasPlaying = this.isPlaying;

        this.currentStationIndex = index;
        this.container.setAttribute( 'data-station-index', index );
        this.persistStationIndex();

        // Refresh per-station feature flags on station switch so the
        // LIVE indicator and listener count keep pace with the newly
        // selected station.
        this.applyStationFeatureFlags();

        this.audio.pause();
        this.setAudioSource();
        this.renderCurrentStation();

        // Clear listener count chip and LIVE badge so stale state from
        // the previous station does not linger while the next metadata
        // fetch is in flight.
        this.applyListenerCount( null );
        this.applyLiveState( false );

        this.fetchMetadata();

        if ( wasPlaying ) { this.play(); }

        this.postToParent( 'vmplayer:state', this.getState() );
    };

    /**
     * Refresh the popout's per-station feature flags to match the
     * current station in config.stations. Called from init after
     * loadPersistedState and from loadStation on prev/next. Keeps
     * the JS state in sync with the currentStationIndex regardless
     * of what the popout HTML was rendered with.
     */
    VMPlayerPopout.prototype.applyStationFeatureFlags = function () {
        var station = config.stations[ this.currentStationIndex ] || {};
        this.showListenerCount = !! station.show_listener_count;
        this.detectLive        = !! station.detect_live;
        if ( this.container ) {
            this.container.setAttribute( 'data-show-listener-count', this.showListenerCount ? '1' : '0' );
            this.container.setAttribute( 'data-detect-live',         this.detectLive        ? '1' : '0' );
        }
    };

    VMPlayerPopout.prototype.renderCurrentStation = function () {
        var station = this.getCurrentStation();
        if ( ! station ) { return; }

        if ( this.dom.chromeName ) {
            this.dom.chromeName.textContent = station.name || '';
        }
        if ( this.dom.chromeLogo && station.logo_url ) {
            this.dom.chromeLogo.setAttribute( 'src', station.logo_url );
        }
        if ( this.dom.trackTitle ) {
            this.dom.trackTitle.textContent = station.name || '';
        }
        if ( this.dom.compactTitle ) {
            this.dom.compactTitle.textContent = station.name || '';
        }
        if ( this.dom.artist ) {
            this.dom.artist.textContent = '\u00A0';
        }
        if ( this.dom.genre ) {
            this.dom.genre.textContent = station.genre || '';
        }
        if ( this.dom.description ) {
            this.dom.description.textContent = station.description || '';
        }
        this.updateArtwork( station.logo_url || '' );
        this.highlightFormatBadge( station.stream_type );
        this.renderSocial( station.social || {} );
        this.updateStreamInfoSeparators();
        this.updateStationCounter();

        // Update document title to include current station name.
        document.title = station.name + ' - Pop Out Player';
    };

    VMPlayerPopout.prototype.updateStationCounter = function () {
        if ( ! this.dom.stationCounter ) { return; }
        var stations = Array.isArray( config.stations ) ? config.stations : [];
        var total    = stations.length;
        var single   = total <= 1;
        if ( single ) {
            this.dom.stationCounter.classList.add( 'is-hidden' );
            return;
        }
        this.dom.stationCounter.classList.remove( 'is-hidden' );
        var current = this.currentStationIndex + 1;
        var template = ( config.strings && config.strings.stationOf )
            ? config.strings.stationOf
            : 'Station %1$d of %2$d';
        var text = template.replace( '%1$d', current ).replace( '%2$d', total );
        this.dom.stationCounter.textContent = text;
        this.dom.stationCounter.setAttribute( 'aria-label', text );
        this.dom.stationCounter.setAttribute( 'data-total', String( total ) );
    };

    VMPlayerPopout.prototype.updatePrevNextEnabled = function () {
        var stations = Array.isArray( config.stations ) ? config.stations : [];
        var single   = stations.length <= 1;
        var setState = function ( btn ) {
            if ( ! btn ) { return; }
            btn.disabled = single;
            btn.setAttribute( 'aria-disabled', single ? 'true' : 'false' );
        };
        setState( this.dom.prev );
        setState( this.dom.next );
    };

    VMPlayerPopout.prototype.subscribeToStationSync = function () {
        if ( 'undefined' === typeof window.BroadcastChannel ) { return; }
        var self = this;
        try {
            this.stationSyncChannel = new window.BroadcastChannel( 'vmplayer_stations' );
        } catch ( e ) {
            this.stationSyncChannel = null;
            return;
        }
        this.stationSyncChannel.addEventListener( 'message', function ( event ) {
            if ( ! event || ! event.data || 'vmplayer:stations_updated' !== event.data.type ) {
                return;
            }
            self.applyStationListUpdate( event.data.stations );
        } );
    };

    VMPlayerPopout.prototype.applyStationListUpdate = function ( incomingStations ) {
        if ( ! Array.isArray( incomingStations ) ) { return; }

        var defaultLogo = config.defaultLogoUrl || '';
        var stations = incomingStations.map( function ( station ) {
            var out = {};
            for ( var key in station ) {
                if ( Object.prototype.hasOwnProperty.call( station, key ) ) {
                    out[ key ] = station[ key ];
                }
            }
            if ( ! out.logo_url || '' === String( out.logo_url ).trim() ) {
                out.logo_url = defaultLogo;
            }
            return out;
        } );

        var currentStation = config.stations && config.stations[ this.currentStationIndex ];
        var currentId      = currentStation ? currentStation.id : '';
        var newIndex       = -1;
        if ( currentId ) {
            for ( var i = 0; i < stations.length; i++ ) {
                if ( stations[ i ].id === currentId ) { newIndex = i; break; }
            }
        }
        config.stations = stations;

        if ( newIndex >= 0 ) {
            this.currentStationIndex = newIndex;
        } else if ( stations.length > 0 ) {
            this.currentStationIndex = 0;
            this.renderCurrentStation();
        } else if ( this.audio ) {
            try { this.audio.pause(); } catch ( _ ) {}
        }

        this.updateStationCounter();
        this.updatePrevNextEnabled();
    };

    /* ------------------------------------------------------------------
     * Metadata
     * --------------------------------------------------------------- */

    VMPlayerPopout.prototype.applyMetadata = function ( meta ) {
        if ( ! meta || 'object' !== typeof meta ) { return; }
        this.currentMetadata = meta;

        // LIVE broadcast detection. Mirrors the main-player behavior:
        // when the server flags this station as live and the admin
        // enabled detection, replace the track title area with the
        // LIVE indicator so listeners do not see a leftover song title
        // from before the live source took over. When off, normal
        // metadata flow updates title and artist as usual.
        var isLive = this.detectLive && !! meta.is_live_broadcast;
        this.applyLiveState( isLive );

        if ( ! isLive ) {
            if ( this.dom.trackTitle && meta.title ) {
                this.dom.trackTitle.textContent = meta.title;
            }
            if ( this.dom.artist ) {
                this.dom.artist.textContent = meta.artist || '\u00A0';
            }
        }
        if ( this.dom.compactTitle && meta.title ) {
            var artist = meta.artist ? meta.artist + ' - ' : '';
            this.dom.compactTitle.textContent = isLive ? 'LIVE' : ( artist + meta.title );
        }
        if ( this.dom.genre && meta.genre ) {
            this.dom.genre.textContent = meta.genre;
        }
        if ( this.dom.bitrate ) {
            this.dom.bitrate.textContent = meta.bitrate ? meta.bitrate + ' kbps' : '';
        }
        if ( this.dom.samplerate ) {
            var sr = parseInt( meta.sample_rate, 10 );
            this.dom.samplerate.textContent = sr > 0 ? ( sr / 1000 ) + ' kHz' : '';
        }
        if ( this.dom.channels ) {
            var ch = parseInt( meta.channels, 10 );
            this.dom.channels.textContent = 1 === ch ? 'Mono' : ( 2 === ch ? 'Stereo' : '' );
        }

        // Listener count. Only rendered when the station admin enabled
        // it AND the server actually returned a positive number.
        this.applyListenerCount( meta.listener_count );

        if ( meta.artwork_url ) {
            this.updateArtwork( meta.artwork_url );
        }
        if ( meta.stream_type ) {
            this.highlightFormatBadge( meta.stream_type );
        }
        this.updateStreamInfoSeparators();
    };

    /**
     * Toggle the LIVE indicator on or off. When live: show badge,
     * blank the artist line, fall back to the station name for the
     * track title so the display reads sensibly instead of showing
     * the leftover automation song title.
     */
    VMPlayerPopout.prototype.applyLiveState = function ( isLive ) {
        var badge = this.dom.liveBadge;
        if ( badge ) {
            if ( isLive ) {
                badge.removeAttribute( 'hidden' );
            } else {
                badge.setAttribute( 'hidden', '' );
            }
        }
        if ( isLive ) {
            if ( this.dom.artist ) {
                this.dom.artist.textContent = '\u00A0';
            }
            if ( this.dom.trackTitle && this.currentMetadata && this.currentMetadata.station_name ) {
                this.dom.trackTitle.textContent = this.currentMetadata.station_name;
            }
        }
    };

    /**
     * Show or hide the listener count chip. Requires the admin toggle
     * AND a positive count. Formats larger numbers with a thousands
     * separator.
     */
    VMPlayerPopout.prototype.applyListenerCount = function ( count ) {
        var chip = this.dom.listenerCount;
        if ( ! chip ) { return; }
        var num = parseInt( count, 10 );
        if ( ! this.showListenerCount || isNaN( num ) || num <= 0 ) {
            chip.setAttribute( 'hidden', '' );
            return;
        }
        if ( this.dom.listenerCountValue ) {
            this.dom.listenerCountValue.textContent = num.toLocaleString();
        }
        chip.removeAttribute( 'hidden' );
    };

    VMPlayerPopout.prototype.updateStreamInfoSeparators = function () {
        var hasGenre      = this.dom.genre      && '' !== this.dom.genre.textContent.trim();
        var hasBitrate    = this.dom.bitrate    && '' !== this.dom.bitrate.textContent.trim();
        var hasSampleRate = this.dom.samplerate && '' !== this.dom.samplerate.textContent.trim();
        var hasChannels   = this.dom.channels   && '' !== this.dom.channels.textContent.trim();
        // Listener count is present when the chip itself is not
        // hidden. Mirrors the main-player logic so the leading
        // separator only appears when listeners are actually shown.
        var hasListeners  = this.dom.listenerCount && ! this.dom.listenerCount.hasAttribute( 'hidden' );

        this.toggleHidden( this.dom.sepBitrate,    ! ( hasGenre && hasBitrate ) );
        this.toggleHidden( this.dom.sepSampleRate, ! ( ( hasBitrate || hasGenre ) && hasSampleRate ) );
        this.toggleHidden( this.dom.sepChannels,   ! ( ( hasSampleRate || hasBitrate || hasGenre ) && hasChannels ) );
        this.toggleHidden( this.dom.sepListeners,  ! ( ( hasChannels || hasSampleRate || hasBitrate || hasGenre ) && hasListeners ) );
    };

    VMPlayerPopout.prototype.toggleHidden = function ( el, hidden ) {
        if ( ! el ) { return; }
        el.classList.toggle( 'is-hidden', hidden );
    };

    VMPlayerPopout.prototype.updateArtwork = function ( url ) {
        if ( ! this.dom.artwork ) { return; }
        var img = this.dom.artworkImg || this.dom.artwork.querySelector( 'img' );

        if ( ! url ) { return; }
        if ( img && img.getAttribute( 'src' ) === url ) { return; }

        if ( ! img ) {
            img = document.createElement( 'img' );
            img.setAttribute( 'alt', '' );
            img.setAttribute( 'data-role', 'artwork-img' );
            this.dom.artwork.innerHTML = '';
            this.dom.artwork.appendChild( img );
            this.dom.artworkImg = img;
        }

        // Fall back to the station logo when the remote artwork fails.
        // See main frontend for the reasoning.
        var self = this;
        var onError = function () {
            img.removeEventListener( 'error', onError );
            var station = self.getCurrentStation();
            var fallback = station && station.logo_url ? station.logo_url : '';
            if ( fallback && fallback !== url ) {
                img.setAttribute( 'src', fallback );
            } else if ( img.parentNode ) {
                img.parentNode.removeChild( img );
                self.dom.artworkImg = null;
            }
        };
        img.addEventListener( 'error', onError );

        img.setAttribute( 'src', url );
    };

    VMPlayerPopout.prototype.highlightFormatBadge = function ( streamType ) {
        var st = ( streamType || '' ).toLowerCase();
        for ( var i = 0; i < this.dom.formatBadges.length; i++ ) {
            var badge = this.dom.formatBadges[ i ];
            badge.classList.toggle( 'is-active', badge.getAttribute( 'data-format' ) === st );
        }
    };

    VMPlayerPopout.prototype.renderSocial = function ( social ) {
        if ( ! this.dom.social || ! social || 'object' !== typeof social ) { return; }
        var links = this.dom.social.querySelectorAll( '.vmplayer__social-link' );
        for ( var i = 0; i < links.length; i++ ) {
            var link = links[ i ];
            var platform = link.getAttribute( 'data-platform' );
            var url = social[ platform ] || '';
            if ( url ) {
                link.setAttribute( 'href', url );
                link.classList.remove( 'is-hidden' );
            } else {
                link.classList.add( 'is-hidden' );
            }
        }
    };

    VMPlayerPopout.prototype.setOnAir = function ( on ) {
        if ( this.dom.onair ) {
            this.dom.onair.setAttribute( 'data-state', on ? 'on' : 'off' );
        }
    };

    /* ------------------------------------------------------------------
     * Clock
     * --------------------------------------------------------------- */

    VMPlayerPopout.prototype.startClock = function () {
        this.updateClock();
        this.clockTimer = window.setInterval( this.updateClock.bind( this ), 1000 );
    };

    VMPlayerPopout.prototype.updateClock = function () {
        var now = new Date();
        if ( this.dom.time ) {
            this.dom.time.textContent = this.formatTime( now );
        }
        if ( this.dom.date ) {
            this.dom.date.textContent = this.formatDate( now );
        }
    };

    VMPlayerPopout.prototype.formatTime = function ( d ) {
        try {
            return new Intl.DateTimeFormat( undefined, {
                hour: 'numeric', minute: '2-digit', second: '2-digit', hour12: true
            } ).format( d );
        } catch ( e ) { return d.toLocaleTimeString(); }
    };

    VMPlayerPopout.prototype.formatDate = function ( d ) {
        try {
            return new Intl.DateTimeFormat( undefined, {
                weekday: 'long', month: 'long', day: 'numeric', year: 'numeric'
            } ).format( d );
        } catch ( e ) { return d.toLocaleDateString(); }
    };

    /* ------------------------------------------------------------------
     * Metadata polling
     * --------------------------------------------------------------- */

    VMPlayerPopout.prototype.startMetadataPolling = function () {
        var self = this;
        var interval = Math.max( 5, parseInt( config.refreshInterval, 10 ) || 15 ) * 1000;
        this.metadataTimer = window.setInterval( function () {
            self.fetchMetadata();
        }, interval );
    };

    VMPlayerPopout.prototype.fetchMetadata = function () {
        var self = this;
        var body = new FormData();
        body.append( 'action', 'vmplayer_get_metadata' );
        body.append( 'nonce', config.nonce );
        body.append( 'station_index', this.currentStationIndex );

        fetch( config.ajaxUrl, {
            method: 'POST',
            credentials: 'same-origin',
            body: body
        } )
            .then( function ( r ) { return r.json(); } )
            .then( function ( response ) {
                if ( response && response.success && response.data ) {
                    self.applyMetadata( response.data );
                }
            } )
            .catch( function () {} );
    };

    /* ------------------------------------------------------------------
     * Visualizer
     * --------------------------------------------------------------- */

    VMPlayerPopout.prototype.startVisualizer = function () {
        if ( ! config.enableVisualizer ) { return; }
        if ( ! this.dom.visualizer || 'undefined' === typeof window.VMPlayerVisualizer ) { return; }
        this.visualizer = new window.VMPlayerVisualizer( this.audio, this.dom.visualizer );
        this.visualizer.stop();
    };

    /* ------------------------------------------------------------------
     * Window controls (dock / minimize / close)
     * --------------------------------------------------------------- */

    VMPlayerPopout.prototype.dockToParent = function () {
        this.postToParent( 'vmplayer:dock', this.getState() );
        window.close();
    };

    VMPlayerPopout.prototype.closeWindow = function () {
        this.postToParent( 'vmplayer:close', this.getState() );
        window.close();
    };

    VMPlayerPopout.prototype.enterCompactMode = function () {
        this.isCompactMode = true;
        document.body.classList.add( 'vmplayer-popout--compact' );
        this.container.classList.add( 'vmplayer-popout--compact' );
        if ( this.dom.compactBar ) {
            this.dom.compactBar.removeAttribute( 'hidden' );
        }
        // Ask the browser to resize the window to the compact height.
        // Not every browser honors this, but Chrome, Firefox, and Safari do.
        try {
            window.resizeTo( window.outerWidth, 100 );
        } catch ( e ) {}
    };

    VMPlayerPopout.prototype.exitCompactMode = function () {
        this.isCompactMode = false;
        document.body.classList.remove( 'vmplayer-popout--compact' );
        this.container.classList.remove( 'vmplayer-popout--compact' );
        if ( this.dom.compactBar ) {
            this.dom.compactBar.setAttribute( 'hidden', '' );
        }
        try {
            window.resizeTo( 420, 780 );
        } catch ( e ) {}
    };

    VMPlayerPopout.prototype.handleBeforeUnload = function () {
        // Final state check-in.
        this.postToParent( 'vmplayer:close', this.getState() );
    };

    /* ------------------------------------------------------------------
     * postMessage protocol
     * --------------------------------------------------------------- */

    VMPlayerPopout.prototype.getState = function () {
        return {
            stationIndex: this.currentStationIndex,
            isPlaying:    this.isPlaying,
            isMuted:      this.isMuted,
            volume:       this.volume
        };
    };

    VMPlayerPopout.prototype.postToParent = function ( type, payload ) {
        if ( ! this.parentWindow || this.parentWindow.closed ) { return; }
        try {
            this.parentWindow.postMessage( {
                source: 'vmplayer-popout',
                type: type,
                payload: payload || {}
            }, '*' );
        } catch ( e ) {}
    };

    VMPlayerPopout.prototype.bindMessageListener = function () {
        var self = this;
        window.addEventListener( 'message', function ( event ) {
            var data = event.data;
            if ( ! data || 'vmplayer-main' !== data.source ) { return; }

            switch ( data.type ) {
                case 'vmplayer:init':
                    if ( data.payload ) {
                        if ( 'number' === typeof data.payload.stationIndex &&
                             data.payload.stationIndex !== self.currentStationIndex ) {
                            self.loadStation( data.payload.stationIndex );
                        }
                        if ( 'number' === typeof data.payload.volume ) {
                            self.setVolume( data.payload.volume );
                        }
                        if ( 'boolean' === typeof data.payload.isMuted && data.payload.isMuted !== self.isMuted ) {
                            self.toggleMute();
                        }
                        // Take over playback if parent was playing.
                        if ( data.payload.isPlaying ) {
                            self.play();
                        }
                    }
                    break;

                case 'vmplayer:close':
                    // Parent is closing us. Say goodbye and close.
                    self.closeWindow();
                    break;
            }
        } );
    };

    /* ------------------------------------------------------------------
     * Event binding
     * --------------------------------------------------------------- */

    VMPlayerPopout.prototype.bindControls = function () {
        var self = this;

        if ( this.dom.playToggle ) {
            this.dom.playToggle.addEventListener( 'click', function () { self.togglePlay(); } );
        }
        if ( this.dom.compactPlay ) {
            this.dom.compactPlay.addEventListener( 'click', function () { self.togglePlay(); } );
        }
        if ( this.dom.muteToggle ) {
            this.dom.muteToggle.addEventListener( 'click', function () { self.toggleMute(); } );
        }
        if ( this.dom.volumeSlider ) {
            this.dom.volumeSlider.addEventListener( 'input', function ( e ) {
                self.setVolume( parseFloat( e.target.value ) / 100 );
            } );
        }
        if ( this.dom.compactVolume ) {
            this.dom.compactVolume.addEventListener( 'input', function ( e ) {
                self.setVolume( parseFloat( e.target.value ) / 100 );
            } );
        }
        if ( this.dom.volUp ) {
            this.dom.volUp.addEventListener( 'click', function () { self.setVolume( 1.0 ); } );
        }
        if ( this.dom.prev ) {
            this.dom.prev.addEventListener( 'click', function () { self.previousStation(); } );
        }
        if ( this.dom.next ) {
            this.dom.next.addEventListener( 'click', function () { self.nextStation(); } );
        }
        if ( this.dom.kebab ) {
            this.dom.kebab.addEventListener( 'click', function ( e ) {
                e.stopPropagation();
                self.toggleKebabMenu();
            } );
        }
    };

    VMPlayerPopout.prototype.bindWindowControls = function () {
        var self = this;
        if ( this.dom.dock ) {
            this.dom.dock.addEventListener( 'click', function () { self.dockToParent(); } );
        }
        if ( this.dom.minimize ) {
            this.dom.minimize.addEventListener( 'click', function () { self.enterCompactMode(); } );
        }
        if ( this.dom.restore ) {
            this.dom.restore.addEventListener( 'click', function () { self.exitCompactMode(); } );
        }
        if ( this.dom.close ) {
            this.dom.close.addEventListener( 'click', function () { self.closeWindow(); } );
        }
    };

    /* ------------------------------------------------------------------
     * Kebab menu
     * --------------------------------------------------------------- */

    VMPlayerPopout.prototype.toggleKebabMenu = function () {
        if ( this.kebabMenuEl ) {
            this.closeKebabMenu();
            return;
        }
        this.openKebabMenu();
    };

    VMPlayerPopout.prototype.openKebabMenu = function () {
        if ( 'undefined' === typeof window.VMPlayerKebab ) { return; }
        var self = this;
        this.kebabMenuEl = window.VMPlayerKebab.render( {
            anchor:      this.dom.kebab,
            strings:     config.strings || {},
            currentStation: this.getCurrentStation(),
            currentIndex:   this.currentStationIndex,
            settings:    { autoResume: config.autoResume, enableVisualizer: config.enableVisualizer },
            onSelectMount: function ( mountIndex ) {
                self.switchToMount( mountIndex );
            },
            onToggleSetting: function ( key, value ) {
                self.toggleSetting( key, value );
            },
            onClose: function () {
                self.kebabMenuEl = null;
                self.kebabBackdropEl = null;
            }
        } );
        this.kebabBackdropEl = document.querySelector( '.vmplayer-kebab-backdrop' );
    };

    VMPlayerPopout.prototype.closeKebabMenu = function () {
        if ( window.VMPlayerKebab ) {
            window.VMPlayerKebab.close();
        }
        this.kebabMenuEl = null;
        this.kebabBackdropEl = null;
    };

    VMPlayerPopout.prototype.switchToMount = function ( mountIndex ) {
        var station = this.getCurrentStation();
        if ( ! station || ! station.mounts || ! station.mounts[ mountIndex ] ) { return; }
        var mount = station.mounts[ mountIndex ];
        if ( ! mount.url ) { return; }

        var wasPlaying = this.isPlaying;
        this.audio.pause();
        this.audio.src = mount.url;
        if ( wasPlaying ) {
            this.play();
        }
    };

    VMPlayerPopout.prototype.toggleSetting = function ( key, value ) {
        if ( 'enableVisualizer' === key ) {
            config.enableVisualizer = value;
            if ( ! value && this.visualizer ) {
                this.visualizer.stop();
                if ( this.dom.visualizer ) {
                    this.dom.visualizer.style.visibility = 'hidden';
                }
            } else if ( value && this.dom.visualizer ) {
                this.dom.visualizer.style.visibility = '';
                if ( this.isPlaying && this.visualizer ) {
                    this.visualizer.start();
                }
            }
        } else if ( 'autoResume' === key ) {
            config.autoResume = value;
            // Persist to parent so behavior stays consistent.
            this.postToParent( 'vmplayer:state', { autoResume: value } );
        }
    };

    /* ==================================================================
     * Bootstrap
     * =============================================================== */

    function boot() {
        var container = document.querySelector( '.vmplayer-popout' );
        if ( ! container ) { return; }
        var popout = new VMPlayerPopout( container );
        popout.init();
        window.VMPlayerPopoutInstance = popout;
    }

    if ( 'loading' === document.readyState ) {
        document.addEventListener( 'DOMContentLoaded', boot );
    } else {
        boot();
    }

} )();
