/**
 * SoundManager — воспроизведение пользовательских звуков (Фаза 5.5).
 *
 * Берёт dataUrl из SoundLibrary, декодирует через Web Audio и проигрывает.
 * Поддерживает 3D-позиционный звук: громкость падает с расстоянием от
 * игрока до точки звука. Звук может быть:
 *   - 2D (обычный, фикс. громкость) — UI-клики, музыка;
 *   - 3D в точке мира — взрыв, выстрел в точке;
 *   - 3D, привязанный к объекту — звук следует за объектом, громкость
 *     обновляется каждый кадр.
 *
 * Живёт только в Play-режиме.
 */

// Дистанция, дальше которой 3D-звук не слышно (м).
const MAX_HEAR_DIST = 40;
// Дистанция, ближе которой громкость максимальная (м).
const FULL_VOLUME_DIST = 4;

let _soundInstSeq = 1;

export class SoundManager {
    constructor(scene3d) {
        this.scene3d = scene3d;
        this.scene = scene3d.scene;
        this._ctx = null;
        // Декодированные AudioBuffer по soundId (кэш — декодируем один раз).
        this._buffers = new Map();
        // ВСЕ играющие звуки (2D и 3D) по instId: {instId,gain,src,getPos,baseVol}.
        this._byInstId = new Map();
        // Активные 3D-звуки — подмножество _byInstId, для обновления
        // громкости в _tick каждый кадр.
        this._active = new Map();
        // instId звуков, остановленных через stopSound ДО конца декодирования
        // — play().then() их не стартует.
        this._stoppedIds = new Set();
        this._renderHook = null;
        // Идёт ли Play-сессия. play() стартует звук в async-then —
        // если за это время был Stop, _running=false и звук НЕ стартует
        // (иначе звук «переживает» Stop из-за гонки с декодированием).
        this._running = false;
    }

    start() {
        this._running = true;
        if (this._renderHook) return;
        this._ensureCtx();
        this._renderHook = () => this._tick();
        this.scene.registerBeforeRender(this._renderHook);
    }

    stop() {
        this._running = false;
        if (this._renderHook) {
            try { this.scene.unregisterBeforeRender(this._renderHook); } catch (e) {}
            this._renderHook = null;
        }
        // Останавливаем ВСЕ играющие звуки (2D и 3D).
        for (const a of this._byInstId.values()) {
            try { a.src.stop(); } catch (e) { /* ignore */ }
        }
        this._byInstId.clear();
        this._active.clear();
        this._stoppedIds.clear();
    }

    _ensureCtx() {
        if (this._ctx) return;
        try {
            const Ctx = window.AudioContext || window.webkitAudioContext;
            if (Ctx) this._ctx = new Ctx();
        } catch (e) { /* ignore */ }
    }

    /**
     * Получить AudioBuffer звука (декодирует и кэширует).
     * Возвращает Promise<AudioBuffer|null>.
     */
    async _getBuffer(soundId) {
        if (this._buffers.has(soundId)) return this._buffers.get(soundId);
        const lib = this.scene3d.soundLibrary;
        const dataUrl = lib && lib.getDataUrl(soundId);
        if (!dataUrl || !this._ctx) return null;
        try {
            // dataUrl → ArrayBuffer → decodeAudioData.
            const resp = await fetch(dataUrl);
            const arr = await resp.arrayBuffer();
            const buf = await this._ctx.decodeAudioData(arr);
            this._buffers.set(soundId, buf);
            return buf;
        } catch (e) {
            return null;
        }
    }

    /**
     * Проиграть звук. opts:
     *   { volume: 0..1 (базовая громкость),
     *     loop: bool,
     *     at: {x,y,z} — 3D-точка (громкость по дистанции),
     *     attachRef: ref-строка — 3D-звук следует за объектом }.
     * Возвращает instId (для stopSound) или null.
     */
    play(soundId, opts = {}) {
        this._ensureCtx();
        if (!this._ctx) return null;
        // Web Audio мог быть suspended (autoplay policy) — будим.
        if (this._ctx.state === 'suspended') {
            this._ctx.resume().catch(() => {});
        }
        const instId = _soundInstSeq++;
        const baseVol = Number.isFinite(opts.volume) ? opts.volume : 1;
        const loop = !!opts.loop;
        // getPos — функция текущей позиции источника, или null для 2D-звука.
        let getPos = null;
        if (opts.attachRef) {
            getPos = () => this._resolvePos(opts.attachRef);
        } else if (opts.at && Number.isFinite(opts.at.x)) {
            const fixed = { x: opts.at.x, y: opts.at.y, z: opts.at.z };
            getPos = () => fixed;
        }

        // Декодируем (async) и запускаем.
        this._getBuffer(soundId).then((buf) => {
            if (!buf) return;
            // За время декодирования мог быть Stop — не стартуем «осиротевший»
            // звук (иначе он играет уже после выхода из Play).
            if (!this._running) return;
            // Звук мог быть остановлен через stop() ещё ДО декодирования —
            // тогда он помечен в _stoppedIds, не стартуем.
            if (this._stoppedIds.has(instId)) {
                this._stoppedIds.delete(instId);
                return;
            }
            const src = this._ctx.createBufferSource();
            src.buffer = buf;
            src.loop = loop;
            const gain = this._ctx.createGain();
            gain.gain.value = getPos
                ? this._volumeAt(getPos(), baseVol)   // 3D — по дистанции
                : baseVol;                            // 2D — фикс.
            src.connect(gain);
            gain.connect(this._ctx.destination);
            src.start();
            // Единый реестр ВСЕХ звуков (2D и 3D) по instId — для stopSound.
            this._byInstId.set(instId, { instId, gain, src, getPos, baseVol });
            if (getPos) {
                // 3D-звук — обновляем громкость в _tick (подмножество _byInstId).
                this._active.set(instId, this._byInstId.get(instId));
            }
            // Звук завершился (или остановлен) — снимаем из реестров.
            src.onended = () => {
                this._active.delete(instId);
                this._byInstId.delete(instId);
            };
        });
        return instId;
    }

    /** Остановить звук по instId — работает для 2D и 3D. */
    stopSound(instId) {
        const id = Number(instId);
        const a = this._byInstId.get(id);
        if (a) {
            try { a.src.stop(); } catch (e) { /* ignore */ }
            this._byInstId.delete(id);
            this._active.delete(id);
        } else {
            // Звук ещё декодируется (src не создан) — помечаем, чтобы
            // play().then() не стартовал его.
            this._stoppedIds.add(id);
        }
    }

    // ===== внутреннее =====

    _tick() {
        if (this._active.size === 0) return;
        for (const a of this._active.values()) {
            const pos = a.getPos();
            if (!pos) continue;
            const v = this._volumeAt(pos, a.baseVol);
            try { a.gain.gain.value = v; } catch (e) { /* ignore */ }
        }
    }

    /** Громкость 3D-звука в точке pos с учётом дистанции до игрока. */
    _volumeAt(pos, baseVol) {
        if (!pos) return 0;
        const p = this.scene3d.player && this.scene3d.player._pos;
        if (!p) return baseVol;
        const d = Math.hypot(pos.x - p.x, (pos.y || 0) - p.y, pos.z - p.z);
        if (d <= FULL_VOLUME_DIST) return baseVol;
        if (d >= MAX_HEAR_DIST) return 0;
        // Линейное затухание между FULL_VOLUME_DIST и MAX_HEAR_DIST.
        const k = 1 - (d - FULL_VOLUME_DIST) / (MAX_HEAR_DIST - FULL_VOLUME_DIST);
        return baseVol * Math.max(0, k);
    }

    /** Позиция объекта по ref ('player' | 'primitive:N' | 'model:N'). */
    _resolvePos(ref) {
        if (ref === 'player') {
            const p = this.scene3d.player && this.scene3d.player._pos;
            return p ? { x: p.x, y: p.y, z: p.z } : null;
        }
        const rt = this.scene3d.gameRuntime;
        let r = ref;
        if (rt && rt._localToReal && rt._localToReal.has(r)) {
            r = rt._localToReal.get(r);
        }
        const colon = r.indexOf(':');
        if (colon < 0) return null;
        const kind = r.slice(0, colon);
        const rest = r.slice(colon + 1);
        if (kind === 'block') {
            const [bx, by, bz] = rest.split(',').map(Number);
            if ([bx, by, bz].every(Number.isFinite)) return { x: bx, y: by, z: bz };
            return null;
        }
        const mgr = kind === 'primitive' ? this.scene3d.primitiveManager
            : (kind === 'model' ? this.scene3d.modelManager : null);
        if (!mgr || !mgr.instances) return null;
        let d = mgr.instances.get(rest);
        if (!d) {
            const n = Number(rest);
            if (Number.isFinite(n)) d = mgr.instances.get(n);
        }
        return d ? { x: d.x, y: d.y, z: d.z } : null;
    }
}
