/******************************************************************************
 * The MIT License (MIT)
 *
 * Copyright (c) 2019-2021 Baldur Karlsson
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 ******************************************************************************/

#include "LiveCapture.h"
#include <QDesktopServices>
#include <QMenu>
#include <QMetaProperty>
#include <QMouseEvent>
#include <QPainter>
#include <QProcess>
#include <QScrollBar>
#include <QStyledItemDelegate>
#include <QToolBar>
#include <QToolButton>
#include "Code/QRDUtils.h"
#include "Code/Resources.h"
#include "Code/qprocessinfo.h"
#include "Widgets/Extended/RDLabel.h"
#include "Windows/MainWindow.h"
#include "toolwindowmanager/ToolWindowManager.h"
#include "ui_LiveCapture.h"

static const int PIDRole = Qt::UserRole + 1;
static const int IdentRole = Qt::UserRole + 2;
static const int CapPtrRole = Qt::UserRole + 3;

class NameEditOnlyDelegate : public QStyledItemDelegate
{
public:
  LiveCapture *live;
  NameEditOnlyDelegate(LiveCapture *l) : live(l) {}
  void setEditorData(QWidget *editor, const QModelIndex &index) const override
  {
    QByteArray n = editor->metaObject()->userProperty().name();
    QListWidgetItem *item = live->ui->captures->item(index.row());

    if(!n.isEmpty() && item)
    {
      LiveCapture::Capture *cap = live->GetCapture(item);
      if(cap)
        editor->setProperty(n, cap->name);
    }
  }

  void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override
  {
    QByteArray n = editor->metaObject()->userProperty().name();
    QListWidgetItem *item = live->ui->captures->item(index.row());

    if(!n.isEmpty() && item)
    {
      LiveCapture::Capture *cap = live->GetCapture(item);
      if(cap)
      {
        cap->name = editor->property(n).toString();
        item->setText(live->MakeText(cap));
      }
    }
  }
};

LiveCapture::LiveCapture(ICaptureContext &ctx, const QString &hostname, const QString &friendlyname,
                         uint32_t ident, MainWindow *main, QWidget *parent)
    : QFrame(parent),
      ui(new Ui::LiveCapture),
      m_Ctx(ctx),
      m_Hostname(hostname),
      m_HostFriendlyname(friendlyname),
      m_RemoteIdent(ident),
      m_Main(main)
{
  ui->setupUi(this);

  m_Disconnect.release();

  QObject::connect(&childUpdateTimer, &QTimer::timeout, this, &LiveCapture::childUpdate);
  childUpdateTimer.setSingleShot(false);
  childUpdateTimer.setInterval(1000);
  childUpdateTimer.start();

  QObject::connect(&countdownTimer, &QTimer::timeout, this, &LiveCapture::captureCountdownTick);
  countdownTimer.setSingleShot(true);
  countdownTimer.setInterval(1000);

  childUpdate();

  ui->previewSplit->setCollapsible(1, true);
  ui->previewSplit->setSizes({1, 0});

  QObject::connect(ui->preview, &RDLabel::clicked, this, &LiveCapture::preview_mouseClick);
  QObject::connect(ui->preview, &RDLabel::mouseMoved, this, &LiveCapture::preview_mouseMove);

  ui->preview->setMouseTracking(true);

  setTitle(tr("Connecting"));
  ui->connectionStatus->setText(tr("Connecting"));
  ui->connectionIcon->setPixmap(Pixmaps::hourglass(ui->connectionIcon));

  ui->apiIcon->setVisible(false);

  ui->triggerDelayedCapture->setEnabled(false);
  ui->triggerImmediateCapture->setEnabled(false);
  ui->queueCap->setEnabled(false);
  ui->cycleActiveWindow->setEnabled(false);

  ui->target->setText(QString());

  ui->progressLabel->setVisible(false);
  ui->progressBar->setVisible(false);

  ui->captures->setItemDelegate(new NameEditOnlyDelegate(this));

  ui->captures->verticalScrollBar()->setSingleStep(20);

  {
    QToolBar *bottomTools = new QToolBar(this);

    QWidget *rightAlign = new QWidget(this);
    rightAlign->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
    bottomTools->addWidget(rightAlign);

    previewToggle = new QAction(tr("Preview"), this);
    previewToggle->setCheckable(true);

    bottomTools->addAction(previewToggle);

    QMenu *openMenu = new QMenu(tr("&Open in..."), this);
    QAction *thisAction = new QAction(tr("This instance"), this);
    newWindowAction = new QAction(tr("New instance"), this);

    openMenu->addAction(thisAction);
    openMenu->addAction(newWindowAction);

    openButton = new QToolButton(this);
    openButton->setText(tr("Open"));
    openButton->setPopupMode(QToolButton::MenuButtonPopup);
    openButton->setMenu(openMenu);

    bottomTools->addWidget(openButton);

    saveAction = new QAction(tr("Save"), this);
    deleteAction = new QAction(tr("Delete"), this);

    bottomTools->addAction(saveAction);
    bottomTools->addAction(deleteAction);

    QObject::connect(previewToggle, &QAction::toggled, this, &LiveCapture::previewToggle_toggled);
    QObject::connect(openButton, &QToolButton::clicked, this, &LiveCapture::openCapture_triggered);
    QObject::connect(thisAction, &QAction::triggered, this, &LiveCapture::openCapture_triggered);
    QObject::connect(newWindowAction, &QAction::triggered, this,
                     &LiveCapture::openNewWindow_triggered);
    QObject::connect(saveAction, &QAction::triggered, this, &LiveCapture::saveCapture_triggered);
    QObject::connect(deleteAction, &QAction::triggered, this, &LiveCapture::deleteCapture_triggered);

    QObject::connect(ui->captures, &RDListWidget::keyPress, this, &LiveCapture::captures_keyPress);

    ui->mainLayout->addWidget(bottomTools);
  }
}

LiveCapture::~LiveCapture()
{
  m_Main->LiveCaptureClosed(this);

  cleanItems();
  killThread();

  delete ui;
}

void LiveCapture::QueueCapture(int frameNumber, int numFrames)
{
  m_QueueCaptureFrameNum = frameNumber;
  m_CaptureNumFrames = numFrames;
  m_QueueCapture.release();
}

void LiveCapture::showEvent(QShowEvent *event)
{
  if(!m_ConnectThread)
  {
    m_ConnectThread = new LambdaThread([this]() { this->connectionThreadEntry(); });
    m_ConnectThread->start();
  }

  on_captures_itemSelectionChanged();
}

void LiveCapture::on_captures_mouseClicked(QMouseEvent *e)
{
  if(e->buttons() & Qt::RightButton && !ui->captures->selectedItems().empty())
  {
    QMenu contextMenu(this);

    QMenu contextOpenMenu(tr("&Open in..."), this);
    QAction thisAction(tr("This instance"), this);
    QAction newAction(tr("New instance"), this);

    contextOpenMenu.addAction(&thisAction);
    contextOpenMenu.addAction(&newAction);

    QAction contextSaveAction(tr("&Save"), this);
    QAction contextDeleteAction(tr("&Delete"), this);

    contextMenu.addAction(contextOpenMenu.menuAction());
    contextMenu.addAction(&contextSaveAction);
    contextMenu.addAction(&contextDeleteAction);

    if(ui->captures->selectedItems().size() == 1)
    {
      newAction.setEnabled(GetCapture(ui->captures->selectedItems()[0])->local);
    }
    else
    {
      contextOpenMenu.setEnabled(false);
      contextSaveAction.setText(tr("&Save All"));
      contextDeleteAction.setText(tr("&Delete All"));
    }

    QObject::connect(&thisAction, &QAction::triggered, this, &LiveCapture::openCapture_triggered);
    QObject::connect(&newAction, &QAction::triggered, this, &LiveCapture::openNewWindow_triggered);
    QObject::connect(&contextSaveAction, &QAction::triggered, this,
                     &LiveCapture::saveCapture_triggered);
    QObject::connect(&contextDeleteAction, &QAction::triggered, this,
                     &LiveCapture::deleteCapture_triggered);

    m_ContextMenu = &contextMenu;
    RDDialog::show(&contextMenu, QCursor::pos());
    m_ContextMenu = NULL;
  }
}

void LiveCapture::on_captures_itemActivated(QListWidgetItem *item)
{
  openCapture_triggered();
}

void LiveCapture::on_childProcesses_itemActivated(QListWidgetItem *item)
{
  QList<QListWidgetItem *> sel = ui->childProcesses->selectedItems();
  if(sel.count() == 1)
  {
    uint32_t ident = sel[0]->data(IdentRole).toUInt();
    if(ident > 0)
    {
      LiveCapture *live =
          new LiveCapture(m_Ctx, m_Hostname, m_HostFriendlyname, ident, m_Main, m_Main);
      m_Main->ShowLiveCapture(live);
    }
  }
}

void LiveCapture::on_queueCap_clicked()
{
  m_CaptureNumFrames = (int)ui->numFrames->value();
  m_QueueCaptureFrameNum = (int)ui->captureFrame->value();
  m_QueueCapture.release();
}

void LiveCapture::on_triggerImmediateCapture_clicked()
{
  m_CaptureNumFrames = (int)ui->numFrames->value();
  m_TriggerCapture.release();
}

void LiveCapture::on_cycleActiveWindow_clicked()
{
  m_CycleWindow.release();
}

void LiveCapture::on_triggerDelayedCapture_clicked()
{
  if(ui->captureDelay->value() == 0.0)
  {
    on_triggerImmediateCapture_clicked();
  }
  else
  {
    m_CaptureCounter = (int)ui->captureDelay->value();
    countdownTimer.start();
    ui->triggerDelayedCapture->setEnabled(false);
    ui->triggerDelayedCapture->setText(tr("Triggering in %1s").arg(m_CaptureCounter));
  }
}

void LiveCapture::openCapture_triggered()
{
  if(ui->captures->selectedItems().size() == 1)
    openCapture(GetCapture(ui->captures->selectedItems()[0]));
}

void LiveCapture::openNewWindow_triggered()
{
  if(ui->captures->selectedItems().size() == 1)
  {
    Capture *cap = GetCapture(ui->captures->selectedItems()[0]);

    QString temppath = m_Ctx.TempCaptureFilename(lit("newwindow"));

    if(!cap->local)
    {
      RDDialog::critical(this, tr("Cannot open new instance"),
                         tr("Can't open capture in new instance with remote server in use"));
      return;
    }

    QFile f(cap->path);

    if(!f.copy(temppath))
    {
      RDDialog::critical(this, tr("Cannot save temporary capture"),
                         tr("Couldn't save capture to temporary location\n%1").arg(f.errorString()));
      return;
    }

    QStringList args;
    args << lit("--tempfile") << temppath;
    QProcess::startDetached(qApp->applicationFilePath(), args);
  }
}

void LiveCapture::saveCapture_triggered()
{
  if(ui->captures->selectedItems().size() == 1)
  {
    saveCapture(GetCapture(ui->captures->selectedItems()[0]), QString());
  }
  else
  {
    QString path = m_Main->GetSavePath(tr("Save All Captures As"));

    if(!path.isEmpty())
    {
      if(path.endsWith(lit(".rdc")))
        path.chop(4);

      // don't save duplicates if we have multiple captures from the same frame (possible if the
      // application is not presenting at all and using the API to capture)
      QMap<uint32_t, uint32_t> existingFiles;

      for(QListWidgetItem *item : ui->captures->selectedItems())
      {
        Capture *cap = GetCapture(item);

        QString filename = QFormatStr("%1-frame%2").arg(path).arg(cap->frameNumber);
        if(cap->frameNumber == ~0U)
          filename = QFormatStr("%1-capture").arg(path);

        if(existingFiles.contains(cap->frameNumber))
        {
          filename += QFormatStr("_%1").arg(existingFiles[cap->frameNumber]);
          existingFiles[cap->frameNumber]++;
        }
        else
        {
          // start on 2 next time
          existingFiles[cap->frameNumber] = 2;
        }

        saveCapture(cap, QFormatStr("%1.rdc").arg(filename));
      }
    }
  }
}

void LiveCapture::deleteCapture_triggered()
{
  bool allow = checkAllowDelete();

  if(!allow)
    return;

  QList<QListWidgetItem *> sel = ui->captures->selectedItems();

  for(QListWidgetItem *item : sel)
  {
    Capture *cap = GetCapture(item);

    if(!cap->saved)
    {
      if(cap->path == m_Ctx.GetCaptureFilename())
      {
        m_Main->takeCaptureOwnership();
        m_Main->CloseCapture();
      }
      else
      {
        // if connected, prefer using the live connection
        if(m_Connected.available() && !cap->local)
        {
          QMutexLocker l(&m_DeleteCapturesLock);
          m_DeleteCaptures.push_back(cap->remoteID);
        }
        else
        {
          m_Ctx.Replay().DeleteCapture(cap->path, cap->local);
        }

        if(cap->local)
        {
          m_Main->RemoveRecentCapture(cap->path);
        }
      }
    }

    delete cap;

    delete ui->captures->takeItem(ui->captures->row(item));
  }
}

void LiveCapture::childUpdate()
{
  // first do a small lock and check if the list is currently empty
  {
    QMutexLocker l(&m_ChildrenLock);

    if(m_Children.empty())
    {
      ui->childProcessLabel->setVisible(false);
      ui->childProcesses->setVisible(false);
    }
  }

  // enumerate processes outside of the lock
  QProcessList processes = QProcessInfo::enumerate(false);

  // now since we're adding and removing, we lock around the whole rest of the function. It won't be
  // too slow.
  {
    QMutexLocker l(&m_ChildrenLock);

    if(!m_Children.empty())
    {
      // remove any stale processes
      for(int i = 0; i < m_Children.size(); i++)
      {
        bool found = false;

        for(QProcessInfo &p : processes)
        {
          if(p.pid() == m_Children[i].PID)
          {
            found = true;
            break;
          }
        }

        if(!found)
        {
          if(m_Children[i].added)
          {
            for(int c = 0; c < ui->childProcesses->count(); c++)
            {
              QListWidgetItem *item = ui->childProcesses->item(c);
              if(item->data(PIDRole).toUInt() == m_Children[i].PID)
                delete ui->childProcesses->takeItem(c);
            }
          }

          // process expired/doesn't exist anymore
          m_Children.removeAt(i);

          // don't increment i, check the next element at i (if we weren't at the end
          i--;
        }
      }

      for(int i = 0; i < m_Children.size(); i++)
      {
        if(!m_Children[i].added)
        {
          QString name = tr("Unknown Process");

          // find the name
          for(QProcessInfo &p : processes)
          {
            if(p.pid() == m_Children[i].PID)
            {
              name = p.name();
              break;
            }
          }

          QString text = QFormatStr("%1 [PID %2]").arg(name).arg(m_Children[i].PID);

          m_Children[i].added = true;
          QListWidgetItem *item = new QListWidgetItem(text, ui->childProcesses);
          item->setData(PIDRole, QVariant(m_Children[i].PID));
          item->setData(IdentRole, QVariant(m_Children[i].ident));
          ui->childProcesses->addItem(item);
        }
      }
    }

    if(!m_Children.empty())
    {
      ui->childProcessLabel->setVisible(true);
      ui->childProcesses->setVisible(true);
    }
    else
    {
      ui->childProcessLabel->setVisible(false);
      ui->childProcesses->setVisible(false);
    }
  }
}

void LiveCapture::captureCountdownTick()
{
  m_CaptureCounter--;

  if(m_CaptureCounter == 0)
  {
    m_CaptureNumFrames = (int)ui->numFrames->value();
    ui->triggerDelayedCapture->setEnabled(true);
    ui->triggerDelayedCapture->setText(tr("Trigger After Delay"));
    m_TriggerCapture.release();
  }
  else
  {
    countdownTimer.start();
    ui->triggerDelayedCapture->setText(tr("Triggering in %1s").arg(m_CaptureCounter));
  }
}

void LiveCapture::killThread()
{
  if(m_ConnectThread)
  {
    m_Disconnect.acquire();
    m_ConnectThread->wait();
    m_ConnectThread->deleteLater();
  }
}

void LiveCapture::setTitle(const QString &title)
{
  setWindowTitle((!m_HostFriendlyname.isEmpty() ? (m_HostFriendlyname + lit(" - ")) : QString()) +
                 title);
}

LiveCapture::Capture *LiveCapture::GetCapture(QListWidgetItem *item)
{
  return (Capture *)item->data(CapPtrRole).value<void *>();
}

void LiveCapture::AddCapture(QListWidgetItem *item, Capture *cap)
{
  item->setData(CapPtrRole, QVariant::fromValue<void *>(cap));
}

QImage LiveCapture::MakeThumb(const QImage &screenshot)
{
  const QSizeF thumbSize(ui->captures->iconSize());
  const QSizeF imSize(screenshot.size());

  float x = 0, y = 0;
  float width = 0, height = 0;

  const float srcaspect = imSize.width() / imSize.height();
  const float dstaspect = thumbSize.width() / thumbSize.height();

  if(srcaspect > dstaspect)
  {
    width = thumbSize.width();
    height = width / srcaspect;

    y = (thumbSize.height() - height) / 2;
  }
  else
  {
    height = thumbSize.height();
    width = height * srcaspect;

    x = (thumbSize.width() - width) / 2;
  }

  QImage ret(thumbSize.width(), thumbSize.height(), QImage::Format_RGBA8888);
  ret.fill(Qt::transparent);
  QPainter paint(&ret);
  paint.drawImage(QRectF(x, y, width, height),
                  screenshot.scaled(width, height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
  return ret;
}

bool LiveCapture::checkAllowDelete()
{
  bool needcheck = false;

  for(int i = 0; i < ui->captures->count(); i++)
  {
    Capture *cap = GetCapture(ui->captures->item(i));
    needcheck |= !cap->saved;
  }

  if(!needcheck || ui->captures->selectedItems().empty())
    return true;

  ToolWindowManager::raiseToolWindow(this);

  QMessageBox::StandardButton res =
      RDDialog::question(this, tr("Unsaved capture(s)", "", ui->captures->selectedItems().size()),
                         tr("Are you sure you wish to delete the capture(s)?\nAny capture "
                            "currently opened will be closed",
                            "", ui->captures->selectedItems().size()),
                         RDDialog::YesNoCancel);

  return (res == QMessageBox::Yes);
}

void LiveCapture::updateAPIStatus()
{
  QString apiStatus;

  bool nonpresenting = false;

  // add any fully working APIs first in the list.
  for(QString api : m_APIs.keys())
  {
    if(m_APIs[api].supported && m_APIs[api].presenting)
      apiStatus += lit(", <b>%1 (Active)</b>").arg(api);
  }

  // then add any problem APIs
  for(QString api : m_APIs.keys())
  {
    if(!m_APIs[api].supported)
    {
      apiStatus += tr(", %1 (Unsupported)").arg(api);
    }
    else if(!m_APIs[api].presenting)
    {
      apiStatus += tr(", %1 (Not Presenting)").arg(api);
      nonpresenting = true;
    }
  }

  // remove the redundant starting ", "
  apiStatus.remove(0, 2);

  ui->apiStatus->setText(apiStatus);

  ui->apiIcon->setVisible(nonpresenting);
}

QString LiveCapture::MakeText(Capture *cap)
{
  QString text = cap->name;
  if(!cap->local)
    text += tr(" (Remote)");

  text += lit("\n") + cap->api;
  if(cap->frameNumber == ~0U)
    text += tr("\nUser-defined Capture");
  else
    text += tr("\nFrame #%1").arg(cap->frameNumber);

  if(cap->byteSize > 0)
    text += QFormatStr(" (%1 MB)").arg(double(cap->byteSize) / 1000000.0, 0, 'f', 2);

  text += cap->timestamp.toString(lit("\nyyyy-MM-dd HH:mm:ss"));

  return text;
}

bool LiveCapture::checkAllowClose()
{
  m_IgnoreThreadClosed = true;

  bool suppressRemoteWarning = false;
  bool notoall = false;

  QMessageBox::StandardButtons msgFlags = RDDialog::YesNoCancel;

  if(ui->captures->count() > 1)
    msgFlags |= QMessageBox::NoToAll;

  for(int i = 0; i < ui->captures->count(); i++)
  {
    QListWidgetItem *item = ui->captures->item(i);
    Capture *cap = GetCapture(ui->captures->item(i));

    if(cap->saved)
      continue;

    ui->captures->clearSelection();
    ToolWindowManager::raiseToolWindow(this);
    ui->captures->setFocus();
    item->setSelected(true);

    QMessageBox::StandardButton res = QMessageBox::No;

    if(!suppressRemoteWarning && !notoall)
    {
      QString frameName = tr("Frame #%1").arg(cap->frameNumber);
      if(cap->frameNumber == ~0U)
        frameName = tr("User-defined Capture");

      res = RDDialog::question(this, tr("Unsaved capture"),
                               tr("Save this capture '%1 %2' at %3?")
                                   .arg(cap->name)
                                   .arg(frameName)
                                   .arg(cap->timestamp.toString(lit("HH:mm:ss"))),
                               msgFlags);

      if(res == QMessageBox::NoToAll)
      {
        notoall = true;
        res = QMessageBox::No;
      }
    }

    if(res == QMessageBox::Cancel)
    {
      m_IgnoreThreadClosed = false;
      return false;
    }

    // we either have to save or delete the capture. Make sure that if it's remote that we are able
    // to by having an active connection or replay context on that host.
    if(suppressRemoteWarning == false && !m_Connected.available() && !cap->local &&
       m_Ctx.Replay().CurrentRemote().Hostname() != rdcstr(m_Hostname))
    {
      QMessageBox::StandardButton res2 = RDDialog::question(
          this, tr("No active replay context"),
          tr("This capture is on remote host %1 and there is no active replay context on that "
             "host.\n")
                  .arg(m_HostFriendlyname) +
              tr("Without an active replay context the capture cannot be %1.\n\n")
                  .arg(tr(res == QMessageBox::Yes ? "saved" : "deleted")) +
              tr("Would you like to continue and discard this capture and any others, to be left "
                 "in the temporary folder on the remote machine?"),
          RDDialog::YesNoCancel);

      if(res2 == QMessageBox::Yes)
      {
        suppressRemoteWarning = true;
        res = QMessageBox::No;
      }
      else
      {
        m_IgnoreThreadClosed = false;
        return false;
      }
    }

    if(res == QMessageBox::Yes)
    {
      bool success = saveCapture(cap, QString());

      if(!success)
      {
        m_IgnoreThreadClosed = false;
        return false;
      }
    }
  }

  m_IgnoreThreadClosed = false;
  return true;
}

void LiveCapture::openCapture(Capture *cap)
{
  cap->opened = true;

  if(!cap->local && m_Ctx.Replay().CurrentRemote().Hostname() != rdcstr(m_Hostname))
  {
    RDDialog::critical(
        this, tr("No active replay context"),
        tr("This capture is on remote host %1 and there is no active replay context on that "
           "host.\nYou can either save the capture locally, or switch to a replay context on %1.")
            .arg(m_HostFriendlyname));
    return;
  }

  m_Main->LoadCapture(cap->path, m_Ctx.Config().DefaultReplayOptions, !cap->saved, cap->local);
}

bool LiveCapture::saveCapture(Capture *cap, QString path)
{
  if(path.isEmpty())
  {
    path = m_Main->GetSavePath();

    if(path.isEmpty())
      return false;
  }

  if(QString(m_Ctx.GetCaptureFilename()) == path)
  {
    RDDialog::critical(this, tr("Cannot save"), tr("Can't overwrite currently open capture at %1\n"
                                                   "Close the capture or save to another location.")
                                                    .arg(path));
    return false;
  }

  // we copy the temp capture to the desired path, but the capture item remains referring to the
  // temp path.
  // This ensures that if the user deletes the saved path we can still open or re-save it.
  if(cap->local)
  {
    QFile src(cap->path);
    QFile dst(path);

    // remove any existing file, the user was already prompted to overwrite
    if(dst.exists())
    {
      if(!dst.remove())
      {
        RDDialog::critical(this, tr("Cannot save"),
                           tr("Couldn't remove file at %1\n%2").arg(path).arg(dst.errorString()));
        return false;
      }
    }

    if(!src.copy(path))
    {
      RDDialog::critical(this, tr("Cannot save"),
                         tr("Couldn't copy file to %1\n%2").arg(path).arg(src.errorString()));
      return false;
    }
  }
  else if(m_Connected.available())
  {
    // if we have a current live connection, prefer using it
    m_CopyCaptureLocalPath = path;
    m_CopyCaptureID = cap->remoteID;

    m_CopyCapture.release();
  }
  else
  {
    if(m_Ctx.Replay().CurrentRemote().Hostname() != rdcstr(m_Hostname))
    {
      RDDialog::critical(this, tr("No active replay context"),
                         tr("This capture is on remote host %1 and there is no active replay "
                            "context on that host.\n")
                                 .arg(m_Hostname) +
                             tr("Without an active replay context the capture cannot be saved, "
                                "try switching to a replay context on %1.")
                                 .arg(m_Hostname));
      return false;
    }

    m_Ctx.Replay().CopyCaptureFromRemote(cap->path, path, this);

    if(!QFile::exists(path))
    {
      RDDialog::critical(this, tr("Cannot save"),
                         tr("File couldn't be transferred from remote host"));
      return false;
    }

    m_Ctx.Replay().DeleteCapture(cap->path, false);
  }

  // delete the temporary copy
  if(!cap->saved)
    m_Ctx.Replay().DeleteCapture(cap->path, cap->local);

  m_Main->RemoveRecentCapture(cap->path);
  cap->saved = true;
  cap->path = path;
  AddRecentFile(m_Ctx.Config().RecentCaptureFiles, path);
  m_Main->PopulateRecentCaptureFiles();
  return true;
}

void LiveCapture::cleanItems()
{
  for(int i = 0; i < ui->captures->count(); i++)
  {
    Capture *cap = GetCapture(ui->captures->item(i));

    if(!cap->saved)
    {
      if(cap->path == m_Ctx.GetCaptureFilename())
      {
        m_Main->takeCaptureOwnership();
      }
      else
      {
        // if connected, prefer using the live connection
        if(m_Connected.available() && !cap->local)
        {
          QMutexLocker l(&m_DeleteCapturesLock);
          m_DeleteCaptures.push_back(cap->remoteID);
        }
        else
        {
          m_Ctx.Replay().DeleteCapture(cap->path, cap->local);
        }

        if(cap->local)
        {
          m_Main->RemoveRecentCapture(cap->path);
        }
      }
    }

    delete cap;
  }
  ui->captures->clear();
}

void LiveCapture::previewToggle_toggled(bool checked)
{
  if(m_IgnorePreviewToggle)
    return;

  if(checked)
    ui->previewSplit->setSizes({1, 1});
  else
    ui->previewSplit->setSizes({1, 0});
}

void LiveCapture::on_previewSplit_splitterMoved(int pos, int index)
{
  m_IgnorePreviewToggle = true;

  QList<int> sizes = ui->previewSplit->sizes();

  previewToggle->setChecked(sizes[1] != 0);

  m_IgnorePreviewToggle = false;
}

void LiveCapture::on_apiIcon_clicked(QMouseEvent *event)
{
  QDesktopServices::openUrl(QUrl(lit("https://renderdoc.org/docs/in_application_api.html")));
}

void LiveCapture::captures_keyPress(QKeyEvent *e)
{
  if(e->key() == Qt::Key_Delete)
  {
    deleteCapture_triggered();
  }
}

void LiveCapture::preview_mouseClick(QMouseEvent *e)
{
  QPoint mouse = QCursor::pos();
  if(e->buttons() & Qt::LeftButton)
  {
    previewDragStart = mouse;
    ui->preview->setCursor(QCursor(Qt::SizeAllCursor));
  }
}

void LiveCapture::preview_mouseMove(QMouseEvent *e)
{
  QPoint mouse = QCursor::pos();
  if(e->buttons() & Qt::LeftButton)
  {
    QScrollBar *h = ui->previewScroll->horizontalScrollBar();
    QScrollBar *v = ui->previewScroll->verticalScrollBar();

    h->setValue(h->value() + previewDragStart.x() - mouse.x());
    v->setValue(v->value() + previewDragStart.y() - mouse.y());

    previewDragStart = mouse;
  }
  else
  {
    ui->preview->unsetCursor();
  }
}

void LiveCapture::on_captures_itemSelectionChanged()
{
  int numSelected = ui->captures->selectedItems().size();

  openButton->setEnabled(numSelected == 1);

  if(ui->captures->selectedItems().size() == 1)
  {
    QListWidgetItem *item = ui->captures->selectedItems()[0];
    Capture *cap = GetCapture(item);

    newWindowAction->setEnabled(cap->local);

    if(cap->thumb.width() > 0)
    {
      ui->preview->setPixmap(QPixmap::fromImage(cap->thumb));
      ui->preview->setMinimumSize(cap->thumb.size());
      ui->preview->setMaximumSize(cap->thumb.size());
    }
    else
    {
      ui->preview->setPixmap(QPixmap());
      ui->preview->setMinimumSize(QSize(16, 16));
      ui->preview->setMaximumSize(QSize(16, 16));
    }
  }
}

void LiveCapture::captureCopied(uint32_t ID, const QString &localPath)
{
  for(int i = 0; i < ui->captures->count(); i++)
  {
    QListWidgetItem *item = ui->captures->item(i);
    Capture *cap = GetCapture(ui->captures->item(i));

    if(cap && cap->remoteID == ID)
    {
      cap->local = true;
      cap->path = localPath;
      QFont f = item->font();
      f.setItalic(false);
      item->setFont(f);
      item->setText(MakeText(cap));
    }
  }
}

void LiveCapture::captureAdded(const QString &name, const NewCaptureData &newCapture)
{
  Capture *cap = new Capture();

  cap->name = name;

  cap->api = newCapture.api;

  cap->timestamp =
      QDateTime(QDate(1970, 1, 1), QTime(0, 0, 0), Qt::UTC).addSecs(newCapture.timestamp).toLocalTime();
  cap->byteSize = newCapture.byteSize;

  cap->thumb = QImage(newCapture.thumbnail.data(), newCapture.thumbWidth, newCapture.thumbHeight,
                      newCapture.thumbWidth * 3, QImage::Format_RGB888)
                   .copy(0, 0, newCapture.thumbWidth, newCapture.thumbHeight);

  cap->remoteID = newCapture.captureId;
  cap->saved = false;
  cap->path = newCapture.path;
  cap->local = newCapture.local;
  cap->frameNumber = newCapture.frameNumber;

  QListWidgetItem *item = new QListWidgetItem();
  item->setFlags(item->flags() | Qt::ItemIsEditable);
  item->setText(MakeText(cap));
  item->setIcon(QIcon(QPixmap::fromImage(MakeThumb(cap->thumb))));
  if(!newCapture.local)
  {
    QFont f = item->font();
    f.setItalic(true);
    item->setFont(f);
  }

  AddCapture(item, cap);

  ui->captures->addItem(item);
}

void LiveCapture::connectionClosed()
{
  ui->progressLabel->setVisible(false);
  ui->progressBar->setVisible(false);

  if(m_IgnoreThreadClosed)
    return;

  if(ui->captures->count() <= 1)
  {
    if(ui->captures->count() == 1)
    {
      Capture *cap = GetCapture(ui->captures->item(0));

      // only auto-open a non-local capture if we are successfully connected
      // to this machine as a remote context
      if(!cap->local)
      {
        if(m_Ctx.Replay().CurrentRemote().Hostname() != rdcstr(m_Hostname))
          return;
      }

      // don't close if a dialog is open
      if(QApplication::activeModalWidget() || QApplication::activePopupWidget())
        return;

      if(cap->opened)
        return;

      openCapture(cap);
      if(!cap->saved)
      {
        cap->saved = true;
        m_Main->takeCaptureOwnership();
      }
    }

    // auto-close and load capture if we got a capture. If we
    // don't have any captures but DO have child processes,
    // then don't close just yet.
    if(ui->captures->count() == 1 || m_Children.count() == 0)
    {
      // raise the texture viewer if it exists, instead of falling back to most likely the capture
      // executable dialog which is not useful.
      if(ui->captures->count() == 1 && m_Ctx.HasTextureViewer())
        m_Ctx.ShowTextureViewer();
      selfClose();
      return;
    }

    // if we have no captures and only one child, close and
    // open up a connection to it (similar to behaviour with
    // only one capture
    if(ui->captures->count() == 0 && m_Children.count() == 1)
    {
      LiveCapture *live =
          new LiveCapture(m_Ctx, m_Hostname, m_HostFriendlyname, m_Children[0].ident, m_Main);
      m_Main->ShowLiveCapture(live);
      selfClose();
      return;
    }
  }
}

void LiveCapture::selfClose()
{
  if(m_ContextMenu)
  {
    qInfo() << "preventing race";
    // hide the menu and close our window shortly after
    m_ContextMenu->close();
    QTimer *timer = new QTimer(this);
    QObject::connect(timer, &QTimer::timeout, [this]() { ToolWindowManager::closeToolWindow(this); });
    timer->setSingleShot(true);
    timer->start(250);
  }
  else
  {
    ToolWindowManager::closeToolWindow(this);
  }
}

void LiveCapture::connectionThreadEntry()
{
  ITargetControl *conn =
      RENDERDOC_CreateTargetControl(m_Hostname, m_RemoteIdent, GetSystemUsername(), true);
  m_Connected.release();

  if(!conn || !conn->Connected())
  {
    if(conn)
      conn->Shutdown();

    GUIInvoke::call(this, [this]() {
      setTitle(tr("Connection failed"));
      ui->connectionStatus->setText(tr("Failed"));
      ui->connectionIcon->setPixmap(Pixmaps::del(ui->connectionIcon));

      connectionClosed();
    });

    m_Connected.acquire();
    return;
  }

  uint32_t pid = conn->GetPID();
  QString target = conn->GetTarget();

  GUIInvoke::call(this, [this, pid, target]() {
    if(!m_Connected.available())
      return;

    if(pid)
      setTitle(QFormatStr("%1 [PID %2]").arg(target).arg(pid));
    else
      setTitle(target);

    ui->target->setText(windowTitle());
    ui->connectionIcon->setPixmap(Pixmaps::connect(ui->connectionIcon));
    ui->connectionStatus->setText(tr("Established"));
  });

  while(conn && conn->Connected())
  {
    if(m_TriggerCapture.tryAcquire())
    {
      conn->TriggerCapture((uint)m_CaptureNumFrames);
      m_CaptureNumFrames = 1;
    }

    if(m_QueueCapture.tryAcquire())
    {
      conn->QueueCapture((uint32_t)m_QueueCaptureFrameNum, (uint32_t)m_CaptureNumFrames);
      m_QueueCaptureFrameNum = 0;
      m_CaptureNumFrames = 1;
    }

    if(m_CopyCapture.tryAcquire())
    {
      conn->CopyCapture(m_CopyCaptureID, m_CopyCaptureLocalPath);
      m_CopyCaptureLocalPath = QString();
      m_CopyCaptureID = ~0U;
    }

    if(m_CycleWindow.tryAcquire())
    {
      conn->CycleActiveWindow();
    }

    QVector<uint32_t> dels;
    {
      QMutexLocker l(&m_DeleteCapturesLock);
      dels.swap(m_DeleteCaptures);
    }

    for(uint32_t del : dels)
      conn->DeleteCapture(del);

    if(!m_Disconnect.available())
    {
      conn->Shutdown();
      conn = NULL;
      m_Connected.acquire();
      return;
    }

    TargetControlMessage msg = conn->ReceiveMessage([this](float progress) {
      GUIInvoke::call(this, [this, progress]() {
        if(progress >= 0.0f && progress < 1.0f)
        {
          ui->progressLabel->setText(tr("Copy in Progress:"));
          ui->progressLabel->setVisible(true);
          ui->progressBar->setVisible(true);
          ui->progressBar->setMaximum(1000);
          ui->progressBar->setValue(1000 * progress);
        }
        else
        {
          ui->progressLabel->setVisible(false);
          ui->progressBar->setVisible(false);
        }
      });
    });

    if(msg.type == TargetControlMessageType::RegisterAPI)
    {
      QString api = msg.apiUse.name;
      bool presenting = msg.apiUse.presenting;
      bool supported = msg.apiUse.supported;
      GUIInvoke::call(this, [this, api, presenting, supported]() {
        m_APIs[api] = APIStatus(presenting, supported);

        if(presenting && supported)
        {
          ui->triggerImmediateCapture->setEnabled(true);
          ui->triggerDelayedCapture->setEnabled(true);
          ui->queueCap->setEnabled(true);
        }

        updateAPIStatus();
      });
    }

    if(msg.type == TargetControlMessageType::CaptureProgress)
    {
      float progress = msg.capProgress;
      GUIInvoke::call(this, [this, progress]() {

        if(progress >= 0.0f && progress < 1.0f)
        {
          ui->progressLabel->setText(tr("Capture in Progress:"));
          ui->progressLabel->setVisible(true);
          ui->progressBar->setVisible(true);
          ui->progressBar->setMaximum(1000);
          ui->progressBar->setValue(1000 * progress);
        }
        else
        {
          ui->progressLabel->setVisible(false);
          ui->progressBar->setVisible(false);
        }

      });
    }

    if(msg.type == TargetControlMessageType::NewCapture)
    {
      NewCaptureData cap = msg.newCapture;
      if(cap.api.isEmpty())
        cap.api = conn->GetAPI();
      QString name = conn->GetTarget();
      GUIInvoke::call(this, [this, name, cap]() { captureAdded(name, cap); });
    }

    if(msg.type == TargetControlMessageType::CaptureCopied)
    {
      uint32_t capID = msg.newCapture.captureId;
      QString path = msg.newCapture.path;

      GUIInvoke::call(this, [this, capID, path]() { captureCopied(capID, path); });
    }

    if(msg.type == TargetControlMessageType::NewChild)
    {
      if(msg.newChild.processId != 0)
      {
        ChildProcess c;
        c.PID = (int)msg.newChild.processId;
        c.ident = msg.newChild.ident;

        {
          QMutexLocker l(&m_ChildrenLock);
          m_Children.push_back(c);
        }

        // force a child update immediately, don't wait for the tick which is intended for decaying
        // processes that exit
        GUIInvoke::call(this, [this]() { childUpdate(); });
      }
    }

    if(msg.type == TargetControlMessageType::CapturableWindowCount)
    {
      uint32_t windows = msg.capturableWindowCount;
      GUIInvoke::call(this, [this, windows]() { ui->cycleActiveWindow->setEnabled(windows > 1); });
    }
  }

  if(conn)
  {
    conn->Shutdown();
    conn = NULL;
    m_Connected.acquire();
  }

  GUIInvoke::call(this, [this]() {
    ui->connectionStatus->setText(tr("Closed"));
    ui->connectionIcon->setPixmap(Pixmaps::disconnect(ui->connectionIcon));

    ui->numFrames->setEnabled(false);
    ui->captureDelay->setEnabled(false);
    ui->captureFrame->setEnabled(false);
    ui->triggerDelayedCapture->setEnabled(false);
    ui->triggerImmediateCapture->setEnabled(false);
    ui->queueCap->setEnabled(false);
    ui->cycleActiveWindow->setEnabled(false);

    ui->apiStatus->setText(tr("None"));
    ui->apiIcon->setVisible(false);

    connectionClosed();
  });
}
