/*
 * Ogg/Opus streaming music support, loosely based on several open source
 * Quake engine based projects with many modifications.
 *
 * Copyright (C) 2012-2013 O.Sezer <sezero@users.sourceforge.net>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or (at
 * your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#include "quakedef.hpp"
#include "fshandle.hpp"
#include "console.hpp"

#if defined(USE_CODEC_OPUS)
#include "snd_codec.hpp"
#include "snd_codeci.hpp"
#include "snd_opus.hpp"

#include <errno.h>
#include <opusfile.h>


/* CALLBACK FUNCTIONS: */

static int opc_fclose(void* f)
{
    return 0; /* we fclose() elsewhere. */
}

static int opc_fread(void* f, unsigned char* buf, int size)
{
    int ret;

    if(size < 0)
    {
        errno = EINVAL;
        return -1;
    }

    ret = (int)FS_fread(buf, 1, (size_t)size, (fshandle_t*)f);
    if(ret == 0 && errno != 0) ret = -1;
    return ret;
}

static int opc_fseek(void* f, opus_int64 off, int whence)
{
    if(f == nullptr) return (-1);
    return FS_fseek((fshandle_t*)f, (long)off, whence);
}

static opus_int64 opc_ftell(void* f)
{
    return (opus_int64)FS_ftell((fshandle_t*)f);
}

static const OpusFileCallbacks opc_qfs = {
    (int (*)(void*, unsigned char*, int))opc_fread,
    (int (*)(void*, opus_int64, int))opc_fseek, (opus_int64(*)(void*))opc_ftell,
    (int (*)(void*))opc_fclose};

static bool S_OPUS_CodecInitialize()
{
    return true;
}

static void S_OPUS_CodecShutdown()
{
}

static bool S_OPUS_CodecOpenStream(snd_stream_t* stream)
{
    OggOpusFile* opFile;
    const OpusHead* op_info;
    long numstreams;
    int res;

    opFile = op_open_callbacks(&stream->fh, &opc_qfs, nullptr, 0, &res);
    if(!opFile)
    {
        Con_Printf(
            "%s is not a valid Opus file (error %i).\n", stream->name, res);
        goto _fail;
    }

    stream->priv = opFile;

    if(!op_seekable(opFile))
    {
        Con_Printf("Opus stream %s not seekable.\n", stream->name);
        goto _fail;
    }

    op_info = op_head(opFile, -1);
    if(!op_info)
    {
        Con_Printf("Unable to get stream information for %s.\n", stream->name);
        goto _fail;
    }

    /* FIXME: handle section changes */
    numstreams = op_info->stream_count;
    if(numstreams != 1)
    {
        Con_Printf("More than one (%ld) stream in %s\n",
            (long)op_info->stream_count, stream->name);
        goto _fail;
    }

    if(op_info->channel_count != 1 && op_info->channel_count != 2)
    {
        Con_Printf("Unsupported number of channels %d in %s\n",
            op_info->channel_count, stream->name);
        goto _fail;
    }

    /* All Opus audio is coded at 48 kHz, and should also be decoded
     * at 48 kHz for playback: info->input_sample_rate only tells us
     * the sampling rate of the original input before opus encoding.
     * S_RawSamples() shall already downsample this, as necessary.  */
    stream->info.rate = 48000;
    stream->info.channels = op_info->channel_count;
    /* op_read() yields 16-bit output using native endian ordering: */
    stream->info.bits = 16;
    stream->info.width = 2;

    return true;
_fail:
    if(opFile) op_free(opFile);
    return false;
}

static int S_OPUS_CodecReadStream(snd_stream_t* stream, int bytes, void* buffer)
{
    int section; /* FIXME: handle section changes */
    int cnt, res, rem;
    opus_int16* ptr;

    rem = bytes / stream->info.width;
    if(rem / stream->info.channels <= 0) return 0;

    cnt = 0;
    ptr = (opus_int16*)buffer;
    while(1)
    {
        /* op_read() yields 16-bit output using native endian ordering. returns
         * the number of samples read per channel on success, or a negative
         * value on failure. */
        res = op_read((OggOpusFile*)stream->priv, ptr, rem, &section);
        if(res <= 0) break;
        cnt += res;
        res *= stream->info.channels;
        rem -= res;
        if(rem <= 0) break;
        ptr += res;
    }

    if(res < 0) return res;

    cnt *= (stream->info.channels * stream->info.width);
    return cnt;
}

static void S_OPUS_CodecCloseStream(snd_stream_t* stream)
{
    op_free((OggOpusFile*)stream->priv);
    S_CodecUtilClose(&stream);
}

static int S_OPUS_CodecRewindStream(snd_stream_t* stream)
{
    return op_pcm_seek((OggOpusFile*)stream->priv, 0);
}

snd_codec_t opus_codec = {CODECTYPE_OPUS, true, /* always available. */
    "opus", S_OPUS_CodecInitialize, S_OPUS_CodecShutdown,
    S_OPUS_CodecOpenStream, S_OPUS_CodecReadStream, S_OPUS_CodecRewindStream,
    S_OPUS_CodecCloseStream, nullptr};

#endif /* USE_CODEC_OPUS */
