//=========================================================================
//  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 "pqACE3PExportBehavior.h"

#include "plugin/pqACE3PAutoStart.h"
#include "plugin/pqACE3PJobsBehavior.h"
#include "plugin/pqACE3PSaveBehavior.h"
#include "smtk/newt/qtNewtInterface.h"
#include "smtk/simulation/ace3p/operations/Export.h"
#include "smtk/simulation/ace3p/qt/qtNewtJobSubmitter.h"
#include "smtk/simulation/ace3p/qt/qtProgressDialog.h"
#include "smtk/simulation/ace3p/qt/qtProjectRuntime.h"
#include "smtk/simulation/ace3p/utility/AttributeUtils.h"

// SMTK
#include "smtk/attribute/Attribute.h"
#include "smtk/attribute/DirectoryItem.h"
#include "smtk/attribute/FileItem.h"
#include "smtk/attribute/GroupItem.h"
#include "smtk/attribute/IntItem.h"
#include "smtk/attribute/Resource.h"
#include "smtk/attribute/ResourceItem.h"
#include "smtk/attribute/StringItem.h"
#include "smtk/common/StringUtil.h"
#include "smtk/extension/paraview/appcomponents/pqSMTKBehavior.h"
#include "smtk/extension/paraview/appcomponents/pqSMTKWrapper.h"
#include "smtk/extension/qt/qtOperationDialog.h"
#include "smtk/io/Logger.h"
#include "smtk/operation/Manager.h"
#include "smtk/project/Project.h"
#include "smtk/project/ResourceContainer.h"

// Paraview client
#include "pqCoreUtilities.h"
#include "pqFileDialog.h"
#include "pqPresetDialog.h"
#include "pqServer.h"

// Qt
#include <QAction>
#include <QDebug>
#include <QGridLayout>
#include <QIcon>
#include <QMessageBox>
#include <QPushButton>
#include <QSharedPointer>
#include <QSpacerItem>
#include <QString>
#include <QTextStream>
#include <QtGlobal>

#include "nlohmann/json.hpp"

#include <algorithm> // std::replace
#include <cassert>
#include <regex>

using json = nlohmann::json;

namespace
{
const int OP_SUCCEEDED = static_cast<int>(smtk::operation::Operation::Outcome::SUCCEEDED);
const QString ALERT_ICON_PATH(":/icons/attribute/errorAlert.png"); // get icon from smtk

// Macro for handling internal errors
#define InternalCheckMacro(condition, msg)                                                         \
  do                                                                                               \
  {                                                                                                \
    if (!(condition))                                                                              \
    {                                                                                              \
      QMessageBox::critical(pqCoreUtilities::mainWidget(), "Internal Error", msg);                 \
      return;                                                                                      \
    }                                                                                              \
  } while (0)
} // namespace

//-----------------------------------------------------------------------------
pqACE3PExportReaction::pqACE3PExportReaction(QAction* parentObject)
  : Superclass(parentObject)
{
}

void pqACE3PExportReaction::onTriggered()
{
  pqACE3PExportBehavior::instance()->exportProject();
}

//-----------------------------------------------------------------------------
void pqACE3PExportBehavior::exportProject()
{
  // Get the operation manager from the builtin server
  pqServer* server = pqACE3PAutoStart::builtinServer();
  pqSMTKWrapper* wrapper = pqSMTKBehavior::instance()->resourceManagerForServer(server);
  auto opManager = wrapper->smtkOperationManager();

  // Instantiate the Export operator
  auto exportOp = opManager->create<smtk::simulation::ace3p::Export>();
  InternalCheckMacro(exportOp != nullptr, "Internal Error: Export operation not created.");

  auto project = smtk::simulation::ace3p::qtProjectRuntime::instance()->ace3pProject();
  if (!this->cleanProject(project))
  {
    qWarning() << "Project is in modified state. Cannot export.";
    return;
  }
  int stageIndex = static_cast<int>(project->currentStageIndex());
  qDebug() << "Exporting stage" << stageIndex;
  if (stageIndex < 0)
  {
    qWarning() << "No project stage selected. Cannot export";
    return;
  }

  exportOp->parameters()->associate(project);
  exportOp->parameters()->findInt("stage-index")->setValue(stageIndex);

  std::string analysisName = "analysis"; // default
  std::string solverName;                // which ACE3P code (Omega3P, S3P, T3P, etc)

  // Check that Analysis is specified in the attribute resource
  auto attResource = project->stage(stageIndex)->attributeResource();
  smtk::simulation::ace3p::AttributeUtils attUtils;
  auto analysisAtt = attUtils.getAnalysisAtt(attResource);
  InternalCheckMacro(analysisAtt != nullptr, "Internal Error: Analysis attribute not found.");

  auto analysisItem = analysisAtt->findString("Analysis");
  InternalCheckMacro(analysisItem != nullptr, "Internal Error: Analysis item not found.");
  if (!analysisItem->isSet())
  {
    // Cannot export if no analysis is set so tell the user as much and exit.
    QSharedPointer<QMessageBox> noAnalysisDialog =
      QSharedPointer<QMessageBox>(new QMessageBox(pqCoreUtilities::mainWidget()));
    noAnalysisDialog->setWindowTitle("No Analysis Set!");
    noAnalysisDialog->setText("Please set an Analysis attribute before exporting.");
    auto okButton = noAnalysisDialog->addButton("Continue", QMessageBox::RejectRole);
    noAnalysisDialog->setDefaultButton(okButton);
    noAnalysisDialog->exec();
    return;
  }
  solverName = analysisItem->value();

  // Set default path for output file
  std::string projectLocation = project->location();
  QDir projectDir;
  if (!projectLocation.empty())
  {
    QFileInfo projectFileInfo(QString::fromStdString(projectLocation));
    projectDir = projectFileInfo.absoluteDir();
    QDir exportDir(projectDir);
    exportDir.mkdir("export");
    bool exists = exportDir.cd("export");
    std::string outputFolder = exportDir.absolutePath().toStdString();
    InternalCheckMacro(!!exists, "Export directory doesn't exist: ");

    auto folderItem = exportOp->parameters()->findDirectory("OutputFolder");
    InternalCheckMacro(folderItem != nullptr, "Internal Error: OutputFolder item not found.");
    folderItem->setIsEnabled(true);
    folderItem->setValue(outputFolder);

    auto prefixItem = exportOp->parameters()->findString("OutputFilePrefix");
    InternalCheckMacro(prefixItem != nullptr, "Internal Error: OutputFilePrefix item not found.");
    prefixItem->setIsEnabled(true);
    prefixItem->setValue(analysisName);
  }

  // Check for newt session id and configure parameters
  newt::qtNewtInterface* newtInterface = newt::qtNewtInterface::instance();
  const QString newtSessionId = newtInterface->newtSessionId();
  if (!newtSessionId.isEmpty())
  {
    auto idItem = exportOp->parameters()->findAs<smtk::attribute::StringItem>(
      "NEWTSessionId", smtk::attribute::SearchStyle::RECURSIVE);
    if (idItem == nullptr)
    {
      QMessageBox::critical(
        pqCoreUtilities::mainWidget(),
        "Internal Error",
        "Internal Error: Failed to find NEWTSessionId item.");
      return;
    }
    idItem->setValue(newtSessionId.toStdString());
  }

  // Check if user is signed into NERSC
  bool loggedIn = newtInterface->isLoggedIn();

  // #ifndef NDEBUG
  // loggedIn = true; // for testing only
  // #endif

  // If not, use advance level to hide option to submit job
  // Future: use the pending "ignore item" feature
  auto nerscItem = exportOp->parameters()->find("NERSCSimulation");
  if (nerscItem)
  {
    nerscItem->setLocalAdvanceLevel(0, loggedIn ? 0 : 99);
    nerscItem->setIsEnabled(loggedIn);
  }

  auto noSubmitItem = exportOp->parameters()->find("no-submit");
  if (noSubmitItem)
  {
    noSubmitItem->setLocalAdvanceLevel(0, loggedIn ? 99 : 0);
  }

  // Switch from cumulus to newt I/O
  auto useNewtItem = nerscItem->find("UseNewtInterface");
  useNewtItem->setIsEnabled(true);
  auto cumulusItem = nerscItem->find("CumulusHost");
  cumulusItem->setLocalAdvanceLevel(0, 99);
  auto appendItem = nerscItem->find("AppendJobNameFolder");
  appendItem->setLocalAdvanceLevel(0, 99);

  // Configure job-submit options if users is logged in
  if (loggedIn && nerscItem)
  {
    auto nameItem = nerscItem->findAs<smtk::attribute::StringItem>(
      "JobName", smtk::attribute::SearchStyle::RECURSIVE);
    if (nameItem)
    {
      QString jobName =
        m_jobSubmitter->getUniqueJobName(project, QString::fromStdString(analysisName));
      nameItem->setValue(jobName.toStdString());
    }

    // Insert NEWT session id
    const QString newtSessionId = newtInterface->newtSessionId();
    if (!newtSessionId.isEmpty())
    {
      auto idItem = exportOp->parameters()->findAs<smtk::attribute::StringItem>(
        "NEWTSessionId", smtk::attribute::SearchStyle::RECURSIVE);
      if (idItem == nullptr)
      {
        QMessageBox::critical(
          pqCoreUtilities::mainWidget(),
          "Internal Error",
          "Internal Error: Failed to find NEWTSessionId item.");
        return;
      }
      idItem->setValue(newtSessionId.toStdString());
    }

    // Set remote folder to cmb/${project-name}
    auto subfolderItem = nerscItem->findAs<smtk::attribute::StringItem>(
      "SubFolder", smtk::attribute::SearchStyle::RECURSIVE);
    if (subfolderItem)
    {
      std::string dirName = projectDir.dirName().toStdString();
      std::string subfolder = std::string("modelbuilder/projects/") + dirName;
      subfolderItem->setValue(subfolder);
    }

    // Set the default results directory
    auto resFolderItem = nerscItem->findAs<smtk::attribute::StringItem>(
      "ResultsDirectory", smtk::attribute::SearchStyle::RECURSIVE);
    if (resFolderItem)
    {
      resFolderItem->setIsEnabled(true);

      smtk::common::StringUtil::lower(solverName);
      std::string folder = solverName + "_results";
      resFolderItem->setValue(folder);
      qDebug() << "Set resFolder item to" << folder.c_str();
    }
    else
    {
      qWarning() << "ResultsDirectory item not found";
    }
  } // (if)

  // Construct a modal dialog for the operation spec
  auto dialog =
    QSharedPointer<smtk::extension::qtOperationDialog>(new smtk::extension::qtOperationDialog(
      exportOp,
      wrapper->smtkResourceManager(),
      wrapper->smtkViewManager(),
      true,
      pqCoreUtilities::mainWidget()));
  if (loggedIn)
  {
    dialog->setMinimumHeight(640);
  }
  dialog->setObjectName("ExportACE3PDialog");
  dialog->setWindowTitle("Specify Export Properties");

  QObject::connect(
    dialog.get(),
    &smtk::extension::qtOperationDialog::operationExecuted,
    this,
    &pqACE3PExportBehavior::onOperationExecuted);
  dialog->exec();
} // exportProject()

//-----------------------------------------------------------------------------
void pqACE3PExportBehavior::onOperationExecuted(const smtk::operation::Operation::Result& result)
{
  if (result->findInt("outcome")->value() != OP_SUCCEEDED)
  {
    QMessageBox msgBox(pqCoreUtilities::mainWidget());
    msgBox.setStandardButtons(QMessageBox::Ok);
    // Create a spacer so it doesn't look weird
    QSpacerItem* horizontalSpacer =
      new QSpacerItem(300, 0, QSizePolicy::Minimum, QSizePolicy::Expanding);
    msgBox.setText("Export failed. See the \"Output Messages\" view for more details.");
    QGridLayout* layout = (QGridLayout*)msgBox.layout();
    layout->addItem(horizontalSpacer, layout->rowCount(), 0, 1, layout->columnCount());
    msgBox.exec();
    return;
  }

  qInfo() << "Export operation completed";

  // Check for warnings - some scripts use a string item
  std::size_t numWarnings = 0;
  auto warningsItem = result->findString("warnings");
  if (warningsItem && warningsItem->numberOfValues() > 0)
  {
    numWarnings = warningsItem->numberOfValues();
    for (std::size_t i = 0; i < numWarnings; ++i)
    {
      qWarning() << QString::fromStdString(warningsItem->value(i));
    }
  }

  // Alternatively check log item
  if (warningsItem == nullptr)
  {
    auto logItem = result->findString("log");
    std::string logString = logItem->value(0);
    if (!logString.empty())
    {
      auto jsonLog = json::parse(logString);
      assert(jsonLog.is_array());
      for (json::iterator it = jsonLog.begin(); it != jsonLog.end(); ++it)
      {
        auto jsRecord = *it;
        assert(jsRecord.is_object());
        if (jsRecord["severity"] >= static_cast<int>(smtk::io::Logger::WARNING))
        {
          numWarnings++;
          std::string content = jsRecord["message"];
          // Escape any quote signs (formats better)
          std::replace(content.begin(), content.end(), '"', '\"');
          qWarning("%zu. %s", numWarnings, content.c_str());
        } // if (>= warning)
      }   // for (it)
    }     // if (logString)
  }       // if (warningsItem)
  qDebug() << "Number of warnings:" << numWarnings;

  QWidget* parentWidget = pqCoreUtilities::mainWidget();
  if (numWarnings > 0)
  {
    QString text;
    QTextStream qs(&text);
    qs << "WARNING: The generated file is INCOMPLETE or INVALID."
       << " You can find more details in the Output Messages view."
       << " You will generally need to correct all invalid input fields"
       << " in the Attribute Editor in order to generate a valid"
       << " ACE3P input file."
       << " Also look in the Attribute Editor panel for red \"alert\""
       << " icons and input fields with red background."
       << "\n\nNumber of errors: " << numWarnings;
    QMessageBox msgBox(
      QMessageBox::NoIcon, "Export Warnings", text, QMessageBox::NoButton, parentWidget);
    msgBox.setIconPixmap(QIcon(ALERT_ICON_PATH).pixmap(32, 32));
    msgBox.exec();
    return;
  }

  // Initialize the progress dialog
  if (m_progressDialog == nullptr)
  {
    m_progressDialog =
      new qtProgressDialog(pqCoreUtilities::mainWidget(), 0, 0, "Submitting ACE3P Job");
    m_progressDialog->setModal(true);
    m_progressDialog->setCancelButtonVisible(false);
    m_progressDialog->setMessageBoxVisible(true, false);
    m_progressDialog->setAutoClose(true);
    m_progressDialog->setMinDuration(3);
    m_progressDialog->setAutoCloseDelay(5);
  }
  m_progressDialog->clearProgressMessages();
  m_progressDialog->setLabelText("Progress");
  m_progressDialog->setProgressText("Export operation completed.");
  m_progressDialog->show();
  m_progressDialog->raise();

  // Extract the exportParams
  // Future: redesign export/submit logic to work together more seamlessly
  auto paramsAtts = result->attributeResource()->findAttributes("ace3p-export");
  if (paramsAtts.size() != 1)
  {
    QString msg = QString("Didn't find exportParams, n %1").arg(paramsAtts.size());
    QMessageBox::critical(parentWidget, "Error", msg);
    return;
  }
  auto exportParams = paramsAtts[0];

  // qDebug() << "Calling submitAnalysisJob";
  auto project = smtk::simulation::ace3p::qtProjectRuntime::instance()->project();
  m_jobSubmitter->submitAnalysisJob(project, exportParams, result);
}

bool pqACE3PExportBehavior::cleanProject(smtk::project::ProjectPtr project)
{
  if (project->clean())
  {
    return true;
  }

  QMessageBox confirmDialog(pqCoreUtilities::mainWidget());
  confirmDialog.setWindowTitle("Save Project?");
  confirmDialog.setText("You must save current changes before exporting.");
  auto noButton = confirmDialog.addButton("Cancel", QMessageBox::RejectRole);
  auto yesButton = confirmDialog.addButton("Save Project", QMessageBox::AcceptRole);
  confirmDialog.setDefaultButton(yesButton);
  auto reply = confirmDialog.exec();
  if (reply != QDialog::Accepted)
  {
    return false;
  }

  return pqACE3PSaveBehavior::instance()->saveProject();
}

//-----------------------------------------------------------------------------
static pqACE3PExportBehavior* g_instance = nullptr;

pqACE3PExportBehavior::pqACE3PExportBehavior(QObject* parent)
  : Superclass(parent)
  , m_jobSubmitter(new smtk::simulation::ace3p::qtNewtJobSubmitter(this))
  , m_progressDialog(nullptr)
{
  QObject::connect(
    m_jobSubmitter,
    &smtk::simulation::ace3p::qtNewtJobSubmitter::jobSubmitted,
    [this](const QString& jobId) {
      emit this->jobAdded(jobId); // notify rest of application

      m_progressDialog->progressFinished();
    });
  QObject::connect(
    m_jobSubmitter,
    &smtk::simulation::ace3p::qtNewtJobSubmitter::progressMessage,
    [this](const QString& text) {
      qDebug() << text;
      m_progressDialog->setProgressText(text);
    });
  QObject::connect(
    m_jobSubmitter,
    &smtk::simulation::ace3p::qtNewtJobSubmitter::errorMessage,
    [this](const QString& text) {
      qCritical() << text;
      m_progressDialog->close();
      QMessageBox::critical(pqCoreUtilities::mainWidget(), "Internal Error", text);
    });

  // Connect jobOverwritten signal to jobs behavior
  auto jobsBehavior = pqACE3PJobsBehavior::instance();
  QObject::connect(
    m_jobSubmitter,
    &smtk::simulation::ace3p::qtNewtJobSubmitter::jobOverwritten,
    jobsBehavior,
    &pqACE3PJobsBehavior::onJobOverwritten);
}

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

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

  return g_instance;
}

pqACE3PExportBehavior::~pqACE3PExportBehavior()
{
  if (g_instance == this)
  {
    g_instance = nullptr;
  }

  QObject::disconnect(this);
}
