//=========================================================================
//  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 "smtk/simulation/windtunnel/qt/qtOpenFoamRunner.h"

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

#include <QDebug>
#include <QDialog>
#include <QDialogButtonBox>
#include <QDir>
#include <QFile>
#include <QFont>
#include <QMessageBox>
#include <QPlainTextEdit>
#include <QProcess>
#include <QProcessEnvironment>
#include <QTextStream>
#include <QTimer>
#include <QVBoxLayout>
#include <QWidget>
#include <QtGlobal>

#include <nlohmann/json.hpp>

namespace
{
// Singleton instance
static smtk::simulation::windtunnel::qtOpenFoamRunner* g_instance = nullptr;
} // namespace

namespace smtk
{
namespace simulation
{
namespace windtunnel
{

qtOpenFoamRunner* qtOpenFoamRunner::instance(QObject* parent)
{
  if (!g_instance)
  {
    g_instance = new qtOpenFoamRunner(parent);
  }

  if (g_instance->parent() == nullptr && parent)
  {
    g_instance->setParent(parent);
  }

  return g_instance;
}

qtOpenFoamRunner::qtOpenFoamRunner(QObject* parent)
  : QObject(parent)
  , m_process(new QProcess(this))
  , m_processDir(new QDir)
  , m_logFile(new QFile(this))
{
  QObject::connect(m_process, &QProcess::started, this, &qtOpenFoamRunner::onProcessStarted);

  QObject::connect(m_process, &QProcess::readyReadStandardOutput, [this]() {
    QByteArray byteArray = m_process->readAllStandardOutput();
    QString text = byteArray.data();
    m_textEdit->appendPlainText(text);
    if (m_logFile->isOpen())
    {
      m_logFile->write(byteArray);
    }
  });

  QObject::connect(m_process, &QProcess::readyReadStandardError, [this]() {
    m_process->setReadChannel(QProcess::StandardError);
    while (m_process->canReadLine())
    {
      QByteArray byteArray = m_process->readLine();
      qCritical() << byteArray.data();
    }
  });

  QObject::connect(
    m_process,
    QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
    [this](int exitCode, QProcess::ExitStatus exitStatus) {
      qDebug() << "Process " << this->m_pid << "finished; exit code" << exitCode;
      Q_EMIT this->finished(m_pid, exitCode);

      m_pid = 0;
      this->m_state = ProcessState::NotRunning;

      if (this->m_refineMeshLastStep > 0)
      {
        this->continueRefineMesh();
        return;
      }

      m_logFile->close();
    });
}

qtOpenFoamRunner::~qtOpenFoamRunner()
{
  if (m_logFile->isOpen())
  {
    m_logFile->close();
  }
}

void qtOpenFoamRunner::setParentWidget(QWidget* widget)
{
  m_parentWidget = widget;

  // Initialize m_logDialog
  m_logDialog = new QDialog(widget);
  m_logDialog->resize(640, 480);
  m_logDialog->setModal(false);
  m_logDialog->setSizeGripEnabled(true);

  QVBoxLayout* dialogLayout = new QVBoxLayout;

  // Initialize m_textEdit
  m_textEdit = new QPlainTextEdit;
  dialogLayout->addWidget(m_textEdit);
  QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Close);
  dialogLayout->addWidget(buttonBox);
  m_logDialog->setLayout(dialogLayout);

  // Configure m_textEdit *after* added to dialog
  // Hoping this will fix startup crash in QTextEngine::shapeText() (macOS)
  m_textEdit->setReadOnly(true);
  m_textEdit->setPlaceholderText("Checking for logfile...");
  const QFont fixedFont = QFontDatabase::systemFont(QFontDatabase::FixedFont);
  m_textEdit->document()->setDefaultFont(fixedFont);

  // Set connections
  QObject::connect(buttonBox, &QDialogButtonBox::accepted, m_logDialog, &QDialog::accept);
  QObject::connect(buttonBox, &QDialogButtonBox::rejected, m_logDialog, &QDialog::reject);
}

void qtOpenFoamRunner::start(
  const QString& application,
  const QString& caseFolder,
  const QStringList& arguments)
{
  if (this->state() != ProcessState::NotRunning)
  {
    qWarning() << __FILE__ << __LINE__ << "QProcess is already running";
    return;
  }

  // If we aren't in the middle of a refineMesh sequence...
  if (m_refineMeshCurrentStep == 0)
  {
    // Set up logs directory
    QDir logsDir(caseFolder);
    logsDir.mkdir("logs");
    logsDir.cd("logs");

    QString logFile = QString("%1.log").arg(application);
    QString logPath = logsDir.absoluteFilePath(logFile);
    // Should we use a separate file for stderr?

    m_logDialog->setWindowTitle(logFile);
    m_logFile->setFileName(logPath);
    if (!m_logFile->open(QIODevice::Truncate | QIODevice::WriteOnly))
    {
      qWarning() << "Failed to open logfile" << m_logFile->fileName();
      return;
    }

    // Check if starting refineMesh sequence
    if (application == "refineMesh")
    {
      this->startRefineMesh(caseFolder);
      return;
    }
  } // if (m_refineMeshStep == 0)

  auto* sessionData = qtSessionData::instance();

  // Setup QProcess and start
  // Future: add support for binary OpenFOAM install?
  QString program = sessionData->containerEngine();
  qDebug() << "Process Program:" << program;

  QStringList processArguments;
  processArguments << "run"
                   << "--rm";

  QString uid = sessionData->dockerUID();
  QString gid = sessionData->dockerGID();
  QString userArg = QString("--user=%1:%2").arg(uid).arg(gid);
  processArguments << userArg;

  QString volumeArg = QString("--volume=%1:/home/openfoam").arg(caseFolder);
  processArguments << volumeArg;

  processArguments << OPENFOAM_DOCKER_IMAGE << application << arguments;
  // processArguments << ">" << logPath << "2&>1";

  qDebug() << "Process Args:" << processArguments;

  m_process->setProgram(program);
  m_process->setArguments(processArguments);
  m_process->setWorkingDirectory(caseFolder);

  // Also set PWD in env - needed for some binary OpenFOAM installs
  QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
  env.insert("PWD", caseFolder);
  m_process->setProcessEnvironment(env);

  m_process->start(QIODevice::ReadOnly);
  m_state = ProcessState::Starting;
}

void qtOpenFoamRunner::onProcessStarted()
{
  m_pid = this->m_process->processId();
  qDebug() << "Started process" << m_pid;
  m_state = ProcessState::Running;
  Q_EMIT this->started(m_pid);

  if (m_refineMeshCurrentStep == 0)
  {
    m_textEdit->clear();
  }

  m_logDialog->show(); // non-modal
  m_logDialog->raise();
}

void qtOpenFoamRunner::showLogFile(const QString& path)
{
  if (this->state() == ProcessState::Running)
  {
    m_logDialog->show();
    m_logDialog->raise();
    return;
  }

  // Sanity check
  if (m_logFile->isOpen())
  {
    qWarning() << "Closing m_logFile";
    m_logFile->close();
  }

  QFile file;
  file.setFileName(path);
  if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
  {
    qWarning() << "Failed to open logfile" << path;
    return;
  }

  // We don't expect big (MB-size) files, so just read the whole thing
  QByteArray byteArray = file.readAll();
  QString text = byteArray.data();
  m_textEdit->setPlainText(text);
  m_logDialog->show(); // non-modal
  m_logDialog->raise();
}

void qtOpenFoamRunner::startRefineMesh(const QString& caseFolder)
{
  // The operation created refineMeshDict and topoSetDict.* files
  // in the system subdirectory. Find out how many.
  QDir systemDir(caseFolder);
  systemDir.cd("system");
  QStringList nameFilters;
  nameFilters << "topoSetDict.*";
  QStringList topoSetList = systemDir.entryList(nameFilters);
  int count = topoSetList.size();
  if (count < 1)
  {
    qWarning() << "Did not find any topoSetDict.* files";
    return;
  }

  m_refineMeshCurrentStep = 0;
  m_refineMeshLastStep = count;
  m_refineMeshApp.clear();
  m_refineMeshCaseFolder = caseFolder;

  m_textEdit->clear();
  this->continueRefineMesh();
}

void qtOpenFoamRunner::continueRefineMesh()
{
  QStringList arguments;

  if (m_refineMeshApp.isEmpty())
  {
    // Proceed to first iteration
    m_refineMeshApp = "topoSet";
    m_refineMeshCurrentStep = 1;
    arguments << "-dict"
              << "system/topoSetDict.1";
  }
  else if (m_refineMeshApp == "topoSet")
  {
    m_refineMeshApp = "refineMesh";
    arguments << "-dict"
              << "system/refineMeshDict"
              << "-overwrite";
  }
  else if (m_refineMeshApp == "refineMesh")
  {
    // Check if we are done
    if (m_refineMeshCurrentStep >= m_refineMeshLastStep)
    {
      m_refineMeshCurrentStep = 0;
      m_refineMeshLastStep = 0;
      m_refineMeshApp.clear();
      m_refineMeshCaseFolder.clear();

      m_logFile->close();
      this->m_state = ProcessState::NotRunning;
      m_textEdit->appendPlainText("\nAll refineMesh processing steps completed.\n");
      return;
    }

    // (else) Proceed to next iteration
    m_refineMeshCurrentStep++;
    m_refineMeshApp = "topoSet";
    QString dictFile = QString("system/topoSetDict.%1").arg(m_refineMeshCurrentStep);
    arguments << "-dict" << dictFile;
  }
  else
  {
    qWarning() << "Internal error: unrecognized m_refineMeshApp" << m_refineMeshApp;
    return;
  }

  // Add message to the dialog
  QString divider = QString("\n%1 %2 of %3:\n\n")
                      .arg(m_refineMeshApp)
                      .arg(m_refineMeshCurrentStep)
                      .arg(m_refineMeshLastStep);
  m_textEdit->appendPlainText(divider);

  this->start(m_refineMeshApp, m_refineMeshCaseFolder, arguments);
}

} // namespace windtunnel
} // namespace simulation
} // namespace smtk
