#include "ops/playback_clock.h"
#define MA_EXPERIMENTAL__DATA_LOOPING_AND_CHAINING
#define MA_NO_ENGINE
#define MA_ENABLE_PIPEWIRE
#define MINIAUDIO_IMPLEMENTATION

/**
 * @file sound.c
 * @brief High-level audio playback interface.
 *
 * Provides a unified API for creating an audio device
 * and switching decoders.
 */

#include "sound.h"

#include "common/appstate.h"
#include "common/model.h"

#include "playback.h"
#include "sound_facade.h"
#ifdef USE_FAAD
#include "m4a.h"
#endif
#include "audio_file_info.h"
#include "audiobuffer.h"
#include "audiotypes.h"
#include "decoders.h"
#include "volume.h"

#include "loader/song_loader.h"

#include "utils/file.h"
#include "utils/k_log.h"
#include "utils/utils.h"

#include <miniaudio.h>

#include <fcntl.h>
#include <inttypes.h>
#include <pthread.h>
#include <sched.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#if defined(__linux__) || defined(__FreeBSD__) || defined(__ANDROID__)
#include <sys/resource.h>
#endif
#include <sys/types.h>
#include <unistd.h>

static ma_context context;
static bool context_initialized = false;
long long lastCursor = 0;

sound_system_t *sound_s = NULL;

sound_playback_repeat_state_t repeat_state = SOUND_STATE_REPEAT_OFF;

ma_pcm_rb pcm_rb;

sound_result_t create_audio_device(
    void *user_data,
    sound_system_t *sound,
    ma_device *device,
    ma_context *context,
    decoder_getter_func get_decoder,
    ma_data_callback_proc callback);

bool valid_file_path(char *file_path)
{
        if (file_path == NULL || file_path[0] == '\0' || file_path[0] == '\r')
                return false;

        if (exists_file(file_path) < 0)
                return false;

        return true;
}

static bool should_switch(ma_uint64 frames_to_read,
                          ma_result result, ma_uint64 cursor)
{
        bool at_end = (((cursor != 0ULL && cursor == (ma_uint64)lastCursor) ||
                        frames_to_read == 0 || is_skip_to_next() ||
                        result != MA_SUCCESS) &&
                       !pb_is_EOF_reached());

        return at_end;
}

static bool execute_seek(sound_system_t *sound, ma_decoder *decoder, ma_uint64 targetFrame, const CodecOps *ops)
{
        stop_playback();

        ma_result result = ops->seek_to_pcm_frame(decoder, targetFrame, 0);

        if (result == MA_SUCCESS) {
                atomic_store(&sound->buffer_ready, 0);
                ma_pcm_rb_reset(&pcm_rb);
                atomic_store(&sound->decode_finished, false);
                sound->current_frame = targetFrame;

                uint64_t played = atomic_load_explicit(&sound_s->track_frames_sent, memory_order_relaxed);
                sound->total_frames = played;
        }

        sound_system_play(sound);

        return (result == MA_SUCCESS);
}

static bool perform_seek_if_requested(sound_system_t *sound, ma_decoder *decoder)
{
        if (!is_seek_requested())
                return false;

        set_seek_requested(false);

        ma_uint64 totalFrames = 0;

        ma_data_source_get_length_in_pcm_frames(decoder, &totalFrames);

        double seek_percent = get_seek_percentage();
        if (seek_percent > 100.0)
                seek_percent = 100.0;

        ma_uint64 targetFrame =
            (ma_uint64)((totalFrames - 1) * seek_percent / 100.0);
        if (targetFrame >= totalFrames)
                targetFrame = totalFrames - 1;

        const CodecOps *ops = get_codec_ops(get_current_decoder_decoder_type());

        return execute_seek(sound, decoder, targetFrame, ops);
}

double db_to_linear(double db)
{
        return pow(10.0, db / 20.0);
}

bool is_valid_gain(double gain)
{
        return gain > -50.0 && gain < 50.0 && !isnan(gain) && isfinite(gain) && gain != 0;
}

static bool compute_replay_gain(double *out_gain_db)
{
        bool result = false;
        double gain_db = 0.0f;

        SongData *song = sound_get_current_song_data();

        if (song != NULL && song->magic == SONG_MAGIC && song->metadata != NULL) {
                double track_gain = song->metadata->replaygainTrack;
                double album_gain = song->metadata->replaygainAlbum;

                int checkFirst = sound_s->replay_gain_check_first;

                if (checkFirst == 0 && is_valid_gain(track_gain)) {
                        gain_db = track_gain;
                        result = true;
                } else if (checkFirst == 1 && is_valid_gain(album_gain)) {
                        gain_db = album_gain;
                        result = true;
                } else if (checkFirst == 1 && is_valid_gain(track_gain)) {
                        gain_db = track_gain;
                        result = true;
                } else if (checkFirst == 0 && is_valid_gain(album_gain)) {
                        gain_db = album_gain;
                        result = true;
                }
        }

        if (result) {
                *out_gain_db = gain_db;
        }

        return result;
}

static inline void
apply_gain_f32(float *restrict samples,
               ma_uint64 total_samples,
               float gain)
{
        for (ma_uint64 i = 0; i < total_samples; i++) {
                samples[i] *= gain;
        }
}

void reset_ring_buffer(sound_system_t *sound)
{
        bool resume = (sound->state == SOUND_STATE_PLAYING);
        stop_playback();

        ma_pcm_rb_reset(&pcm_rb);

        if (resume)
                sound_system_play(sound);
}

void set_switch_decoder(bool value)
{
        atomic_store(&sound_s->request_switch_decoder, value);
}

void set_replay_gain(sound_system_t *sound)
{
        sound->gain_linear = 1.0f;

        double gain_db = 0.0;
        if (compute_replay_gain(&gain_db)) {
                float g = (float)db_to_linear(gain_db);

                if (g < 0.0001f)
                        g = 0.0001f;
                if (g > 10.0f)
                        g = 10.0f;

                sound->gain_linear = g;
        }
}

void switch_metadata(sound_system_t *sound)
{
        atomic_store_explicit(&sound_s->clock_reset_done, true, memory_order_relaxed);

        set_skip_to_next(false);

        if (atomic_load(&sound->request_pause) && !pb_is_paused()) {
                pause_playback();
                atomic_store(&sound->request_pause, false);
                atomic_store(&sound->drain_callbacks_remaining, 0);
        }

        if (!pb_is_repeat_enabled() || (pb_is_repeat_enabled() && pb_is_paused())) {

                bool using_slot_A = atomic_load(&sound->using_song_slot_A); // fetch

                using_slot_A = !using_slot_A; // invert

                atomic_store(&sound->using_song_slot_A, using_slot_A); // store
        }

        long long fade_boundary = atomic_load_explicit(&sound->fade_boundary, memory_order_acquire);

        if (fade_boundary < 0) // if there's no fade going on we want to switch decoder after this
                set_switch_decoder(true);

        atomic_store_explicit(&sound->fade_boundary, -1, memory_order_release);
        atomic_store_explicit(&sound_s->clock_reset_ms, sound_s->fade_enter_song_ms, memory_order_relaxed);

        set_seek_elapsed(0.0);
        set_metadata_switch_reached(true);
}

void switch_current_decoder(sound_system_t *sound)
{
        atomic_store(&sound->buffer_ready, 0);

        PlaybackState *ps = get_playback_state();

        pthread_mutex_lock(&ps->switch_mutex);

        atomic_store(&sound->drain_callbacks_remaining, 0);

        lastCursor = 0;

        // What we have already read from the next song in cross fade, becomes the current song frame
        sound->current_frame = sound->fade_current_frame;

        sound->fade_enter_frame = 0;
        sound->fade_current_frame = 0;
        sound->total_song_frames = 0;
        sound->fade_frames = (sound->always_fade_ms * sound->sample_rate) / 1000;

        set_switch_decoder(false);

        LoaderData *loader = get_loader_data();
        loader->dirty = true;

        switch_decoder_index();

        set_replay_gain(sound);
        set_EOF_reached(true);

        long long fade_boundary = atomic_load_explicit(&sound->fade_boundary, memory_order_acquire);
        bool fade_boundary_reached = atomic_load_explicit(&sound->fade_boundary_reached, memory_order_acquire);

        if (fade_boundary < 0) {
                atomic_store_explicit(&sound->clock_reset_ms, sound->fade_enter_song_ms, memory_order_release);
        }

        if (fade_boundary < 0 || fade_boundary_reached) {
                atomic_store_explicit(&sound_s->clock_reset_done, false, memory_order_relaxed);
        }

        pthread_mutex_unlock(&ps->switch_mutex);
}

void set_decode_thread_priority(pthread_t thread)
{
#if defined(__linux__) || defined(__FreeBSD__)
        struct sched_param param = {.sched_priority = 1};
        int ret = pthread_setschedparam(thread, SCHED_RR, &param);
        if (ret != 0) {
                ret = setpriority(PRIO_PROCESS, 0, -5);
        }
#elif defined(__APPLE__)
        (void)thread;
        pthread_set_qos_class_self_np(QOS_CLASS_USER_INTERACTIVE, 0);
#elif defined(__ANDROID__)
        (void)thread;
        setpriority(PRIO_PROCESS, 0, -8);
#elif defined(_WIN32)

#include <windows.h>

        (void)thread;

        HANDLE hThread = GetCurrentThread();

        // Similar to "above normal" / responsive decoding thread
        if (!SetThreadPriority(hThread, THREAD_PRIORITY_ABOVE_NORMAL)) {
                // fallback: try normal priority
                SetThreadPriority(hThread, THREAD_PRIORITY_NORMAL);
        }
#endif
}

void drain_audio_and_pause(sound_system_t *sound)
{
        atomic_store(&sound->drain_callbacks_remaining, 10);

        // Prevent decoding while draining
        while (atomic_load(&sound->drain_callbacks_remaining) > 0 && atomic_load(&sound->decode_thread_running)) {
                struct timespec ts = {0, 10000000}; // 10ms
                nanosleep(&ts, NULL);

                // Break out if someone unpaused
                if (!atomic_load(&sound->request_pause)) {
                        // Tell callback to stop draining
                        atomic_store(&sound_s->drain_callbacks_remaining, 0);
                        break;
                }
        }

        // If we still are supposed to pause
        if (atomic_load(&sound->request_pause)) {

                // Pause
                pause_playback();

                // Clear the flag
                atomic_store(&sound->request_pause, false);
        }
}

void perform_crossfade(sound_system_t *sound, void *decoder, void *next_decoder, float *current_buf, float *next_buf, float *out_buf, ma_uint32 frames_to_decode, ma_uint64 *frames_to_read)
{
        if (!sound->fade_seek_performed) {

                clear_decoder_chain(); // Prevents a read at the end of decoder from spilling
                                       // over into the next decoder and resetting the position

                atomic_store(&sound->clock_reset_done, false);
                atomic_store_explicit(&sound_s->fade_boundary, -1, memory_order_release);

                if (sound->fade_enter_song_ms > 0) {

                        sound->fade_enter_frame =
                            ((ma_uint64)sound->fade_enter_song_ms *
                             sound->sample_rate) /
                            1000;

                        ma_data_source_seek_to_pcm_frame(
                            next_decoder,
                            sound->fade_enter_frame);
                }

                sound->fade_current_frame = 0;
                sound->fade_seek_performed = true;
                atomic_store_explicit(&sound_s->fade_boundary, sound->total_frames, memory_order_release);
        }

        ma_uint64 current_read = 0;
        ma_uint64 next_read = 0;

        ma_data_source_read_pcm_frames(
            decoder,
            current_buf,
            (ma_uint64)frames_to_decode,
            &current_read);

        ma_data_source_read_pcm_frames(
            next_decoder,
            next_buf,
            current_read,
            &next_read);

        ma_uint64 frames_read =
            current_read < next_read
                ? current_read
                : next_read;

        if (frames_read > 0) {

                float *mixed = out_buf;

                for (ma_uint64 i = 0; i < frames_read; i++) {

                        float t = (float)(sound->fade_current_frame + i) /
                                  (float)sound->fade_total_frames;

                        t = powf(t, 0.7f);

                        if (t > 1.0f)
                                t = 1.0f;

                        // Equal-power fade
                        float fadeOut = cosf(t * MA_PI * 0.5f);
                        float fadeIn = sinf(t * MA_PI * 0.5f);

                        for (ma_uint32 ch = 0;
                             ch < sound->channels;
                             ch++) {

                                ma_uint64 idx =
                                    i * sound->channels + ch;

                                mixed[idx] =
                                    current_buf[idx] * fadeOut +
                                    next_buf[idx] * fadeIn;
                        }
                }

                *frames_to_read = frames_read;

                sound->current_frame += frames_read;
                sound->fade_current_frame += frames_read;
                sound->total_frames += frames_read;
        }
}

void write_to_ring_buffer(sound_system_t *sound, ma_uint64 frames_to_read, float *out_buf)
{
        ma_uint32 framesRemaining = (ma_uint32)frames_to_read;
        ma_uint32 framesWritten = 0;

        while (framesRemaining > 0 && atomic_load(&sound->decode_thread_running)) {

                ma_uint32 framesToWrite = framesRemaining;
                void *pWriteBuffer = NULL;
                ma_result wr = ma_pcm_rb_acquire_write(&pcm_rb, &framesToWrite, &pWriteBuffer);

                if (wr != MA_SUCCESS || framesToWrite == 0 || pWriteBuffer == NULL)
                        break;

                MA_COPY_MEMORY(
                    pWriteBuffer,
                    (float *)out_buf + framesWritten * sound->channels,
                    framesToWrite * sound->channels * sizeof(float));

                ma_pcm_rb_commit_write(&pcm_rb, framesToWrite);

                framesWritten += framesToWrite;
                framesRemaining -= framesToWrite;

                if (!atomic_load(&sound->buffer_ready))
                        atomic_store(&sound->buffer_ready, 1);
        }
}

static inline void *aligned64_alloc(size_t size)
{
#if defined(_WIN32)
        return _aligned_malloc(size, 64);
#else
        void *ptr = NULL;
        if (posix_memalign(&ptr, 64, size) != 0)
                return NULL;
        return ptr;
#endif
}

static inline void aligned64_free(void *ptr)
{
#if defined(_WIN32)
        _aligned_free(ptr);
#else
        free(ptr);
#endif
}

bool memalign_buffers(float **current_buf, float **next_buf, float **mixed_buf, int buf_size)
{
        *current_buf = aligned64_alloc(buf_size);
        if (!*current_buf)
                return false;

        *next_buf = aligned64_alloc(buf_size);
        if (!*next_buf) {
                aligned64_free(*current_buf);
                return false;
        }

        *mixed_buf = aligned64_alloc(buf_size);
        if (!*mixed_buf) {
                aligned64_free(*next_buf);
                aligned64_free(*current_buf);
                return false;
        }

        return true;
}

// Main decoding loop, produces data to the miniaudio ringbuffer
void *decode_loop(void *arg)
{
        sound_system_t *sound = (sound_system_t *)arg;

        size_t buf_size = sound->chunk_frames * sound->channels * sizeof(float);
        float *mixed_buf = NULL;

        // cross-fade buffers
        float *current_buf = NULL;
        float *next_buf = NULL;

        if (!memalign_buffers(&current_buf, &next_buf, &mixed_buf, buf_size)) {
                atomic_store(&sound->decode_thread_running, false);
                return NULL;
        }

        bool unpaused = false;
        lastCursor = 0;
        set_decode_thread_priority(pthread_self());

        void *decoder = NULL;

        sound->fade_frames = (sound->always_fade_ms * sound->sample_rate) / 1000;
        sound->fade_current_frame = 0;
        sound->fade_enter_song_ms = 0;
        atomic_store_explicit(&sound->fade_boundary, -1, memory_order_release);
        atomic_store_explicit(&sound->clock_reset_ms, 0, memory_order_relaxed);

        while (atomic_load(&sound->decode_thread_running)) {

                // Handle pending switch
                if (atomic_load_explicit(&sound->request_switch_metadata, memory_order_acquire)) {
                        atomic_store_explicit(&sound->request_switch_metadata, false, memory_order_release);
                        atomic_store_explicit(&sound->decode_finished, false, memory_order_release);

                        switch_metadata(sound);
                }

                // Handle file switch
                if (atomic_load_explicit(&sound->request_switch_decoder, memory_order_acquire)) {
                        atomic_store_explicit(&sound->decode_finished, false, memory_order_release);

                        if (!pb_is_paused()) {

                                switch_current_decoder(sound);
                                continue;
                        }
                }

                // Decoder type switch reached
                if (get_current_decoder_type() != get_current_decoder_decoder_type() ||
                    is_decoder_type_switch_reached()) {
                        break;
                }

                ma_uint32 writable = 0;

                while (atomic_load(&sound->decode_thread_running)) {

                        atomic_thread_fence(memory_order_seq_cst);
                        writable = ma_pcm_rb_available_write(&pcm_rb);

                        if (writable > 0) {
                                break;
                        }

                        atomic_store(&sound->buffer_ready, 1);

                        struct timespec ts = {0, 1000000}; // 1ms
                        nanosleep(&ts, NULL);
                }

                if (atomic_load(&sound->request_pause)) {

                        if (!atomic_load_explicit(&sound->request_switch_decoder, memory_order_acquire)) {
                                drain_audio_and_pause(sound);
                                continue;
                        } else {
                                pause_playback();
                        }
                }

                // Handle pause
                unpaused = false;
                while (atomic_load(&sound->decode_thread_running)) {

                        if (pb_is_paused()) {
                                c_sleep(10);
                                unpaused = true;
                        } else {
                                break;
                        }
                }

                if (unpaused)
                        continue;

                if (writable > 0) {
                        atomic_store(&sound->buffer_ready, 1);
                }

                ma_uint32 frames_to_decode = (writable > sound->chunk_frames) ? sound->chunk_frames : writable;

                const CodecOps *ops = get_codec_ops(get_current_decoder_decoder_type());

                decoder = get_current_decoder();
                if (!decoder || !atomic_load(&sound->decode_thread_running)) {
                        c_sleep(10);
                        continue;
                }

                if (perform_seek_if_requested(sound, decoder)) {
                        continue;
                }

                ma_result result = MA_SUCCESS;
                ma_uint64 frames_to_read = 0;
                LoaderData *loader = get_loader_data();
                void *next_decoder = get_other_decoder();

                if (sound->total_song_frames == 0) {
                        ma_data_source_get_length_in_pcm_frames(decoder, &sound->total_song_frames);
#ifdef DEBUG
                        k_log("Frame count: %" PRIu64, sound->total_song_frames);
#endif
                }

                ma_uint64 frames_remaining = sound->total_song_frames - sound->current_frame;

                if (sound->always_fade && !sound->fade_requested && frames_remaining > 0 &&
                    frames_remaining <= sound->fade_frames && next_decoder && !loader->dirty) {
                        sound->fade_frames -= (long long)(sound->fade_frames - frames_remaining);

                        if (frames_remaining > 100) {
                                if (request_crossfade(sound->always_fade_ms, 0))
                                        sound->fade_frames = frames_remaining;
                        }
                }

                // Handle crossfade
                if (sound->fade_requested && !loader->loadingFirstDecoder && next_decoder && !loader->dirty) {

                        perform_crossfade(sound, decoder, next_decoder, current_buf, next_buf, mixed_buf, frames_to_decode, &frames_to_read);

                } else {
                        bool finished = atomic_load_explicit(&sound->decode_finished, memory_order_relaxed);

                        if (finished) {
                                pthread_mutex_lock(&sound->decoder_mutex);

                                struct timespec ts;
                                clock_gettime(CLOCK_REALTIME, &ts);
                                ts.tv_sec += 10;

                                while (atomic_load_explicit(&sound->decode_finished, memory_order_acquire) &&
                                       atomic_load_explicit(&sound->decode_thread_running, memory_order_acquire) &&
                                       !atomic_load_explicit(&sound->request_switch_metadata, memory_order_acquire)) {

                                        int rc = pthread_cond_timedwait(&sound->decoder_cond,
                                                                        &sound->decoder_mutex,
                                                                        &ts);

                                        if (rc == ETIMEDOUT) {
                                                k_log("Decoder thread timed out waiting for condition.");
                                                atomic_store_explicit(&sound->decode_thread_running,
                                                                      false,
                                                                      memory_order_release);

#ifdef DEBUG
                                                uint64_t played = atomic_load_explicit(&sound_s->track_frames_sent, memory_order_relaxed);
                                                uint64_t boundary = atomic_load_explicit(&sound_s->track_end_frame, memory_order_relaxed);
                                                k_log("Played: %" PRIu64 "Boundary: %" PRIu64, played, boundary);
#endif
                                                break;
                                        }
                                }

                                pthread_mutex_unlock(&sound->decoder_mutex);

                                continue;
                        } else {
                                result = ma_data_source_read_pcm_frames(decoder,
                                                                        mixed_buf,
                                                                        frames_to_decode,
                                                                        &frames_to_read);

                                sound->current_frame += frames_to_read;
                                sound->total_frames += frames_to_read;
                        }
                }

                long long cursor = 0;
                ops->get_cursor(decoder, &cursor);

                // Handle end-of-track switching
                if (!atomic_load(&sound->request_switch_metadata) &&
                    !atomic_load(&sound->decode_finished) &&

                    should_switch(frames_to_read, result, (ma_uint64)cursor)) {

                        if (cursor != lastCursor) { // mid-song switch, clear buffer
                                atomic_store(&sound->request_switch_metadata, true);
                                atomic_store_explicit(&sound->fade_boundary, -1, memory_order_release);
                                atomic_store_explicit(&sound->clock_reset_ms, 0, memory_order_relaxed);
                                sound->fade_requested = false;
                                sound->fade_current_frame = 0;
                                sound->fade_enter_song_ms = 1;
                                sound->fade_requested = false;
                                sound->fade_seek_performed = false;

                                reset_ring_buffer(sound);
                                continue;

                        } else { // end song, but play the remaining buffer

                                ma_uint32 framesWritten = 0;

                                // Write the last frames before marking finished
                                if (frames_to_read > 0) {
                                        ma_uint32 framesRemaining = (ma_uint32)frames_to_read;
                                        while (framesRemaining > 0) {

                                                ma_uint32 framesToWrite = framesRemaining;
                                                void *pWriteBuffer = NULL;
                                                ma_result wr = ma_pcm_rb_acquire_write(&pcm_rb, &framesToWrite, &pWriteBuffer);

                                                if (wr != MA_SUCCESS || framesToWrite == 0) {

                                                        struct timespec ts = {0, 500000};
                                                        nanosleep(&ts, NULL);
                                                        continue;
                                                }

                                                memcpy(pWriteBuffer,
                                                       mixed_buf + framesWritten * sound->channels,
                                                       framesToWrite * sound->channels * sizeof(float));

                                                ma_pcm_rb_commit_write(&pcm_rb, framesToWrite);

                                                framesWritten += framesToWrite;
                                                framesRemaining -= framesToWrite;
                                        }
                                }

                                atomic_thread_fence(memory_order_seq_cst);

                                long long sent = atomic_load(&sound->track_frames_sent);
                                sent = sent + (long long)ma_pcm_rb_available_read(&pcm_rb);
                                atomic_store_explicit(&sound->track_end_frame, sent, memory_order_release);
                                atomic_store_explicit(&sound->decode_finished, true, memory_order_release);
                                atomic_store_explicit(&sound->fade_boundary, -1, memory_order_release);
                                atomic_store_explicit(&sound->clock_reset_ms, 0, memory_order_relaxed);

                                continue;
                        }
                }

                lastCursor = cursor;

                // Write decoded frames to ring buffer
                if (frames_to_read > 0) {

                        write_to_ring_buffer(sound, frames_to_read, mixed_buf);
                }

                if (sound->fade_requested && (sound->fade_current_frame >=
                                                  sound->fade_frames ||
                                              frames_to_read == 0)) {

                        sound->fade_requested = false;
                        sound->fade_seek_performed = false;

                        set_switch_decoder(true);
                }
        }

        aligned64_free(mixed_buf);
        aligned64_free(current_buf);
        aligned64_free(next_buf);

        pthread_mutex_lock(&sound_s->decoder_mutex);
        atomic_store(&sound->decode_thread_running, false);
        pthread_cond_signal(&sound_s->decoder_cond);
        pthread_mutex_unlock(&sound_s->decoder_mutex);

        atomic_store(&sound->decode_thread_running, false);
        atomic_store(&sound->drain_callbacks_remaining, 0);
        atomic_store(&sound->request_pause, false);

        return NULL;
}

// Audio callback, consumes data from the miniaudio ringbuffer, and feeds the output device
void on_audio_frames(ma_device *device, void *pOutput, const void *input, ma_uint32 frameCount)
{
        (void)device;
        (void)input;

        if (!atomic_load(&sound_s->buffer_ready)) {
                memset(pOutput, 0, frameCount * sound_s->channels * sizeof(float));
                return;
        }

        // Handle audio drainage
        int remaining = atomic_load(&sound_s->drain_callbacks_remaining);
        if (remaining > 0) {
                memset(pOutput, 0, frameCount * sound_s->channels * sizeof(float));
                remaining--;
                atomic_store(&sound_s->drain_callbacks_remaining, remaining);
                return;
        }

        if (!sound_s->audio_thread_priority_set) {
                set_decode_thread_priority(pthread_self());
                sound_s->audio_thread_priority_set = MA_TRUE;
        }

        const ma_uint32 frameSize = sound_s->channels * sizeof(float);
        ma_uint32 framesRemaining = frameCount;
        ma_uint32 totalFramesRead = 0;
        uint8_t *writePtr = (uint8_t *)pOutput;
        bool boundary_handled = false;

        while (framesRemaining > 0) {

                ma_uint32 framesToRead = framesRemaining;
                void *pReadBuffer = NULL;

                ma_result result = ma_pcm_rb_acquire_read(&pcm_rb, &framesToRead, &pReadBuffer);

                if (result != MA_SUCCESS || framesToRead == 0 || pReadBuffer == NULL) {

                        memset((uint8_t *)pOutput + totalFramesRead * frameSize,
                               0,
                               framesRemaining * frameSize);

                        totalFramesRead += framesRemaining;
                        break;
                }

                // Success, copy data
                float gain = sound_s->gain_linear;

                // Determine how many frames to copy this iteration
                ma_uint32 framesToCopy = framesToRead;

                // Load current played and boundary
                uint64_t played = atomic_load_explicit(&sound_s->track_frames_sent, memory_order_relaxed);
                uint64_t boundary = atomic_load_explicit(&sound_s->track_end_frame, memory_order_relaxed);

                atomic_load_explicit(&sound_s->fade_boundary, memory_order_acquire);
                atomic_load_explicit(&sound_s->clock_reset_done, memory_order_acquire);
                if (sound_s->fade_boundary >= 0 &&
                    sound_s->fade_boundary <= (long long)played + framesToCopy &&
                    !sound_s->clock_reset_done) {

                        pthread_mutex_lock(&sound_s->decoder_mutex);

                        atomic_store_explicit(&sound_s->fade_boundary_reached, true, memory_order_relaxed);
                        atomic_store_explicit(&sound_s->request_switch_metadata, true, memory_order_relaxed);

                        pthread_cond_signal(&sound_s->decoder_cond);
                        pthread_mutex_unlock(&sound_s->decoder_mutex);
                }

                if (!boundary_handled &&
                    played + framesToCopy >= boundary &&
                    atomic_load_explicit(&sound_s->decode_finished, memory_order_acquire)) {

                        pthread_mutex_lock(&sound_s->decoder_mutex);

                        // Only copy up to the track boundary
                        framesToCopy = (ma_uint32)(boundary - played);
                        atomic_store_explicit(&sound_s->request_switch_metadata, true, memory_order_relaxed);
                        boundary_handled = true;

                        pthread_cond_signal(&sound_s->decoder_cond);
                        pthread_mutex_unlock(&sound_s->decoder_mutex);
                }

                // Copy frames
                size_t bytesToCopy = framesToCopy * frameSize;
                MA_COPY_MEMORY(writePtr, pReadBuffer, bytesToCopy);

                // Apply Replay Gain
                if (gain != 1.0f) {
                        ma_uint64 total = framesToCopy * sound_s->channels;
                        apply_gain_f32((float *)writePtr, total, gain);
                }

                ma_pcm_rb_commit_read(&pcm_rb, framesToCopy);

                writePtr += bytesToCopy;
                totalFramesRead += framesToCopy;
                framesRemaining -= framesToCopy;

                atomic_fetch_add_explicit(&sound_s->track_frames_sent, framesToCopy, memory_order_relaxed);
        }

        visualizer_ringbuffer_push(pOutput, frameCount, sound_s->channels);
}

sound_result_t handle_codec(
    const char *file_path,
    CodecOps ops,
    sound_system_t *sound,
    ma_context *context)
{
        sound_result_t result = SOUND_OK;

        ma_uint32 sample_rate, channels, nSampleRate, nChannels;
        ma_format format, nFormat;
        ma_channel channel_map[MA_MAX_CHANNELS], nChannelMap[MA_MAX_CHANNELS];
        enum decoder_type_t current_implementation = get_current_decoder_type();

#ifdef USE_FAAD
        int avg_bit_rate = 0;
        k_m4adec_filetype file_type = 0;

        if (ops.decoder_type == M4A) {
                get_m4a_file_info_full(file_path, &format, &channels, &sample_rate, channel_map, &avg_bit_rate, &file_type);
                sound_s->avg_bit_rate = avg_bit_rate;
        } else {
                ops.get_file_info(file_path, &format, &channels, &sample_rate, channel_map);
        }
#else
        ops.get_file_info(file_path, &format, &channels, &sample_rate, channel_map);
#endif

        format = ma_format_f32; // Hardcoded, float is used throughout

        void *decoder = get_current_decoder();
        if (decoder != NULL && ops.get_decoder_format)
                ops.get_decoder_format(decoder, &nFormat, &nChannels, &nSampleRate,
                                       nChannelMap, MA_MAX_CHANNELS);

        // sameFormat computation
        bool sameFormat = false;
        if (ops.supportsGapless && decoder != NULL) {
                sameFormat = (ops.decoder_type == get_current_decoder_decoder_type() && format == nFormat && channels == nChannels &&
                              sample_rate == nSampleRate);
#ifdef USE_FAAD
                if (ops.decoder_type == M4A && decoder != NULL) {
                        sameFormat = sameFormat &&
                                     (((ma_m4a *)decoder)->file_type == file_type) &&
                                     (((ma_m4a *)decoder)->file_type != k_rawAAC);
                }
#endif
        }

        if (repeat_state == SOUND_STATE_REPEAT ||
            !(sameFormat && current_implementation == ops.decoder_type)) {
                set_decoder_type_switch_reached(true);

                set_current_decoder_type(ops.decoder_type);

                cleanup_playback_device();

                reset_decoders();
                reset_audio_buffer();

                sound->sample_rate = sample_rate;

                result = create_audio_device(
                    NULL,
                    sound,
                    get_device(),
                    context,
                    get_current_decoder,
                    on_audio_frames);

                if (result < 0) {
                        set_current_decoder_type(NONE);
                        set_decoder_type_switch_reached(false);
                        set_EOF_reached(true);
                        return result;
                }

                set_decoder_type_switch_reached(false);

                atomic_store(&sound_s->decode_thread_running, true);

                reset_clock();

                pthread_create(&sound_s->decode_thread,
                               NULL,
                               decode_loop,
                               sound_s);
        }

        return result;
}

static int init_audio_data_from_codec_decoder(const CodecOps *ops, void *decoder, sound_system_t *sound)
{
        if (!ops || !decoder || !sound_s)
                return -1;

        ma_channel channel_map[MA_MAX_CHANNELS];

        switch (ops->decoder_type) {
        case BUILTIN: {
                ma_decoder *d = (ma_decoder *)decoder;
                sound->format = ma_format_f32;
                sound->channels = d->outputChannels;
                sound->sample_rate = d->outputSampleRate;
                break;
        }

        case OPUS: {
                ma_libopus *d = (ma_libopus *)decoder;
                ma_libopus_ds_get_data_format(d, &sound->format, &sound->channels,
                                              &sound->sample_rate, channel_map, MA_MAX_CHANNELS);

                ((ma_data_source_base *)d)->pCurrent = d;
                d->pReadSeekTellUserData = sound;
                break;
        }

        case VORBIS: {
                ma_libvorbis *d = (ma_libvorbis *)decoder;
                ma_libvorbis_ds_get_data_format(d, &sound->format, &sound->channels,
                                                &sound->sample_rate, channel_map, MA_MAX_CHANNELS);

                ((ma_data_source_base *)d)->pCurrent = d;
                d->pReadSeekTellUserData = sound;
                break;
        }

        case WEBM: {
                ma_webm *d = (ma_webm *)decoder;
                ma_webm_ds_get_data_format(d, &sound->format, &sound->channels,
                                           &sound->sample_rate, channel_map, MA_MAX_CHANNELS);

                ((ma_data_source_base *)d)->pCurrent = d;
                d->pReadSeekTellUserData = sound;
                break;
        }

#ifdef USE_FAAD
        case M4A: {
                ma_m4a *d = (ma_m4a *)decoder;
                m4a_decoder_ds_get_data_format(d, &sound->format, &sound->channels,
                                               &sound->sample_rate, channel_map, MA_MAX_CHANNELS);

                ((ma_data_source_base *)d)->pCurrent = d;
                d->pReadSeekTellUserData = sound;
                break;
        }
#endif

        default:
                return -1;
        }

        sound->format = ma_format_f32; // hard-coded. float is used throughout

        return 0;
}

int init_first_datasource(sound_system_t *sound)
{
        if (!sound)
                return MA_ERROR;

        SongData *song_data = (sound->using_song_slot_A) ? sound->songdataA : sound->songdataB;

        if (!song_data)
                return MA_ERROR;

        const char *file_path = song_data->file_path;
        if (!file_path)
                return MA_ERROR;

        const CodecOps *ops = find_codec_ops(file_path);
        if (!ops)
                return MA_ERROR;

        int result = prepare_next_decoder(file_path, song_data, ops);
        if (result == -1)
                return -1;

        void *decoder = get_first_decoder();
        if (!decoder)
                return -1;

        result = init_audio_data_from_codec_decoder(ops, decoder, sound);
        if (result < 0)
                return -1;

        sound->current_frame = 0;
        sound->fade_current_frame = 0;
        sound->total_song_frames = 0;

        sound->audio_thread_priority_set = MA_FALSE;

        set_replay_gain(sound);

        return MA_SUCCESS;
}

void safe_ringbuffer_reset(ma_pcm_rb *pcm_rb)
{
        if (pcm_rb->rb.ownsBuffer) {
                ma_pcm_rb_uninit(pcm_rb);
        }
        memset(pcm_rb, 0, sizeof(*pcm_rb)); // reset the entire struct safely
}

int init_ring_buffer(sound_system_t *sound)
{
        // 3 seconds of buffer regardless of sample rate
        ma_uint32 rbFrames = sound->sample_rate * sound->ring_buffer_secs;

        atomic_store(&sound->decode_finished, false);
        atomic_store(&sound->request_switch_metadata, false);

        safe_ringbuffer_reset(&pcm_rb);

        pcm_rb.rb.pBuffer = NULL;
        pcm_rb.rb.ownsBuffer = false;

        ma_result rbResult = ma_pcm_rb_init(sound->format,
                                            sound->channels,
                                            rbFrames,
                                            NULL,
                                            NULL,
                                            &pcm_rb);
        if (rbResult != MA_SUCCESS) {
                k_log("Failed to initialize PCM ring buffer: %d\n", rbResult);
                return -1;
        }

        return 0;
}

sound_result_t create_audio_device(
    void *user_data,
    sound_system_t *sound,
    ma_device *device,
    ma_context *context,
    decoder_getter_func get_decoder,
    ma_data_callback_proc callback)
{
        ma_result result = init_first_datasource(sound_s);

        sound_s->chunk_frames = sound_s->sample_rate / 10;
        sound_s->ring_buffer_secs = 3;

        if (result != MA_SUCCESS)
                return SOUND_ERROR;

        void *decoder = get_decoder();
        if (!decoder)
                return SOUND_ERROR;

        init_ring_buffer(sound_s);

        atomic_store(&sound_sys->track_frames_sent, 0);
        sound_sys->total_frames = 0;

        sound_result_t sound_result = init_playback_device(
            context,
            sound,
            device,
            callback,
            user_data);
        set_current_volume(get_current_volume());

        sound_result = SOUND_NOTIFY_SWITCH;

        return sound_result;
}

sound_result_t sound_switch_decoder_type(char *file_path)
{
        if (sound_system_is_end_of_list_reached(sound_s)) {
                pb_set_EOF_handled();
                set_current_decoder_type(NONE);
                return 0;
        }

        if (!file_path) {
                pb_set_EOF_handled();
                return 0;
        }

        if (!valid_file_path(file_path)) {
                free(file_path);
                set_EOF_reached(true);
                return -1;
        }

        const CodecOps *ops = find_codec_ops(file_path);
        if (!ops) {
                free(file_path);
                return -1;
        }

        sound_result_t sound_result = handle_codec(file_path, *ops, sound_s, &context);

        if (sound_result < 0) {
                set_current_decoder_type(NONE);
                set_decoder_type_switch_reached(false);
                set_EOF_reached(true);
                return sound_result;
        }

        pb_set_EOF_handled();
        return sound_result;
}

bool is_context_initialized(void)
{
        return context_initialized;
}

void cleanup_audio_context(void)
{
        if (context_initialized) {
                ma_context_uninit(&context);
                context_initialized = false;
        }
}

sound_result_t sound_create_audio_device(void)
{
        cleanup_playback_device();

        if (!is_context_initialized()) {
                ma_context_init(NULL, 0, NULL, &context);
                context_initialized = true;
        }

        SongData *current_song = sound_get_current_song_data();

        if (!current_song)
                return sound_switch_decoder_type(NULL);
        else
                return sound_switch_decoder_type(current_song->file_path);
}

SongData *sound_get_current_song_data(void)
{
        return (atomic_load(&sound_s->using_song_slot_A)) ? sound_s->songdataA : sound_s->songdataB;
}

int load_decoder(SongData *song_data, bool *song_data_deleted)
{
        LoaderData *loader_data = get_loader_data();

        int result = 0;

        if (song_data != NULL) {
                *song_data_deleted = false;

                // This should only be done for the second song, as
                // switch_audio_implementation() handles the first one
                const CodecOps *ops = find_codec_ops(song_data->file_path);
                if (!loader_data->loadingFirstDecoder) {

                        if (!ops)
                                result = -1;
                        else {
                                result = prepare_next_decoder(song_data->file_path, song_data, ops);
                                sound_s->fade_allowed = (result != -2);
                        }
                } else {
                        result = is_decoding_possible(song_data->file_path, ops);
                }
        }
        return result;
}

void set_song_data(bool slotA, SongData *songdata)
{
        if (!sound_s)
                return;

        if (slotA)
                sound_s->songdataA = songdata;
        else
                sound_s->songdataB = songdata;
}

int assign_loaded_data(void)
{
        LoaderData *loader_data = get_loader_data();

        int result = 0;

        if (loader_data->loadInSlotA) {

                set_song_data(loader_data->loadInSlotA, loader_data->songdataA);

                result = load_decoder(loader_data->songdataA,
                                      &(loader_data->songdataADeleted));
        } else {
                set_song_data(loader_data->loadInSlotA, loader_data->songdataB);

                result = load_decoder(loader_data->songdataB,
                                      &(loader_data->songdataBDeleted));
        }

        return result;
}

bool sound_is_using_slot_A()
{
        if (!sound_s)
                return true;

        return atomic_load(&sound_s->using_song_slot_A);
}

void sound_set_using_slot_A(bool value)
{
        if (!sound_s)
                return;

        atomic_store(&sound_s->using_song_slot_A, value);
}

int sound_get_bit_depth(ma_format format)
{

        if (format == ma_format_unknown)
                return -1;

        int bit_depth = 32;

        switch (format) {
        case ma_format_u8:
                bit_depth = 8;
                break;

        case ma_format_s16:
                bit_depth = 16;
                break;

        case ma_format_s24:
                bit_depth = 24;
                break;

        case ma_format_f32:
        case ma_format_s32:
                bit_depth = 32;
                break;
        default:
                break;
        }

        return bit_depth;
}

void *song_data_reader_thread(void *arg)
{
        PlaybackState *ps = (PlaybackState *)arg;
        LoaderData *loader_data = get_loader_data();

        pthread_mutex_lock(&(loader_data->mutex));

        char filepath[KEW_PATH_MAX];
        c_strcpy(filepath, loader_data->file_path, sizeof(filepath));

        SongData *songdata = exists_file(filepath) >= 0 ? load_song_data(filepath) : NULL;

        if (loader_data->replaceNextSong) {
                loader_data->loadInSlotA = !sound_is_using_slot_A();
        }

        if (loader_data->loadingFirstDecoder) {
                loader_data->loadInSlotA = true;
                sound_set_using_slot_A(true);
        }

        if (loader_data->loadInSlotA) {

                song_loader_assign_slot_A(songdata);

        } else {

                song_loader_assign_slot_B(songdata);
        }

        loader_data->dirty = false; // The loader no longer contains unused data in the slot

        int result = assign_loaded_data();

        if (result == -1)
                songdata->hasErrors = true;

        pthread_mutex_unlock(&(loader_data->mutex));

        if (songdata == NULL || songdata->hasErrors) {
                
                if (loader_data->loadInSlotA) {

                        song_loader_unload_song_A();

                } else {

                        song_loader_unload_song_B();
                }

                ps->songHasErrors = true;
                ps->clearingErrors = true;
                set_next_song(NULL);
        } else {
                loader_data->loadInSlotA = !loader_data->loadInSlotA;
                ps->songHasErrors = false;
                ps->clearingErrors = false;
                set_next_song(get_try_next_song());
                set_try_next_song(NULL);
        }

        ps->loadedNextSong = true;
        ps->skipping = false;
        ps->songLoading = false;

        return NULL;
}

sound_result_t sound_load_song(const char *file_path, int is_first_decoder, int replace_next_song)
{
        LoaderData *loader_data = get_loader_data();
        PlaybackState *ps = get_playback_state();

        sound_result_t sound_result = SOUND_OK;

        if (find_codec_ops(file_path) == NULL) {
                sound_result = SOUND_ERROR_SONG;
                return sound_result;
        }

        c_strcpy(loader_data->file_path, file_path,
                 sizeof(loader_data->file_path));

        loader_data->loadingFirstDecoder = is_first_decoder;
        loader_data->replaceNextSong = replace_next_song;

        pthread_t loading_thread;
        pthread_create(&loading_thread, NULL, song_data_reader_thread, ps);

        return sound_result;
}

void sound_switch_track_immediate(void)
{
        switch_metadata(sound_s);
}

void sound_shutdown(void)
{
        pthread_mutex_destroy(&sound_s->decoder_mutex);
        pthread_cond_destroy(&sound_s->decoder_cond);

        if (is_context_initialized()) {
                cleanup_playback_device();
                cleanup_audio_context();
        }
}

void sound_ringbuffer_cleanup(void)
{
        if (pcm_rb.rb.ownsBuffer) {
                ma_pcm_rb_uninit(&pcm_rb);
        }
        memset(&pcm_rb, 0, sizeof(pcm_rb));
}
