/***

  Olive - Non-Linear Video Editor
  Copyright (C) 2019 Olive Team

  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 3 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, see <http://www.gnu.org/licenses/>.

***/

#include "ffmpegdecoder.h"

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
#include <libavutil/pixdesc.h>
}

#include <QDebug>
#include <QFile>
#include <QFileInfo>
#include <QString>
#include <QtMath>

#include "codec/waveinput.h"
#include "common/define.h"
#include "common/filefunctions.h"
#include "common/timecodefunctions.h"
#include "ffmpegcommon.h"
#include "render/diskmanager.h"
#include "render/pixelservice.h"

FFmpegDecoder::FFmpegDecoder() :
  fmt_ctx_(nullptr),
  codec_ctx_(nullptr),
  scale_ctx_(nullptr),
  pkt_(nullptr),
  frame_(nullptr),
  opts_(nullptr)
{
}

FFmpegDecoder::~FFmpegDecoder()
{
  Close();
}

bool FFmpegDecoder::Open()
{
  if (open_) {
    return true;
  }

  if (!stream()) {
    Error(QStringLiteral("Tried to open a decoder with no footage stream set"));
    return false;
  }

  int error_code;

  // Convert QString to a C string
  QByteArray ba = stream()->footage()->filename().toUtf8();
  const char* filename = ba.constData();

  // Open file in a format context
  error_code = avformat_open_input(&fmt_ctx_, filename, nullptr, nullptr);

  // Handle format context error
  if (error_code != 0) {
    FFmpegError(error_code);
    return false;
  }

  // Get stream information from format
  error_code = avformat_find_stream_info(fmt_ctx_, nullptr);

  // Handle get stream information error
  if (error_code < 0) {
    FFmpegError(error_code);
    return false;
  }

  // Dump format information
  av_dump_format(fmt_ctx_, stream()->index(), filename, 0);

  // Get reference to correct AVStream
  avstream_ = fmt_ctx_->streams[stream()->index()];

  // Find decoder
  AVCodec* codec = avcodec_find_decoder(avstream_->codecpar->codec_id);

  // Handle failure to find decoder
  if (codec == nullptr) {
    Error(QStringLiteral("Failed to find appropriate decoder for this codec (%1:%2 - %3)")
          .arg(stream()->footage()->filename(),
               QString::number(avstream_->index),
               QString::number(avstream_->codecpar->codec_id)));
    return false;
  }

  // Allocate context for the decoder
  codec_ctx_ = avcodec_alloc_context3(codec);
  if (codec_ctx_ == nullptr) {
    Error(QStringLiteral("Failed to allocate codec context (%1 :: %2)").arg(stream()->footage()->filename(), stream()->index()));
    return false;
  }

  // Copy parameters from the AVStream to the AVCodecContext
  error_code = avcodec_parameters_to_context(codec_ctx_, avstream_->codecpar);

  // Handle failure to copy parameters
  if (error_code < 0) {
    FFmpegError(error_code);
    return false;
  }

  // enable multithreading on decoding
  error_code = av_dict_set(&opts_, "threads", "auto", 0);

  // Handle failure to set multithreaded decoding
  if (error_code < 0) {
    FFmpegError(error_code);
    return false;
  }

  // Open codec
  error_code = avcodec_open2(codec_ctx_, codec, &opts_);
  if (error_code < 0) {
    FFmpegError(error_code);
    return false;
  }

  if (avstream_->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
    // Get an Olive compatible AVPixelFormat
    ideal_pix_fmt_ = FFmpegCommon::GetCompatiblePixelFormat(static_cast<AVPixelFormat>(avstream_->codecpar->format));

    // Determine which Olive native pixel format we retrieved
    // Note that FFmpeg doesn't support float formats
    if (ideal_pix_fmt_ == AV_PIX_FMT_RGBA) {
      native_pix_fmt_ = PixelFormat::PIX_FMT_RGBA8;
    } else if (ideal_pix_fmt_ == AV_PIX_FMT_RGBA64) {
      native_pix_fmt_ = PixelFormat::PIX_FMT_RGBA16U;
    } else {
      // We should never get here, but just in case...
      qFatal("Invalid output format");
    }

    scale_ctx_ = sws_getContext(avstream_->codecpar->width,
                                avstream_->codecpar->height,
                                static_cast<AVPixelFormat>(avstream_->codecpar->format),
                                avstream_->codecpar->width,
                                avstream_->codecpar->height,
                                ideal_pix_fmt_,
                                0,
                                nullptr,
                                nullptr,
                                nullptr);

    if (!scale_ctx_) {
      Error(QStringLiteral("Failed to allocate SwsContext"));
      return false;
    }
  }

  pkt_ = av_packet_alloc();
  if (!pkt_) {
    Error(QStringLiteral("Failed to allocate AVPacket"));
    return false;
  }

  frame_ = av_frame_alloc();
  if (!frame_) {
    Error(QStringLiteral("Failed to allocate AVFrame"));
    return false;
  }

  // All allocation succeeded so we set the state to open
  open_ = true;

  return true;
}

FramePtr FFmpegDecoder::RetrieveVideo(const rational &timecode)
{
  if (!open_ && !Open()) {
    return nullptr;
  }

  if (avstream_->codecpar->codec_type != AVMEDIA_TYPE_VIDEO) {
    return nullptr;
  }

  // Convert timecode to AVStream timebase
  int64_t target_ts = GetTimestampFromTime(timecode);

  if (target_ts < 0) {
    Error(QStringLiteral("Index failed to produce a valid timestamp"));
    return nullptr;
  }

  // Allocate frame that we'll return
  FramePtr frame_container = Frame::Create();
  frame_container->set_width(avstream_->codecpar->width);
  frame_container->set_height(avstream_->codecpar->height);
  frame_container->set_format(native_pix_fmt_);
  frame_container->set_timestamp(Timecode::timestamp_to_time(target_ts, avstream_->time_base));
  frame_container->set_sample_aspect_ratio(av_guess_sample_aspect_ratio(fmt_ctx_, avstream_, nullptr));
  frame_container->allocate();

  bool got_frame = (frame_->pts == target_ts);

  QByteArray frame_loader;
  uint8_t* input_data[4];
  int input_linesize[4];

  // If we already have the frame, we'll need to set the pointers to it
  for (int i=0;i<4;i++) {
    input_data[i] = frame_->data[i];
    input_linesize[i] = frame_->linesize[i];
  }

  // See if we stored this frame in the disk cache
  if (!got_frame) {
    QFile compressed_frame(GetIndexFilename().append(QString::number(target_ts)));
    if (compressed_frame.exists()
        && compressed_frame.size() > 0
        && compressed_frame.open(QFile::ReadOnly)) {
      DiskManager::instance()->Accessed(compressed_frame.fileName());

      // Read data
      frame_loader = qUncompress(compressed_frame.readAll());
      //frame_loader = compressed_frame.readAll();

      av_image_fill_arrays(input_data,
                           input_linesize,
                           reinterpret_cast<uint8_t*>(frame_loader.data()),
                           static_cast<AVPixelFormat>(avstream_->codecpar->format),
                           avstream_->codecpar->width,
                           avstream_->codecpar->height,
                           1);

      got_frame = true;
    }
  }

  // If we have no disk cache, we'll need to find this frame ourselves
  if (!got_frame) {
    int64_t second_ts = qRound64(av_q2d(av_inv_q(avstream_->time_base)));

    if (frame_->pts < target_ts - 2*second_ts || frame_->pts > target_ts) {
      Seek(target_ts);
    }

    int64_t seek_ts = target_ts;

    int ret;

    while (true) {
      ret = GetFrame(pkt_, frame_);

      if (ret < 0) {
        FFmpegError(ret);
        break;
      }

      if (frame_->pts > target_ts) {
        // Seek failed, try again
        seek_ts -= second_ts;
        Seek(seek_ts);
        continue;
      }

      if (frame_->pts == target_ts) {
        // We found the frame we want
        got_frame = true;

        // Set data arrays to the frame's data
        for (int i=0;i<4;i++) {
          input_data[i] = frame_->data[i];
          input_linesize[i] = frame_->linesize[i];
        }

        QFile save_frame(GetIndexFilename().append(QString::number(frame_->pts)));
        if (save_frame.open(QFile::WriteOnly)) {

          // Save frame to media index
          int cached_buffer_sz = av_image_get_buffer_size(static_cast<AVPixelFormat>(frame_->format),
                                                          frame_->width,
                                                          frame_->height,
                                                          1);

          QByteArray cached_frame(cached_buffer_sz, Qt::Uninitialized);

          av_image_copy_to_buffer(reinterpret_cast<uint8_t*>(cached_frame.data()),
                                  cached_frame.size(),
                                  frame_->data,
                                  frame_->linesize,
                                  static_cast<AVPixelFormat>(frame_->format),
                                  frame_->width,
                                  frame_->height,
                                  1);

          save_frame.write(qCompress(cached_frame, 1));
          save_frame.close();

          DiskManager::instance()->CreatedFile(save_frame.fileName(), QByteArray());
        }
        break;
      }
    }
  }

  // If we're here and got the frame, we'll convert it and return it
  if (got_frame) {
    // Convert frame to RGBA for the rest of the pipeline
    uint8_t* output_data = reinterpret_cast<uint8_t*>(frame_container->data());
    int output_linesize = frame_container->width() * kRGBAChannels * PixelService::BytesPerChannel(native_pix_fmt_);

    sws_scale(scale_ctx_,
              input_data,
              input_linesize,
              0,
              avstream_->codecpar->height,
              &output_data,
              &output_linesize);

    return frame_container;
  }

  return nullptr;
}

FramePtr FFmpegDecoder::RetrieveAudio(const rational &timecode, const rational &length, const AudioRenderingParams &params)
{
  if (!open_ && !Open()) {
    return nullptr;
  }

  if (avstream_->codecpar->codec_type != AVMEDIA_TYPE_AUDIO) {
    return nullptr;
  }

  Index();

  Conform(params);

  WaveInput input(GetConformedFilename(params));

  if (input.open()) {
    const AudioRenderingParams& input_params = input.params();

    FramePtr audio_frame = Frame::Create();
    audio_frame->set_audio_params(input_params);
    audio_frame->set_sample_count(input_params.time_to_samples(length));
    audio_frame->allocate();

    input.read(input_params.time_to_bytes(timecode),
               audio_frame->data(),
               audio_frame->allocated_size());

    input.close();

    return audio_frame;
  }

  return nullptr;
}

void FFmpegDecoder::Close()
{
  frame_index_.clear();

  if (opts_) {
    av_dict_free(&opts_);
    opts_ = nullptr;
  }

  if (frame_) {
    av_frame_free(&frame_);
    frame_ = nullptr;
  }

  if (pkt_) {
    av_packet_free(&pkt_);
    pkt_ = nullptr;
  }

  if (scale_ctx_) {
    sws_freeContext(scale_ctx_);
    scale_ctx_ = nullptr;
  }

  if (codec_ctx_) {
    avcodec_free_context(&codec_ctx_);
    codec_ctx_ = nullptr;
  }

  if (fmt_ctx_) {
    avformat_close_input(&fmt_ctx_);
    fmt_ctx_ = nullptr;
  }

  open_ = false;
}

QString FFmpegDecoder::id()
{
  return "ffmpeg";
}

int64_t FFmpegDecoder::GetTimestampFromTime(const rational &time)
{
  if (!open_ && !Open()) {
    return -1;
  }

  // Get rough approximation of what the timestamp would be in this timebase
  int64_t target_ts = Timecode::time_to_timestamp(time, avstream_->time_base);

  // Adjust target by stream's start time
  target_ts += avstream_->start_time;

  // Find closest actual timebase in the file
  target_ts = GetClosestTimestampInIndex(target_ts);

  return target_ts;
}

void FFmpegDecoder::Conform(const AudioRenderingParams &params)
{
  if (avstream_->codecpar->codec_type != AVMEDIA_TYPE_AUDIO) {
    // Nothing to be done
    return;
  }

  Index();

  // Get indexed WAV file
  WaveInput input(GetIndexFilename());

  if (input.open()) {
    // If the parameters are equal, nothing to be done
    // FIXME: Technically we only need to conform if the SAMPLE RATE is not equal. Format and channel layout conversion
    //        could be done on the fly so we could perhaps conform less often at some point.
    if (input.params() == params) {
      input.close();
      return;
    }

    // Otherwise, let's start converting the format

    // Generate destination filename for this conversion to see if it exists
    QString conformed_fn = GetConformedFilename(params);

    if (QFileInfo::exists(conformed_fn)) {
      // We must have already conformed this format
      input.close();
      return;
    }

    // Set up resampler
    SwrContext* resampler = swr_alloc_set_opts(nullptr,
                                               static_cast<int64_t>(params.channel_layout()),
                                               FFmpegCommon::GetFFmpegSampleFormat(params.format()),
                                               params.sample_rate(),
                                               static_cast<int64_t>(input.params().channel_layout()),
                                               FFmpegCommon::GetFFmpegSampleFormat(input.params().format()),
                                               input.params().sample_rate(),
                                               0,
                                               nullptr);

    swr_init(resampler);

    WaveOutput conformed_output(conformed_fn, params);
    if (!conformed_output.open()) {
      qWarning() << "Failed to open conformed output:" << conformed_fn;
      input.close();
      return;
    }

    // Convert one second of audio at a time
    int input_buffer_sz = input.params().time_to_bytes(1);

    while (!input.at_end()) {
      // Read up to one second of audio from WAV file
      QByteArray read_samples = input.read(input_buffer_sz);

      // Determine how many samples this is
      int in_sample_count = input.params().bytes_to_samples(read_samples.size());

      ConformInternal(resampler, &conformed_output, read_samples.data(), in_sample_count);
    }

    // Flush resampler
    ConformInternal(resampler, &conformed_output, nullptr, 0);

    // Clean up
    swr_free(&resampler);
    conformed_output.close();
    input.close();
  } else {
    qWarning() << "Failed to conform file:" << stream()->footage()->filename();
  }
}

bool FFmpegDecoder::SupportsVideo()
{
  return true;
}

bool FFmpegDecoder::SupportsAudio()
{
  return true;
}

void FFmpegDecoder::ConformInternal(SwrContext* resampler, WaveOutput* output, const char* in_data, int in_sample_count)
{
  // Determine how many samples the output will be
  int out_sample_count = swr_get_out_samples(resampler, in_sample_count);

  // Allocate array for the amount of samples we'll need
  QByteArray out_samples;
  out_samples.resize(output->params().samples_to_bytes(out_sample_count));

  char* out_data = out_samples.data();

  // Convert samples
  int convert_count = swr_convert(resampler,
                                  reinterpret_cast<uint8_t**>(&out_data),
                                  out_sample_count,
                                  reinterpret_cast<const uint8_t**>(&in_data),
                                  in_sample_count);

  if (convert_count != out_sample_count) {
    out_samples.resize(output->params().samples_to_bytes(convert_count));
  }

  output->write(out_samples);
}

bool FFmpegDecoder::Probe(Footage *f)
{
  if (open_) {
    qWarning() << "Probe must be called while the Decoder is closed";
    return false;
  }

  // Variable for receiving errors from FFmpeg
  int error_code;

  // Result to return
  bool result = false;

  // Convert QString to a C strng
  QByteArray ba = f->filename().toUtf8();
  const char* filename = ba.constData();

  // Open file in a format context
  error_code = avformat_open_input(&fmt_ctx_, filename, nullptr, nullptr);

  bool need_manual_duration = false;

  // Handle format context error
  if (error_code == 0) {

    // Retrieve metadata about the media
    av_dump_format(fmt_ctx_, 0, filename, 0);

    // Dump it into the Footage object
    for (unsigned int i=0;i<fmt_ctx_->nb_streams;i++) {

      avstream_ = fmt_ctx_->streams[i];

      StreamPtr str;

      if (avstream_->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {

        // Create a video stream object
        VideoStreamPtr video_stream = std::make_shared<VideoStream>();

        video_stream->set_width(avstream_->codecpar->width);
        video_stream->set_height(avstream_->codecpar->height);
        video_stream->set_frame_rate(av_guess_frame_rate(fmt_ctx_, avstream_, nullptr));

        str = video_stream;

      } else if (avstream_->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {

        // Create an audio stream object
        AudioStreamPtr audio_stream = std::make_shared<AudioStream>();

        uint64_t channel_layout = avstream_->codecpar->channel_layout;
        if (!channel_layout) {
          channel_layout = static_cast<uint64_t>(av_get_default_channel_layout(avstream_->codecpar->channels));
        }

        audio_stream->set_channel_layout(channel_layout);
        audio_stream->set_channels(avstream_->codecpar->channels);
        audio_stream->set_sample_rate(avstream_->codecpar->sample_rate);

        str = audio_stream;

      } else {

        // This is data we can't utilize at the moment, but we make a Stream object anyway to keep parity with the file
        str = std::make_shared<Stream>();

        // Set the correct codec type based on FFmpeg's result
        switch (avstream_->codecpar->codec_type) {
        case AVMEDIA_TYPE_UNKNOWN:
          str->set_type(Stream::kUnknown);
          break;
        case AVMEDIA_TYPE_DATA:
          str->set_type(Stream::kData);
          break;
        case AVMEDIA_TYPE_SUBTITLE:
          str->set_type(Stream::kSubtitle);
          break;
        case AVMEDIA_TYPE_ATTACHMENT:
          str->set_type(Stream::kAttachment);
          break;
        default:
          // We should never realistically get here, but we make an "invalid" stream just in case
          str->set_type(Stream::kUnknown);
          break;
        }

      }

      str->set_index(avstream_->index);
      str->set_timebase(avstream_->time_base);
      str->set_duration(avstream_->duration);

      // The container/stream info may not contain a duration, so we'll need to manually retrieve it
      if (avstream_->duration == AV_NOPTS_VALUE) {
        need_manual_duration = true;
      }

      f->add_stream(str);
    }

    // As long as we can open the container and retrieve information, this was a successful probe
    result = true;
  }

  // Free all memory
  Close();

  // If the metadata did not contain a duration, we'll need to loop through the file to retrieve it
  if (need_manual_duration) {
    // Index the first stream to retrieve the duration

    set_stream(f->stream(0));

    Open();

    // Use index to find duration
    Index();

    // Use last frame index as the duration
    // FIXME: Does this skip the last frame?
    int64_t duration = frame_index_.last();

    f->stream(0)->set_duration(duration);

    // Assume all durations are the same and set for each
    for (int i=1;i<f->stream_count();i++) {
      int64_t new_dur = av_rescale_q(duration,
                                     f->stream(0)->timebase().toAVRational(),
                                     f->stream(i)->timebase().toAVRational());

      f->stream(i)->set_duration(new_dur);
    }

    Close();
  }

  return result;
}

void FFmpegDecoder::FFmpegError(int error_code)
{
  char err[1024];
  av_strerror(error_code, err, 1024);

  Error(QStringLiteral("Error decoding %1 - %2 %3").arg(stream()->footage()->filename(),
                                                        QString::number(error_code),
                                                        err));
}

void FFmpegDecoder::Error(const QString &s)
{
  qWarning() << s;

  Close();
}

void FFmpegDecoder::Index()
{
  if (!open_) {
    qWarning() << "Indexing function tried to run while decoder was closed";
    return;
  }

  stream()->index_lock_.lock();

  if (!LoadIndex()) {

    // Reset state
    Seek(0);

    if (avstream_->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
      IndexVideo(pkt_, frame_);
    } else if (avstream_->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
      IndexAudio(pkt_, frame_);
    }

    // Reset state
    Seek(0);

  }

  stream()->index_lock_.unlock();
}

QString FFmpegDecoder::GetIndexFilename()
{
  if (!open_) {
    qWarning() << "GetIndexFilename tried to run while decoder was closed";
    return QString();
  }

  return GetMediaIndexFilename(GetUniqueFileIdentifier(stream()->footage()->filename()))
      .append(QString::number(avstream_->index));
}

QString FFmpegDecoder::GetConformedFilename(const AudioRenderingParams &params)
{
  QString index_fn = GetIndexFilename();
  WaveInput input(GetIndexFilename());

  if (input.open()) {
    // If the parameters are equal, nothing to be done
    AudioRenderingParams index_params = input.params();
    input.close();

    if (index_params == params) {
      // Source file matches perfectly, no conform required
      return index_fn;
    }
  }

  index_fn.append('.');
  index_fn.append(QString::number(params.sample_rate()));
  index_fn.append('.');
  index_fn.append(QString::number(params.format()));
  index_fn.append('.');
  index_fn.append(QString::number(params.channel_layout()));

  return index_fn;
}

bool FFmpegDecoder::LoadIndex()
{
  switch (avstream_->codecpar->codec_type) {
  case AVMEDIA_TYPE_VIDEO:
  {
    // Load index from file
    QFile index_file(GetIndexFilename());

    if (!index_file.exists()) {
      return false;
    }

    if (index_file.open(QFile::ReadOnly)) {
      // Resize based on filesize
      frame_index_.resize(static_cast<int>(static_cast<size_t>(index_file.size()) / sizeof(int64_t)));

      // Read frame index into vector
      index_file.read(reinterpret_cast<char*>(frame_index_.data()),
                      index_file.size());

      index_file.close();

      return true;
    }
    break;
  }
  case AVMEDIA_TYPE_AUDIO:
  {
    return QFileInfo::exists(GetIndexFilename());
  }
  default:
    break;
  }

  return false;
}

void FFmpegDecoder::SaveIndex()
{
  // Save index to file
  QFile index_file(GetIndexFilename());
  if (index_file.open(QFile::WriteOnly)) {
    // Write index in binary
    index_file.write(reinterpret_cast<const char*>(frame_index_.constData()),
                     frame_index_.size() * static_cast<int>(sizeof(int64_t)));

    index_file.close();
  } else {
    qWarning() << QStringLiteral("Failed to save index for %1").arg(stream()->footage()->filename());
  }
}

void FFmpegDecoder::IndexAudio(AVPacket *pkt, AVFrame *frame)
{
  // Iterate through each audio frame and extract the PCM data

  uint64_t channel_layout = avstream_->codecpar->channel_layout;
  if (!channel_layout) {
    if (!avstream_->codecpar->channels) {
      // No channel data - we can't do anything with this
      return;
    }

    channel_layout = static_cast<uint64_t>(av_get_default_channel_layout(avstream_->codecpar->channels));
  }

  SwrContext* resampler = nullptr;
  AVSampleFormat src_sample_fmt = static_cast<AVSampleFormat>(avstream_->codecpar->format);
  AVSampleFormat dst_sample_fmt;

  // We don't use planar types internally, so if this is a planar format convert it now
  if (av_sample_fmt_is_planar(src_sample_fmt)) {
    dst_sample_fmt = av_get_packed_sample_fmt(src_sample_fmt);

    resampler = swr_alloc_set_opts(nullptr,
                                   static_cast<int64_t>(avstream_->codecpar->channel_layout),
                                   dst_sample_fmt,
                                   avstream_->codecpar->sample_rate,
                                   static_cast<int64_t>(avstream_->codecpar->channel_layout),
                                   src_sample_fmt,
                                   avstream_->codecpar->sample_rate,
                                   0,
                                   nullptr);
  } else {
    dst_sample_fmt = src_sample_fmt;
  }

  WaveOutput wave_out(GetIndexFilename(),
                      AudioRenderingParams(avstream_->codecpar->sample_rate,
                                           channel_layout,
                                           FFmpegCommon::GetNativeSampleFormat(dst_sample_fmt)));

  int ret;

  if (wave_out.open()) {
    while (true) {
      ret = GetFrame(pkt, frame);

      if (ret < 0) {
        break;
      } else {
        AVFrame* data_frame;

        if (resampler != nullptr) {
          // We must need to resample this (mainly just convert from planar to packed if necessary)
          data_frame = av_frame_alloc();
          data_frame->sample_rate = frame->sample_rate;
          data_frame->channel_layout = frame->channel_layout;
          data_frame->channels = frame->channels;
          data_frame->format = dst_sample_fmt;
          av_frame_make_writable(data_frame);

          ret = swr_convert_frame(resampler, data_frame, frame);

          if (ret != 0) {
            char err_str[50];
            av_strerror(ret, err_str, 50);
            qWarning() << "libswresample failed with error:" << ret << err_str;
          }
        } else {
          // No resampling required, we can write directly from te frame buffer
          data_frame = frame;
        }

        int buffer_sz = av_samples_get_buffer_size(nullptr,
                                                   avstream_->codecpar->channels,
                                                   data_frame->nb_samples,
                                                   dst_sample_fmt,
                                                   0); // FIXME: Documentation unclear - should this be 0 or 1?

        // Write packed WAV data to the disk cache
        wave_out.write(reinterpret_cast<char*>(data_frame->data[0]), buffer_sz);

        // If we allocated an output for the resampler, delete it here
        if (data_frame != frame) {
          av_frame_free(&data_frame);
        }
      }
    }

    wave_out.close();
  } else {
    qWarning() << "Failed to open WAVE output for indexing";
  }

  if (resampler != nullptr) {
    swr_free(&resampler);
  }
}

void FFmpegDecoder::IndexVideo(AVPacket* pkt, AVFrame* frame)
{
  // This should be unnecessary, but just in case...
  frame_index_.clear();

  // Iterate through every single frame and get each timestamp
  // NOTE: Expects no frames to have been read so far

  int ret;

  while (true) {
    ret = GetFrame(pkt, frame);

    if (ret >= 0) {
      frame_index_.append(frame->pts);
    } else {
      // Assume we've reached the end of the file
      break;
    }
  }

  // Save index to file
  SaveIndex();
}

int FFmpegDecoder::GetFrame(AVPacket *pkt, AVFrame *frame)
{
  bool eof = false;

  int ret;

  // Clear any previous frames
  av_frame_unref(frame);

  while ((ret = avcodec_receive_frame(codec_ctx_, frame)) == AVERROR(EAGAIN) && !eof) {

    // Find next packet in the correct stream index
    do {
      // Free buffer in packet if there is one
      av_packet_unref(pkt);

      // Read packet from file
      ret = av_read_frame(fmt_ctx_, pkt);
    } while (pkt->stream_index != avstream_->index && ret >= 0);

    if (ret == AVERROR_EOF) {
      // Don't break so that receive gets called again, but don't try to read again
      eof = true;

      // Send a null packet to signal end of
      avcodec_send_packet(codec_ctx_, nullptr);
    } else if (ret < 0) {
      // Handle other error
      break;
    } else {
      // Successful read, send the packet
      ret = avcodec_send_packet(codec_ctx_, pkt);

      // We don't need the packet anymore, so free it
      av_packet_unref(pkt);

      if (ret < 0) {
        break;
      }
    }
  }

  return ret;
}

int64_t FFmpegDecoder::GetClosestTimestampInIndex(const int64_t &ts)
{
  // Index now if we haven't already
  if (frame_index_.isEmpty()) {
    Index();
  }

  if (frame_index_.isEmpty()) {
    return -1;
  }

  if (ts <= 0) {
    return frame_index_.first();
  }

  // Use index to find closest frame in file
  for (int i=1;i<frame_index_.size();i++) {
    int64_t this_ts = frame_index_.at(i);

    if (this_ts == ts) {
      return ts;
    } else if (this_ts > ts) {
      return frame_index_.at(i - 1);
    }
  }

  return frame_index_.last();
}

void FFmpegDecoder::Seek(int64_t timestamp)
{
  avcodec_flush_buffers(codec_ctx_);
  av_seek_frame(fmt_ctx_, avstream_->index, timestamp, AVSEEK_FLAG_BACKWARD);
}
