/**
 * SDL_sound; An abstract sound format decoding API.
 *
 * Please see the file LICENSE.txt in the source's root directory.
 *
 *  This file written by Ryan C. Gordon.
 */

/*
 * FLAC decoder for SDL_sound.
 *
 * FLAC stands for Free Lossless Audio Codec. It's lossless and
 *  high-resolution, so it's become popular with tapers and audiophiles, at
 *  the cost of a much larger filesize than lossy things like Ogg, MP3, AAC,
 *  etc. More details can be found at https://xiph.org/flac/ ...
 *
 * This doesn't use libFLAC from xiph.org, but rather dr_flac, a public
 *  domain, single-header library, for decoding. It handles both FLAC and
 *  "Ogg FLAC" (FLAC codec wrapped in an Ogg container) files.
 *
 * dr_flac is here: https://github.com/mackron/dr_libs/
 */

#define __SDL_SOUND_INTERNAL__
#include "SDL_sound_internal.h"

#if SOUND_SUPPORTS_FLAC

#define DR_FLAC_IMPLEMENTATION
#define DR_FLAC_NO_STDIO 1
#define DR_FLAC_NO_WIN32_IO 1
#define DRFLAC_ASSERT(x) SDL_assert((x))
#define DRFLAC_MALLOC(sz) SDL_malloc((sz))
#define DRFLAC_REALLOC(p, sz) SDL_realloc((p), (sz))
#define DRFLAC_FREE(p) SDL_free((p))
#define DRFLAC_COPY_MEMORY(dst, src, sz) SDL_memcpy((dst), (src), (sz))
#define DRFLAC_ZERO_MEMORY(p, sz) SDL_memset((p), 0, (sz))

#include "dr_flac.h"

static size_t flac_read(void* pUserData, void* pBufferOut, size_t bytesToRead)
{
    /* !!! FIXME: dr_flac treats returning less than bytesToRead as EOF. So we can't EAGAIN. */
    Uint8 *ptr = (Uint8 *) pBufferOut;
    Sound_Sample *sample = (Sound_Sample *) pUserData;
    Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
    SDL_IOStream *io = internal->io;
    size_t retval = 0;

    /* !!! FIXME: dr_flac treats returning less than bytesToRead as EOF. So we can't EAGAIN. */
    while (bytesToRead)
    {
        const size_t rc = SDL_ReadIO(io, ptr, bytesToRead);
        if (rc == 0) break;
        bytesToRead -= rc;
        retval += rc;
        ptr += rc;
    } /* while */

    return retval;
} /* flac_read */

static drflac_bool32 flac_seek(void* pUserData, int offset, drflac_seek_origin origin)
{
    Sound_Sample *sample = (Sound_Sample *) pUserData;
    Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
    int whence;
    switch (origin) {
    case DRFLAC_SEEK_SET:
        whence = SDL_IO_SEEK_SET;
        break;
    case DRFLAC_SEEK_CUR:
        whence = SDL_IO_SEEK_CUR;
        break;
    case DRFLAC_SEEK_END:
        whence = SDL_IO_SEEK_END;
        break;
    default:
        return DRFLAC_FALSE;
    }
    return (SDL_SeekIO(internal->io, offset, whence) != -1) ? DRFLAC_TRUE : DRFLAC_FALSE;
} /* flac_seek */

static drflac_bool32 flac_tell(void* pUserData, drflac_int64* pCursor)
{
    Sound_Sample *sample = (Sound_Sample *) pUserData;
    Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
    *pCursor = SDL_TellIO(internal->io);
    return (*pCursor != -1) ? DRFLAC_TRUE : DRFLAC_FALSE;
} /* flac_tell */

static bool FLAC_init(void)
{
    return true; /* always succeeds. */
} /* FLAC_init */


static void FLAC_quit(void)
{
    /* it's a no-op. */
} /* FLAC_quit */


static int FLAC_open(Sound_Sample *sample, const char *ext)
{
    Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
    drflac *dr = drflac_open(flac_read, flac_seek, flac_tell, sample, NULL);

    if (!dr)
    {
        BAIL_IF_MACRO(sample->flags & SOUND_SAMPLEFLAG_ERROR, ERR_IO_ERROR, 0);
        BAIL_MACRO("FLAC: Not a FLAC stream.", 0);
    } /* if */

    SNDDBG(("FLAC: Accepting data stream.\n"));
    sample->flags = SOUND_SAMPLEFLAG_CANSEEK;

    sample->actual.channels = dr->channels;
    sample->actual.freq = dr->sampleRate;
    sample->actual.format = SDL_AUDIO_S32; /* dr_flac only does Sint32. */

    if (dr->totalPCMFrameCount == 0)
        internal->total_time = -1;
    else
    {
        const Uint32 freq = (Uint32) dr->sampleRate;
        const Uint64 frames = (Uint64) dr->totalPCMFrameCount;
        internal->total_time = (frames / freq) * 1000;
        internal->total_time += ((dr->totalPCMFrameCount % dr->sampleRate) * 1000) / dr->sampleRate;
    } /* else */

    internal->decoder_private = dr;

    return 1;
} /* FLAC_open */

static void FLAC_close(Sound_Sample *sample)
{
    Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
    drflac *dr = (drflac *) internal->decoder_private;
    drflac_close(dr);
} /* FLAC_close */

static Uint32 FLAC_read(Sound_Sample *sample)
{
    Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
    const int channels = (int) sample->actual.channels;
    drflac *dr = (drflac *) internal->decoder_private;
    const drflac_uint64 frames_to_read = (internal->buffer_size / channels) / sizeof (drflac_int32);
    const drflac_uint64 rc = drflac_read_pcm_frames_s32(dr, frames_to_read, (drflac_int32 *) internal->buffer);
    /* !!! FIXME: we only set the EOF flags, but this only tells you we're done, not about i/o errors, nor corruption. */
    if (rc < frames_to_read)
        sample->flags |= SOUND_SAMPLEFLAG_EOF;
    return rc * channels * sizeof (drflac_int32);
} /* FLAC_read */

static int FLAC_rewind(Sound_Sample *sample)
{
    Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
    drflac *dr = (drflac *) internal->decoder_private;
    return (drflac_seek_to_pcm_frame(dr, 0) == DRFLAC_TRUE);
} /* FLAC_rewind */

static int FLAC_seek(Sound_Sample *sample, Uint32 ms)
{
    Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
    drflac *dr = (drflac *) internal->decoder_private;
    const float frames_per_ms = ((float) sample->actual.freq) / 1000.0f;
    const drflac_uint64 frame_offset = (drflac_uint64) (frames_per_ms * ((float) ms));
    return (drflac_seek_to_pcm_frame(dr, frame_offset) == DRFLAC_TRUE);
} /* FLAC_seek */

static const char *extensions_flac[] = { "FLAC", "FLA", NULL };
const Sound_DecoderFunctions __Sound_DecoderFunctions_FLAC =
{
    {
        extensions_flac,
        "Free Lossless Audio Codec",
        "Ryan C. Gordon <icculus@icculus.org>",
        "https://icculus.org/SDL_sound/"
    },

    FLAC_init,       /*   init() method */
    FLAC_quit,       /*   quit() method */
    FLAC_open,       /*   open() method */
    FLAC_close,      /*  close() method */
    FLAC_read,       /*   read() method */
    FLAC_rewind,     /* rewind() method */
    FLAC_seek        /*   seek() method */
};

#endif /* SOUND_SUPPORTS_FLAC */

/* end of SDL_sound_flac.c ... */
