//=========================================================================
//  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/pqACE3PSaveBehavior.h"
#include "smtk/newt/qtNewtInterface.h"
#include "smtk/simulation/ace3p/Metadata.h"
#include "smtk/simulation/ace3p/operations/Export.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/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 "pqActiveObjects.h"
#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>

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()
{
  // Access the active server
  pqServer* server = pqActiveObjects::instance().activeServer();
  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()->project();
  if (!this->cleanProject(project))
  {
    return;
  }
  exportOp->parameters()->associate(project);

//Check for single analysis resource
#if 0
  // Todo smtk::project::ResourceContainer::find() won't compile:
  auto attResourceSet = project->resources().find<smtk::attribute::Resource>();
#else
  // So do it brute force instead
  std::set<smtk::attribute::ResourcePtr> attResourceSet;
  for (auto iter = project->resources().begin(); iter != project->resources().end(); ++iter)
  {
    auto resource = *iter;
    if (resource->isOfType<smtk::attribute::Resource>())
    {
      auto attResource = std::dynamic_pointer_cast<smtk::attribute::Resource>(resource);
      attResourceSet.insert(attResource);
    }
  }
#endif
  if (attResourceSet.size() == 1)
  {
    auto resourceItem = exportOp->parameters()->findResource("analysis");
    const auto attResource = *attResourceSet.begin();
    resourceItem->setValue(attResource);

    // check if the user has specified an Analysis attribute
    smtk::simulation::ace3p::AttributeUtils attUtils;
    auto analysisAtt = attUtils.getAnalysisAtt(attResource);
    InternalCheckMacro(analysisAtt != nullptr, "Internal Error: Analysis attribute not found.");

    smtk::attribute::ConstStringItemPtr 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;
    }
  } // if (single attribute resource)

  // Future: set analysis name to slugified version of analysis resource name
  std::string analysisName = "analysis1";

  // 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: ");

    // Future: make outFilePrefix a slugified version of the analysis/task name
    const std::string outFilePrefix = "analysis1";

    // Get native mesh filename from metadata
    smtk::simulation::ace3p::Metadata mdata;
    mdata.getFromResource(project);

    auto meshfileItem = exportOp->parameters()->findFile("MeshFile");
    InternalCheckMacro(meshfileItem != nullptr, "Internal Error: MeshFile item not found.");
    meshfileItem->setIsEnabled(true);
    meshfileItem->setValue(mdata.NativeAnalysisMeshLocation);

    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(outFilePrefix);
  }

  // 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();

  // 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);
  }

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

  // Configure job-submit options if users is logged in
  if (loggedIn && nerscItem)
  {
    // Set job name to the project folder name
    auto nameItem = nerscItem->findAs<smtk::attribute::StringItem>(
      "JobName", smtk::attribute::SearchStyle::RECURSIVE);
    if (nameItem)
    {
      nameItem->setValue(analysisName);
    }

    // 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("cmb/") + dirName;
      subfolderItem->setValue(subfolder);
    }
  } // (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";

  // Get the cumulus job id
  auto idItem = result->findString("CumulusJobId");
  if (idItem && idItem->isEnabled() && idItem->isSet())
  {
    QString jobId = idItem->value().c_str();
#ifndef NDEBUG
    qDebug() << __FILE__ << ": C++ operation returned job id" << jobId;
#endif
    emit jobAdded(jobId);
  }

  // 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;

  if (numWarnings > 0)
  {
    QWidget* parentWidget = pqCoreUtilities::mainWidget();
    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();
  }
}

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)
{
}

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);
}
