#ifdef OPENMW_USE_FFMPEG


#include "ffmpeg_decoder.hpp"

// auto_ptr
#include <memory>

#include <stdexcept>

namespace MWSound
{

static void fail(const std::string &msg)
{ throw std::runtime_error("FFmpeg exception: "+msg); }


int FFmpeg_Decoder::readPacket(void *user_data, uint8_t *buf, int buf_size)
{
    Ogre::DataStreamPtr stream = static_cast<FFmpeg_Decoder*>(user_data)->mDataStream;
    return stream->read(buf, buf_size);
}

int FFmpeg_Decoder::writePacket(void *user_data, uint8_t *buf, int buf_size)
{
    Ogre::DataStreamPtr stream = static_cast<FFmpeg_Decoder*>(user_data)->mDataStream;
    return stream->write(buf, buf_size);
}

int64_t FFmpeg_Decoder::seek(void *user_data, int64_t offset, int whence)
{
    Ogre::DataStreamPtr stream = static_cast<FFmpeg_Decoder*>(user_data)->mDataStream;

    whence &= ~AVSEEK_FORCE;
    if(whence == AVSEEK_SIZE)
        return stream->size();
    if(whence == SEEK_SET)
        stream->seek(offset);
    else if(whence == SEEK_CUR)
        stream->seek(stream->tell()+offset);
    else if(whence == SEEK_END)
        stream->seek(stream->size()+offset);
    else
        return -1;

    return stream->tell();
}


/* Used by getAV*Data to search for more compressed data, and buffer it in the
 * correct stream. It won't buffer data for streams that the app doesn't have a
 * handle for. */
bool FFmpeg_Decoder::getNextPacket()
{
    if(!mStream)
        return false;

    int stream_idx = mStream - mFormatCtx->streams;
    while(av_read_frame(mFormatCtx, &mPacket) >= 0)
    {
        /* Check if the packet belongs to this stream */
        if(stream_idx == mPacket.stream_index)
        {
            if((uint64_t)mPacket.pts != AV_NOPTS_VALUE)
                mNextPts = av_q2d((*mStream)->time_base)*mPacket.pts;
            return true;
        }

        /* Free the packet and look for another */
        av_free_packet(&mPacket);
    }

    return false;
}

bool FFmpeg_Decoder::getAVAudioData()
{
    int got_frame, len;

    if((*mStream)->codec->codec_type != AVMEDIA_TYPE_AUDIO)
        return false;

    do {
        if(mPacket.size == 0 && !getNextPacket())
            return false;

        /* Decode some data, and check for errors */
        if((len=avcodec_decode_audio4((*mStream)->codec, mFrame, &got_frame, &mPacket)) < 0)
            return false;

        /* Move the unread data to the front and clear the end bits */
        int remaining = mPacket.size - len;
        if(remaining <= 0)
            av_free_packet(&mPacket);
        else
        {
            memmove(mPacket.data, &mPacket.data[len], remaining);
            av_shrink_packet(&mPacket, remaining);
        }
    } while(got_frame == 0 || mFrame->nb_samples == 0);
    mNextPts += (double)mFrame->nb_samples / (double)(*mStream)->codec->sample_rate;

    return true;
}

size_t FFmpeg_Decoder::readAVAudioData(void *data, size_t length)
{
    size_t dec = 0;

    while(dec < length)
    {
        /* If there's no decoded data, find some */
        if(mFramePos >= mFrameSize)
        {
            if(!getAVAudioData())
                break;
            mFramePos = 0;
            mFrameSize = mFrame->nb_samples * (*mStream)->codec->channels *
                         av_get_bytes_per_sample((*mStream)->codec->sample_fmt);
        }

        /* Get the amount of bytes remaining to be written, and clamp to
         * the amount of decoded data we have */
        size_t rem = std::min<size_t>(length-dec, mFrameSize-mFramePos);

        /* Copy the data to the app's buffer and increment */
        memcpy(data, mFrame->data[0]+mFramePos, rem);
        data = (char*)data + rem;
        dec += rem;
        mFramePos += rem;
    }

    /* Return the number of bytes we were able to get */
    return dec;
}

static AVSampleFormat ffmpegNonPlanarSampleFormat (AVSampleFormat format)
{
    switch (format)
    {
    case AV_SAMPLE_FMT_U8P:  return AV_SAMPLE_FMT_U8;
    case AV_SAMPLE_FMT_S16P: return AV_SAMPLE_FMT_S16;
    case AV_SAMPLE_FMT_S32P: return AV_SAMPLE_FMT_S32;
    case AV_SAMPLE_FMT_FLTP: return AV_SAMPLE_FMT_FLT;
    case AV_SAMPLE_FMT_DBLP: return AV_SAMPLE_FMT_DBL;
    default:return format;
    }
}

void FFmpeg_Decoder::open(const std::string &fname)
{
    close();
    mDataStream = mResourceMgr.openResource(fname);

    if((mFormatCtx=avformat_alloc_context()) == NULL)
        fail("Failed to allocate context");

    mFormatCtx->pb = avio_alloc_context(NULL, 0, 0, this, readPacket, writePacket, seek);
    if(!mFormatCtx->pb || avformat_open_input(&mFormatCtx, fname.c_str(), NULL, NULL) != 0)
    {
        if (mFormatCtx->pb != NULL)
        {
          if (mFormatCtx->pb->buffer != NULL)
          {
            av_free(mFormatCtx->pb->buffer);
            mFormatCtx->pb->buffer = NULL;
          }
          av_free(mFormatCtx->pb);
          mFormatCtx->pb = NULL;
        }
        avformat_free_context(mFormatCtx);
        mFormatCtx = NULL;
        fail("Failed to allocate input stream");
    }

    try
    {
        if(avformat_find_stream_info(mFormatCtx, NULL) < 0)
            fail("Failed to find stream info in "+fname);

        for(size_t j = 0;j < mFormatCtx->nb_streams;j++)
        {
            if(mFormatCtx->streams[j]->codec->codec_type == AVMEDIA_TYPE_AUDIO)
            {
                mStream = &mFormatCtx->streams[j];
                break;
            }
        }
        if(!mStream)
            fail("No audio streams in "+fname);

        (*mStream)->codec->request_sample_fmt = ffmpegNonPlanarSampleFormat ((*mStream)->codec->sample_fmt);

        AVCodec *codec = avcodec_find_decoder((*mStream)->codec->codec_id);
        if(!codec)
        {
            std::stringstream ss("No codec found for id ");
            ss << (*mStream)->codec->codec_id;
            fail(ss.str());
        }
        if(avcodec_open2((*mStream)->codec, codec, NULL) < 0)
            fail("Failed to open audio codec " + std::string(codec->long_name));

        mFrame = avcodec_alloc_frame();
    }
    catch(std::exception &e)
    {
        if (mFormatCtx->pb->buffer != NULL)
        {
          av_free(mFormatCtx->pb->buffer);
          mFormatCtx->pb->buffer = NULL;
        }
        av_free(mFormatCtx->pb);
        mFormatCtx->pb = NULL;

        avformat_close_input(&mFormatCtx);
        throw;
    }
}

void FFmpeg_Decoder::close()
{
    if(mStream)
        avcodec_close((*mStream)->codec);
    mStream = NULL;

    av_free_packet(&mPacket);
    av_freep(&mFrame);

    if(mFormatCtx)
    {
        if (mFormatCtx->pb != NULL)
        {
          // mFormatCtx->pb->buffer must be freed by hand,
          // if not, valgrind will show memleak, see:
          //
          // https://trac.ffmpeg.org/ticket/1357
          //
          if (mFormatCtx->pb->buffer != NULL)
          {
            av_free(mFormatCtx->pb->buffer);
            mFormatCtx->pb->buffer = NULL;
          }
          av_free(mFormatCtx->pb);
          mFormatCtx->pb = NULL;
        }
        avformat_close_input(&mFormatCtx);
    }

    mDataStream.setNull();
}

std::string FFmpeg_Decoder::getName()
{
    return mFormatCtx->filename;
}

void FFmpeg_Decoder::getInfo(int *samplerate, ChannelConfig *chans, SampleType *type)
{
    if(!mStream)
        fail("No audio stream info");

    if((*mStream)->codec->sample_fmt == AV_SAMPLE_FMT_U8)
        *type = SampleType_UInt8;
    else if((*mStream)->codec->sample_fmt == AV_SAMPLE_FMT_S16)
        *type = SampleType_Int16;
    else if((*mStream)->codec->sample_fmt == AV_SAMPLE_FMT_FLT)
        *type = SampleType_Float32;
    else
        fail(std::string("Unsupported sample format: ")+
             av_get_sample_fmt_name((*mStream)->codec->sample_fmt));

    if((*mStream)->codec->channel_layout == AV_CH_LAYOUT_MONO)
        *chans = ChannelConfig_Mono;
    else if((*mStream)->codec->channel_layout == AV_CH_LAYOUT_STEREO)
        *chans = ChannelConfig_Stereo;
    else if((*mStream)->codec->channel_layout == AV_CH_LAYOUT_QUAD)
        *chans = ChannelConfig_Quad;
    else if((*mStream)->codec->channel_layout == AV_CH_LAYOUT_5POINT1)
        *chans = ChannelConfig_5point1;
    else if((*mStream)->codec->channel_layout == AV_CH_LAYOUT_7POINT1)
        *chans = ChannelConfig_7point1;
    else if((*mStream)->codec->channel_layout == 0)
    {
        /* Unknown channel layout. Try to guess. */
        if((*mStream)->codec->channels == 1)
            *chans = ChannelConfig_Mono;
        else if((*mStream)->codec->channels == 2)
            *chans = ChannelConfig_Stereo;
        else
        {
            std::stringstream sstr("Unsupported raw channel count: ");
            sstr << (*mStream)->codec->channels;
            fail(sstr.str());
        }
    }
    else
    {
        char str[1024];
        av_get_channel_layout_string(str, sizeof(str), (*mStream)->codec->channels,
                                     (*mStream)->codec->channel_layout);
        fail(std::string("Unsupported channel layout: ")+str);
    }

    *samplerate = (*mStream)->codec->sample_rate;
}

size_t FFmpeg_Decoder::read(char *buffer, size_t bytes)
{
    if(!mStream)
        fail("No audio stream");
    return readAVAudioData(buffer, bytes);
}

void FFmpeg_Decoder::readAll(std::vector<char> &output)
{
    if(!mStream)
        fail("No audio stream");

    while(getAVAudioData())
    {
        size_t got = mFrame->nb_samples * (*mStream)->codec->channels *
                     av_get_bytes_per_sample((*mStream)->codec->sample_fmt);
        const char *inbuf = reinterpret_cast<char*>(mFrame->data[0]);
        output.insert(output.end(), inbuf, inbuf+got);
    }
}

void FFmpeg_Decoder::rewind()
{
    int stream_idx = mStream - mFormatCtx->streams;
    if(av_seek_frame(mFormatCtx, stream_idx, 0, 0) < 0)
        fail("Failed to seek in audio stream");
    av_free_packet(&mPacket);
    mFrameSize = mFramePos = 0;
    mNextPts = 0.0;
}

size_t FFmpeg_Decoder::getSampleOffset()
{
    int delay = (mFrameSize-mFramePos) / (*mStream)->codec->channels /
                av_get_bytes_per_sample((*mStream)->codec->sample_fmt);
    return (int)(mNextPts*(*mStream)->codec->sample_rate) - delay;
}

FFmpeg_Decoder::FFmpeg_Decoder()
  : mFormatCtx(NULL)
  , mStream(NULL)
  , mFrame(NULL)
  , mFrameSize(0)
  , mFramePos(0)
  , mNextPts(0.0)
{
    memset(&mPacket, 0, sizeof(mPacket));

    /* We need to make sure ffmpeg is initialized. Optionally silence warning
     * output from the lib */
    static bool done_init = false;
    if(!done_init)
    {
        av_register_all();
        av_log_set_level(AV_LOG_ERROR);
        done_init = true;
    }
}

FFmpeg_Decoder::~FFmpeg_Decoder()
{
    close();
}

}

#endif
