import { RATE, renderMusicStereo, renderSpec, type AudioManager, type MusicHandle, type Vec2 } from '@rpg/engine';
import { AudioSystem, type AudioLayerDef } from './AudioSystem';
import { ROOM_TONES } from '../data/sfxSpecs';
import { TENSION_STEM } from '../data/music';
import type { AmbienceLayerDef, AreaDef } from '../data/locations';

/**
 * Аудио-подсистема локации: позиционные слои (громкость от дистанции до
 * источника), room tone интерьеров и динамическая музыка угрозы
 * (tension-стем под темой области). Сцена кормит её тиком
 * и уровнем угрозы боевого мира; свои лупы гасит сама в exit().
 */

export interface LocationAudioDeps {
    audio: AudioManager;
    /** Позиционный звук мира (шаги, звуки боя) — тикается сценой отдельно. */
    worldAudio: AudioSystem;
    area: AreaDef;
    /** Позиции тайлов-источников амбиент-слоёв по id (собирает сцена). */
    layerTilePos: Map<number, Vec2[]>;
    /** Слушатель — герой (мировые юниты). */
    heroPos: () => Vec2;
    /** Тема области/боя (Game.playTheme: кроссфейд, '' — тишина). */
    playTheme: (key: string) => void;
    /** Амбиент области (Game.playAmbience: кроссфейд 1.5 с, тот же ключ не перезапускает). */
    playAmbience: (key: string) => void;
    /** Фактор ночи 0..1 (кривая суток, как у света). */
    nightFactor: () => number;
    /** Уровень угрозы боевого мира 0..1 (погоня — 1, wary — 0.4). */
    threat: () => number;
}

export class LocationAudio {
    /** Tension-стем (враги wary): буфер + хендл + сглаженная громкость. */
    private stemHandle: MusicHandle | null = null;
    private stemBuf: AudioBuffer | null = null;
    private stemLevel = 0;
    /** Стем заводится (промис не вернулся) — не заводить второй. */
    private stemPending = false;
    /** Room tone интерьера (тихий гул на шине ambience). */
    private roomtoneHandle: MusicHandle | null = null;
    /** exit() вызван: гасим хвосты асинхронных заводок (roomtone/стем). */
    private exited = false;
    /** Текущий выбор амбиента день/ночь (для гистерезиса порога). */
    private nightAmbient = false;

    constructor(private deps: LocationAudioDeps) {}

    /** Room tone интерьера (см. ROOM_TONES): тихий луп на шине ambience. */
    startRoomTone(): void {
        const spec = ROOM_TONES[this.deps.area.id];
        if (!spec) return;
        const buf = this.deps.audio.createBuffer(renderSpec(spec));
        if (!buf) return; // до unlock контекста нет — тон появится при следующем входе
        const key = this.deps.area.id;
        void this.deps.audio
            .playLoopBuffer(`roomtone/${key}`, buf, { bus: 'ambience', fade: 2, volume: 0.6 })
            .then((h) => {
                // Сцена уже вышла, пока луп заводился, — гасим без хвостов.
                if (this.exited) {
                    h?.stop(1);
                    return;
                }
                this.roomtoneHandle = h;
            });
    }

    /** Погасить свои лупы (exit сцены) — без хвостов поверх следующей сцены. */
    exit(): void {
        this.exited = true;
        this.roomtoneHandle?.stop(1);
        this.roomtoneHandle = null;
        this.stemHandle?.stop(1);
        this.stemHandle = null;
        this.stemBuf = null;
        this.stemPending = false;
        this.stemLevel = 0;
    }

    /**
     * Тик амбиент-слоёв: громкость каждого — от дистанции героя до ближайшего
     * источника (тайлы или точка); глобальный слой (без источников) — константа.
     * Пустой набор — setLayers сам снимет лишние.
     */
    updateLayers(): void {
        const p = this.deps.heroPos();
        const defs: AudioLayerDef[] = [];
        for (const layer of this.deps.area.ambienceLayers ?? []) {
            const d = this.layerDistance(layer, p);
            const k = layer.radius === undefined ? 1 : Math.max(0, 1 - d / layer.radius);
            const volume = (layer.volume ?? 0.5) * k;
            if (volume > 0.01) defs.push({ key: layer.key, volume, fade: 0.5 });
        }
        this.deps.worldAudio.setLayers(defs);
    }

    /**
     * Дистанция до ближайшего источника слоя (юниты; тайл — центр ромба).
     * Слой без at, positions и tiles — глобальный (ветер): источников нет.
     */
    private layerDistance(layer: AmbienceLayerDef, p: Vec2): number {
        if (!layer.at && !(layer.positions && layer.positions.length > 0) && !(layer.tiles && layer.tiles.length > 0))
            return 0;
        if (layer.at) return Math.hypot(p.x - (layer.at.x + 0.5), p.y - (layer.at.y + 0.5));
        let best = Infinity;
        for (const tile of layer.positions ?? []) {
            const d = Math.hypot(p.x - (tile.x + 0.5), p.y - (tile.y + 0.5));
            if (d < best) best = d;
        }
        for (const id of layer.tiles ?? []) {
            for (const tile of this.deps.layerTilePos.get(id) ?? []) {
                const d = Math.hypot(p.x - (tile.x + 0.5), p.y - (tile.y + 0.5));
                if (d < best) best = d;
            }
        }
        return best;
    }

    /**
     * Амбиент дня/ночи: за порогом кривой суток (с гистерезисом ±0.05, чтобы
     * не трещало на закате) играем ночной луп области — кроссфейд даёт сам
     * Game.playAmbience (1.5 с). Ночной ключ — суффикс `_night` у дневного.
     */
    updateDayNightAmbient(): void {
        const base = this.deps.area.ambience;
        if (!base) return; // интерьеры без амбиента — нечего переключать
        const night = this.deps.nightFactor() >= (this.nightAmbient ? 0.45 : 0.55);
        if (night === this.nightAmbient) return;
        this.nightAmbient = night;
        this.deps.playAmbience(night ? `${base}_night` : base);
    }

    /** Начальный амбиент (enter сцены): день или ночь по текущим часам. */
    startAmbient(): void {
        this.nightAmbient = this.deps.nightFactor() >= 0.5;
        const base = this.deps.area.ambience;
        if (base) this.deps.playAmbience(this.nightAmbient ? `${base}_night` : base);
    }

    /**
     * Динамическая музыка по уровню угрозы: погоня — боевая тема вместо темы
     * области; настороженность — тихий tension-стем под темой (громкость
     * по угрозе, сглаженная); спокойствие — тема области. Сцена зовёт после
     * combat.update — угроза свежая.
     */
    updateMusic(dt: number): void {
        const threat = this.deps.threat();
        // Боевая тема вытесняет тему области; обратно — как только погоня кончилась.
        this.deps.playTheme(threat >= 1 ? 'combat' : (this.deps.area.theme ?? ''));

        // Стем: тихо в бою (тема боя сама громкая), громче при wary, ноль в тишине.
        const target = threat >= 1 ? 0.15 : threat > 0 ? 0.6 * threat : 0;
        if (!this.stemHandle && !this.stemPending && target > 0) {
            this.stemBuf ??= this.deps.audio.createBuffer(renderMusicStereo(TENSION_STEM), RATE, 2);
            if (this.stemBuf) {
                this.stemPending = true;
                void this.deps.audio
                    .playLoopBuffer('music/tension', this.stemBuf, { fade: 1, volume: 0 })
                    .then((h) => {
                        this.stemPending = false;
                        if (!h) return;
                        // Сцена сменилась, пока луп заводился, — гасим без хвостов.
                        if (!this.stemBuf) h.stop(1);
                        else this.stemHandle = h;
                    });
            }
        }
        // Громкость ведём плавно (dt-подход), stop в exit.
        this.stemLevel += (target - this.stemLevel) * Math.min(1, dt * 2);
        this.stemHandle?.setVolume(this.stemLevel * 0.8);
    }
}