#include "core/audio/jail_audio.hpp"

#include <algorithm>
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <memory>
#include <optional>
#include <vector>

#include "core/audio/audio_effects.hpp"

// Solo declaracions de stb_vorbis: STB_VORBIS_HEADER_ONLY omet el bloc
// d'implementació. Les definicions las aporta source/external/stb_vorbis_impl.cpp
// (TU aïllat porque clang-analyzer no dispari fals positius al nostre codi).
#define STB_VORBIS_HEADER_ONLY
// clang-format off
// NOLINTNEXTLINE(bugprone-suspicious-include) -- stb_vorbis es single-file: la macro de dalt limita este TU a solo-declaracions; la implementació viu a external/stb_vorbis_impl.cpp.
#include "external/stb_vorbis.c"
// clang-format on

namespace Ja {

    // --- Streaming internals (file-scope constants) ---
    namespace {
        // Bytes-per-sample per canal (siempre s16)
        constexpr int MUSIC_BYTES_PER_SAMPLE = 2;
        // Quants shorts decodifiquem per crida a get_samples_short_interleaved.
        // 8192 shorts = 4096 samples/channel en estèreo ≈ 85ms de so a 48kHz.
        constexpr int MUSIC_CHUNK_SHORTS = 8192;
        // Umbral d'audio per davant del cursor de reproducció. Mantenim ≥ 0.5 s a
        // l'SDL_AudioStream per absorbir jitter de frame i evitar underruns.
        constexpr float MUSIC_LOW_WATER_SECONDS = 0.5F;
    }  // namespace

    // --- Engine::active_ storage ---
    Engine* Engine::active_ = nullptr;

    auto Engine::active() noexcept -> Engine* { return active_; }

    // --- Ctor/Dtor ---

    Engine::Engine(const int freq, const SDL_AudioFormat format, const int num_channels) {
        assert(active_ == nullptr && "Ja::Engine: més d'una instància activa no está suportat");
        active_ = this;

        audio_spec_ = {.format = format, .channels = num_channels, .freq = freq};
        sdl_audio_device_ = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &audio_spec_);
        if (sdl_audio_device_ == 0) { std::fprintf(stderr, "Ja::Engine: Failed to initialize SDL audio!\n"); }
        for (auto& channel : channels_) { channel.state = ChannelState::FREE; }
    }

    Engine::~Engine() {
        if (outgoing_music_.stream != nullptr) {
            SDL_DestroyAudioStream(outgoing_music_.stream);
            outgoing_music_.stream = nullptr;
        }
        if (sdl_audio_device_ != 0) { SDL_CloseAudioDevice(sdl_audio_device_); }
        sdl_audio_device_ = 0;

        if (active_ == this) { active_ = nullptr; }
    }

    // --- Helpers stateless (no toquen membres d'Engine) ---
    namespace {

        auto feedMusicChunk(Music* music) -> int {
            if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return 0; }

            short chunk[MUSIC_CHUNK_SHORTS];
            const int NUM_CHANNELS = music->spec.channels;
            const int SAMPLES_PER_CHANNEL = stb_vorbis_get_samples_short_interleaved(
                music->vorbis,
                NUM_CHANNELS,
                static_cast<short*>(chunk),
                MUSIC_CHUNK_SHORTS);
            if (SAMPLES_PER_CHANNEL <= 0) { return 0; }

            const int BYTES = SAMPLES_PER_CHANNEL * NUM_CHANNELS * MUSIC_BYTES_PER_SAMPLE;
            SDL_PutAudioStreamData(music->stream, static_cast<const void*>(chunk), BYTES);
            return SAMPLES_PER_CHANNEL;
        }

        void pumpMusic(Music* music) {
            if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return; }

            const int BYTES_PER_SECOND = music->spec.freq * music->spec.channels * MUSIC_BYTES_PER_SAMPLE;
            const int LOW_WATER_BYTES = static_cast<int>(MUSIC_LOW_WATER_SECONDS * static_cast<float>(BYTES_PER_SECOND));

            while (SDL_GetAudioStreamAvailable(music->stream) < LOW_WATER_BYTES) {
                const int DECODED = feedMusicChunk(music);
                if (DECODED > 0) { continue; }

                // EOF: si queden loops, rebobinar; si no, tallar y deixar drenar.
                if (music->times != 0) {
                    stb_vorbis_seek_start(music->vorbis);
                    if (music->times > 0) { music->times--; }
                } else {
                    break;
                }
            }
        }

        void preFillOutgoing(Music* music, const int duration_ms) {
            if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return; }

            const int BYTES_PER_SECOND = music->spec.freq * music->spec.channels * MUSIC_BYTES_PER_SAMPLE;
            const int NEEDED_BYTES = static_cast<int>((static_cast<std::int64_t>(duration_ms) * BYTES_PER_SECOND) / 1000);

            while (SDL_GetAudioStreamAvailable(music->stream) < NEEDED_BYTES) {
                const int DECODED = feedMusicChunk(music);
                if (DECODED <= 0) { break; }
            }
        }

        // Retorna el progrés lineal [0..1] d'un fade. 1.0 vol dir completat. Única
        // font de la corba del fade: si es vol canviar a logarítmica/quadràtica,
        // s'edita aquí i afecta fade-in i fade-out alhora.
        auto fadeProgress(const FadeState& fade) -> float {
            if (fade.duration_ms <= 0) { return 1.0F; }
            const Uint64 ELAPSED = SDL_GetTicks() - fade.start_time;
            if (ELAPSED >= static_cast<Uint64>(fade.duration_ms)) { return 1.0F; }
            return static_cast<float>(ELAPSED) / static_cast<float>(fade.duration_ms);
        }

    }  // namespace

    void Engine::updateOutgoingFade() {
        if (outgoing_music_.stream == nullptr || !outgoing_music_.fade.active) { return; }

        // Mentre la fosa está activa, mantenim el stream con una reserva
        // de samples per davant del cursor (mismo patró que pumpMusic
        // para el current_music_). Así el stream no es buida ni cuando SDL
        // drena més ràpid del previst en haver sounds bound a la misma
        // device. Si l'OGG arriba a EOF, rebobina (la fosa pot ser més
        // llarga que la pista).
        if (outgoing_music_.music != nullptr && outgoing_music_.music->vorbis != nullptr) {
            const Music& music = *outgoing_music_.music;
            const int BYTES_PER_SECOND = music.spec.freq * music.spec.channels * MUSIC_BYTES_PER_SAMPLE;
            const int LOW_WATER = static_cast<int>(MUSIC_LOW_WATER_SECONDS * static_cast<float>(BYTES_PER_SECOND));
            while (SDL_GetAudioStreamAvailable(outgoing_music_.stream) < LOW_WATER) {
                short chunk[MUSIC_CHUNK_SHORTS];
                const int SAMPLES = stb_vorbis_get_samples_short_interleaved(
                    music.vorbis,
                    music.spec.channels,
                    static_cast<short*>(chunk),
                    MUSIC_CHUNK_SHORTS);
                if (SAMPLES <= 0) {
                    stb_vorbis_seek_start(music.vorbis);
                    continue;
                }
                const int BYTES = SAMPLES * music.spec.channels * MUSIC_BYTES_PER_SAMPLE;
                SDL_PutAudioStreamData(outgoing_music_.stream, static_cast<const void*>(chunk), BYTES);
            }
        }

        const float PROGRESS = fadeProgress(outgoing_music_.fade);
        if (PROGRESS >= 1.0F) {
            SDL_DestroyAudioStream(outgoing_music_.stream);
            outgoing_music_.stream = nullptr;
            outgoing_music_.fade.active = false;
            // Deixem el Vorbis del Music original en un estat conegut per
            // a la pròxima reproducció. (playMusic también fa seek_start,
            // pero fer-ho ací evita estats intermedis si algú consulta.)
            if (outgoing_music_.music != nullptr && outgoing_music_.music->vorbis != nullptr) {
                stb_vorbis_seek_start(outgoing_music_.music->vorbis);
            }
            outgoing_music_.music = nullptr;
        } else {
            SDL_SetAudioStreamGain(outgoing_music_.stream, outgoing_music_.fade.initial_volume * (1.0F - PROGRESS));
        }
    }

    void Engine::updateIncomingFade() {
        if (!incoming_fade_.active) { return; }

        const float PROGRESS = fadeProgress(incoming_fade_);
        if (PROGRESS >= 1.0F) {
            incoming_fade_.active = false;
            SDL_SetAudioStreamGain(current_music_->stream, music_volume_);
        } else {
            SDL_SetAudioStreamGain(current_music_->stream, music_volume_ * PROGRESS);
        }
    }

    void Engine::updateCurrentMusic() {
        if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; }

        updateIncomingFade();

        pumpMusic(current_music_);
        if (current_music_->times == 0 && SDL_GetAudioStreamAvailable(current_music_->stream) == 0) {
            // La pista ha acabat de drenar naturalment. L'aturem primer (deixa
            // l'engine en estat consistent) i entonces invoquem el callback;
            // así un eventual playMusic des del callback comença net.
            stopMusic();
            if (on_music_ended_) { on_music_ended_(); }
        }
    }

    void Engine::updateSoundChannels() {
        for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; ++i) {
            if (channels_[i].state != ChannelState::PLAYING) { continue; }

            if (channels_[i].times != 0) {
                if (static_cast<Uint32>(SDL_GetAudioStreamAvailable(channels_[i].stream)) < (channels_[i].sound->length / 2)) {
                    SDL_PutAudioStreamData(channels_[i].stream, channels_[i].sound->buffer.get(), channels_[i].sound->length);
                    if (channels_[i].times > 0) { channels_[i].times--; }
                }
            } else if (SDL_GetAudioStreamAvailable(channels_[i].stream) == 0) {
                stopChannel(i);
            }
        }
    }

    void Engine::stealCurrentIntoOutgoing(const int duration_ms) {
        if (outgoing_music_.stream != nullptr) {
            SDL_DestroyAudioStream(outgoing_music_.stream);
            outgoing_music_.stream = nullptr;
            outgoing_music_.fade.active = false;
        }

        if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING || current_music_->stream == nullptr) {
            return;
        }

        preFillOutgoing(current_music_, duration_ms);

        outgoing_music_.stream = current_music_->stream;
        // Guardem la referència al Music porque updateOutgoingFade puga
        // seguir bombant Vorbis sin al stream durante tota la fosa. NO fem
        // seek_start ací: la decompressió ha de continuar des d'on estava
        // porque el so siga continu. El seek_start es farà cuando la fosa
        // acabe (o cuando playMusic la interrompi via stopMusic).
        outgoing_music_.music = current_music_;
        outgoing_music_.fade = {
            .active = true,
            .start_time = SDL_GetTicks(),
            .duration_ms = duration_ms,
            .initial_volume = music_volume_,
        };
        current_music_->stream = nullptr;
        current_music_->state = MusicState::STOPPED;
    }

    template <typename Fn>
    void Engine::forEachTargetChannel(const int channel, Fn&& fn) {
        if (channel == -1) {
            for (auto& ch : channels_) { fn(ch); }
        } else if (channel >= 0 && channel < MAX_SIMULTANEOUS_CHANNELS) {
            fn(channels_[channel]);
        }
    }

    // --- Engine public API ---

    void Engine::update() {
        updateOutgoingFade();
        updateCurrentMusic();
        updateSoundChannels();
    }

    void Engine::playMusic(Music* music, const int loop) {
        if (music == nullptr || music->vorbis == nullptr) { return; }

        stopMusic();

        current_music_ = music;
        current_music_->state = MusicState::PLAYING;
        current_music_->times = loop;

        stb_vorbis_seek_start(current_music_->vorbis);

        current_music_->stream = SDL_CreateAudioStream(&current_music_->spec, &audio_spec_);
        if (current_music_->stream == nullptr) {
            std::fprintf(stderr, "Ja::Engine::playMusic: Failed to create audio stream!\n");
            current_music_->state = MusicState::STOPPED;
            return;
        }
        SDL_SetAudioStreamGain(current_music_->stream, music_volume_);

        pumpMusic(current_music_);

        if (!SDL_BindAudioStream(sdl_audio_device_, current_music_->stream)) {
            std::fprintf(stderr, "Ja::Engine::playMusic: SDL_BindAudioStream failed!\n");
        }
    }

    void Engine::setMusicSpeed(float ratio) {
        if (current_music_ == nullptr || current_music_->stream == nullptr) { return; }
        SDL_SetAudioStreamFrequencyRatio(current_music_->stream, ratio);
    }

    void Engine::pauseMusic() {
        if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; }

        current_music_->state = MusicState::PAUSED;
        SDL_UnbindAudioStream(current_music_->stream);
    }

    void Engine::resumeMusic() {
        if (current_music_ == nullptr || current_music_->state != MusicState::PAUSED) { return; }

        current_music_->state = MusicState::PLAYING;
        SDL_BindAudioStream(sdl_audio_device_, current_music_->stream);
    }

    void Engine::stopMusic() {
        if (outgoing_music_.stream != nullptr) {
            SDL_DestroyAudioStream(outgoing_music_.stream);
            outgoing_music_.stream = nullptr;
            outgoing_music_.fade.active = false;
            if (outgoing_music_.music != nullptr && outgoing_music_.music->vorbis != nullptr) {
                stb_vorbis_seek_start(outgoing_music_.music->vorbis);
            }
            outgoing_music_.music = nullptr;
        }
        incoming_fade_.active = false;

        if (current_music_ == nullptr || current_music_->state == MusicState::INVALID || current_music_->state == MusicState::STOPPED) { return; }

        current_music_->state = MusicState::STOPPED;
        if (current_music_->stream != nullptr) {
            SDL_DestroyAudioStream(current_music_->stream);
            current_music_->stream = nullptr;
        }
        if (current_music_->vorbis != nullptr) {
            stb_vorbis_seek_start(current_music_->vorbis);
        }
    }

    void Engine::fadeOutMusic(const int milliseconds) {
        if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; }

        stealCurrentIntoOutgoing(milliseconds);
        incoming_fade_.active = false;
    }

    void Engine::crossfadeMusic(Music* music, const int crossfade_ms, const int loop) {
        if (music == nullptr || music->vorbis == nullptr) { return; }

        stealCurrentIntoOutgoing(crossfade_ms);

        current_music_ = music;
        current_music_->state = MusicState::PLAYING;
        current_music_->times = loop;

        stb_vorbis_seek_start(current_music_->vorbis);
        current_music_->stream = SDL_CreateAudioStream(&current_music_->spec, &audio_spec_);
        if (current_music_->stream == nullptr) {
            std::fprintf(stderr, "Ja::Engine::crossfadeMusic: Failed to create audio stream!\n");
            current_music_->state = MusicState::STOPPED;
            return;
        }
        SDL_SetAudioStreamGain(current_music_->stream, 0.0F);
        pumpMusic(current_music_);
        SDL_BindAudioStream(sdl_audio_device_, current_music_->stream);

        incoming_fade_ = {
            .active = true,
            .start_time = SDL_GetTicks(),
            .duration_ms = crossfade_ms,
            .initial_volume = 0.0F,
        };
    }

    auto Engine::getMusicState() const -> MusicState {
        if (current_music_ == nullptr) { return MusicState::INVALID; }
        return current_music_->state;
    }

    auto Engine::setMusicVolume(float volume) -> float {
        music_volume_ = SDL_clamp(volume, 0.0F, 1.0F);
        if (current_music_ != nullptr && current_music_->stream != nullptr) {
            SDL_SetAudioStreamGain(current_music_->stream, music_volume_);
        }
        return music_volume_;
    }

    void Engine::setOnMusicEnded(std::function<void()> callback) {
        on_music_ended_ = std::move(callback);
    }

    void Engine::onMusicDeleted(const Music* music) {
        if (music == nullptr) { return; }
        if (current_music_ == music) {
            stopMusic();
            current_music_ = nullptr;
        }
    }

    // --- Sound ---

    auto Engine::playSound(Sound* sound, const int loop, const int group) -> int {
        if (sound == nullptr) { return -1; }

        int channel = 0;
        while (channel < MAX_SIMULTANEOUS_CHANNELS && channels_[channel].state != ChannelState::FREE) { channel++; }
        if (channel == MAX_SIMULTANEOUS_CHANNELS) {
            // No hay canal libre, reemplazamos el primero
            channel = 0;
        }

        return playSoundOnChannel(sound, channel, loop, group);
    }

    auto Engine::playSoundOnChannel(Sound* sound, const int channel, const int loop, const int group) -> int {
        if (sound == nullptr) { return -1; }
        if (channel < 0 || channel >= MAX_SIMULTANEOUS_CHANNELS) { return -1; }

        stopChannel(channel);

        channels_[channel].sound = sound;
        channels_[channel].times = loop;
        channels_[channel].pos = 0;
        channels_[channel].group = group;
        channels_[channel].state = ChannelState::PLAYING;
        channels_[channel].stream = SDL_CreateAudioStream(&channels_[channel].sound->spec, &audio_spec_);

        if (channels_[channel].stream == nullptr) {
            std::fprintf(stderr, "Ja::Engine::playSoundOnChannel: Failed to create audio stream!\n");
            channels_[channel].state = ChannelState::FREE;
            return -1;
        }

        SDL_PutAudioStreamData(channels_[channel].stream, channels_[channel].sound->buffer.get(), channels_[channel].sound->length);
        SDL_SetAudioStreamGain(channels_[channel].stream, sound_volume_[group]);
        SDL_BindAudioStream(sdl_audio_device_, channels_[channel].stream);

        return channel;
    }

    void Engine::setChannelSpeed(const int channel, const float ratio) {
        if (channel < 0 || channel >= MAX_SIMULTANEOUS_CHANNELS) { return; }
        if (channels_[channel].stream == nullptr) { return; }
        SDL_SetAudioStreamFrequencyRatio(channels_[channel].stream, ratio);
    }

    void Engine::pauseChannel(const int channel) {
        forEachTargetChannel(channel, [](Channel& ch) {
            if (ch.state == ChannelState::PLAYING) {
                ch.state = ChannelState::PAUSED;
                SDL_UnbindAudioStream(ch.stream);
            }
        });
    }

    void Engine::resumeChannel(const int channel) {
        const SDL_AudioDeviceID DEVICE = sdl_audio_device_;
        forEachTargetChannel(channel, [DEVICE](Channel& ch) {
            if (ch.state == ChannelState::PAUSED) {
                ch.state = ChannelState::PLAYING;
                SDL_BindAudioStream(DEVICE, ch.stream);
            }
        });
    }

    void Engine::stopChannel(const int channel) {
        forEachTargetChannel(channel, [this](Channel& ch) {
            if (ch.state != ChannelState::FREE) {
                if (ch.stream != nullptr) { SDL_DestroyAudioStream(ch.stream); }
                ch.stream = nullptr;
                ch.state = ChannelState::FREE;
                ch.pos = 0;
                ch.sound = nullptr;
                if (ch.has_effect) {
                    ch.has_effect = false;
                    if (effect_channels_active_ > 0) { --effect_channels_active_; }
                }
            }
        });
    }

    auto Engine::setSoundVolume(float volume, const int group) -> float {
        const float V = SDL_clamp(volume, 0.0F, 1.0F);

        if (group == -1) {
            std::ranges::fill(sound_volume_, V);
        } else if (group >= 0 && group < MAX_GROUPS) {
            sound_volume_[group] = V;
        } else {
            return V;
        }

        for (auto& ch : channels_) {
            if ((ch.state == ChannelState::PLAYING) || (ch.state == ChannelState::PAUSED)) {
                if (group == -1 || ch.group == group) {
                    if (ch.stream != nullptr) {
                        SDL_SetAudioStreamGain(ch.stream, sound_volume_[ch.group]);
                    }
                }
            }
        }
        return V;
    }

    void Engine::onSoundDeleted(const Sound* sound) {
        if (sound == nullptr) { return; }
        for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; ++i) {
            if (channels_[i].sound == sound) { stopChannel(i); }
        }
    }

    auto Engine::playProcessedOnFreeChannel(const std::vector<std::uint8_t>& bytes, const SDL_AudioSpec& spec, const int group) -> int {
        // El sin de canals con efecte es valida antes de reservar slot —
        // así evitem crear y destruir un stream solo per descartar el play.
        if (effect_channels_active_ >= MAX_EFFECT_CHANNELS) { return -1; }

        int channel = 0;
        while (channel < MAX_SIMULTANEOUS_CHANNELS && channels_[channel].state != ChannelState::FREE) { ++channel; }
        if (channel == MAX_SIMULTANEOUS_CHANNELS) { channel = 0; }

        stopChannel(channel);

        // El stream es crea contra l'spec del buffer processat (S16, ...)
        // porque SDL faci el resampling sin a audio_spec_ del device.
        channels_[channel].stream = SDL_CreateAudioStream(&spec, &audio_spec_);
        if (channels_[channel].stream == nullptr) {
            std::fprintf(stderr, "Ja::Engine::playProcessedOnFreeChannel: Failed to create audio stream!\n");
            return -1;
        }

        channels_[channel].sound = nullptr;  // El buffer no es propietat de sin Ja::Sound.
        channels_[channel].times = 0;
        channels_[channel].pos = 0;
        const int CLAMPED_GROUP = (group >= 0 && group < MAX_GROUPS) ? group : 0;
        channels_[channel].group = CLAMPED_GROUP;
        channels_[channel].state = ChannelState::PLAYING;
        channels_[channel].has_effect = true;
        ++effect_channels_active_;

        SDL_PutAudioStreamData(channels_[channel].stream, bytes.data(), static_cast<int>(bytes.size()));
        SDL_SetAudioStreamGain(channels_[channel].stream, sound_volume_[CLAMPED_GROUP]);
        SDL_BindAudioStream(sdl_audio_device_, channels_[channel].stream);

        return channel;
    }

    auto Engine::playSoundWithEcho(const Sound* sound, const EchoParams& params, const int group) -> int {
        if (sound == nullptr) { return -1; }
        auto processed = AudioEffects::applyEcho(*sound, params);
        if (!processed) { return -1; }
        return playProcessedOnFreeChannel(processed->bytes, processed->spec, group);
    }

    auto Engine::playSoundWithReverb(const Sound* sound, const ReverbParams& params, const int group) -> int {
        if (sound == nullptr) { return -1; }
        auto processed = AudioEffects::applyReverb(*sound, params);
        if (!processed) { return -1; }
        return playProcessedOnFreeChannel(processed->bytes, processed->spec, group);
    }

    // --- Factories y destructors (permanents) ---

    auto loadMusic(const Uint8* buffer, Uint32 length) -> Music* {
        if (buffer == nullptr || length == 0) { return nullptr; }

        // Allocem el Music primer per aprofitar el seu `std::vector<Uint8>`
        // como a propietari del OGG comprimit. stb_vorbis guarda un punter
        // persistent al buffer; como que ací no el resize'jem, el .data() es
        // estable durante tot el cicle de vida del music.
        auto music = std::make_unique<Music>();
        music->ogg_data.assign(buffer, buffer + length);

        int vorbis_error = 0;
        music->vorbis = stb_vorbis_open_memory(music->ogg_data.data(),
            static_cast<int>(length),
            &vorbis_error,
            nullptr);
        if (music->vorbis == nullptr) {
            std::fprintf(stderr, "Ja::loadMusic: stb_vorbis_open_memory failed (error %d)\n", vorbis_error);
            return nullptr;
        }

        const stb_vorbis_info INFO = stb_vorbis_get_info(music->vorbis);
        music->spec.channels = static_cast<int>(INFO.channels);
        music->spec.freq = static_cast<int>(INFO.sample_rate);
        music->spec.format = SDL_AUDIO_S16;
        // Pre-cálculo de la duración en ms a partir del header. stb_vorbis ya
        // ha decodificat la informació necessària a `stb_vorbis_open_memory`;
        // esta consulta no descodifica àudio, solo llig el comptador
        // de samples. Si el sample_rate fos 0 (header malmès) deixem
        // duration_ms a 0.
        if (INFO.sample_rate > 0) {
            const auto SAMPLES = stb_vorbis_stream_length_in_samples(music->vorbis);
            music->duration_ms = static_cast<int>((static_cast<std::uint64_t>(SAMPLES) * 1000ULL) / INFO.sample_rate);
        }
        music->state = MusicState::STOPPED;

        return music.release();
    }

    // Overload con filename. Resource::Cache l'usa per registrar el path dins
    // del propi Ja::Music (camp `filename`); la capa Audio l'usa per recuperar
    // el nom después d'un playMusic(Ja::Music*, ...) — veure PATCH-02.
    auto loadMusic(const Uint8* buffer, Uint32 length, const char* filename) -> Music* {
        Music* music = loadMusic(buffer, length);
        if (music != nullptr && filename != nullptr) { music->filename = filename; }
        return music;
    }

    void deleteMusic(Music* music) {
        if (music == nullptr) { return; }
        // Notifiquem el motor actiu porque pari la pista si es la current_music.
        // Si no hay motor (shutdown-order invertit), passem: los recursos
        // propis del Music es lliberen igualment a sota.
        if (Engine* eng = Engine::active()) { eng->onMusicDeleted(music); }

        if (music->stream != nullptr) { SDL_DestroyAudioStream(music->stream); }
        if (music->vorbis != nullptr) { stb_vorbis_close(music->vorbis); }
        delete music;
    }

    auto loadSound(std::uint8_t* buffer, std::uint32_t size) -> Sound* {
        auto sound = std::make_unique<Sound>();
        Uint8* raw = nullptr;
        if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), true, &sound->spec, &raw, &sound->length)) {
            std::fprintf(stderr, "Ja::loadSound: Failed to load WAV from memory: %s\n", SDL_GetError());
            return nullptr;
        }
        sound->buffer.reset(raw);  // adopta el SDL_malloc'd buffer
        return sound.release();
    }

    void deleteSound(Sound* sound) {
        if (sound == nullptr) { return; }
        if (Engine* eng = Engine::active()) { eng->onSoundDeleted(sound); }
        // buffer es destrueix automàticament via RAII (SdlFreeDeleter).
        delete sound;
    }

}  // namespace Ja

// --- stb_vorbis macro leak cleanup ---
// stb_vorbis.c filtra noms curts (L, C, R i PLAYBACK_*) al TU que el compila.
// Xocarien con parámetros de plantilla d'altres headers si estas definicions
// s'escapessin. Els netegem al final del TU per tancar la porta.
// clang-format off
#undef L
#undef C
#undef R
#undef PLAYBACK_MONO
#undef PLAYBACK_LEFT
#undef PLAYBACK_RIGHT
// clang-format on
