/*
  Simple DirectMedia Layer
  Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>

  This software is provided 'as-is', without any express or implied
  warranty.  In no event will the authors be held liable for any damages
  arising from the use of this software.

  Permission is granted to anyone to use this software for any purpose,
  including commercial applications, and to alter it and redistribute it
  freely, subject to the following restrictions:

  1. The origin of this software must not be misrepresented; you must not
     claim that you wrote the original software. If you use this software
     in a product, an acknowledgment in the product documentation would be
     appreciated but is not required.
  2. Altered source versions must be plainly marked as such, and must not be
     misrepresented as being the original software.
  3. This notice may not be removed or altered from any source distribution.
*/
#include "SDL_internal.h"

#include "SDL_audioqueue.h"
#include "SDL_sysaudio.h"

#define AUDIO_SPECS_EQUAL(x, y) (((x).format == (y).format) && ((x).channels == (y).channels) && ((x).freq == (y).freq))

typedef struct SDL_MemoryPool SDL_MemoryPool;

struct SDL_MemoryPool
{
    void *free_blocks;
    size_t block_size;
    size_t num_free;
    size_t max_free;
};

struct SDL_AudioTrack
{
    SDL_AudioSpec spec;
    SDL_bool flushed;
    SDL_AudioTrack *next;

    void *userdata;
    SDL_ReleaseAudioBufferCallback callback;

    Uint8 *data;
    size_t head;
    size_t tail;
    size_t capacity;
};

struct SDL_AudioQueue
{
    SDL_AudioTrack *head;
    SDL_AudioTrack *tail;

    Uint8 *history_buffer;
    size_t history_length;
    size_t history_capacity;

    SDL_MemoryPool track_pool;
    SDL_MemoryPool chunk_pool;
};

// Allocate a new block, avoiding checking for ones already in the pool
static void *AllocNewMemoryPoolBlock(const SDL_MemoryPool *pool)
{
    return SDL_malloc(pool->block_size);
}

// Allocate a new block, first checking if there are any in the pool
static void *AllocMemoryPoolBlock(SDL_MemoryPool *pool)
{
    if (pool->num_free == 0) {
        return AllocNewMemoryPoolBlock(pool);
    }

    void *block = pool->free_blocks;
    pool->free_blocks = *(void **)block;
    --pool->num_free;
    return block;
}

// Free a block, or add it to the pool if there's room
static void FreeMemoryPoolBlock(SDL_MemoryPool *pool, void *block)
{
    if (pool->num_free < pool->max_free) {
        *(void **)block = pool->free_blocks;
        pool->free_blocks = block;
        ++pool->num_free;
    } else {
        SDL_free(block);
    }
}

// Destroy a pool and all of its blocks
static void DestroyMemoryPool(SDL_MemoryPool *pool)
{
    void *block = pool->free_blocks;
    pool->free_blocks = NULL;
    pool->num_free = 0;

    while (block) {
        void *next = *(void **)block;
        SDL_free(block);
        block = next;
    }
}

// Keeping a list of free chunks reduces memory allocations,
// But also increases the amount of work to perform when freeing the track.
static void InitMemoryPool(SDL_MemoryPool *pool, size_t block_size, size_t max_free)
{
    SDL_zerop(pool);

    SDL_assert(block_size >= sizeof(void *));
    pool->block_size = block_size;
    pool->max_free = max_free;
}

// Allocates a number of blocks and adds them to the pool
static int ReserveMemoryPoolBlocks(SDL_MemoryPool *pool, size_t num_blocks)
{
    for (; num_blocks; --num_blocks) {
        void *block = AllocNewMemoryPoolBlock(pool);

        if (block == NULL) {
            return SDL_OutOfMemory();
        }

        *(void **)block = pool->free_blocks;
        pool->free_blocks = block;
        ++pool->num_free;
    }

    return 0;
}

void SDL_DestroyAudioQueue(SDL_AudioQueue *queue)
{
    SDL_ClearAudioQueue(queue);

    DestroyMemoryPool(&queue->track_pool);
    DestroyMemoryPool(&queue->chunk_pool);
    SDL_aligned_free(queue->history_buffer);

    SDL_free(queue);
}

SDL_AudioQueue *SDL_CreateAudioQueue(size_t chunk_size)
{
    SDL_AudioQueue *queue = (SDL_AudioQueue *)SDL_calloc(1, sizeof(*queue));

    if (!queue) {
        return NULL;
    }

    InitMemoryPool(&queue->track_pool, sizeof(SDL_AudioTrack), 8);
    InitMemoryPool(&queue->chunk_pool, chunk_size, 4);

    if (ReserveMemoryPoolBlocks(&queue->track_pool, 2) != 0) {
        SDL_DestroyAudioQueue(queue);
        return NULL;
    }

    return queue;
}

static void DestroyAudioTrack(SDL_AudioQueue *queue, SDL_AudioTrack *track)
{
    track->callback(track->userdata, track->data, (int)track->capacity);

    FreeMemoryPoolBlock(&queue->track_pool, track);
}

void SDL_ClearAudioQueue(SDL_AudioQueue *queue)
{
    SDL_AudioTrack *track = queue->head;

    queue->head = NULL;
    queue->tail = NULL;
    queue->history_length = 0;

    while (track) {
        SDL_AudioTrack *next = track->next;
        DestroyAudioTrack(queue, track);
        track = next;
    }
}

static void FlushAudioTrack(SDL_AudioTrack *track)
{
    track->flushed = SDL_TRUE;
}

void SDL_FlushAudioQueue(SDL_AudioQueue *queue)
{
    SDL_AudioTrack *track = queue->tail;

    if (track) {
        FlushAudioTrack(track);
    }
}

void SDL_PopAudioQueueHead(SDL_AudioQueue *queue)
{
    SDL_AudioTrack *track = queue->head;

    for (;;) {
        SDL_bool flushed = track->flushed;

        SDL_AudioTrack *next = track->next;
        DestroyAudioTrack(queue, track);
        track = next;

        if (flushed) {
            break;
        }
    }

    queue->head = track;
    queue->history_length = 0;

    if (!track) {
        queue->tail = NULL;
    }
}

SDL_AudioTrack *SDL_CreateAudioTrack(
    SDL_AudioQueue *queue, const SDL_AudioSpec *spec,
    Uint8 *data, size_t len, size_t capacity,
    SDL_ReleaseAudioBufferCallback callback, void *userdata)
{
    SDL_AudioTrack *track = AllocMemoryPoolBlock(&queue->track_pool);

    if (!track) {
        return NULL;
    }

    SDL_zerop(track);
    SDL_copyp(&track->spec, spec);

    track->userdata = userdata;
    track->callback = callback;
    track->data = data;
    track->head = 0;
    track->tail = len;
    track->capacity = capacity;

    return track;
}

static void SDLCALL FreeChunkedAudioBuffer(void *userdata, const void *buf, int len)
{
    SDL_AudioQueue *queue = userdata;

    FreeMemoryPoolBlock(&queue->chunk_pool, (void *)buf);
}

static SDL_AudioTrack *CreateChunkedAudioTrack(SDL_AudioQueue *queue, const SDL_AudioSpec *spec)
{
    void *chunk = AllocMemoryPoolBlock(&queue->chunk_pool);

    if (!chunk) {
        return NULL;
    }

    size_t capacity = queue->chunk_pool.block_size;
    capacity -= capacity % SDL_AUDIO_FRAMESIZE(*spec);

    SDL_AudioTrack *track = SDL_CreateAudioTrack(queue, spec, chunk, 0, capacity, FreeChunkedAudioBuffer, queue);

    if (!track) {
        FreeMemoryPoolBlock(&queue->chunk_pool, chunk);
        return NULL;
    }

    return track;
}

void SDL_AddTrackToAudioQueue(SDL_AudioQueue *queue, SDL_AudioTrack *track)
{
    SDL_AudioTrack *tail = queue->tail;

    if (tail) {
        // If the spec has changed, make sure to flush the previous track
        if (!AUDIO_SPECS_EQUAL(tail->spec, track->spec)) {
            FlushAudioTrack(tail);
        }

        tail->next = track;
    } else {
        queue->head = track;
    }

    queue->tail = track;
}

static size_t WriteToAudioTrack(SDL_AudioTrack *track, const Uint8 *data, size_t len)
{
    if (track->flushed || track->tail >= track->capacity) {
        return 0;
    }

    len = SDL_min(len, track->capacity - track->tail);
    SDL_memcpy(&track->data[track->tail], data, len);
    track->tail += len;

    return len;
}

int SDL_WriteToAudioQueue(SDL_AudioQueue *queue, const SDL_AudioSpec *spec, const Uint8 *data, size_t len)
{
    if (len == 0) {
        return 0;
    }

    SDL_AudioTrack *track = queue->tail;

    if (track) {
        if (!AUDIO_SPECS_EQUAL(track->spec, *spec)) {
            FlushAudioTrack(track);
        }
    } else {
        SDL_assert(!queue->head);
        track = CreateChunkedAudioTrack(queue, spec);

        if (!track) {
            return -1;
        }

        queue->head = track;
        queue->tail = track;
    }

    for (;;) {
        size_t written = WriteToAudioTrack(track, data, len);
        data += written;
        len -= written;

        if (len == 0) {
            break;
        }

        SDL_AudioTrack *new_track = CreateChunkedAudioTrack(queue, spec);

        if (!new_track) {
            return -1;
        }

        track->next = new_track;
        queue->tail = new_track;
        track = new_track;
    }

    return 0;
}

void *SDL_BeginAudioQueueIter(SDL_AudioQueue *queue)
{
    return queue->head;
}

size_t SDL_NextAudioQueueIter(SDL_AudioQueue *queue, void **inout_iter, SDL_AudioSpec *out_spec, SDL_bool *out_flushed)
{
    SDL_AudioTrack *iter = (SDL_AudioTrack *)(*inout_iter);
    SDL_assert(iter != NULL);

    SDL_copyp(out_spec, &iter->spec);

    SDL_bool flushed = SDL_FALSE;
    size_t queued_bytes = 0;

    while (iter) {
        SDL_AudioTrack *track = iter;
        iter = iter->next;

        size_t avail = track->tail - track->head;

        if (avail >= SDL_SIZE_MAX - queued_bytes) {
            queued_bytes = SDL_SIZE_MAX;
            flushed = SDL_FALSE;
            break;
        }

        queued_bytes += avail;
        flushed = track->flushed;

        if (flushed) {
            break;
        }
    }

    *inout_iter = iter;
    *out_flushed = flushed;

    return queued_bytes;
}

static const Uint8 *PeekIntoAudioQueuePast(SDL_AudioQueue *queue, Uint8 *data, size_t len)
{
    SDL_AudioTrack *track = queue->head;

    if (track->head >= len) {
        return &track->data[track->head - len];
    }

    size_t past = len - track->head;

    if (past > queue->history_length) {
        return NULL;
    }

    SDL_memcpy(data, &queue->history_buffer[queue->history_length - past], past);
    SDL_memcpy(&data[past], track->data, track->head);

    return data;
}

static void UpdateAudioQueueHistory(SDL_AudioQueue *queue,
                                    const Uint8 *data, size_t len)
{
    Uint8 *history_buffer = queue->history_buffer;
    size_t history_bytes = queue->history_length;

    if (len >= history_bytes) {
        SDL_memcpy(history_buffer, &data[len - history_bytes], history_bytes);
    } else {
        size_t preserve = history_bytes - len;
        SDL_memmove(history_buffer, &history_buffer[len], preserve);
        SDL_memcpy(&history_buffer[preserve], data, len);
    }
}

static const Uint8 *ReadFromAudioQueue(SDL_AudioQueue *queue, Uint8 *data, size_t len)
{
    SDL_AudioTrack *track = queue->head;

    if (track->tail - track->head >= len) {
        const Uint8 *ptr = &track->data[track->head];
        track->head += len;
        return ptr;
    }

    size_t total = 0;

    for (;;) {
        size_t avail = SDL_min(len - total, track->tail - track->head);
        SDL_memcpy(&data[total], &track->data[track->head], avail);
        track->head += avail;
        total += avail;

        if (total == len) {
            break;
        }

        if (track->flushed) {
            SDL_SetError("Reading past end of flushed track");
            return NULL;
        }

        SDL_AudioTrack *next = track->next;

        if (!next) {
            SDL_SetError("Reading past end of incomplete track");
            return NULL;
        }

        UpdateAudioQueueHistory(queue, track->data, track->tail);

        queue->head = next;
        DestroyAudioTrack(queue, track);
        track = next;
    }

    return data;
}

static const Uint8 *PeekIntoAudioQueueFuture(SDL_AudioQueue *queue, Uint8 *data, size_t len)
{
    SDL_AudioTrack *track = queue->head;

    if (track->tail - track->head >= len) {
        return &track->data[track->head];
    }

    size_t total = 0;

    for (;;) {
        size_t avail = SDL_min(len - total, track->tail - track->head);
        SDL_memcpy(&data[total], &track->data[track->head], avail);
        total += avail;

        if (total == len) {
            break;
        }

        if (track->flushed) {
            // If we have run out of data, fill the rest with silence.
            SDL_memset(&data[total], SDL_GetSilenceValueForFormat(track->spec.format), len - total);
            break;
        }

        track = track->next;

        if (!track) {
            SDL_SetError("Peeking past end of incomplete track");
            return NULL;
        }
    }

    return data;
}

const Uint8 *SDL_ReadFromAudioQueue(SDL_AudioQueue *queue,
                                    Uint8 *dst, SDL_AudioFormat dst_format, int dst_channels,
                                    int past_frames, int present_frames, int future_frames,
                                    Uint8 *scratch)
{
    SDL_AudioTrack *track = queue->head;

    if (!track) {
        return NULL;
    }

    SDL_AudioFormat src_format = track->spec.format;
    int src_channels = track->spec.channels;

    size_t src_frame_size = SDL_AUDIO_BYTESIZE(src_format) * src_channels;
    size_t dst_frame_size = SDL_AUDIO_BYTESIZE(dst_format) * dst_channels;

    size_t src_past_bytes = past_frames * src_frame_size;
    size_t src_present_bytes = present_frames * src_frame_size;
    size_t src_future_bytes = future_frames * src_frame_size;

    size_t dst_past_bytes = past_frames * dst_frame_size;
    size_t dst_present_bytes = present_frames * dst_frame_size;
    size_t dst_future_bytes = future_frames * dst_frame_size;

    SDL_bool convert = (src_format != dst_format) || (src_channels != dst_channels);

    if (convert && !dst) {
        // The user didn't ask for the data to be copied, but we need to convert it, so store it in the scratch buffer
        dst = scratch;
    }

    // Can we get all of the data straight from this track?
    if ((track->head >= src_past_bytes) && ((track->tail - track->head) >= (src_present_bytes + src_future_bytes))) {
        const Uint8 *ptr = &track->data[track->head - src_past_bytes];
        track->head += src_present_bytes;

        // Do we still need to copy/convert the data?
        if (dst) {
            ConvertAudio(past_frames + present_frames + future_frames, ptr,
                         src_format, src_channels, dst, dst_format, dst_channels, scratch);
            ptr = dst;
        }

        return ptr;
    }

    if (!dst) {
        // The user didn't ask for the data to be copied, but we need to, so store it in the scratch buffer
        dst = scratch;
    } else if (!convert) {
        // We are only copying, not converting, so copy straight into the dst buffer
        scratch = dst;
    }

    Uint8 *ptr = dst;

    if (src_past_bytes) {
        ConvertAudio(past_frames, PeekIntoAudioQueuePast(queue, scratch, src_past_bytes), src_format, src_channels, dst, dst_format, dst_channels, scratch);
        dst += dst_past_bytes;
        scratch += dst_past_bytes;
    }

    if (src_present_bytes) {
        ConvertAudio(present_frames, ReadFromAudioQueue(queue, scratch, src_present_bytes), src_format, src_channels, dst, dst_format, dst_channels, scratch);
        dst += dst_present_bytes;
        scratch += dst_present_bytes;
    }

    if (src_future_bytes) {
        ConvertAudio(future_frames, PeekIntoAudioQueueFuture(queue, scratch, src_future_bytes), src_format, src_channels, dst, dst_format, dst_channels, scratch);
        dst += dst_future_bytes;
        scratch += dst_future_bytes;
    }

    return ptr;
}

size_t SDL_GetAudioQueueQueued(SDL_AudioQueue *queue)
{
    size_t total = 0;
    void *iter = SDL_BeginAudioQueueIter(queue);

    while (iter) {
        SDL_AudioSpec src_spec;
        SDL_bool flushed;

        size_t avail = SDL_NextAudioQueueIter(queue, &iter, &src_spec, &flushed);

        if (avail >= SDL_SIZE_MAX - total) {
            total = SDL_SIZE_MAX;
            break;
        }

        total += avail;
    }

    return total;
}

int SDL_ResetAudioQueueHistory(SDL_AudioQueue *queue, int num_frames)
{
    SDL_AudioTrack *track = queue->head;

    if (!track) {
        return -1;
    }

    size_t length = num_frames * SDL_AUDIO_FRAMESIZE(track->spec);
    Uint8 *history_buffer = queue->history_buffer;

    if (queue->history_capacity < length) {
        history_buffer = SDL_aligned_alloc(SDL_SIMDGetAlignment(), length);
        if (!history_buffer) {
            return -1;
        }
        SDL_aligned_free(queue->history_buffer);
        queue->history_buffer = history_buffer;
        queue->history_capacity = length;
    }

    queue->history_length = length;
    SDL_memset(history_buffer, SDL_GetSilenceValueForFormat(track->spec.format), length);

    return 0;
}
