#pragma once

// --- Includes ---
#include <SDL3/SDL.h>

#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <vector>

// Forward-declaració del decoder de vorbis. La implementació viu a
// jail_audio.cpp (únic TU que compila external/stb_vorbis.c). Qualsevol caller
// solo necessita `stb_vorbis*` per punter — nunca per valor — así que el
// forward decl n'hay prou i evita arrossegar el .c a tots los TU.
// NOLINTNEXTLINE(readability-identifier-naming) — nom imposat per l'API de stb_vorbis
struct stb_vorbis;

// Deleter stateless para buffers reservats con `SDL_malloc` / `SDL_LoadWAV*`.
// Compatible con `std::unique_ptr<Uint8[], SdlFreeDeleter>` — zero size overhead
// gràcies a EBO, igual que un unique_ptr con default_delete.
struct SdlFreeDeleter {
    void operator()(Uint8* p) const noexcept {
        if (p != nullptr) { SDL_free(p); }
    }
};

// Motor de baix nivell d'àudio del projecte jailgames: streaming OGG
// (stb_vorbis) + N canals d'efectes (SDL3 audio). No depèn d'Options ni de sin
// singleton del joc; solo de SDL3 i stb_vorbis. La capa superior (Audio) li
// passa recursos pel punter i fa el bookkeeping d'usuari.
namespace Ja {

    // --- Public Enums ---
    enum class ChannelState : std::uint8_t {
        FREE,
        PLAYING,
        PAUSED,
    };

    enum class MusicState : std::uint8_t {
        INVALID,  // Music carregat pero nunca play-ejat
        PLAYING,
        PAUSED,
        STOPPED,
    };

    // --- Constants ---
    inline constexpr int MAX_SIMULTANEOUS_CHANNELS = 50;
    inline constexpr int MAX_GROUPS = 2;
    // Cap superior de canals que poden estar simultàniament reproduint un so
    // con efecte (eco/reverb). Si está al límit, las noves crides con efecte
    // cauen al camí sec — l'usuari sent el so igualment, sin la cua.
    inline constexpr int MAX_EFFECT_CHANNELS = 4;

    // --- Paràmetres d'efectes ---
    // Els camps los fixa el caller (Audio) llegint sounds.yaml; el motor solo
    // los passa a AudioEffects::applyEcho/applyReverb. Els defaults són
    // sensats pero los presets los sobreescriuen.
    struct EchoParams {
        float delay_ms{220.0F};  // Temps hasta al primer rebot.
        float feedback{0.45F};   // Reinjecció (0..0.95).
        float wet{0.35F};        // Mescla humida (0..1).
    };

    struct ReverbParams {
        float room_size{0.7F};  // Tamaño percebuda (0..1).
        float damping{0.5F};    // Atenuació d'aguts per rebot (0..1).
        float wet{0.4F};        // Mescla humida (0..1).
    };

    // Spec de fallback del dispositiu. S'aplica antes que l'Engine s'iniciï i
    // como a valor inicial de Sound/Music. L'spec real d'ús l'imposa el ctor
    // d'Engine, alimentat des de Defaults::Audio via Audio.
    inline constexpr SDL_AudioSpec DEFAULT_SPEC{SDL_AUDIO_S16, 2, 48000};

    // --- Struct Definitions ---
    struct Sound {
        SDL_AudioSpec spec{DEFAULT_SPEC};
        Uint32 length{0};
        // Buffer descomprimit (PCM) propietat del sound. Reservat per SDL_LoadWAV
        // via SDL_malloc; el deleter `SdlFreeDeleter` allibera con SDL_free.
        std::unique_ptr<Uint8[], SdlFreeDeleter> buffer;
    };

    // L'ordre (punters primer, ints después, enum de 8 bits al final) minimitza
    // el padding a 64-bit (evita avisos de clang-analyzer-optin.performance.Padding).
    struct Channel {
        Sound* sound{nullptr};
        SDL_AudioStream* stream{nullptr};
        int pos{0};
        int times{0};
        int group{0};
        ChannelState state{ChannelState::FREE};
        // Marca si este canal va arrencar con so processat per un efecte.
        // El motor compta canals actius con efecte per fer complir
        // MAX_EFFECT_CHANNELS i alliberar el comptador en parar.
        bool has_effect{false};
    };

    struct Music {
        SDL_AudioSpec spec{DEFAULT_SPEC};

        // OGG comprimit en memòria. Propietat nostra; es copia des del buffer
        // d'entrada una sola vegada en loadMusic i es descomprimix en chunks
        // per streaming. Como que stb_vorbis guarda un punter persistent al
        // `.data()` d'este vector, no el podem resize'jar un cop establert
        // (una reallocation invalidaria el punter que el decoder conserva).
        std::vector<Uint8> ogg_data;
        stb_vorbis* vorbis{nullptr};  // handle del decoder, viu tot el cicle del Music

        std::string filename;

        int times{0};  // loops restants (-1 = infinit, 0 = un sol play)
        // Duración total de la pista en mil·lisegons, mesurada via
        // `stb_vorbis_stream_length_in_samples / sample_rate` al
        // `loadMusic`. 0 si el cálculo no es possible (header malmès).
        // L'usen consumidors que necessiten un timeline pre-calculat —
        // p. ex. la FSM de sala — sin dependre de callbacks de fi.
        int duration_ms{0};
        SDL_AudioStream* stream{nullptr};
        MusicState state{MusicState::INVALID};
    };

    struct FadeState {
        bool active{false};
        Uint64 start_time{0};
        int duration_ms{0};
        float initial_volume{0.0F};
    };

    struct OutgoingMusic {
        SDL_AudioStream* stream{nullptr};
        // Referència al Music original porque updateOutgoingFade puga
        // continuar descomprimint des de Vorbis sin al stream durante
        // tota la fosa. Sense això, solo tenim el pre-fill puntual i
        // SDL drena el stream més ràpid del previst cuando hay sounds
        // bound a la misma device (~2x), buidant-lo a meitat del
        // fade i sentint-se como un tall sec.
        Music* music{nullptr};
        FadeState fade;
    };

    // --- Engine ---
    // Encapsula tot l'estat que antes vivia como a globals inline. Un sol Engine
    // viu per procés (enforceat via assert al ctor contra `active_`). El ctor
    // obre el device SDL; el dtor el tanca (RAII). Els deleters
    // `Ja::deleteMusic`/`Ja::deleteSound` accedeixen al motor actiu via
    // `Engine::active()` per parar canals antes d'alliberar.
    class Engine {
       public:
        Engine(int freq, SDL_AudioFormat format, int num_channels);
        ~Engine();
        Engine(const Engine&) = delete;
        auto operator=(const Engine&) -> Engine& = delete;
        Engine(Engine&&) = delete;
        auto operator=(Engine&&) -> Engine& = delete;

        // Retorna el motor actiu o nullptr si sin ha estat construït. L'usen
        // los deleters de recursos porque no los arriba sin referència directa.
        [[nodiscard]] static auto active() noexcept -> Engine*;

        void update();

        // --- Música ---
        void playMusic(Music* music, int loop = -1);
        void pauseMusic();
        void resumeMusic();
        void stopMusic();
        void fadeOutMusic(int milliseconds);
        void crossfadeMusic(Music* music, int crossfade_ms, int loop = -1);
        [[nodiscard]] auto getMusicState() const -> MusicState;
        auto setMusicVolume(float volume) -> float;
        // Multiplicador de velocitat de reproducció de la música actual
        // via `SDL_SetAudioStreamFrequencyRatio`. 1.0 = normal, 2.0 =
        // doble velocitat. Cal saber que también puja el to (efecte
        // "chipmunk") — es el comportament arcade clàssic dels comptes
        // enrere. Cada `playMusic` crea un stream nuevo con ratio 1.0,
        // así que un canvi de track reseteja la velocitat
        // implícitament. No-op si no hay música activa.
        void setMusicSpeed(float ratio);
        // Registra un callback que es disparà cuando la música actual acabi de
        // drenar naturalment (times == 0 + stream buit). Es crida DESPRÉS de
        // stopMusic, así que el callback pot invocar playMusic sin córrer.
        // S'executa al mismo thread que Engine::update (render loop); no fer
        // operacions blocants.
        void setOnMusicEnded(std::function<void()> callback);
        // Notifica al motor que un Music s'está destruint: si es el current_music
        // s'atura antes que los seus recursos (stream/vorbis) deixin de ser vàlids.
        void onMusicDeleted(const Music* music);

        // --- So ---
        auto playSound(Sound* sound, int loop = 0, int group = 0) -> int;
        auto playSoundOnChannel(Sound* sound, int channel, int loop = 0, int group = 0) -> int;
        // Ajusta la velocitat de reproducció d'un canal actiu via
        // `SDL_SetAudioStreamFrequencyRatio`. 1.0 = normal. Igual que a
        // `setMusicSpeed`, puja/baixa el to junt con la velocitat
        // (efecte "chipmunk"); para SFX curts arcade es el que volem.
        // No-op si el canal no está actiu. Cridar-lo just después de
        // `playSound`/`playSoundOnChannel` porque el ratio cobreixi
        // tota la reproducció.
        void setChannelSpeed(int channel, float ratio);
        // Reproducció con so processat per un efecte. Retorna el canal
        // assignat o -1 si no queden slots d'efecte (MAX_EFFECT_CHANNELS).
        // El sound original solo s'usa per consultar el spec/buffer; el
        // canal manipula el buffer ya processat (no reapunta a `sound`).
        auto playSoundWithEcho(const Sound* sound, const EchoParams& params, int group = 0) -> int;
        auto playSoundWithReverb(const Sound* sound, const ReverbParams& params, int group = 0) -> int;
        void pauseChannel(int channel);
        void resumeChannel(int channel);
        void stopChannel(int channel);
        auto setSoundVolume(float volume, int group = -1) -> float;
        // Notifica al motor que un Sound s'está destruint: los canals que el
        // referenciïn es paren antes d'alliberar el buffer.
        void onSoundDeleted(const Sound* sound);

       private:
        void stealCurrentIntoOutgoing(int duration_ms);
        void updateOutgoingFade();
        void updateIncomingFade();
        void updateCurrentMusic();
        void updateSoundChannels();
        // Empenta un buffer ya processat (S16) a un canal lliure y el deixa
        // sonar sin bucle. Camí comú dels dos overloads playSoundWith*.
        // Retorna el canal o -1 si no queden slots.
        auto playProcessedOnFreeChannel(const std::vector<std::uint8_t>& bytes, const SDL_AudioSpec& spec, int group) -> int;

        template <typename Fn>
        void forEachTargetChannel(int channel, Fn&& fn);

        Music* current_music_{nullptr};
        Channel channels_[MAX_SIMULTANEOUS_CHANNELS]{};
        SDL_AudioSpec audio_spec_{DEFAULT_SPEC};
        float music_volume_{1.0F};
        float sound_volume_[MAX_GROUPS]{};
        SDL_AudioDeviceID sdl_audio_device_{0};
        OutgoingMusic outgoing_music_;
        FadeState incoming_fade_;
        std::function<void()> on_music_ended_;
        // Comptador derivat de Channel::has_effect — evita haver-lo de
        // recalcular cada vegada que algú demana un play con efecte.
        int effect_channels_active_{0};

        // NOLINTNEXTLINE(readability-identifier-naming) — convenció projecte: private static con sufix _
        static Engine* active_;
    };

    // --- Factories y destructors (permanents) ---
    // No depenen de l'estat del motor: loadMusic/loadSound solo construeixen
    // objectes, deleteMusic/deleteSound consulten Engine::active() per parar
    // canals antes d'alliberar (si el motor aún viu).
    [[nodiscard]] auto loadMusic(const Uint8* buffer, Uint32 length) -> Music*;
    [[nodiscard]] auto loadMusic(const Uint8* buffer, Uint32 length, const char* filename) -> Music*;
    void deleteMusic(Music* music);
    [[nodiscard]] auto loadSound(std::uint8_t* buffer, std::uint32_t size) -> Sound*;
    void deleteSound(Sound* sound);

}  // namespace Ja
