/**
 * Музыкальный секвенсор движка: партитура (ноты + инструменты) → сэмплы-луп.
 * Адресат — ИИ-агент: описывает партию нотами (MIDI), движок детерминированно
 * компилирует её в буфер (AudioManager.playLoopBuffer — стем на шине music).
 * Инструменты — осциллятор + огибающая поверх примитивов synth.mjs; сам
 * рендер чистый (без ГПСЧ) — детерминизм мелодии обеспечивают сиды
 * генератора нот на стороне игры/агента, а не рендера.
 */
import { RATE, decay, loopify, normalize } from '@rpg/engine/tools/synth.mjs';

/** Инструменты: осциллятор + огибающая (набор минимальный, расширять по мере надобности). */
export type InstrumentKind = 'pluck' | 'bell' | 'pad';

/** Нота: старт и длина в долях (beats), высота в MIDI (69 = A4 = 440 Гц). */
export interface NoteEvent {
    beat: number;
    dur: number;
    midi: number;
    /** Громкость ноты 0..1 (по умолчанию 0.8). */
    vel?: number;
}

/** Партия = трек = стем: инструмент + ноты + громкость трека. */
export interface TrackSpec {
    instrument: InstrumentKind;
    notes: NoteEvent[];
    /** Громкость трека (по умолчанию 1). */
    gain?: number;
    /** Панорама -1..1 (только в стерео-рендере; по умолчанию 0 — центр). */
    pan?: number;
}

/** Партитура лупа: темп, длина в долях, партии. */
export interface MusicSpec {
    /** Темп, долей в минуту. */
    bpm: number;
    /** Длина лупа в долях (ноты, вылезшие за край, обрежаются). */
    beats: number;
    tracks: TrackSpec[];
}

const midiHz = (midi: number): number => 440 * Math.pow(2, (midi - 69) / 12);

/** Партиалы [множитель частоты, доля амплитуды] для мелодических инструментов. */
const PLUCK_PARTIALS: [number, number][] = [
    [1, 0.6],
    [2, 0.25],
    [3, 0.15]
];
const BELL_PARTIALS: [number, number][] = [
    [1, 0.5],
    [1.52, 0.3],
    [2.38, 0.2]
];

/** Сумма партиалов с экспоненциальным затуханием — pluck/bell. */
function partialVoice(partials: [number, number][], f: number, ndur: number, power: number): (t: number) => number {
    return (t) => {
        let v = 0;
        for (const [k, amp] of partials) {
            v += Math.sin(2 * Math.PI * f * k * t) * amp * decay(t, ndur, power);
        }
        return v;
    };
}

/** Пэд: sine + слегка расстроенная копия, мягкая атака/релиз вместо щелчка. */
function padVoice(f: number, ndur: number): (t: number) => number {
    const attack = Math.min(0.3, ndur * 0.25);
    const release = Math.min(0.5, ndur * 0.35);
    return (t) => {
        const env = Math.min(1, t / attack) * Math.min(1, Math.max(0, (ndur - t) / release));
        return (Math.sin(2 * Math.PI * f * t) + Math.sin(2 * Math.PI * f * 1.004 * t)) * 0.5 * env;
    };
}

/** Голос ноты: (t сек от начала ноты) → значение волны. */
function voice(kind: InstrumentKind, f: number, ndur: number): (t: number) => number {
    switch (kind) {
        case 'pluck':
            return partialVoice(PLUCK_PARTIALS, f, ndur, 3);
        case 'bell':
            return partialVoice(BELL_PARTIALS, f, ndur, 4);
        case 'pad':
            return padVoice(f, ndur);
    }
}

/**
 * Суммирование нот всех партий в два канала. Моно-рендер кладёт всё в левый
 * канал с весами 1 (pan не слышен); стерео — раскладывает по equal-power
 * панораме трека.
 */
function mixTracks(
    spec: MusicSpec,
    total: number,
    stereo: boolean
): [Float32Array, Float32Array] {
    const spb = 60 / Math.max(1, spec.bpm); // сек на долю
    const left = new Float32Array(total);
    const right = new Float32Array(total);
    for (const track of spec.tracks) {
        const gain = track.gain ?? 1;
        // Equal-power панорама: 0 → центр (обе 0.707), -1 → лево, 1 → право.
        const a = ((Math.max(-1, Math.min(1, track.pan ?? 0)) + 1) * Math.PI) / 4;
        const gl = stereo ? Math.cos(a) : 1;
        const gr = stereo ? Math.sin(a) : 1;
        for (const n of track.notes) {
            const start = Math.round(n.beat * spb * RATE);
            const len = Math.max(1, Math.round(n.dur * spb * RATE));
            const wave = voice(track.instrument, midiHz(n.midi), len / RATE);
            const vel = (n.vel ?? 0.8) * gain;
            for (let i = 0; i < len && start + i < total; i++) {
                const v = wave(i / RATE) * vel;
                left[start + i] += v * gl;
                right[start + i] += v * gr;
            }
        }
    }
    return [left, right];
}

/** Нормализация к пику 0.8 по общему пику каналов + кроссфейд краёв лупа. */
function finalizeLoop(left: Float32Array, right: Float32Array): void {
    let peakMax = 0;
    for (let i = 0; i < left.length; i++)
        peakMax = Math.max(peakMax, Math.abs(left[i]!), Math.abs(right[i]!));
    const k = peakMax > 0 ? 0.8 / peakMax : 0;
    if (k !== 1) {
        for (let i = 0; i < left.length; i++) {
            left[i] = left[i]! * k;
            right[i] = right[i]! * k;
        }
    }
    const fade = Math.min(0.5, (left.length / RATE) * 0.25);
    loopify(left, fade);
    loopify(right, fade);
}

/**
 * Партитура → моно-сэмплы-луп (RATE): ноты всех партий суммируются, луп
 * сшивается кроссфейдом и нормализуется к пику 0.8. Одинаковые спеки дают
 * одинаковые сэмплы (рендер чистый); pan треков не слышен.
 */
export function renderMusic(spec: MusicSpec): Float32Array {
    const total = Math.max(1, Math.round(spec.beats * (60 / Math.max(1, spec.bpm)) * RATE));
    const [left] = mixTracks(spec, total, false);
    return loopify(normalize(left, 0.8), Math.min(0.5, (total / RATE) * 0.25));
}

/**
 * Партитура → стерео-сэмплы-луп (RATE, interleaved L,R): треки раскладываются
 * по панораме `pan` (equal-power), нормализация к пику 0.8 — по общему пику
 * каналов (баланс не искажается), кроссфейд краёв синхронный. Проигрывается
 * как обычный AudioBuffer: `createBuffer(data, RATE, 2)`.
 */
export function renderMusicStereo(spec: MusicSpec): Float32Array {
    const total = Math.max(1, Math.round(spec.beats * (60 / Math.max(1, spec.bpm)) * RATE));
    const [left, right] = mixTracks(spec, total, true);
    finalizeLoop(left, right);
    const out = new Float32Array(total * 2);
    for (let i = 0; i < total; i++) {
        out[i * 2] = left[i]!;
        out[i * 2 + 1] = right[i]!;
    }
    return out;
}