// voice-glow.js — the my-sea voice-affordance glow/pulse state machine (Phase 3
// of the my-sea invite/voice batch, user-spec 2026-05-29). Drives --quaUser /
// --ninUser glow + pulse on the burger btn + its voice sub-btn off two signals:
//   • the voice sub-btn's availability (.active, server-rendered voice_active),
//   • the live mesh state (VoiceRoom.setOnStateChange → {inCall, peerCount}).
//
// Rules (mirrors the sea-btn glow handoff, but for voice):
//   PRE-JOIN nudge (voice available, mic not yet live):
//     • burger open  → the voice sub-btn glows (steady --quaUser). The sea-btn
//       draw nudge keeps burger precedence; voice is the second-place reveal.
//     • burger closed → the burger glows, but ONLY once the sea nudge has
//       cleared (sea takes precedence on the burger).
//   LIVE (mic in use):
//     • the glow PULSES (ease in/out). burger open → the voice sub-btn pulses;
//       fan collapsed → the burger pulses instead, so the mic-live cue rides
//       whichever surface is showing.
//     • alone in the room → base 2s cadence (.voice-pulse). A 2nd+ party →
//       doubled cadence (.voice-pulse--fast) — an equalizer stand-in (a true
//       volume-reactive equalizer is a live-only enhancement).
//
// `bindVoiceGlow()` reads the DOM, wires the observers + mesh subscription, and
// returns { render, setVoiceState, destroy } for tests / re-binds. Class writes
// are reconciled (idempotent) so the burger-class MutationObserver doesn't feed
// back into itself.
(function () {
    'use strict';

    var OUR_CLASSES = ['voice-glow', 'voice-pulse', 'voice-eq', 'voice-muted'];

    function bindVoiceGlow() {
        var burger = document.getElementById('id_burger_btn');
        var voice  = document.getElementById('id_voice_btn');
        var sea    = document.getElementById('id_sea_btn');
        if (!burger || !voice) return null;

        var inCall = false;
        var peerCount = 0;
        var muted = false;
        var observers = [];
        var _raf = 0;

        // Reconcile el's OUR_CLASSES to exactly `want` — only mutate on an
        // actual diff so the MutationObserver below doesn't loop on our own
        // writes.
        function _apply(el, want) {
            OUR_CLASSES.forEach(function (c) {
                var has = el.classList.contains(c);
                var need = want.indexOf(c) !== -1;
                if (need && !has) el.classList.add(c);
                else if (!need && has) el.classList.remove(c);
            });
        }

        function render() {
            var voiceWant = [];
            var burgerWant = [];

            if (voice.classList.contains('active')) {   // voice available
                var burgerOpen = burger.classList.contains('active');
                var live = inCall || voice.classList.contains('in-call');
                if (live) {
                    // Mic live → the glow lands on the showing surface (voice
                    // sub-btn fan-open, burger fan-closed). Others connected →
                    // a volume-reactive equalizer (.voice-eq, fed --voice-level
                    // by the RAF loop); alone → a steady pulse. Muted recolors
                    // the lot to --priRd + flips the sub-btn icon to .fa-ban
                    // (the "disconnected from talking" cue, item 7).
                    var want = ['voice-glow', peerCount > 0 ? 'voice-eq' : 'voice-pulse'];
                    if (muted) want.push('voice-muted');
                    var arr = burgerOpen ? voiceWant : burgerWant;
                    want.forEach(function (c) { arr.push(c); });
                } else {
                    var seaGlow = (sea && sea.classList.contains('glow-handoff'))
                                  || burger.classList.contains('glow-handoff');
                    if (burgerOpen) voiceWant.push('voice-glow');
                    else if (!seaGlow) burgerWant.push('voice-glow');
                }
            }

            _apply(voice, voiceWant);
            _apply(burger, burgerWant);
            _ensureEqLoop();
        }

        // While an .voice-eq surface is showing, drive its --voice-level CSS
        // var off the mesh's incoming RMS each frame (the glow box-shadow
        // scales with it). Self-stops when no .voice-eq remains.
        function _eqTick() {
            var lvl = (window.VoiceRoom && window.VoiceRoom.inputLevel)
                ? window.VoiceRoom.inputLevel() : 0;
            var anyEq = false;
            [voice, burger].forEach(function (el) {
                if (el.classList.contains('voice-eq')) {
                    el.style.setProperty('--voice-level', lvl.toFixed(3));
                    anyEq = true;
                } else {
                    el.style.removeProperty('--voice-level');
                }
            });
            _raf = (anyEq && typeof requestAnimationFrame === 'function')
                ? requestAnimationFrame(_eqTick) : 0;
        }

        function _ensureEqLoop() {
            var anyEq = voice.classList.contains('voice-eq')
                     || burger.classList.contains('voice-eq');
            if (anyEq && !_raf && typeof requestAnimationFrame === 'function') {
                _raf = requestAnimationFrame(_eqTick);
            }
        }

        function setVoiceState(st) {
            st = st || {};
            inCall = !!st.inCall;
            peerCount = st.peerCount || 0;
            muted = inCall && !!st.muted;
            // Keep the voice btn's own flags in sync with the mesh truth so a
            // rejoin starts clean (dataset.inCall gates join-vs-mute in
            // burger-btn.js) and the render fallback can't get stuck "live".
            // SYMMETRIC (set on true, not only delete on false): `setOnStateChange`
            // pushes the CURRENT state immediately on subscribe, and voice-glow
            // often subscribes mid-join (getUserMedia pending → inCall=false),
            // which used to DELETE the `dataset.inCall` burger-btn.js had just
            // set — so the next click re-joined instead of muting (+ dropped the
            // peer, killing the equalizer). Re-setting it on the inCall=true
            // notify restores it once the stream resolves (bug fix 2026-05-30).
            voice.classList.toggle('in-call', inCall);
            if (inCall) { voice.dataset.inCall = '1'; }
            else { delete voice.dataset.inCall; }
            render();
        }

        // Burger open/close + sea-btn glow handoff are both class changes.
        [burger, sea].forEach(function (el) {
            if (!el) return;
            var mo = new MutationObserver(render);
            mo.observe(el, { attributes: true, attributeFilter: ['class'] });
            observers.push(mo);
        });

        // Subscribe to the mesh. VoiceRoom loads lazily — on the first voice
        // CLICK (manual join) OR on an AUTO-REJOIN (no click, burger-btn.js calls
        // withVoiceRoom directly). Catch BOTH: the `voiceroom:ready` event
        // voice-mesh.js fires when its singleton is created (covers auto-rejoin)
        // + a post-click poll (covers the manual path even if the event is
        // missed). Previously only the click-poll existed, so after a refresh the
        // glow was never mesh-driven — no peer-count equalizer (2026-05-30).
        function subscribe() {
            if (window.VoiceRoom && window.VoiceRoom.setOnStateChange) {
                window.VoiceRoom.setOnStateChange(setVoiceState);
                return true;
            }
            return false;
        }
        if (!subscribe()) {
            document.addEventListener('voiceroom:ready', subscribe);
            voice.addEventListener('click', function () {
                var tries = 0;
                var iv = setInterval(function () {
                    if (subscribe() || ++tries > 40) clearInterval(iv);
                }, 50);
            });
        }

        render();
        return {
            render: render,
            setVoiceState: setVoiceState,
            destroy: function () {
                observers.forEach(function (o) { o.disconnect(); });
                observers = [];
                if (_raf && typeof cancelAnimationFrame === 'function') {
                    cancelAnimationFrame(_raf);
                }
                _raf = 0;
            },
        };
    }

    window.bindVoiceGlow = bindVoiceGlow;

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