/**
 * AudioToggle Component
 *
 * Plays a looping ambient soundtrack, toggled from the header button.
 * Sound is ON by default; only an explicit opt-out is stored in localStorage,
 * so a first-time visitor gets music and a visitor who muted it stays muted
 * across navigation.
 *
 * Browsers refuse unmuted playback until the visitor has interacted with the
 * document, so "on by default" means: start immediately when allowed, else
 * start on the first pointer / key / scroll event.
 */
const STORAGE_KEY = 'audio-enabled';
const TARGET_VOLUME = 0.35;
const FADE_DURATION = 600;

export class AudioToggle {
    // Several buttons can control the same soundtrack (header + burger menu on mobile)
    private buttons: HTMLButtonElement[];
    private audio: HTMLAudioElement | null = null;
    private enabled: boolean = false;
    private fadeFrame: number | null = null;

    constructor() {
        this.buttons = Array.from(document.querySelectorAll<HTMLButtonElement>('[data-audio-toggle]'));

        this.init();
    }

    private init(): void {
        const src = this.buttons.find((button) => button.dataset.audioSrc)?.dataset.audioSrc;
        if (!src) {
            return;
        }

        this.audio = new Audio(src);
        this.audio.loop = true;
        this.audio.preload = 'auto';
        this.audio.volume = 0;

        // Absent key => opted in. Only 'false' (explicit mute) turns it off.
        this.enabled = localStorage.getItem(STORAGE_KEY) !== 'false';
        this.updateState();

        this.buttons.forEach((button) => {
            button.addEventListener('click', () => {
                this.toggle();
            });
        });

        if (this.enabled) {
            // play() falls back to a first-gesture listener if autoplay is blocked.
            this.play();
        }
    }

    private toggle(): void {
        this.enabled = !this.enabled;
        localStorage.setItem(STORAGE_KEY, String(this.enabled));
        this.updateState();

        if (this.enabled) {
            this.play();
        } else {
            this.fadeOut();
        }
    }

    /** Autoplay is blocked until the visitor interacts with the document. */
    private resumeOnFirstGesture(): void {
        const start = (): void => {
            document.removeEventListener('pointerdown', start);
            document.removeEventListener('keydown', start);
            document.removeEventListener('scroll', start);

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

        document.addEventListener('pointerdown', start, { once: true });
        document.addEventListener('keydown', start, { once: true });
        document.addEventListener('scroll', start, { once: true, passive: true });
    }

    private play(): void {
        if (!this.audio) {
            return;
        }

        const playback = this.audio.play();

        // A rejected promise means the browser refused playback (no gesture yet):
        // keep the stored preference and wait for the next interaction.
        if (playback !== undefined) {
            playback
                .then(() => this.fadeTo(TARGET_VOLUME))
                .catch(() => this.resumeOnFirstGesture());

            return;
        }

        this.fadeTo(TARGET_VOLUME);
    }

    private fadeOut(): void {
        this.fadeTo(0, () => {
            this.audio?.pause();
        });
    }

    private fadeTo(target: number, onComplete?: () => void): void {
        if (!this.audio) {
            return;
        }

        if (this.fadeFrame !== null) {
            cancelAnimationFrame(this.fadeFrame);
        }

        const audio = this.audio;
        const from = audio.volume;
        const start = performance.now();

        const step = (now: number): void => {
            // rAF hands us the frame start time, which can predate `start`
            // (Firefox) — clamp both ends, an out-of-range volume throws.
            const progress = Math.min(Math.max((now - start) / FADE_DURATION, 0), 1);
            audio.volume = Math.min(Math.max(from + (target - from) * progress, 0), 1);

            if (progress < 1) {
                this.fadeFrame = requestAnimationFrame(step);

                return;
            }

            this.fadeFrame = null;
            onComplete?.();
        };

        this.fadeFrame = requestAnimationFrame(step);
    }

    private updateState(): void {
        this.buttons.forEach((button) => {
            button.classList.toggle('site-header__audio-btn--active', this.enabled);
            button.querySelector('.audio-icon')?.classList.toggle('audio-icon--playing', this.enabled);
            button.setAttribute('aria-pressed', String(this.enabled));

            const label = this.enabled ? button.dataset.labelOn : button.dataset.labelOff;
            if (label) {
                button.setAttribute('aria-label', label);
                button.setAttribute('title', label);
            }
        });
    }
}
