//=========================================================================
//  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 "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 "boost/filesystem.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)
}

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

  // Set default path for output file
  std::string projectLocation = project->location();
  if (!projectLocation.empty())
  {
    boost::filesystem::path projectFilePath(projectLocation);
    boost::filesystem::path projectPath = projectFilePath.parent_path();
    boost::filesystem::path outputFilePath = projectPath / "export";
    std::string outputFolder = outputFilePath.string();

    // TODO make outFilePrefix configurable
    const std::string outFilePrefix = "ace3p";

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

  // Insert NEWT session id
  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());
  }

  // Construct a modal dialog for the operation spec
  auto dialog = QSharedPointer<smtk::extension::qtOperationDialog>(
    new smtk::extension::qtOperationDialog(exportOp,
      wrapper->smtkResourceManager(),
      wrapper->smtkViewManager(),
      pqCoreUtilities::mainWidget()));
  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";

  // export occured, update the job list
  emit this->jobsUpdated();

  // Here is how to check for and get the job id
  std::string jobId;
  auto idItem = result->findString("CumulusJobId");
  if (idItem && idItem->isEnabled())
  {
    jobId = idItem->value();
#ifndef NDEBUG
    std::cout << __FILE__ << ":" << __LINE__ << " "
              << "C++ operation returned job id" << jobId << std::endl;
#endif

    // And here is a slightly klugey way to get input specs
    auto specResource = result->attributeResource();
    auto attList = specResource->findAttributes("ace3p-export");
    auto paramAtt = attList[0];
    auto nerscItem = paramAtt->findGroup("NERSCSimulation");

    // See ace3p-xml for the full list
    auto machineItem = nerscItem->findAs<smtk::attribute::StringItem>("Machine");
    std::string machine = machineItem->value();

    auto nodesItem = nerscItem->findAs<smtk::attribute::IntItem>("NumberOfNodes");
    int numberOfNodes = nodesItem->value();

#ifndef NDEBUG
    std::cout << __FILE__ << ":" << __LINE__ << " "
              << "Remote machine " << machine << ", node count " << numberOfNodes << std::endl;
#endif
  }

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

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