//! Audio-Treiber auf tinyaudio — Frontend-Gegenstück zur headless
//! Emitter-Logik in engine::audio (wie `Gpu` zu `scene`: die Engine kennt
//! dieses Modul nie).
//!
//! Eingebettete Fassung des irl3d-Mixers (../irl3d/src/audio.rs), auf die
//! WDS-Regeln gebracht: kein Panic — kein Gerät oder fehlende/kaputte
//! WAVs sind eine Meldung und Stille, nie fatal ([`Audio::open`] liefert
//! `None`, alle Aufrufe darauf entfallen still beim Aufrufer).
//!
//! Mono, 44100 Hz (fest, siehe engine::wav). Zwei Voice-Klassen:
//!   - 8 SFX-Voices: einmal durchlaufen, dann geräumt; sind alle belegt,
//!     wird die am weitesten gespielte verdrängt (`play`).
//!   - Ambient-Voices: eine je Emitter, loopen immer; ihr Gain ist die
//!     Distance ramp, die das Frontend jeden Frame aus
//!     `Session::emitter_gains` zieht ([`Audio::set_ambient_gains`]).
//!
//! Gemischt wird im Callback-Thread in f32 über alle aktiven Voices
//! (Sample × Gain), am Ende auf [-1, 1] geklemmt. Der Mixer hängt hinter
//! einem Mutex; Frontend-Seite fasst ihn nur kurz pro Frame/SFX an.

use std::sync::{Arc, Mutex};

use tinyaudio::{run_output_device, OutputDevice, OutputDeviceParameters};

use crate::engine::wav;

const SFX_VOICES: usize = 8;
/// Callback-Puffergröße in Samples (~6 ms bei 44,1 kHz).
const BUFFER_LEN: usize = 256;

/// Dekodierte Mono-Samples, geteilt zwischen Cache und laufenden Voices.
#[derive(Clone)]
pub struct Sound {
    samples: Arc<Vec<i16>>,
}

impl Sound {
    pub fn new(w: wav::Wav) -> Self {
        Sound { samples: Arc::new(w.samples) }
    }
}

struct Voice {
    samples: Arc<Vec<i16>>,
    cursor:  usize,
    gain:    f32,
}

#[derive(Default)]
struct Mixer {
    sfx:     Vec<Option<Voice>>,
    /// Loop-Voices der Ambient-Emitter, Index = Emitter-Reihenfolge.
    ambient: Vec<Voice>,
}

impl Mixer {
    fn mix(&mut self, out: &mut [f32]) {
        for s in out.iter_mut() {
            let mut acc = 0.0f32;
            for slot in self.sfx.iter_mut() {
                if let Some(v) = slot {
                    acc += v.samples[v.cursor] as f32 * v.gain;
                    v.cursor += 1;
                    if v.cursor >= v.samples.len() { *slot = None; }
                }
            }
            for v in self.ambient.iter_mut() {
                // Auch bei Gain 0 weiterlaufen lassen: ein Emitter, in dessen
                // Radius man zurückkehrt, spielt mitten in seiner Schleife —
                // nicht jedes Mal von vorn.
                acc += v.samples[v.cursor] as f32 * v.gain;
                v.cursor = (v.cursor + 1) % v.samples.len();
            }
            *s = (acc / 32768.0).clamp(-1.0, 1.0);
        }
    }
}

pub struct Audio {
    mixer:   Arc<Mutex<Mixer>>,
    _device: OutputDevice, // lebt, solange Audio lebt
}

impl Audio {
    /// Ausgabegerät öffnen. `None`, wenn keins da ist (Meldung, Spiel läuft
    /// stumm weiter).
    pub fn open() -> Option<Self> {
        // pipewire-pulse/PulseAudio respektiert diese Variable als
        // Ziel-Latenz des Streams (Default ~50–200 ms) — herabsetzen für
        // snappige SFX. Muss vor `run_output_device` gesetzt sein; auf
        // Windows/macOS ignoriert.
        // SAFETY: einmalig beim Start, bevor weitere Threads laufen (wird
        // in render::run vor dem stdin-Thread gerufen).
        unsafe { std::env::set_var("PULSE_LATENCY_MSEC", "15"); }

        let mixer = Arc::new(Mutex::new(Mixer {
            sfx: (0..SFX_VOICES).map(|_| None).collect(),
            ambient: Vec::new(),
        }));
        let cb_mixer = Arc::clone(&mixer);
        let device = run_output_device(
            OutputDeviceParameters {
                channels_count:       1,
                sample_rate:          wav::SAMPLE_RATE as usize,
                channel_sample_count: BUFFER_LEN,
            },
            move |buf: &mut [f32]| { cb_mixer.lock().unwrap().mix(buf); },
        );
        match device {
            Ok(device) => Some(Audio { mixer, _device: device }),
            Err(e) => {
                eprintln!("[audio] kein Ausgabegerät ({e}) — Spiel läuft stumm");
                None
            }
        }
    }

    /// Einmal-SFX starten: freier Slot, sonst wird die am weitesten
    /// gespielte Voice verdrängt.
    pub fn play(&self, s: &Sound) {
        if s.samples.is_empty() { return; }
        let mut m = self.mixer.lock().unwrap();
        let target = m.sfx.iter().enumerate()
            .max_by_key(|(_, slot)| slot.as_ref().map_or(usize::MAX, |v| v.cursor))
            .map_or(0, |(i, _)| i);
        m.sfx[target] = Some(Voice {
            samples: Arc::clone(&s.samples), cursor: 0, gain: 1.0,
        });
    }

    /// Die Ambient-Loop-Voices anlegen — einmal beim Start, Reihenfolge =
    /// `Session::emitters` (die Gains kommen dann parallel dazu, deshalb
    /// bekommt auch ein nicht ladbarer Sound seine — stille — Voice).
    pub fn set_ambients(&self, sounds: &[Option<Sound>]) {
        let mut m = self.mixer.lock().unwrap();
        m.ambient = sounds.iter().map(|s| Voice {
            samples: s.as_ref().map_or_else(|| Arc::new(vec![0]), |s| Arc::clone(&s.samples)),
            cursor: 0,
            gain: 0.0,
        }).collect();
    }

    /// Distance-ramp-Gains auf die Ambient-Voices legen — jeden Frame, aus
    /// `Session::emitter_gains`.
    pub fn set_ambient_gains(&self, gains: &[f32]) {
        let mut m = self.mixer.lock().unwrap();
        for (v, g) in m.ambient.iter_mut().zip(gains) {
            v.gain = *g;
        }
    }
}
