#include "stream.h"
#include <c10/util/Logging.h>
#include <stdio.h>
#include <string.h>
#include "util.h"

namespace ffmpeg {
const AVRational timeBaseQ = AVRational{1, AV_TIME_BASE};

Stream::Stream(
    AVFormatContext* inputCtx,
    MediaFormat format,
    bool convertPtsToWallTime,
    int64_t loggingUuid)
    : inputCtx_(inputCtx),
      format_(format),
      convertPtsToWallTime_(convertPtsToWallTime),
      loggingUuid_(loggingUuid) {}

Stream::~Stream() {
  if (frame_) {
    av_free(frame_);
  }
  if (codecCtx_) {
    avcodec_free_context(&codecCtx_);
  }
}

// look up the proper CODEC querying the function
AVCodec* Stream::findCodec(AVCodecParameters* params) {
  return (AVCodec*)avcodec_find_decoder(params->codec_id);
}

// Allocate memory for the AVCodecContext, which will hold the context for
// decode/encode process. Then fill this codec context with CODEC parameters
// defined in stream parameters. Open the codec, and allocate the global frame
// defined in the header file
int Stream::openCodec(std::vector<DecoderMetadata>* metadata, int num_threads) {
  AVStream* steam = inputCtx_->streams[format_.stream];

  AVCodec* codec = findCodec(steam->codecpar);
  if (!codec) {
    LOG(ERROR) << "LoggingUuid #" << loggingUuid_
               << ", avcodec_find_decoder failed for codec_id: "
               << int(steam->codecpar->codec_id);
    return AVERROR(EINVAL);
  }

  if (!(codecCtx_ = avcodec_alloc_context3(codec))) {
    LOG(ERROR) << "LoggingUuid #" << loggingUuid_
               << ", avcodec_alloc_context3 failed";
    return AVERROR(ENOMEM);
  }
  // multithreading heuristics
  // if user defined,
  if (num_threads > max_threads) {
    num_threads = max_threads;
  }

  if (num_threads > 0) {
    // if user defined, respect that
    // note that default thread_type will be used
    codecCtx_->thread_count = num_threads;
  } else {
    // otherwise set sensible defaults
    codecCtx_->thread_count = 8;
    codecCtx_->thread_type = FF_THREAD_SLICE;
  }

  int ret;
  // Copy codec parameters from input stream to output codec context
  if ((ret = avcodec_parameters_to_context(codecCtx_, steam->codecpar)) < 0) {
    LOG(ERROR) << "LoggingUuid #" << loggingUuid_
               << ", avcodec_parameters_to_context failed";
    return ret;
  }

  // after avcodec_open2, value of codecCtx_->time_base is NOT meaningful
  if ((ret = avcodec_open2(codecCtx_, codec, nullptr)) < 0) {
    LOG(ERROR) << "LoggingUuid #" << loggingUuid_
               << ", avcodec_open2 failed: " << Util::generateErrorDesc(ret);
    avcodec_free_context(&codecCtx_);
    codecCtx_ = nullptr;
    return ret;
  }

  frame_ = av_frame_alloc();

  switch (format_.type) {
    case TYPE_VIDEO:
      fps_ = av_q2d(av_guess_frame_rate(inputCtx_, steam, nullptr));
      break;
    case TYPE_AUDIO:
      fps_ = codecCtx_->sample_rate;
      break;
    default:
      fps_ = 30.0;
  }

  if ((ret = initFormat())) {
    LOG(ERROR) << "initFormat failed, type: " << format_.type;
  }

  if (metadata) {
    DecoderMetadata header;
    header.format = format_;
    header.fps = fps_;
    header.num = steam->time_base.num;
    header.den = steam->time_base.den;
    header.duration =
        av_rescale_q(steam->duration, steam->time_base, timeBaseQ);
    metadata->push_back(header);
  }

  return ret;
}

// send the raw data packet (compressed frame) to the decoder, through the codec
// context and receive the raw data frame (uncompressed frame) from the
// decoder, through the same codec context
int Stream::analyzePacket(const AVPacket* packet, bool* gotFrame) {
  int consumed = 0;
  int result = avcodec_send_packet(codecCtx_, packet);
  if (result == AVERROR(EAGAIN)) {
    *gotFrame = false; // no bytes get consumed, fetch frame
  } else if (result == AVERROR_EOF) {
    *gotFrame = false; // more than one flush packet
    if (packet) {
      // got packet after flush, this is an error
      return result;
    }
  } else if (result < 0) {
    LOG(ERROR) << "avcodec_send_packet failed, err: "
               << Util::generateErrorDesc(result);
    return result; // error
  } else {
    consumed = packet ? packet->size : 0; // all bytes get consumed
  }

  result = avcodec_receive_frame(codecCtx_, frame_);

  if (result >= 0) {
    *gotFrame = true; // frame is available
  } else if (result == AVERROR(EAGAIN)) {
    *gotFrame = false; // no frames at this time, needs more packets
    if (!consumed) {
      // precaution, if no packages got consumed and no frames are available
      return result;
    }
  } else if (result == AVERROR_EOF) {
    *gotFrame = false; // the last frame has been flushed
    // precaution, if no more frames are available assume we consume all bytes
    consumed = 0;
  } else { // error
    LOG(ERROR) << "avcodec_receive_frame failed, err: "
               << Util::generateErrorDesc(result);
    return result;
  }
  return consumed;
}

// General decoding function:
// given the packet, analyse the metadata, and write the
// metadata and the buffer to the DecoderOutputImage.
int Stream::decodePacket(
    const AVPacket* packet,
    DecoderOutputMessage* out,
    bool headerOnly,
    bool* hasMsg) {
  int consumed;
  bool gotFrame = false;
  *hasMsg = false;
  if ((consumed = analyzePacket(packet, &gotFrame)) >= 0 &&
      (packet == nullptr || gotFrame)) {
    int result;
    if ((result = getMessage(out, !gotFrame, headerOnly)) < 0) {
      return result; // report error
    }
    *hasMsg = result > 0;
  }
  return consumed;
}

int Stream::flush(DecoderOutputMessage* out, bool headerOnly) {
  bool hasMsg = false;
  int result = decodePacket(nullptr, out, headerOnly, &hasMsg);
  if (result < 0) {
    avcodec_flush_buffers(codecCtx_);
    return result;
  }
  if (!hasMsg) {
    avcodec_flush_buffers(codecCtx_);
    return 0;
  }
  return 1;
}

// Sets the header and payload via stream::setHeader and copyFrameBytes
// functions that are defined in type stream subclass (VideoStream, AudioStream,
// ...)
int Stream::getMessage(DecoderOutputMessage* out, bool flush, bool headerOnly) {
  if (flush) {
    // only flush of audio frames makes sense
    if (format_.type == TYPE_AUDIO) {
      int processed = 0;
      size_t total = 0;
      // grab all audio bytes by chunks
      do {
        if ((processed = copyFrameBytes(out->payload.get(), flush)) < 0) {
          return processed;
        }
        total += processed;
      } while (processed);

      if (total) {
        // set header if message bytes are available
        setHeader(&out->header, flush);
        return 1;
      }
    }
    return 0;
  } else {
    if (format_.type == TYPE_AUDIO) {
      int processed = 0;
      if ((processed = copyFrameBytes(out->payload.get(), flush)) < 0) {
        return processed;
      }
      if (processed) {
        // set header if message bytes are available
        setHeader(&out->header, flush);
        return 1;
      }
      return 0;
    } else {
      // set header
      setHeader(&out->header, flush);

      if (headerOnly) {
        // Only header is requisted
        return 1;
      }

      return copyFrameBytes(out->payload.get(), flush);
    }
  }
}

void Stream::setHeader(DecoderHeader* header, bool flush) {
  header->seqno = numGenerator_++;

  setFramePts(header, flush);

  if (convertPtsToWallTime_) {
    keeper_.adjust(header->pts);
  }

  header->format = format_;
  header->keyFrame = 0;
  header->fps = std::numeric_limits<double>::quiet_NaN();
}

void Stream::setFramePts(DecoderHeader* header, bool flush) {
  if (flush) {
    header->pts = nextPts_; // already in us
  } else {
    header->pts = frame_->best_effort_timestamp;
    if (header->pts == AV_NOPTS_VALUE) {
      header->pts = nextPts_;
    } else {
      header->pts = av_rescale_q(
          header->pts,
          inputCtx_->streams[format_.stream]->time_base,
          timeBaseQ);
    }

    switch (format_.type) {
      case TYPE_AUDIO:
        nextPts_ = header->pts + frame_->nb_samples * AV_TIME_BASE / fps_;
        break;
      case TYPE_VIDEO:
        nextPts_ = header->pts + AV_TIME_BASE / fps_;
        break;
      default:
        nextPts_ = header->pts;
    }
  }
}

} // namespace ffmpeg
