#include "toonz/preferences.h"
#include "toonz/toonzfolders.h"

#include "tiio_ffmpeg.h"
#include "tsystem.h"
#include "tsound.h"
#include "timageinfo.h"
#include "toonz/stage.h"

#include <QProcess>
#include <QEventLoop>
#include <QTimer>
#include <QDir>
#include <QtGui/QImage>
#include <QRegExp>
#include "tmsgcore.h"
#include "thirdparty.h"

Ffmpeg::Ffmpeg() {
  m_ffmpegTimeout      = ThirdParty::getFFmpegTimeout() * 1000;
  m_intermediateFormat = "png";
  m_startNumber        = 2147483647;  // Lowest frame determines starting frame
}
Ffmpeg::~Ffmpeg() {}

bool Ffmpeg::checkFormat(std::string format) {
  static std::string strResults = "";
  if (strResults.empty()) {
    QStringList args;
    args << "-formats";
    QProcess ffmpeg;
    ThirdParty::runFFmpeg(ffmpeg, args);
    ffmpeg.waitForFinished();
    QString results = ffmpeg.readAllStandardError();
    results += ffmpeg.readAllStandardOutput();
    ffmpeg.close();
    strResults = results.toStdString();
  }
  std::string::size_type n;
  n = strResults.find(format);
  if (n != std::string::npos)
    return true;
  else
    return false;
}

TFilePath Ffmpeg::getFfmpegCache() {
  QString cacheRoot = ToonzFolder::getCacheRootFolder().getQString();
  if (!TSystem::doesExistFileOrLevel(TFilePath(cacheRoot + "/ffmpeg"))) {
    TSystem::mkDir(TFilePath(cacheRoot + "/ffmpeg"));
  }
  std::string ffmpegPath =
      TFilePath(cacheRoot + "/ffmpeg").getQString().toStdString();
  return TFilePath(cacheRoot + "/ffmpeg");
}

void Ffmpeg::setFrameRate(double fps) { m_frameRate = fps; }

void Ffmpeg::setPath(TFilePath path) { m_path = path; }

void Ffmpeg::createIntermediateImage(const TImageP &img, int frameIndex) {
  m_frameCount++;
  frameIndex--;  // ffmpeg start at 0 by default
  if (frameIndex < m_startNumber) m_startNumber = frameIndex;
  QString tempPath = getFfmpegCache().getQString() + "//" +
                     QString::fromStdString(m_path.getName()) + "tempOut" +
                     QString::number(frameIndex) + "." + m_intermediateFormat;
  std::string saveStatus = "";
  TRasterImageP tempImage(img);
  TRasterImage *image = (TRasterImage *)tempImage->cloneImage();

  m_lx           = image->getRaster()->getLx();
  m_ly           = image->getRaster()->getLy();
  m_bpp          = image->getRaster()->getPixelSize();
  int totalBytes = m_lx * m_ly * m_bpp;
  image->getRaster()->yMirror();

  // lock raster to get data
  image->getRaster()->lock();
  void *buffin = image->getRaster()->getRawData();
  assert(buffin);
  void *buffer = malloc(totalBytes);
  memcpy(buffer, buffin, totalBytes);

  image->getRaster()->unlock();

  // create QImage save format
  QByteArray ba      = m_intermediateFormat.toUpper().toLatin1();
  const char *format = ba.data();

  QImage *qi = new QImage((uint8_t *)buffer, m_lx, m_ly, QImage::Format_ARGB32);
  qi->save(tempPath, format, -1);
  free(buffer);
  m_cleanUpList.push_back(tempPath);

  delete qi;
  delete image;
}

void Ffmpeg::runFfmpeg(QStringList preIArgs, QStringList postIArgs,
                       bool includesInPath, bool includesOutPath,
                       bool overWriteFiles, bool asyncProcess) {
  QString tempName = "//" + QString::fromStdString(m_path.getName()) +
                     "tempOut%d." + m_intermediateFormat;
  tempName = getFfmpegCache().getQString() + tempName;

  QStringList args;
  args = args + preIArgs;
  if (!includesInPath) {  // NOTE:  if including the in path, it needs to be in
                          // the preIArgs argument.
    if (m_startNumber != 0) {
      args << "-start_number";
      args << QString::number(m_startNumber);
    }
    args << "-i";
    args << tempName;
  }
  if (m_hasSoundTrack) args = args + m_audioArgs;
  args = args + postIArgs;
  if (overWriteFiles && !includesOutPath) {  // if includesOutPath is true, you
                                             // need to include the overwrite in
                                             // your postIArgs.
    args << "-y";
  }
  if (!includesOutPath) {
    args << m_path.getQString();
  }

  // write the file
  QProcess ffmpeg;
  ThirdParty::runFFmpeg(ffmpeg, args);
  if (waitFfmpeg(ffmpeg, asyncProcess)) {
    QString results = ffmpeg.readAllStandardError();
    results += ffmpeg.readAllStandardOutput();
    int exitCode           = ffmpeg.exitCode();
    std::string strResults = results.toStdString();
  }
  ffmpeg.close();
}

QString Ffmpeg::runFfprobe(QStringList args) {
  QProcess ffmpeg;
  ThirdParty::runFFprobe(ffmpeg, args);
  if (!waitFfmpeg(ffmpeg, false)) {
    throw TImageException(m_path, "error accessing ffprobe.");
  }
  QString results = ffmpeg.readAllStandardError();
  results += ffmpeg.readAllStandardOutput();
  int exitCode = ffmpeg.exitCode();
  ffmpeg.close();
  // If the url cannot be opened or recognized as a multimedia file, ffprobe
  // returns a positive exit code.
  if (exitCode > 0) throw TImageException(m_path, "error reading info.");
  std::string strResults = results.toStdString();
  return results;
}

bool Ffmpeg::waitFfmpeg(QProcess &ffmpeg, bool asyncProcess) {
  if (!asyncProcess) {
    bool status = ffmpeg.waitForFinished(m_ffmpegTimeout);
    if (!status) {
      DVGui::warning(
          QObject::tr("FFmpeg timed out.\n"
                      "Please check the file for errors.\n"
                      "If the file doesn't play or is incomplete, \n"
                      "Please try raising the FFmpeg timeout in Preferences."));
    }
    return status;
  }

  int exitCode = ThirdParty::waitAsyncProcess(ffmpeg, m_ffmpegTimeout);
  if (exitCode == 0) return true;
  if (exitCode == -1) {
    DVGui::warning(
        QObject::tr("FFmpeg returned error-code: %1").arg(ffmpeg.exitCode()));
  }
  if (exitCode == -2) {
    DVGui::warning(
        QObject::tr("FFmpeg timed out.\n"
                    "Please check the file for errors.\n"
                    "If the file doesn't play or is incomplete, \n"
                    "Please try raising the FFmpeg timeout in Preferences."));
  }
  return false;
}

void Ffmpeg::saveSoundTrack(TSoundTrack *st) {
  m_sampleRate    = st->getSampleRate();
  m_channelCount  = st->getChannelCount();
  m_bitsPerSample = st->getBitPerSample();
  // LONG count = st->getSampleCount();
  int bufSize         = st->getSampleCount() * st->getSampleSize();
  const UCHAR *buffer = st->getRawData();

  m_audioPath = getFfmpegCache().getQString() + "//" +
                QString::fromStdString(m_path.getName()) + "tempOut.raw";
  m_audioFormat = ((st->getSampleType() == TSound::FLOAT) ? "f" : "s") +
                  QString::number(m_bitsPerSample);
  if (m_bitsPerSample > 8) m_audioFormat = m_audioFormat + "le";
  std::string strPath = m_audioPath.toStdString();

  QByteArray data;
  data.insert(0, (char *)buffer, bufSize);

  QFile file(m_audioPath);
  file.open(QIODevice::WriteOnly);
  file.write(data);
  file.close();
  m_hasSoundTrack = true;

  m_audioArgs << "-f";
  m_audioArgs << m_audioFormat;
  m_audioArgs << "-ar";
  m_audioArgs << QString::number(m_sampleRate);
  m_audioArgs << "-ac";
  m_audioArgs << QString::number(m_channelCount);
  m_audioArgs << "-i";
  m_audioArgs << m_audioPath;

  // add file to framesWritten for cleanup
  m_cleanUpList.push_back(m_audioPath);
}

bool Ffmpeg::checkFilesExist() {
  QString ffmpegCachePath = getFfmpegCache().getQString();
  QString tempPath = ffmpegCachePath + "//" + cleanPathSymbols() + "In0001." +
                     m_intermediateFormat;
  if (TSystem::doesExistFileOrLevel(TFilePath(tempPath))) {
    return true;
  } else
    return false;
}

ffmpegFileInfo Ffmpeg::getInfo() {
  QString ffmpegCachePath = getFfmpegCache().getQString();
  QString tempPath = ffmpegCachePath + "//" + cleanPathSymbols() + ".txt";
  if (QFile::exists(tempPath)) {
    QFile infoText(tempPath);
    infoText.open(QIODevice::ReadOnly);
    QByteArray ba = infoText.readAll();
    infoText.close();
    QString text         = QString::fromStdString(ba.toStdString());
    QStringList textlist = text.split(" ");
    if (textlist.count() >= 4) {
      m_lx         = textlist[0].toInt();
      m_ly         = textlist[1].toInt();
      m_frameRate  = textlist[2].toDouble();
      m_frameCount = textlist[3].toInt();
    }
  } else {
    QFile infoText(tempPath);
    getSize();
    getFrameCount();
    getFrameRate();
    infoText.open(QIODevice::WriteOnly);
    std::string infoToWrite =
        std::to_string(m_lx) + " " + std::to_string(m_ly) + " " +
        std::to_string(m_frameRate) + " " + std::to_string(m_frameCount);
    int infoLength = infoToWrite.length();
    infoText.write(infoToWrite.c_str(), infoLength);
    infoText.close();
  }
  ffmpegFileInfo info;
  info.m_lx         = m_lx;
  info.m_ly         = m_ly;
  info.m_frameRate  = m_frameRate;
  info.m_frameCount = m_frameCount;
  return info;
}
TRasterImageP Ffmpeg::getImage(int frameIndex) {
  QString ffmpegCachePath = getFfmpegCache().getQString();
  QString tempPath        = ffmpegCachePath + "//" + cleanPathSymbols();
  std::string tmpPath     = tempPath.toStdString();
  // QString tempPath= m_path.getQString();
  QString number   = QString("%1").arg(frameIndex, 4, 10, QChar('0'));
  QString tempName = "In" + number + ".png";
  tempName         = tempPath + tempName;

  // for debugging
  std::string strPath = tempName.toStdString();
  if (TSystem::doesExistFileOrLevel(TFilePath(tempName))) {
    QImage *temp = new QImage(tempName, "PNG");
    if (temp) {
      QImage tempToo = temp->convertToFormat(QImage::Format_ARGB32);
      delete temp;
      const UCHAR *bits = tempToo.bits();

      TRasterPT<TPixelRGBM32> ret;
      ret.create(m_lx, m_ly);
      ret->lock();
      memcpy(ret->getRawData(), bits, m_lx * m_ly * 4);
      ret->unlock();
      ret->yMirror();
      return TRasterImageP(ret);
    }
  }
  return TRasterImageP();
}

double Ffmpeg::getFrameRate() {
  QStringList fpsArgs;
  int fpsNum = 0, fpsDen = 0;
  fpsArgs << "-v";
  fpsArgs << "error";
  fpsArgs << "-select_streams";
  fpsArgs << "v:0";
  fpsArgs << "-show_entries";
  fpsArgs << "stream=r_frame_rate";
  fpsArgs << "-of";
  fpsArgs << "default=noprint_wrappers=1:nokey=1";
  fpsArgs << m_path.getQString();
  QString fpsResults = runFfprobe(fpsArgs);

  QStringList fpsResultsList = fpsResults.split("/");
  if (fpsResultsList.size() > 1) {
    fpsNum = fpsResultsList[0].toInt();
    fpsDen = fpsResultsList[1].toInt();
  }

  // if for some reason we don't have enough info to calculate it. Use the
  // avg_frame_rate
  if (!fpsDen) {
    fpsArgs.clear();
    fpsArgs << "-v";
    fpsArgs << "error";
    fpsArgs << "-select_streams";
    fpsArgs << "v:0";
    fpsArgs << "-show_entries";
    fpsArgs << "stream=avg_frame_rate";
    fpsArgs << "-of";
    fpsArgs << "default=noprint_wrappers=1:nokey=1";
    fpsArgs << m_path.getQString();
    QString fpsResults = runFfprobe(fpsArgs);

    fpsResultsList = fpsResults.split("/");
    if (fpsResultsList.size() > 1) {
      fpsNum = fpsResultsList[0].toInt();
      fpsDen = fpsResultsList[1].toInt();
    }
  }

  if (fpsDen > 0) {
    m_frameRate = (double)fpsNum / (double)fpsDen;
  }
  return m_frameRate;
}

TDimension Ffmpeg::getSize() {
  QStringList sizeArgs;
  sizeArgs << "-v";
  sizeArgs << "error";
  sizeArgs << "-of";
  sizeArgs << "flat=s=_";
  sizeArgs << "-select_streams";
  sizeArgs << "v:0";
  sizeArgs << "-show_entries";
  sizeArgs << "stream=height,width";
  sizeArgs << m_path.getQString();

  QString sizeResults = runFfprobe(sizeArgs);
  QStringList split   = sizeResults.split("\n");
  m_lx                = split[0].split("=")[1].toInt();
  m_ly                = split[1].split("=")[1].toInt();
  return TDimension(m_lx, m_ly);
}

int Ffmpeg::getFrameCount() {
  // nb_read_frames from files may not be accurate. Let's calculate it based on
  // r_frame_rate * duration
  QStringList frameCountArgs;
  frameCountArgs << "-v";
  frameCountArgs << "error";
  frameCountArgs << "-count_frames";
  frameCountArgs << "-select_streams";
  frameCountArgs << "v:0";
  frameCountArgs << "-show_entries";
  frameCountArgs << "stream=duration";
  frameCountArgs << "-of";
  frameCountArgs << "default=nokey=1:noprint_wrappers=1";
  frameCountArgs << m_path.getQString();

  QString frameResults = runFfprobe(frameCountArgs);
  m_frameCount         = frameResults.toDouble() * getFrameRate();

  // if for some reason we don't have enough info to calculate it. Use the
  // nb_read_frames
  if (!m_frameCount) {
    frameCountArgs.clear();
    frameCountArgs << "-v";
    frameCountArgs << "error";
    frameCountArgs << "-count_frames";
    frameCountArgs << "-select_streams";
    frameCountArgs << "v:0";
    frameCountArgs << "-show_entries";
    frameCountArgs << "stream=nb_read_frames";
    frameCountArgs << "-of";
    frameCountArgs << "default=nokey=1:noprint_wrappers=1";
    frameCountArgs << m_path.getQString();

    frameResults = runFfprobe(frameCountArgs);
    m_frameCount = frameResults.toInt();
  }

  return m_frameCount;
}

void Ffmpeg::getFramesFromMovie(int frame) {
  QString ffmpegCachePath = getFfmpegCache().getQString();
  QString tempPath        = ffmpegCachePath + "//" + cleanPathSymbols();
  std::string tmpPath     = tempPath.toStdString();
  QString tempName        = "In%04d." + m_intermediateFormat;
  tempName                = tempPath + tempName;
  QString tempStart;
  if (frame == -1) {
    tempStart = "In0001." + m_intermediateFormat;
    tempStart = tempPath + tempStart;
  } else {
    QString number = QString("%1").arg(frame, 4, 10, QChar('0'));
    tempStart      = tempPath + "In" + number + "." + m_intermediateFormat;
  }
  QString tempBase = tempPath + "In";
  QString addToDelete;
  if (!TSystem::doesExistFileOrLevel(TFilePath(tempStart))) {
    // for debugging
    std::string strPath = tempName.toStdString();

    QStringList preIFrameArgs;
    QStringList postIFrameArgs;
    // frameArgs << "-accurate_seek";
    // frameArgs << "-ss";
    // frameArgs << "0" + QString::number(frameIndex / m_info->m_frameRate);
    if (m_path.getType() == "webm") {
      // To load in webm transparency
      preIFrameArgs << "-vcodec";
      preIFrameArgs << "libvpx";
    }
    preIFrameArgs << "-i";
    preIFrameArgs << m_path.getQString();
    postIFrameArgs << "-y";
    postIFrameArgs << "-f";
    postIFrameArgs << "image2";

    postIFrameArgs << tempName;

    runFfmpeg(preIFrameArgs, postIFrameArgs, true, true, true, false);

    for (int i = 1; i <= m_frameCount; i++) {
      QString number      = QString("%1").arg(i, 4, 10, QChar('0'));
      addToDelete         = tempBase + number + "." + m_intermediateFormat;
      std::string delPath = addToDelete.toStdString();
      // addToCleanUp(addToDelete);
    }
  }
}

QString Ffmpeg::cleanPathSymbols() {
  return m_path.getQString().remove(QRegExp(
      QString::fromUtf8("[-`~!@#$%^&*()_+=|:;<>«»,.?/{}\'\"\\[\\]\\\\]")));
}

int Ffmpeg::getGifFrameCount() {
  int frame               = 1;
  QString ffmpegCachePath = getFfmpegCache().getQString();
  QString tempPath        = ffmpegCachePath + "//" + cleanPathSymbols();
  std::string tmpPath     = tempPath.toStdString();
  QString tempName        = "In%04d." + m_intermediateFormat;
  tempName                = tempPath + tempName;
  QString tempStart;
  tempStart = "In0001." + m_intermediateFormat;
  tempStart = tempPath + tempStart;
  while (TSystem::doesExistFileOrLevel(TFilePath(tempStart))) {
    frame++;
    QString number = QString("%1").arg(frame, 4, 10, QChar('0'));
    tempStart      = tempPath + "In" + number + "." + m_intermediateFormat;
  }
  return frame - 1;
}

void Ffmpeg::addToCleanUp(QString path) {
  if (TSystem::doesExistFileOrLevel(TFilePath(path))) {
    m_cleanUpList.push_back(path);
  }
}

void Ffmpeg::cleanUpFiles() {
  for (QString path : m_cleanUpList) {
    if (TSystem::doesExistFileOrLevel(TFilePath(path))) {
      TSystem::deleteFile(TFilePath(path));
    }
  }
}

void Ffmpeg::disablePrecompute() {
  Preferences::instance()->setPrecompute(false);
}

//===========================================================
//
//  TImageReaderFFmpeg
//
//===========================================================

class TImageReaderFFmpeg final : public TImageReader {
public:
  int m_frameIndex;

  TImageReaderFFmpeg(const TFilePath &path, int index, TLevelReaderFFmpeg *lra,
                     TImageInfo *info)
      : TImageReader(path), m_lra(lra), m_frameIndex(index), m_info(info) {
    m_lra->addRef();
  }
  ~TImageReaderFFmpeg() { m_lra->release(); }

  TImageP load() override { return m_lra->load(m_frameIndex); }
  TDimension getSize() const { return m_lra->getSize(); }
  TRect getBBox() const { return TRect(); }
  const TImageInfo *getImageInfo() const override { return m_info; }

private:
  TLevelReaderFFmpeg *m_lra;
  TImageInfo *m_info;

  // not implemented
  TImageReaderFFmpeg(const TImageReaderFFmpeg &);
  TImageReaderFFmpeg &operator=(const TImageReaderFFmpeg &src);
};

//===========================================================
//
//  TLevelReaderFFmpeg
//
//===========================================================

TLevelReaderFFmpeg::TLevelReaderFFmpeg(const TFilePath &path)
    : TLevelReader(path) {
  ffmpegReader = new Ffmpeg();
  ffmpegReader->setPath(m_path);
  ffmpegReader->disablePrecompute();
  ffmpegFileInfo tempInfo = ffmpegReader->getInfo();
  double fps              = tempInfo.m_frameRate;
  m_frameCount            = tempInfo.m_frameCount;
  m_size                  = TDimension(tempInfo.m_lx, tempInfo.m_ly);
  m_lx                    = m_size.lx;
  m_ly                    = m_size.ly;

  // set values
  m_info                   = new TImageInfo();
  m_info->m_frameRate      = fps;
  m_info->m_lx             = m_lx;
  m_info->m_ly             = m_ly;
  m_info->m_bitsPerSample  = 8;
  m_info->m_samplePerPixel = 4;
  m_info->m_dpix           = Stage::standardDpi;
  m_info->m_dpiy           = Stage::standardDpi;
}
//-----------------------------------------------------------

TLevelReaderFFmpeg::~TLevelReaderFFmpeg() {}

//-----------------------------------------------------------

TLevelP TLevelReaderFFmpeg::loadInfo() {
  if (m_frameCount == -1) return TLevelP();
  TLevelP level;
  for (int i = 1; i <= m_frameCount; i++) level->setFrame(i, TImageP());
  return level;
}

//-----------------------------------------------------------

TImageReaderP TLevelReaderFFmpeg::getFrameReader(TFrameId fid) {
  if (!fid.getLetter().isEmpty()) return TImageReaderP(0);
  int index = fid.getNumber();

  TImageReaderFFmpeg *irm = new TImageReaderFFmpeg(m_path, index, this, m_info);
  return TImageReaderP(irm);
}

//------------------------------------------------------------------------------

TDimension TLevelReaderFFmpeg::getSize() { return m_size; }

//------------------------------------------------

TImageP TLevelReaderFFmpeg::load(int frameIndex) {
  if (!ffmpegFramesCreated) {
    ffmpegReader->getFramesFromMovie();
    ffmpegFramesCreated = true;
  }
  return ffmpegReader->getImage(frameIndex);
}
