//=========================================================================
//  Copyright (c) Kitware, Inc.
//  All rights reserved.
//  See LICENSE.txt for details.
//
//  This software is distributed WITHOUT ANY WARRANTY; without even
//  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
//  PURPOSE.  See the above copyright notice for more information.
//=========================================================================
#include "plugin/pqWindTunnelContainerEngineSetup.h"

#include "smtk/simulation/windtunnel/qt/qtOpenFoamRunner.h"
#include "smtk/simulation/windtunnel/qt/qtProgressDialog.h"
#include "smtk/simulation/windtunnel/qt/qtSessionData.h"

// Client side
#include "pqActiveObjects.h"
#include "pqCoreUtilities.h"
#include "pqFileDialog.h"
#include "pqServer.h"
#include "vtkSMProperty.h"
#include "vtkSMProxy.h"
#include "vtkSMSessionProxyManager.h"
#include "vtkSMStringVectorProperty.h"

// Qt
#include <QAction>
#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QIODevice>
#include <QIcon>
#include <QMessageBox>
#include <QStringList>
#include <QTextStream>
#include <QTimer>
#include <QWidget>
#include <QtGlobal>

pqWindTunnelContainerEngineSetup::pqWindTunnelContainerEngineSetup(QWidget* parentWidget)
  : m_process(new QProcess(this))
  , m_parentWidget(parentWidget)
  , m_progressDialog(new qtProgressDialog(parentWidget, 0, 0))
{
  // Setup progress dialog
  m_progressDialog->setModal(false);
  m_progressDialog->setCancelButtonVisible(false);
  m_progressDialog->setMessageBoxVisible(true, false);
  m_progressDialog->setAutoClose(true);
  m_progressDialog->setMinDuration(3);
  m_progressDialog->setAutoCloseDelay(5);

  // Setup connections
  QObject::connect(
    m_process, &QProcess::errorOccurred, this, &pqWindTunnelContainerEngineSetup::onProcessError);
  QObject::connect(
    m_process,
    QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
    this,
    &pqWindTunnelContainerEngineSetup::onProcessFinished);
  QObject::connect(m_process, &QProcess::readyReadStandardOutput, [this]() {
    if (m_processMode != ProcessMode::TryPull)
    {
      return;
    }

    while (m_process->canReadLine())
    {
      QByteArray bytes = m_process->readLine();
      m_progressDialog->setInfoText(bytes.data());
    }
  });
}

QString pqWindTunnelContainerEngineSetup::statusAsString() const
{
  QString str("Unknown");
  switch (m_status)
  {
    case Uninitialized:
      str = "Uninitialized";
      break;
    case Pending:
      str = "Pending";
      break;
    case Ready:
      str = "Ready";
      break;
    case Unavailable:
      str = "Unavailable";
      break;
    case Error:
      str = "Error";
      break;
  }
  return str;
}

bool pqWindTunnelContainerEngineSetup::start()
{
  m_containerEnginePath.clear();
  m_status = Pending;

  // Get path from settings
  QString path;
  bool ok = this->readSetting(path);
  if (!ok)
  {
    m_status = Error;
    QString msg;
    QTextStream qs(&msg);
    qs << "There was an internal error"
       << " that prevents modelbuilder from running the container engine."
       << " You will NOT be able to run OpenFOAM from modelbuilder.";
    QMessageBox::critical(m_parentWidget, "Internal Error", msg);
    return false;
  }

  if (path.isEmpty())
  {
    path = DEFAULT_CONTAINER_ENGINE; // default
  }

  this->tryLs(path);
  return true;
}

void pqWindTunnelContainerEngineSetup::tryLs(const QString& path)
{
  // Run container engine to see if openfoam image is in the local repository
  m_process->setProgram(path);
  m_processMode = TryLs;

  QStringList args;
  args << "image"
       << "ls"
       << "-q"
       << "--no-trunc" << OPENFOAM_DOCKER_IMAGE;
  m_process->setArguments(args);

  qDebug() << "Program:" << path;
  qDebug() << "Args:" << args;
  m_process->start(QIODevice::ReadOnly);
}

void pqWindTunnelContainerEngineSetup::askUserForPath(
  const QString& headline,
  const QString& details,
  QString& result) const
{
  result.clear();

  QMessageBox msgBox(m_parentWidget);
  msgBox.setText(headline);
  msgBox.setInformativeText(details);
  msgBox.setStandardButtons(QMessageBox::No | QMessageBox::Yes);
  msgBox.setDefaultButton(QMessageBox::Yes);

  int msgRet = msgBox.exec();
  if (msgRet != QMessageBox::Yes)
  {
    return;
  }

  pqServer* server = pqActiveObjects::instance().activeServer();
  pqFileDialog fileDialog(server, m_parentWidget, "Container Engine Executable");
  fileDialog.setFileMode(pqFileDialog::ExistingFile);
  if (QDialog::Accepted == fileDialog.exec())
  {
    QStringList files = fileDialog.getSelectedFiles();
    if (files.size() > 0)
    {
      result = files.at(0);
    }
  }
}

bool pqWindTunnelContainerEngineSetup::checkPath(const QString& path, QString& reason) const
{
  QFileInfo fileInfo(path);
  if (!fileInfo.exists())
  {
    reason = "File was not found at " + path + ".";
    return false;
  }

  if (!fileInfo.isExecutable())
  {
    reason = "The file is not executable: " + path + ".";
    return false;
  }

  if (!fileInfo.permission(QFileDevice::ExeUser))
  {
    reason = "The current user does not have permission to run " + path + ".";
    return false;
  }

  return true;
}

bool pqWindTunnelContainerEngineSetup::getContainerEnginePath(QString& containerEnginePath) const
{
  // Make sure the path is set to a valid executable
  QString path = containerEnginePath;
  bool done = false;
  while (!done)
  {
    QString headline;
    QString details;

    if (path.isEmpty())
    {
      headline = "The application does not know where the container engine is on your system.";
      details = "You must set it in order run OpenFOAM applications. Do you want to set it now?";
      this->askUserForPath(headline, details, path);
      if (path.isEmpty())
      {
        break;
      }
    } // if (path empty)

    // Check path
    QString reason;
    if (this->checkPath(path, reason))
    {
      done = true;
      break;
    }

    // (else)
    this->askUserForPath(reason, "Do you want to try again?", path);
    if (path.isEmpty())
    {
      break;
    }
  } // while

  if (path.isEmpty())
  {
    containerEnginePath.clear();
    return false;
  }

  // (else)
  containerEnginePath = path;
  return true;
}

void pqWindTunnelContainerEngineSetup::onProcessError(QProcess::ProcessError error)
{
  if (m_processMode == TryLs)
  {
    // Launch retry logic on next event loop
    QTimer::singleShot(0, this, &pqWindTunnelContainerEngineSetup::onTryLsError);
    return;
  }

  else if (m_processMode == TryPull)
  {
    QByteArray bytes = m_process->readAllStandardError();

    QString msg;
    QTextStream qs(&msg);
    qs << "Error pulling OpenFOAM docker image:" << bytes.data() << "\n\n"
       << "You will NOT be able to run OpenFOAM from modelbuilder";
    m_progressDialog->close();

    QMessageBox::critical(m_parentWidget, "Docker Image Error", msg);
    m_status = Unavailable;
    Q_EMIT this->finished();
    return;
  }
}

void pqWindTunnelContainerEngineSetup::onTryLsError()
{
  QByteArray bytes = m_process->readAllStandardError();
  qInfo() << "Error running container engine" << m_process->program() << ": " << bytes.data();

  QString path;
  if (!this->getContainerEnginePath(path))
  {
    m_status = Unavailable;
    Q_EMIT this->finished();
    return;
  }

  // (else)
  this->tryLs(path);
}

void pqWindTunnelContainerEngineSetup::onProcessFinished(
  int exitCode,
  QProcess::ExitStatus exitStatus)
{
  if (exitCode != 0)
  {
    if (m_processMode == TryLs)
    {
      // Launch retry logic on next event loop
      QTimer::singleShot(0, this, &pqWindTunnelContainerEngineSetup::onTryLsError);
      return;
    }
    else if (m_processMode == TryPull)
    {
      m_progressDialog->setProgressText("OpenFOAM setup complete");
      m_progressDialog->progressFinished();

      QByteArray bytes = m_process->readAllStandardError();
      QString msg;
      QTextStream qs(&msg);
      qs << "Error pulling OpenFOAM docker image. " << bytes.data() << "\n"
         << "Without the OpenFOAM docker image, you will NOT be able to run OpenFOAM from "
            "modelbuilder";
      m_progressDialog->close();

      QMessageBox::critical(m_parentWidget, "Docker Image Error", msg);
      m_status = Unavailable;
      Q_EMIT this->finished();
      return;
    }
  }

  auto* sessionData = smtk::simulation::windtunnel::qtSessionData::instance();
  if (m_processMode == TryPull)
  {
    sessionData->setContainerEngine(m_process->program());
    m_status = Ready;
    Q_EMIT this->finished();
    return;
  }

  // (else)  m_processMode == TryLs
  m_containerEnginePath = m_process->program();
  this->updateSetting(m_containerEnginePath);

  // Check container engine "ls" result
  QByteArray bytes = m_process->readAllStandardOutput();
  // qDebug() << "TryLs output:" << bytes.data() << bytes.size();
  if (bytes.size() > 8)
  {
    // Found the docker image
    sessionData->setContainerEngine(m_process->program());
    m_status = Ready;
    Q_EMIT this->finished();
    return;
  }

  // Start fetch-image logic next event loop iteration
  QTimer::singleShot(0, this, &pqWindTunnelContainerEngineSetup::pullDockerImage);
}

void pqWindTunnelContainerEngineSetup::pullDockerImage()
{
  m_processMode = TryPull;

  QString image = OPENFOAM_DOCKER_REGISTRY "/" OPENFOAM_DOCKER_IMAGE;
  QStringList args;
  args << "pull" << image;
  m_process->setArguments(args);

  qDebug() << "Program:" << m_process->program();
  qDebug() << "Args:" << args;
  m_process->start(QIODevice::ReadOnly);

  // Setup progress dialog
  m_progressDialog->setWindowTitle("Pulling Docker Image");
  m_progressDialog->clearProgressMessages();
  m_progressDialog->setLabelText("Pull OpenFOAM docker image");
  QString msg = QString("Request %1").arg(image);
  m_progressDialog->setProgressText(msg);
  m_progressDialog->show();
  m_progressDialog->raise();
}

bool pqWindTunnelContainerEngineSetup::readSetting(QString& containerEnginePath) const
{
  pqServer* server = pqActiveObjects::instance().activeServer();
  vtkSMProxy* proxy = server->proxyManager()->GetProxy("settings", "WindTunnelSettings");
  if (!proxy)
  {
    qCritical() << "Internal Error: Settings proxy for WindTunnel not found.";
    return false;
  }

  vtkSMProperty* pathProp = proxy->GetProperty("ContainerEnginePath");
  auto* pathStringProp = vtkSMStringVectorProperty::SafeDownCast(pathProp);
  if (!pathStringProp)
  {
    qCritical() << "Internal Error: ContainerEnginePath not found in Wind Tunnel settings.";
    return false;
  }

  const char* path = pathStringProp->GetElement(0);
  qDebug() << "Read settings ContainerEnginePath:" << path << ".";
  containerEnginePath = path;
  return true;
}

bool pqWindTunnelContainerEngineSetup::updateSetting(const QString& containerEnginePath) const
{
  pqServer* server = pqActiveObjects::instance().activeServer();
  vtkSMProxy* proxy = server->proxyManager()->GetProxy("settings", "WindTunnelSettings");
  if (!proxy)
  {
    qCritical() << "Internal Error: Settings proxy for WindTunnel not found.";
    return false;
  }

  vtkSMProperty* pathProp = proxy->GetProperty("ContainerEnginePath");
  auto* pathStringProp = vtkSMStringVectorProperty::SafeDownCast(pathProp);
  if (!pathStringProp)
  {
    qCritical() << "Internal Error: ContainerEnginePath not found in Wind Tunnel settings.";
    return false;
  }

  // Update settings
  int isSet = pathStringProp->SetElement(0, containerEnginePath.toStdString().c_str());
  if (isSet == 0)
  {
    return false;
  }

  proxy->UpdateVTKObjects();
  qDebug() << "Wrote settings ContainerEnginePath:" << containerEnginePath;
  return true;
}
