// Created by: Kirill GAVRILOV
// Copyright (c) 2019 OPEN CASCADE SAS
//
// This file is part of Open CASCADE Technology software library.
//
// This library is free software; you can redistribute it and/or modify it under
// the terms of the GNU Lesser General Public License version 2.1 as published
// by the Free Software Foundation, with special exception defined in the file
// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
// distribution for complete text of the license and disclaimer of any warranty.
//
// Alternatively, this file may be used under the terms of Open CASCADE
// commercial license or contractual agreement.

// activate some C99 macros like UINT64_C in "stdint.h" which used by FFmpeg
#ifndef __STDC_CONSTANT_MACROS
  #define __STDC_CONSTANT_MACROS
#endif

#include <Media_FormatContext.hxx>

#include <Message.hxx>
#include <Message_Messenger.hxx>

#ifdef HAVE_FFMPEG
  #include <Standard_WarningsDisable.hxx>
extern "C"
{
  #include <libavformat/avformat.h>
};
  #include <Standard_WarningsRestore.hxx>
#endif

IMPLEMENT_STANDARD_RTTIEXT(Media_FormatContext, Standard_Transient)

namespace
{
static const double THE_SECONDS_IN_HOUR   = 3600.0;
static const double THE_SECONDS_IN_MINUTE = 60.0;
static const double THE_SECOND_IN_HOUR    = 1.0 / THE_SECONDS_IN_HOUR;
static const double THE_SECOND_IN_MINUTE  = 1.0 / THE_SECONDS_IN_MINUTE;

#ifdef HAVE_FFMPEG
static const AVRational ST_AV_TIME_BASE_Q = {1, AV_TIME_BASE};
static const double     ST_AV_TIME_BASE_D = av_q2d(ST_AV_TIME_BASE_Q);

//! Format framerate value.
static TCollection_AsciiString formatFps(double theVal)
{
  const uint64_t aVal = uint64_t(theVal * 100.0 + 0.5);
  char           aBuff[256];
  if (aVal == 0)
  {
    Sprintf(aBuff, "%1.4f", theVal);
  }
  else if (aVal % 100)
  {
    Sprintf(aBuff, "%3.2f", theVal);
  }
  else if (aVal % (100 * 1000))
  {
    Sprintf(aBuff, "%1.0f", theVal);
  }
  else
  {
    Sprintf(aBuff, "%1.0fk", theVal / 1000);
  }
  return aBuff;
}
#endif
} // namespace

//=================================================================================================

TCollection_AsciiString Media_FormatContext::FormatAVErrorDescription(int theErrCodeAV)
{
#ifdef HAVE_FFMPEG
  char aBuff[4096];
  memset(aBuff, 0, sizeof(aBuff));
  if (av_strerror(theErrCodeAV, aBuff, 4096) != -1)
  {
    return TCollection_AsciiString(aBuff);
  }

  #ifdef _MSC_VER
  wchar_t aBuffW[4096];
  memset(aBuffW, 0, sizeof(aBuffW));
  if (_wcserror_s(aBuffW, 4096, AVUNERROR(theErrCodeAV)) == 0)
  {
    return TCollection_AsciiString(aBuffW);
  }
  #elif defined(_WIN32)
  // MinGW has only thread-unsafe variant
  char* anErrDesc = strerror(AVUNERROR(theErrCodeAV));
  if (anErrDesc != NULL)
  {
    return TCollection_AsciiString(anErrDesc);
  }
  #endif
  return TCollection_AsciiString(aBuff);
#else
  return TCollection_AsciiString("AVError #") + theErrCodeAV;
#endif
}

//=================================================================================================

double Media_FormatContext::FormatUnitsToSeconds(int64_t theTimeUnits)
{
#ifdef HAVE_FFMPEG
  return (theTimeUnits != AV_NOPTS_VALUE) ? (ST_AV_TIME_BASE_D * theTimeUnits) : 0.0;
#else
  (void)theTimeUnits;
  return 0.0;
#endif
}

//=================================================================================================

double Media_FormatContext::UnitsToSeconds(const AVRational& theTimeBase, int64_t theTimeUnits)
{
#ifdef HAVE_FFMPEG
  return (theTimeUnits != AV_NOPTS_VALUE) ? (av_q2d(theTimeBase) * theTimeUnits) : 0.0;
#else
  (void)&theTimeBase;
  (void)theTimeUnits;
  return 0.0;
#endif
}

//=================================================================================================

double Media_FormatContext::StreamUnitsToSeconds(const AVStream& theStream, int64_t theTimeUnits)
{
#ifdef HAVE_FFMPEG
  return UnitsToSeconds(theStream.time_base, theTimeUnits);
#else
  (void)&theStream;
  (void)theTimeUnits;
  return 0.0;
#endif
}

//=================================================================================================

int64_t Media_FormatContext::SecondsToUnits(double theTimeSeconds)
{
#ifdef HAVE_FFMPEG
  return int64_t(theTimeSeconds / ST_AV_TIME_BASE_D);
#else
  (void)theTimeSeconds;
  return 0;
#endif
}

//=================================================================================================

int64_t Media_FormatContext::SecondsToUnits(const AVRational& theTimeBase, double theTimeSeconds)
{
#ifdef HAVE_FFMPEG
  return int64_t(theTimeSeconds / av_q2d(theTimeBase));
#else
  (void)&theTimeBase;
  (void)theTimeSeconds;
  return 0;
#endif
}

//=================================================================================================

int64_t Media_FormatContext::StreamSecondsToUnits(const AVStream& theStream, double theTimeSeconds)
{
#ifdef HAVE_FFMPEG
  return SecondsToUnits(theStream.time_base, theTimeSeconds);
#else
  (void)&theStream;
  (void)theTimeSeconds;
  return 0;
#endif
}

//=================================================================================================

Media_FormatContext::Media_FormatContext()
    : myFormatCtx(NULL),
      myPtsStartBase(0.0),
      myDuration(0.0)
{
  //
}

//=================================================================================================

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

//=================================================================================================

unsigned int Media_FormatContext::NbSteams() const
{
#ifdef HAVE_FFMPEG
  return myFormatCtx->nb_streams;
#else
  return 0;
#endif
}

//=================================================================================================

const AVStream& Media_FormatContext::Stream(unsigned int theIndex) const
{
#ifdef HAVE_FFMPEG
  return *myFormatCtx->streams[theIndex];
#else
  (void)theIndex;
  throw Standard_ProgramError("Media_FormatContext::Stream()");
#endif
}

//=================================================================================================

bool Media_FormatContext::OpenInput(const TCollection_AsciiString& theInput)
{
#ifdef HAVE_FFMPEG
  const int avErrCode = avformat_open_input(&myFormatCtx, theInput.ToCString(), NULL, NULL);
  if (avErrCode != 0)
  {
    Message::SendFail(TCollection_AsciiString("FFmpeg: Couldn't open video file '") + theInput
                      + "'\nError: " + FormatAVErrorDescription(avErrCode));
    Close();
    return false;
  }

  // retrieve stream information
  if (avformat_find_stream_info(myFormatCtx, NULL) < 0)
  {
    Message::SendFail(TCollection_AsciiString("FFmpeg: Couldn't find stream information in '")
                      + theInput + "'");
    Close();
    return false;
  }

  #ifdef _DEBUG
  av_dump_format(myFormatCtx, 0, theInput.ToCString(), false);
  #endif

  myDuration     = 0.0;
  myPtsStartBase = 0.0;

  TCollection_AsciiString anExt = theInput;
  anExt.LowerCase();
  if (anExt.EndsWith(".png") || anExt.EndsWith(".jpg") || anExt.EndsWith(".jpeg")
      || anExt.EndsWith(".mpo") || anExt.EndsWith(".bmp") || anExt.EndsWith(".tif")
      || anExt.EndsWith(".tiff"))
  {
    // black-list images to workaround non-zero duration
    return true;
  }

  myDuration = FormatUnitsToSeconds(myFormatCtx->duration);
  if (myFormatCtx->nb_streams != 0)
  {
    myPtsStartBase = 2.e+100;
    for (unsigned int aStreamId = 0; aStreamId < myFormatCtx->nb_streams; ++aStreamId)
    {
      const AVStream& aStream = *myFormatCtx->streams[aStreamId];
      myPtsStartBase = Min(myPtsStartBase, StreamUnitsToSeconds(aStream, aStream.start_time));
      myDuration     = Max(myDuration, StreamUnitsToSeconds(aStream, aStream.duration));
    }
  }

  return true;
#else
  Message::SendFail("Error: FFmpeg library is unavailable");
  (void)theInput;
  return false;
#endif
}

//=================================================================================================

void Media_FormatContext::Close()
{
  if (myFormatCtx != NULL)
  {
#ifdef HAVE_FFMPEG
    avformat_close_input(&myFormatCtx);
    // avformat_free_context (myFormatCtx);
#endif
  }
}

//=================================================================================================

TCollection_AsciiString Media_FormatContext::FormatTime(double theSeconds)
{
  double       aSecIn = theSeconds;
  unsigned int aHours = (unsigned int)(aSecIn * THE_SECOND_IN_HOUR);
  aSecIn -= double(aHours) * THE_SECONDS_IN_HOUR;
  unsigned int aMinutes = (unsigned int)(aSecIn * THE_SECOND_IN_MINUTE);
  aSecIn -= double(aMinutes) * THE_SECONDS_IN_MINUTE;
  unsigned int aSeconds = (unsigned int)aSecIn;
  aSecIn -= double(aSeconds);
  double aMilliSeconds = 1000.0 * aSecIn;

  char aBuffer[64];
  if (aHours > 0)
  {
    Sprintf(aBuffer, "%02u:%02u:%02u", aHours, aMinutes, aSeconds);
    return aBuffer;
  }
  else if (aMinutes > 0)
  {
    Sprintf(aBuffer, "%02u:%02u", aMinutes, aSeconds);
    return aBuffer;
  }
  else if (aSeconds > 0)
  {
    Sprintf(aBuffer, "%2u s", aSeconds);
    return aBuffer;
  }

  return TCollection_AsciiString(aMilliSeconds) + " ms";
}

//=================================================================================================

TCollection_AsciiString Media_FormatContext::FormatTimeProgress(double theProgress,
                                                                double theDuration)
{
  double       aSecIn1 = theProgress;
  unsigned int aHours1 = (unsigned int)(aSecIn1 * THE_SECOND_IN_HOUR);
  aSecIn1 -= double(aHours1) * THE_SECONDS_IN_HOUR;
  unsigned int aMinutes1 = (unsigned int)(aSecIn1 * THE_SECOND_IN_MINUTE);
  aSecIn1 -= double(aMinutes1) * THE_SECONDS_IN_MINUTE;
  unsigned int aSeconds1 = (unsigned int)aSecIn1;
  aSecIn1 -= double(aSeconds1);

  double       aSecIn2 = theDuration;
  unsigned int aHours2 = (unsigned int)(aSecIn2 * THE_SECOND_IN_HOUR);
  aSecIn2 -= double(aHours2) * THE_SECONDS_IN_HOUR;
  unsigned int aMinutes2 = (unsigned int)(aSecIn2 * THE_SECOND_IN_MINUTE);
  aSecIn2 -= double(aMinutes2) * THE_SECONDS_IN_MINUTE;
  unsigned int aSeconds2 = (unsigned int)aSecIn2;
  aSecIn2 -= double(aSeconds2);

  char aBuffer[256];
  if (aHours1 > 0 || aHours2 > 0)
  {
    Sprintf(aBuffer,
            "%02u:%02u:%02u / %02u:%02u:%02u",
            aHours1,
            aMinutes1,
            aSeconds1,
            aHours2,
            aMinutes2,
            aSeconds2);
    return aBuffer;
  }
  Sprintf(aBuffer, "%02u:%02u / %02u:%02u", aMinutes1, aSeconds1, aMinutes2, aSeconds2);
  return aBuffer;
}

//=================================================================================================

TCollection_AsciiString Media_FormatContext::StreamInfo(unsigned int    theIndex,
                                                        AVCodecContext* theCodecCtx) const
{
#ifdef HAVE_FFMPEG
  const AVStream& aStream = *myFormatCtx->streams[theIndex];

  AVCodecContext* aCodecCtx = theCodecCtx;
  if (aCodecCtx == NULL)
  {
    Standard_DISABLE_DEPRECATION_WARNINGS aCodecCtx = aStream.codec;
    Standard_ENABLE_DEPRECATION_WARNINGS
  }

  char aFrmtBuff[4096] = {};
  avcodec_string(aFrmtBuff, sizeof(aFrmtBuff), aCodecCtx, 0);
  TCollection_AsciiString aStreamInfo(aFrmtBuff);

  if (aStream.sample_aspect_ratio.num
      && av_cmp_q(aStream.sample_aspect_ratio, aStream.codecpar->sample_aspect_ratio))
  {
    AVRational aDispAspectRatio;
    av_reduce(&aDispAspectRatio.num,
              &aDispAspectRatio.den,
              aStream.codecpar->width * int64_t(aStream.sample_aspect_ratio.num),
              aStream.codecpar->height * int64_t(aStream.sample_aspect_ratio.den),
              1024 * 1024);
    aStreamInfo = aStreamInfo + ", SAR " + aStream.sample_aspect_ratio.num + ":"
                  + aStream.sample_aspect_ratio.den + " DAR " + aDispAspectRatio.num + ":"
                  + aDispAspectRatio.den;
  }

  if (aStream.codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
  {
    if (aStream.avg_frame_rate.den != 0 && aStream.avg_frame_rate.num != 0)
    {
      aStreamInfo +=
        TCollection_AsciiString(", ") + formatFps(av_q2d(aStream.avg_frame_rate)) + " fps";
    }
    if (aStream.r_frame_rate.den != 0 && aStream.r_frame_rate.num != 0)
    {
      aStreamInfo +=
        TCollection_AsciiString(", ") + formatFps(av_q2d(aStream.r_frame_rate)) + " tbr";
    }
    if (aStream.time_base.den != 0 && aStream.time_base.num != 0)
    {
      aStreamInfo +=
        TCollection_AsciiString(", ") + formatFps(1 / av_q2d(aStream.time_base)) + " tbn";
    }
    if (aCodecCtx->time_base.den != 0 && aCodecCtx->time_base.num != 0)
    {
      aStreamInfo +=
        TCollection_AsciiString(", ") + formatFps(1 / av_q2d(aCodecCtx->time_base)) + " tbc";
    }
  }
  if (myDuration > 0.0)
  {
    aStreamInfo += TCollection_AsciiString(", duration: ") + FormatTime(myDuration);
  }
  return aStreamInfo;
#else
  (void)theIndex;
  (void)theCodecCtx;
  return TCollection_AsciiString();
#endif
}

//=================================================================================================

bool Media_FormatContext::ReadPacket(const Handle(Media_Packet)& thePacket)
{
  if (thePacket.IsNull())
  {
    return false;
  }

#ifdef HAVE_FFMPEG
  return av_read_frame(myFormatCtx, thePacket->ChangePacket()) >= 0;
#else
  return false;
#endif
}

//=================================================================================================

bool Media_FormatContext::SeekStream(unsigned int theStreamId,
                                     double       theSeekPts,
                                     bool         theToSeekBack)
{
#ifdef HAVE_FFMPEG
  const int aFlags  = theToSeekBack ? AVSEEK_FLAG_BACKWARD : 0;
  AVStream& aStream = *myFormatCtx->streams[theStreamId];
  if ((aStream.disposition & AV_DISPOSITION_ATTACHED_PIC) != 0)
  {
    return false;
  }

  int64_t aSeekTarget =
    StreamSecondsToUnits(aStream, theSeekPts + StreamUnitsToSeconds(aStream, aStream.start_time));
  bool isSeekDone = av_seek_frame(myFormatCtx, theStreamId, aSeekTarget, aFlags) >= 0;

  // try 10 more times in backward direction to work-around huge duration between key frames
  // will not work for some streams with undefined cur_dts (AV_NOPTS_VALUE)!!!
  for (int aTries = 10;
       isSeekDone && theToSeekBack && aTries > 0 && (aStream.cur_dts > aSeekTarget);
       --aTries)
  {
    aSeekTarget -= StreamSecondsToUnits(aStream, 1.0);
    isSeekDone = av_seek_frame(myFormatCtx, theStreamId, aSeekTarget, aFlags) >= 0;
  }
  if (isSeekDone)
  {
    return true;
  }

  TCollection_AsciiString aStreamType =
    aStream.codecpar->codec_type == AVMEDIA_TYPE_VIDEO
      ? "Video"
      : (aStream.codecpar->codec_type == AVMEDIA_TYPE_AUDIO ? "Audio" : "");
  Message::SendWarning(
    TCollection_AsciiString("Error while seeking ") + aStreamType + " stream to " + theSeekPts
    + " sec (" + (theSeekPts + StreamUnitsToSeconds(aStream, aStream.start_time)) + " sec)");
  return false;
#else
  (void)theStreamId;
  (void)theSeekPts;
  (void)theToSeekBack;
  return false;
#endif
}

//=================================================================================================

bool Media_FormatContext::Seek(double theSeekPts, bool theToSeekBack)
{
#ifdef HAVE_FFMPEG
  const int aFlags      = theToSeekBack ? AVSEEK_FLAG_BACKWARD : 0;
  int64_t   aSeekTarget = SecondsToUnits(theSeekPts);
  if (av_seek_frame(myFormatCtx, -1, aSeekTarget, aFlags) >= 0)
  {
    return true;
  }

  const char* aFileName =
  #if (LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 7, 100))
    myFormatCtx->url;
  #else
    myFormatCtx->filename;
  #endif

  Message::SendWarning(TCollection_AsciiString("Disaster! Seeking to ") + theSeekPts + " ["
                       + aFileName + "] has failed.");
  return false;
#else
  (void)theSeekPts;
  (void)theToSeekBack;
  return false;
#endif
}
