/**
 * Аудио демки (перенос обвязки v1): sfx на спек-синтезе движка, позиционный
 * звук мира (затухание по дистанции + панорама по изо-экрану) и шаги с
 * вариациями. Спеки — формулы v1 (apps/game/src/data/sfxSpecs.ts), только
 * нужные демке ключи; WAV-пайплайна в v2 нет — всё синтезируется в рантайме.
 */
import { renderSpec, type AudioManager, type PlayOptions, type SoundSpec } from '@rpg/engine';
import type { Hor } from '@rpg/engine';

/** Спек-звуки демки: бой, урон героя и шаги (пепел/трава). */
export const SPEC_SFX: Record<string, SoundSpec> = {
    // Звон удара конусом: 1 партиал + клик, normalize 0.7 (v1 bell_hit)
    'sfx/bell_hit': { kind: 'chime', dur: 0.35, freq: 520, power: 5, seed: 1, peak: 0.7 },
    // Низкий звон резонанса: три тона с тремоло 6 Гц (v1 bell_low)
    'sfx/bell_low': {
        kind: 'tone', dur: 0.9, freq: 110, w: 0.5, power: 2, peak: 0.6,
        wobbleHz: 6, wobbleDepth: 0.3,
        layers: [{ freq: 165, w: 0.25 }, { freq: 220.5, w: 0.12 }],
    },
    // Плевок плевуна: тон со свипом 700→350 Гц + шипящий хвост (v1 spit)
    'sfx/spit': {
        kind: 'tone', dur: 0.2, freq: 700, freqTo: 350, w: 0.6, power: 3, peak: 0.5,
        layers: [{ low: 2000, high: 6000, seed: 37, w: 0.4 }],
    },
    // Пробуждение сгустка: шипение с нарастанием 0.08 с (v1 ash_hiss)
    'sfx/ash_hiss': { kind: 'scrape', dur: 0.4, low: 900, high: 4200, seed: 11, power: 2, attack: 0.08, peak: 0.55 },
    // Развеивание сгустка: шум, громкость спадает вдвое к концу (v1 ash_die)
    'sfx/ash_die': { kind: 'scrape', dur: 0.6, low: 300, high: 2400, seed: 23, power: 3, fadeTo: 0.5, peak: 0.6 },
    // Урон герою: глухой удар 90 Гц + низкий шум (v1 hurt)
    'sfx/hurt': {
        kind: 'tone', dur: 0.25, freq: 90, w: 0.8, power: 3, peak: 0.65,
        layers: [{ low: 80, high: 400, seed: 41, w: 0.3, dur: 0.125, power: 2 }],
    },
    // Шаги: до unlock играет базовый спек, варианты — сид + i·10 (StepVariants)
    'sfx/step_ash': { kind: 'hit', dur: 0.14, low: 200, high: 1100, seed: 254, power: 2.5, peak: 0.3 },
    'sfx/step_grass': { kind: 'hit', dur: 0.12, low: 400, high: 2000, seed: 253, power: 2, peak: 0.3 },
};

/** Единая точка sfx по ключу (спек-звук компилирует движок, буфер кэшируется). */
export function playSfx(audio: AudioManager, key: string, opts?: number | PlayOptions): void {
    const spec = SPEC_SFX[key];
    if (spec) void audio.playSpec(key, spec, opts);
    else void audio.play(key, opts);
}

/** Поверхности шагов демки. */
export type StepSurface = 'grass' | 'ash';

/**
 * Вариативные шаги (перенос v1): 3 варианта на поверхность, сид варианта =
 * базовый + i·10 — звучат похоже, но не идентично. Буферы синтезируются при
 * первом успешном шаге (до unlock контекста нет — играет базовый спек).
 */
const STEP_VARIANTS = 3;
export class StepVariants {
    private cache = new Map<StepSurface, AudioBuffer[]>();
    private seq = 0;

    constructor(private audio: AudioManager) {}

    /** Играть очередной вариант шага (до готовности буферов — базовый спек). */
    play(surface: StepSurface, volume: number): void {
        const key = `sfx/step_${surface}`;
        const list = this.ensure(surface);
        if (list.length > 0) {
            const i = this.seq++ % list.length;
            void this.audio.playBuffer(`${key}#${i}`, list[i]!, { volume });
        } else {
            playSfx(this.audio, key, { volume });
        }
    }

    /** Синтезировать варианты поверхности (нет контекста — пустой список). */
    private ensure(surface: StepSurface): AudioBuffer[] {
        const existing = this.cache.get(surface);
        if (existing) return existing;
        const base = SPEC_SFX[`sfx/step_${surface}`];
        if (!base) return [];
        const list: AudioBuffer[] = [];
        for (let i = 0; i < STEP_VARIANTS; i++) {
            const buf = this.audio.createBuffer(renderSpec({ ...base, seed: (base.seed ?? 1) + i * 10 }));
            if (!buf) return []; // контекста нет — попробуем на следующем шаге
            list.push(buf);
        }
        this.cache.set(surface, list);
        return list;
    }
}

/** Минимальный интервал между запусками одного ключа (полифония, v1). */
const MIN_INTERVAL = 0.08;

/** Панорама источника: изо-экранный сдвиг ∝ (dx − dz), половина кадра 240 px. */
function panOf(source: Hor, listener: Hor): number {
    const dx = (source.x - listener.x) * 16; // 16 px/юнит (тайл v1 32/2)
    return Math.max(-0.8, Math.min(0.8, dx / 240));
}

/**
 * Позиционный звук мира демки: затухание (1 − d/радиус)^1.5, за радиусом —
 * тишина; панорама по разности изо-экранных X источника и слушателя. Шаги —
 * дедуп по поверхности. Слушатель — герой.
 */
export class DemoAudio {
    private clock = 0;
    private lastAt = new Map<string, number>();
    private steps: StepVariants;

    constructor(
        private audio: AudioManager,
        /** Позиция слушателя (герой) в боевых юнитах. */
        private getListener: () => Hor
    ) {
        this.steps = new StepVariants(audio);
    }

    /** Часы полифонии (тик демки). */
    update(dt: number): void {
        this.clock += dt;
    }

    /** Звук в точке мира (юниты): затухание по дистанции + панорама. */
    playAt(key: string, pos: Hor, base = 1, radius = 12): void {
        const l = this.getListener();
        const d = Math.hypot(pos.x - l.x, pos.z - l.z);
        const falloff = Math.max(0, 1 - d / radius);
        const volume = base * Math.pow(falloff, 1.5);
        if (volume < 0.02) return;
        this.play(key, { volume, pan: panOf(pos, l) });
    }

    /** Шаг героя по поверхности: слышен только вблизи слушателя. */
    playStep(surface: StepSurface, pos: Hor, base = 1): void {
        const l = this.getListener();
        const d = Math.hypot(pos.x - l.x, pos.z - l.z);
        const volume = base * Math.max(0, 1 - d / 4);
        if (volume < 0.02) return;
        if ((this.lastAt.get(`step:${surface}`) ?? -1) + MIN_INTERVAL > this.clock) return;
        this.lastAt.set(`step:${surface}`, this.clock);
        this.steps.play(surface, volume);
    }

    /** Разовый sfx с дедупом по ключу (боевые подписки демки). */
    play(key: string, opts: { volume: number; rate?: number; pan?: number }): void {
        if ((this.lastAt.get(key) ?? -1) + MIN_INTERVAL > this.clock) return;
        this.lastAt.set(key, this.clock);
        playSfx(this.audio, key, opts);
    }
}