import {
    RATE,
    bandNoise,
    bellPartial,
    decay,
    loopify,
    makeRng,
    normalize,
    synth
} from '@rpg/engine/tools/synth.mjs';

/**
 * Спек-синтез: детерминированная компиляция описания звука в сэмплы.
 * Адресат — ИИ-агент (движок агент-первый): описывает звук параметрами,
 * без слуха и без библиотек сэмплов; движок компилирует спек в буфер
 * (AudioManager.playSpec), факт запуска наблюдаем через шпион onPlayed.
 * Формулы — те же примитивы, что у файлового генератора игры
 * (tools/synth.mjs), поэтому спек-звук и WAV-звук звучат в одной палитре.
 */

export type SoundKind = 'hit' | 'chime' | 'scrape' | 'hum' | 'tone';

/**
 * Дополнительный голос смеси: шум (low/high) или тон (freq) со своей
 * огибающей. Параметры не заданы — наследуются от спека.
 */
export interface SpecVoice {
    /** Тон голоса, Гц (чистый синус; вместе с delay — «блип» в конце). */
    freq?: number;
    /** Полоса шума, Гц (шумовой голос). */
    low?: number;
    high?: number;
    /** Вес в смеси (дефолт 1). */
    w?: number;
    /** Своя крутизна затухания (по умолчанию — power спека). */
    power?: number;
    /** Своя длительность, сек (по умолчанию — dur спека). */
    dur?: number;
    /** Нарастание, сек (min(1, t/attack) от старта голоса). */
    attack?: number;
    /** Задержка старта, сек — до неё голос молчит. */
    delay?: number;
    /** Свой сид шума (по умолчанию — seed спека). */
    seed?: number;
}

export interface SoundSpec {
    kind: SoundKind;
    /** Длительность, сек (0.05..4; hum — до 8). */
    dur: number;
    /** Полоса шума, Гц (hit/scrape; дефолты по kind). */
    low?: number;
    high?: number;
    /** Низкий тон, Гц: подложка удара/шороха или основной тон гула. */
    tone?: number;
    /** Частота тона/звона, Гц (tone/chime; дефолты по kind). */
    freq?: number;
    /** Конечная частота линейного свипа (tone): f = freq + (freqTo−freq)·t/dur. */
    freqTo?: number;
    /** Вес основного тела в смеси (дефолт 1). */
    w?: number;
    /** Крутизна затухания (дефолты по kind). */
    power?: number;
    /** Нарастание основного тела, сек (min(1, t/attack)) — шипение, скрип. */
    attack?: number;
    /** Линейный спад амплитуды к этому множителю к концу (1 → fadeTo). */
    fadeTo?: number;
    /** Тремоло, Гц: множитель wobbleBase + wobbleDepth·sin(2π·wobbleHz·t + фаза). */
    wobbleHz?: number;
    wobbleDepth?: number;
    /** База тремоло (дефолт 1 — амплитуда гуляет вокруг единицы). */
    wobbleBase?: number;
    /** Фаза тремоло, рад (дефолт 0). */
    wobblePhase?: number;
    /** Дополнительные голоса поверх основного тела (см. SpecVoice). */
    layers?: SpecVoice[];
    /** Сид шума/клика — детерминизм (дефолт 1). */
    seed?: number;
    /** Пик нормализации 0..1 (дефолт 0.5). */
    peak?: number;
    /** Огибающая «0 → 1 → 0» вместо спада с самого громкого старта (воздух, свуш). */
    swell?: boolean;
    /** Зациклить (кроссфейд краёв; для hum — по умолчанию). */
    loop?: boolean;
}

/** Клэмп длительности: снизу — против нулевых буферов, сверху — по kind. */
const clampDur = (dur: number, max: number): number => Math.max(0.05, Math.min(max, dur));

/** Огибающая: спад с самого громкого старта или «0 → 1 → 0» (swell). */
function envelope(spec: SoundSpec, dur: number, power: number): (t: number) => number {
    return spec.swell
        ? (t) => Math.sin(Math.PI * Math.min(1, t / dur))
        : (t) => decay(t, dur, power);
}

/** Нарастание: линейное от старта до полной громкости за attack секунд. */
const attackEnv = (t: number, attack: number | undefined): number =>
    attack ? Math.min(1, t / attack) : 1;

/**
 * Микс: основное тело + доп. голоса (layers), поверх — тремоло и линейный
 * спад. Без модификаторов совпадает с normalize(synth(dur, base), peak)
 * бит-в-бит (множители не применяются вовсе).
 */
function renderMix(spec: SoundSpec, dur: number, base: (t: number) => number): Float32Array {
    const voices = (spec.layers ?? []).map((l) => makeVoice(l, spec, dur));
    const wobbleHz = spec.wobbleHz;
    const wobbleAmp = spec.wobbleDepth ?? 0;
    const wobbleBase = spec.wobbleBase ?? 1;
    const wobblePhase = spec.wobblePhase ?? 0;
    const fadeTo = spec.fadeTo;
    return normalize(
        synth(dur, (t) => {
            let v = base(t);
            for (const voice of voices) v += voice(t);
            if (wobbleHz !== undefined)
                v *= wobbleBase + wobbleAmp * Math.sin(2 * Math.PI * wobbleHz * t + wobblePhase);
            if (fadeTo !== undefined) v *= 1 + (fadeTo - 1) * (t / dur);
            return v;
        }),
        spec.peak ?? 0.5
    );
}

/** Голос слоя: шум (low/high) или тон (freq), своя огибающая и задержка. */
function makeVoice(l: SpecVoice, spec: SoundSpec, dur: number): (t: number) => number {
    const vd = clampDur(l.dur ?? dur, 8);
    const delay = l.delay ?? 0;
    const w = l.w ?? 1;
    const power = l.power ?? spec.power ?? 2;
    if (l.low !== undefined || l.high !== undefined) {
        const noise = bandNoise(l.seed ?? spec.seed ?? 1, l.low ?? 100, l.high ?? 900, vd);
        return (t) => {
            const lt = t - delay;
            if (lt < 0) return 0;
            return noise() * w * attackEnv(lt, l.attack) * decay(lt, vd, power);
        };
    }
    const f = l.freq ?? 440;
    return (t) => {
        const lt = t - delay;
        if (lt < 0) return 0;
        return Math.sin(2 * Math.PI * f * lt) * w * attackEnv(lt, l.attack) * decay(lt, vd, power);
    };
}

/** Шумовой удар: полоса шума с резким спадом; tone — низкая подложка. */
function hitBase(spec: SoundSpec, dur: number, env: (t: number) => number): (t: number) => number {
    const noise = bandNoise(spec.seed ?? 1, spec.low ?? 600, spec.high ?? 3000, dur);
    const tone = spec.tone ?? 0;
    const w = spec.w ?? 1;
    return (t) => {
        let v = noise() * env(t) * w;
        if (tone > 0) v += Math.sin(2 * Math.PI * tone * t) * decay(t, dur, (spec.power ?? 2) * 0.8) * 0.4;
        return v;
    };
}

/** Скрип/шорох: низкая полоса, медленный спад; swell — воздух; attack — нарастание. */
function scrapeBase(spec: SoundSpec, dur: number, env: (t: number) => number): (t: number) => number {
    const noise = bandNoise(spec.seed ?? 1, spec.low ?? 100, spec.high ?? 900, dur);
    const w = spec.w ?? 1;
    const attack = spec.attack;
    return (t) => noise() * w * attackEnv(t, attack) * env(t);
}

/** Чистый тон: синус freq (со свипом к freqTo), огибающая decay/swell. */
function toneBase(spec: SoundSpec, dur: number, env: (t: number) => number): (t: number) => number {
    const f0 = spec.freq ?? 440;
    const f1 = spec.freqTo;
    const w = spec.w ?? 1;
    const attack = spec.attack;
    return (t) => {
        const f = f1 === undefined ? f0 : f0 + (f1 - f0) * (t / dur);
        return Math.sin(2 * Math.PI * f * t) * w * attackEnv(t, attack) * env(t);
    };
}

/** Звон: негармоничные партиалы одной частоты + короткий клик. */
function chimeBase(spec: SoundSpec, dur: number): (t: number) => number {
    const f = spec.freq ?? 520;
    const power = spec.power ?? 5;
    // Клик — спад с одной случайной амплитудой (как у файлового генератора:
    // формулы совпадают дословно — мигрированные звуки звучат один в один).
    const clickAmp = makeRng(spec.seed ?? 1)() * 2 - 1;
    return (t) => {
        const body =
            bellPartial(f, 1.0, dur, power)(t) * 0.5 +
            bellPartial(f, 1.52, dur, power + 1)(t) * 0.3 +
            bellPartial(f, 2.38, dur * 0.6, power + 2)(t) * 0.2 +
            bellPartial(f, 3.91, dur * 0.4, power + 3)(t) * 0.12;
        const click = t < 0.008 ? clickAmp * (1 - t / 0.008) * 0.4 : 0;
        return body * 0.8 + click;
    };
}

/** Гул: два тона с биением (tone и tone×1.5) на «дыхании», луп по умолчанию. */
function humBase(spec: SoundSpec, dur: number): (t: number) => number {
    const tone = spec.tone ?? 55;
    const breath = spec.power ?? 2; // число периодов «дыхания» на луп
    return (t) => {
        const swell = 0.6 + 0.4 * Math.sin(2 * Math.PI * (t / dur) * breath + 0.5);
        const body =
            Math.sin(2 * Math.PI * tone * t) * 0.5 +
            Math.sin(2 * Math.PI * tone * 1.5 * t) * 0.3;
        return body * swell * 0.35;
    };
}

/**
 * Спек → сэмплы (моно, RATE). Одинаковые спеки дают одинаковые сэмплы
 * (детерминизм: сиды фикс, ГПСЧ детерминирован).
 */
export function renderSpec(spec: SoundSpec): Float32Array {
    const dur = clampDur(spec.dur, spec.kind === 'hum' ? 8 : 4);
    const env = envelope(spec, dur, spec.power ?? 2);
    const base =
        spec.kind === 'hit'
            ? hitBase(spec, dur, env)
            : spec.kind === 'chime'
              ? chimeBase(spec, dur)
              : spec.kind === 'scrape'
                ? scrapeBase(spec, dur, env)
                : spec.kind === 'tone'
                  ? toneBase(spec, dur, env)
                  : humBase(spec, dur);
    const samples = renderMix(spec, dur, base);
    if (spec.loop ?? spec.kind === 'hum') {
        return loopify(samples, Math.min(0.5, dur / 4));
    }
    return samples;
}

export { RATE };