// Copyright (c) 2012-2017 VideoStitch SAS
// Copyright (c) 2018 stitchEm

#include "videostitcher.hpp"

#include "libvideostitch-gui/caps/signalcompressioncaps.hpp"
#include "libvideostitch-gui/mainwindow/LibLogHelpers.hpp"

#include "libvideostitch/inputDef.hpp"
#include "libvideostitch/inputFactory.hpp"
#include "libvideostitch/parse.hpp"
#include "libvideostitch/logging.hpp"
#include "libvideostitch/stitchOutput.hpp"
#include <QDir>

template <typename Controller>
VideoStitcher<Controller>::VideoStitcher(QObject* parent, typename Controller::DeviceDefinition& cudaDeviceDef,
                                         ProjectDefinition& project, Controller& controller,
                                         typename Controller::Output& stitchOutput,
                                         std::vector<VideoStitch::Core::ExtractOutput*>& extractsOutputs,
                                         ActivableAlgorithmOutput& algoOutput, QReadWriteLock& setupLk)
    : VideoStitcherSignalSlots(parent),
      cudaDeviceDefinition(cudaDeviceDef),
      project(project),
      controller(controller),
      stitchOutput(stitchOutput),
      algoOutput(algoOutput),
      extractsOutputs(extractsOutputs),
      setupLock(setupLk) {}

template <typename Controller>
VideoStitcher<Controller>::~VideoStitcher() {}

template <typename Controller>
VideoStitch::Status VideoStitcher<Controller>::init() {
  setupLock.lockForWrite();
  auto status = controller.createStitcher();
  setupLock.unlock();
  return status;
}

template <typename Controller>
void VideoStitcher<Controller>::closeProject() {
  setupLock.lockForWrite();
  controller.deleteStitcher();
  setupLock.unlock();
}

// ---------------------------- Setup ---------------------------------------

template <typename Controller>
void VideoStitcher<Controller>::resetMerger() {
  // reset right now, redo setup
  const VideoStitch::Core::ImageMergerFactory* mergerFactory = project.getImageMergerFactory().get();
  setupLock.lockForWrite();
  VideoStitch::Status s = controller.resetMergerFactory(*mergerFactory, true);
  setupLock.unlock();
  if (!s.ok()) {
    showError(VideoStitch::Core::ControllerStatus::fromError({VideoStitch::Origin::Stitcher,
                                                              VideoStitch::ErrType::SetupFailure,
                                                              "Could not apply the changes to the image merger", s}));
    closeProject();
  } else {
    restitch();
  }
}

template <typename Controller>
void VideoStitcher<Controller>::resetAdvancedBlending() {
  // reset right now, redo setup
  const VideoStitch::Core::ImageFlowFactory* flowFactory = project.getImageFlowFactory().get();
  setupLock.lockForWrite();
  VideoStitch::Status statusFlow = controller.resetFlowFactory(*flowFactory, true);
  setupLock.unlock();
  if (!statusFlow.ok()) {
    showError(VideoStitch::Core::ControllerStatus::fromError(
        {VideoStitch::Origin::Stitcher, VideoStitch::ErrType::SetupFailure,
         VideoStitcherSignalSlots::tr("Could not apply the changes to the image flow").toStdString(), statusFlow}));
    closeProject();
  }

  const VideoStitch::Core::ImageWarperFactory* warperFactory = project.getImageWarperFactory().get();
  setupLock.lockForWrite();
  VideoStitch::Status statusWarper = controller.resetWarperFactory(*warperFactory, true);
  setupLock.unlock();
  if (!statusWarper.ok()) {
    showError(VideoStitch::Core::ControllerStatus::fromError(
        {VideoStitch::Origin::Stitcher, VideoStitch::ErrType::SetupFailure,
         VideoStitcherSignalSlots::tr("Could not apply the changes to the image warper").toStdString(), statusWarper}));
    closeProject();
  }

  restitch();
}

// -------------------------- Stitching -------------------------------------

template <typename Controller>
void VideoStitcher<Controller>::stitch() {
  stitchInternal(true, false, true);
}

template <typename Controller>
void VideoStitcher<Controller>::extract() {
  stitchInternal(true, true, false);
}

template <typename Controller>
void VideoStitcher<Controller>::stitchAndExtract() {
  stitchInternal(true, true, true);
}

template <typename Controller>
void VideoStitcher<Controller>::restitch() {
  stitchInternal(false, false, true);
}

template <typename Controller>
void VideoStitcher<Controller>::reextract() {
  stitchInternal(false, true, false);
}

template <typename Controller>
void VideoStitcher<Controller>::restitchAndExtract() {
  stitchInternal(false, true, true);
}

template <typename Controller>
void VideoStitcher<Controller>::stitchInternal(bool readFrame, bool extract, bool stitch) {
  setupLock.lockForRead();
  std::vector<VideoStitch::Core::ExtractOutput*> inputFrames;
  if (extract) {
    inputFrames = extractsOutputs;
  }
  VideoStitch::Core::AlgorithmOutput* alg = nullptr;
  if (!algoOutput.toggle.test_and_set()) {
    alg = algoOutput.algoOutput;
  }

  VideoStitch::Core::ControllerStatus stitchStatus;
  if (!stitch) {
    // only extract input images
    stitchStatus = controller.extract(inputFrames, alg, readFrame);
  } else {
    stitchStatus = controller.stitchAndExtract(&stitchOutput, inputFrames, alg, readFrame);
  }
  setupLock.unlock();

  if (!stitchStatus.ok()) {
    showError(stitchStatus);
  }
}

template <typename Controller>
void VideoStitcher<Controller>::showError(const VideoStitch::Core::ControllerStatus status) {
  emit notifyErrorMessage(status, false);
}

// -------------------------- Orientation ------------------------------------

template <typename Controller>
VideoStitch::Quaternion<double> VideoStitcher<Controller>::getRotation() const {
  Q_ASSERT(QThread::currentThread() == this->thread());
  return controller.getRotation();
}

template <typename Controller>
void VideoStitcher<Controller>::resetOrientation() {
  setupLock.lockForWrite();
  controller.resetRotation();
  setupLock.unlock();
}

template <typename Controller>
VideoStitch::Quaternion<double> VideoStitcher<Controller>::getCurrentOrientation() const {
  Q_ASSERT(QThread::currentThread() == this->thread());
  return controller.getRotation();
}

template <typename Controller>
void VideoStitcher<Controller>::rotatePanorama(double yaw, double pitch, double roll) {
  setupLock.lockForWrite();
  controller.applyRotation(yaw, pitch, roll);
  setupLock.unlock();
}

// -------------------------- Snapshots -------------------------------

namespace {
// Instantiate Stereo and Mono stitchers
template <typename T>
VideoStitch::Potential<T> makeOutputWriter(VideoStitch::Ptv::Value& outputConfig, unsigned int width,
                                           unsigned int height, VideoStitch::FrameRate framerate);

template <>
VideoStitch::Potential<VideoStitch::Output::VideoWriter> makeOutputWriter(VideoStitch::Ptv::Value& outputConfig,
                                                                          unsigned int width, unsigned int height,
                                                                          VideoStitch::FrameRate framerate) {
  auto pot = VideoStitch::Output::create(outputConfig, "snapshot", width, height, framerate);
  FAIL_RETURN(pot.status());
  return pot.release()->getVideoWriter();
}

template <>
VideoStitch::Potential<VideoStitch::Output::StereoWriter> makeOutputWriter(VideoStitch::Ptv::Value& outputConfig,
                                                                           unsigned int width, unsigned int height,
                                                                           VideoStitch::FrameRate framerate) {
  VideoStitch::Potential<VideoStitch::Output::Output> writer =
      VideoStitch::Output::create(outputConfig, "snapshot", width, height, framerate);
  return VideoStitch::Output::StereoWriter::createComposition(
      writer.release()->getVideoWriter(), VideoStitch::Output::StereoWriter::VerticalLayout, AddressSpace::Host);
}
}  // namespace

template <typename Controller>
void VideoStitcher<Controller>::onSnapshotPanorama(const QString& filename) {
  const QString currentDir = QDir::currentPath();
  const QFileInfo fInfo = QFileInfo(filename);
  QDir::setCurrent(fInfo.absolutePath());
  std::unique_ptr<VideoStitch::Ptv::Value> outputConfig(VideoStitch::Ptv::Value::emptyObject());
  const auto tempByteArray = QString(fInfo.path() + QDir::separator() + fInfo.baseName());

  outputConfig->get("filename")->asString() = tempByteArray.toStdString();
  outputConfig->get("type")->asString() = fInfo.completeSuffix().toStdString();
  outputConfig->get("numbered_digits")->asInt() = 0;

  setupLock.lockForRead();

  VideoStitch::Core::PanoSurface* surf = nullptr;
  typename Controller::Output::Writer* writer = nullptr;

  if (project.getPanoConst()->getProjection() == VideoStitch::Core::PanoProjection::Equirectangular) {
    VideoStitch::Potential<typename Controller::Output::Writer> potWriter =
        makeOutputWriter<typename Controller::Output::Writer>(*outputConfig, project.getPanoConst()->getWidth(),
                                                              project.getPanoConst()->getHeight(),
                                                              controller.getFrameRate());
    // Output Writer creation error
    if (!potWriter.ok()) {
      setupLock.unlock();
      showError(VideoStitch::Core::ControllerStatus(
          {VideoStitch::Origin::Output, VideoStitch::ErrType::SetupFailure,
           VideoStitcherSignalSlots::tr("Panorama snapshot failed").toStdString(), potWriter.status()}));
      return;
    }
    writer = potWriter.release();

    VideoStitch::Potential<VideoStitch::Core::PanoSurface> potSurf =
        VideoStitch::Core::OffscreenAllocator::createPanoSurface(
            project.getPanoConst()->getWidth(), project.getPanoConst()->getHeight(), "onSnapshotPanorama");
    if (!potSurf.ok()) {
      showError(VideoStitch::Core::ControllerStatus(
          {VideoStitch::Origin::Output, VideoStitch::ErrType::SetupFailure,
           VideoStitcherSignalSlots::tr("Panorama snapshot failed").toStdString(), potSurf.status()}));
      return;
    }
    surf = potSurf.release();
  } else if (project.getPanoConst()->getProjection() == VideoStitch::Core::PanoProjection::Cubemap ||
             project.getPanoConst()->getProjection() == VideoStitch::Core::PanoProjection::EquiangularCubemap) {
    VideoStitch::Potential<typename Controller::Output::Writer> potWriter =
        makeOutputWriter<typename Controller::Output::Writer>(*outputConfig, project.getPanoConst()->getLength() * 3,
                                                              project.getPanoConst()->getLength() * 2,
                                                              controller.getFrameRate());
    // Output Writer creation error
    if (!potWriter.ok()) {
      setupLock.unlock();
      showError(VideoStitch::Core::ControllerStatus(
          {VideoStitch::Origin::Output, VideoStitch::ErrType::SetupFailure,
           VideoStitcherSignalSlots::tr("Panorama snapshot failed").toStdString(), potWriter.status()}));
      return;
    }
    writer = potWriter.release();

    VideoStitch::Potential<VideoStitch::Core::CubemapSurface> potSurf =
        VideoStitch::Core::OffscreenAllocator::createCubemapSurface(
            project.getPanoConst()->getLength(), "onSnapshotPanorama",
            project.getPanoConst()->getProjection() == VideoStitch::Core::PanoProjection::EquiangularCubemap);
    if (!potSurf.ok()) {
      showError(VideoStitch::Core::ControllerStatus(
          {VideoStitch::Origin::Output, VideoStitch::ErrType::SetupFailure,
           VideoStitcherSignalSlots::tr("Panorama snapshot failed").toStdString(), potSurf.status()}));
      return;
    }
    surf = potSurf.release();
  } else {
    showError(
        VideoStitch::Core::ControllerStatus({VideoStitch::Origin::Output, VideoStitch::ErrType::SetupFailure,
                                             VideoStitcherSignalSlots::tr("Panorama snapshot failed").toStdString()}));
    return;
  }

  VideoStitch::Potential<typename Controller::Output> output =
      controller.createBlockingStitchOutput(std::shared_ptr<VideoStitch::Core::PanoSurface>(surf),
                                            std::shared_ptr<typename Controller::Output::Writer>(writer));
  // Stitcher writer creation error
  if (!output.ok()) {
    setupLock.unlock();
    showError(VideoStitch::Core::ControllerStatus(
        {VideoStitch::Origin::Output, VideoStitch::ErrType::SetupFailure,
         VideoStitcherSignalSlots::tr("Panorama snapshot failed").toStdString(), output.status()}));
    return;
  }

  std::unique_ptr<typename Controller::Output> releasedOutput(output.release());
  VideoStitch::Status status;
  status = controller.stitch(releasedOutput.get(), false).getStatus();
  setupLock.unlock();
  QDir::setCurrent(currentDir);

  // Stitching the output error
  if (!status.ok()) {
    showError(VideoStitch::Core::ControllerStatus(
        {VideoStitch::Origin::Stitcher, VideoStitch::ErrType::RuntimeError,
         VideoStitcherSignalSlots::tr("Panorama snapshot failed").toStdString(), status}));
    return;
  }
  emit snapshotPanoramaExported();
}

template <typename Controller>
void VideoStitcher<Controller>::onSnapshotSources(std::vector<VideoStitch::Ptv::Value*> outputConfigs) {
  VS_TH_ASSERT();
  VideoStitch::Status status = VideoStitch::Status::OK();
  setupLock.lockForRead();
  for (uint output = 0; output < outputConfigs.size(); ++output) {
    auto outputConfig = outputConfigs[output];

    auto extract = [&]() -> VideoStitch::Status {
      VideoStitch::Potential<VideoStitch::Output::Output> writer =
          VideoStitch::Output::create(*outputConfig, "jpg", outputConfig->get("width")->asInt(),
                                      outputConfig->get("height")->asInt(), controller.getFrameRate());
      FAIL_RETURN(writer.status());
      auto surf = VideoStitch::Core::OffscreenAllocator::createSourceSurface(
          project.getPanoConst()->getInput(output).getWidth(), project.getPanoConst()->getInput(output).getHeight(),
          "onSnapshotSources");
      FAIL_RETURN(surf.status());
      std::shared_ptr<VideoStitch::Output::VideoWriter> sharedWriter(writer.release()->getVideoWriter());
      VideoStitch::Potential<VideoStitch::Core::ExtractOutput> extOut = controller.createBlockingExtractOutput(
          output, std::shared_ptr<VideoStitch::Core::SourceSurface>(surf.release()), nullptr, sharedWriter);
      FAIL_RETURN(extOut.status());
      return controller.extract(extOut.object(), false).getStatus();
    };
    VideoStitch::Status extractStatus = extract();
    if (status.ok()) {
      status = extractStatus;
    }
  }
  setupLock.unlock();
  // Report one error with all the file names:
  if (!status.ok()) {
    showError(VideoStitch::Core::ControllerStatus({VideoStitch::Origin::Stitcher, VideoStitch::ErrType::RuntimeError,
                                                   VideoStitcherSignalSlots::tr("Input snapshot failed").toStdString(),
                                                   status}));
  }
  for (auto output : outputConfigs) {
    delete output;
  }
}

// explicit instantiations
template class VideoStitcher<VideoStitch::Core::Controller>;
template class VideoStitcher<VideoStitch::Core::StereoController>;