//=========================================================================
//  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/ace3p/operations/Export.h"

#include "smtk/simulation/ace3p/Metadata.h"
#include "smtk/simulation/ace3p/Project.h"

// SMTK includes
#include "smtk/attribute/Attribute.h"
#include "smtk/attribute/Definition.h"
#include "smtk/attribute/DirectoryItem.h"
#include "smtk/attribute/FileItem.h"
#include "smtk/attribute/IntItem.h"
#include "smtk/attribute/Item.h"
#include "smtk/attribute/ItemDefinition.h"
#include "smtk/attribute/ReferenceItem.h"
#include "smtk/attribute/Resource.h"
#include "smtk/attribute/ResourceItem.h"
#include "smtk/attribute/SearchStyle.h"
#include "smtk/attribute/StringItem.h"
#include "smtk/io/Logger.h"
#include "smtk/model/Resource.h"
#include "smtk/operation/Manager.h"
#include "smtk/operation/operators/ImportPythonOperation.h"
#include "smtk/project/Project.h"

// Build dir includes
#include "smtk/simulation/ace3p/Export_xml.h"

#include <boost/filesystem.hpp>

#include <iostream>
#include <set>
#include <string>
#include <vector>

namespace
{
const int OP_SUCCEEDED = static_cast<int>(smtk::operation::Operation::Outcome::SUCCEEDED);

// Macro for checking condition and returning
#define checkMacro(condition, msg)                                                                 \
  do                                                                                               \
  {                                                                                                \
    if (!(condition))                                                                              \
    {                                                                                              \
      smtkErrorMacro(this->log(), msg);                                                            \
      return this->createResult(smtk::operation::Operation::Outcome::FAILED);                      \
    }                                                                                              \
  } while (0)
} // namespace

namespace smtk
{
namespace simulation
{
namespace ace3p
{

smtk::operation::Operation::Result Export::operateInternal()
{
  auto& logger = this->log();

  // Make sure the python operation (ACE3P.py) exists.
  if (Metadata::WORKFLOWS_DIRECTORY.empty())
  {
    smtkErrorMacro(logger, "Internal Error: Metadata::WORKFLOWS_DIRECTORY not defined.");
    return this->createResult(smtk::operation::Operation::Outcome::FAILED);
  }

  boost::filesystem::path workflowDir(Metadata::WORKFLOWS_DIRECTORY);
  boost::filesystem::path pyPath = workflowDir / "ACE3P.py";
  if (!boost::filesystem::exists(pyPath))
  {
    smtkErrorMacro(logger, "Error: expecing python file at " << pyPath.string());
    return this->createResult(smtk::operation::Operation::Outcome::FAILED);
  }

  // Import the python op
  smtk::operation::ImportPythonOperation::Ptr importPythonOp =
    this->manager()->create<smtk::operation::ImportPythonOperation>();
  if (!importPythonOp)
  {
    smtkErrorMacro(logger, "Unable to create ImportPythonOperation");
    return this->createResult(smtk::operation::Operation::Outcome::FAILED);
  }
  importPythonOp->parameters()->findFile("filename")->setValue(pyPath.string());
  auto loadResult = importPythonOp->operate();
  if (loadResult->findInt("outcome")->value() != OP_SUCCEEDED)
  {
    smtkErrorMacro(logger, "Failed to load export script " << pyPath.string());
    // Copy the outcome
    int loadOutcome = loadResult->findInt("outcome")->value();
    auto thisOutcome = static_cast<smtk::operation::Operation::Outcome>(loadOutcome);
    return this->createResult(thisOutcome);
  }

  // Access the unique name assigned to the operation
  std::string operationName = loadResult->findString("unique_name")->value();

  // Instantiate the operation
  smtk::operation::Operation::Ptr pythonOp = this->manager()->create(operationName);
  if (pythonOp == nullptr)
  {
    smtkErrorMacro(logger, "Failed to instantate operation from export script ");
    return this->createResult(smtk::operation::Operation::Outcome::FAILED);
  }

  // Configure the operation
  auto refItem = this->parameters()->associations();
  auto project = refItem->valueAs<smtk::simulation::ace3p::Project>();

  // Copy POD parameters to the python operation
  std::vector<std::string> itemNames = { "OutputFolder", "OutputFilePrefix" };
  for (auto name : itemNames)
  {
    smtk::attribute::ConstItemPtr sourceItem =
      this->parameters()->find(name, smtk::attribute::SearchStyle::RECURSIVE_ACTIVE);
    auto targetItem =
      pythonOp->parameters()->find(name, smtk::attribute::SearchStyle::RECURSIVE_ACTIVE);
    if (sourceItem != nullptr && targetItem != nullptr)
    {
      targetItem->assign(sourceItem);
    }
  }

  // Get stage index and use it to retrieve resource items
  int stageIndex = this->parameters()->findInt("stage-index")->value();
  checkMacro(stageIndex < project->numberOfStages(), "Invalid stage index " << stageIndex);
  std::shared_ptr<Stage> stage = project->stage(stageIndex);

  // Model resource
  smtk::model::ResourcePtr modelResource = stage->modelResource();
  checkMacro(modelResource != nullptr, "Missing model resource from stage " << stageIndex);
  bool didSetValue;

  auto resItem = pythonOp->parameters()->findResource("model");
  checkMacro(resItem != nullptr, "Internal Error: resource item for model not found in python op");
  didSetValue = resItem->setValue(modelResource);
  checkMacro(didSetValue, "Internal Error: failed to set model resource on python op");

  // Native mesh file location
  std::string meshFileLocation =
    modelResource->properties().at<std::string>(Metadata::METADATA_PROPERTY_KEY);
  boost::filesystem::path meshFilePath(meshFileLocation);
  checkMacro(
    boost::filesystem::exists(meshFilePath), "Native mesh not found at " << meshFileLocation);
  didSetValue = pythonOp->parameters()->findFile("MeshFile")->setValue(meshFileLocation);
  checkMacro(didSetValue, "Internal Error: failed to set mesh file on python op");

  // Attribute resource
  smtk::attribute::ResourcePtr attResource = stage->attributeResource();
  checkMacro(attResource != nullptr, "Missing attribute resource from stage " << stageIndex);

  auto attResItem = pythonOp->parameters()->findResource("attributes");
  checkMacro(
    attResItem != nullptr, "Internal Error: resource item for attribute not found in python op");
  didSetValue = attResItem->setValue(attResource);
  checkMacro(didSetValue, "Internal Error: failed to set attribute resource on python op");

  // Check the submit-to-nersc flag
  smtk::attribute::ConstItemPtr nerscSimItem =
    this->parameters()->find("NERSCSimulation", smtk::attribute::SearchStyle::IMMEDIATE);

  if (nerscSimItem && nerscSimItem->isEnabled())
  {
    auto pyItem =
      pythonOp->parameters()->find("NERSCSimulation", smtk::attribute::SearchStyle::IMMEDIATE);
    pyItem->assign(nerscSimItem);
  }

  bool ready = pythonOp->ableToOperate();
  checkMacro(ready, "Internal Error: python operation return false for ableToOperate().");
  auto pyResult = pythonOp->operate();
  if (pyResult == nullptr)
  {
    std::cout << "WARNING: python export Result is null" << std::endl;
    return this->createResult(smtk::operation::Operation::Outcome::FAILED);
  }

  // Copy the outcome and create result
  int pyOutcome = pyResult->findInt("outcome")->value();
  auto thisOutcome = static_cast<smtk::operation::Operation::Outcome>(pyOutcome);
  auto thisResult = this->createResult(thisOutcome);

  // Copy log messages from the python op
  logger.append(pythonOp->log());

  // Copy file items
  std::vector<std::string> fileItemNames = { "OutputFile", "SlurmScript" };
  for (const auto& name : fileItemNames)
  {
    auto pyItem = pyResult->findFile(name);
    if (pyItem && pyItem->isEnabled())
    {
      thisResult->findFile(name)->setIsEnabled(true);
      thisResult->findFile(name)->setValue(pyItem->value());
    }
#ifndef NDEBUG
    if (pyItem != nullptr)
    {
      std::cout << "Python export returned " << name << " = " << pyItem->value() << std::endl;
    }
#endif
  }

  // Copy string items and save job id
  std::string jobId;
  std::vector<std::string> stringItemNames = { "JobId", "NerscJobFolder", "NerscInputFolder" };
  for (const auto& name : stringItemNames)
  {
    auto pyItem = pyResult->findString(name);
    if (pyItem && pyItem->isEnabled() && pyItem->isSet())
    {
      thisResult->findString(name)->setIsEnabled(true);
      thisResult->findString(name)->setValue(pyItem->value());
      if (name == "JobId")
      {
        jobId = pyItem->value();
      }
    }
#ifndef NDEBUG
    if (pyItem == nullptr)
    {
      std::cout << "Python item \"" << name << "\" not found." << std::endl;
    }
    else if (!pyItem->isEnabled())
    {
      std::cout << "Python item \"" << name << "\" not enabled." << std::endl;
    }
    else if (!pyItem->isSet())
    {
      std::cout << "Python item \"" << name << "\" not set." << std::endl;
      // std::cout << "Python export returned " << name << " = " << pyItem->value() << std::endl;
    }
    else
    {
      std::cout << "Python export returned " << name << " = " << pyItem->value() << std::endl;
    }
#endif
  }

  // Unregister python operation
  this->manager()->unregisterOperation(operationName);

  // If job was submitted through cumulus, notify the project
  if (!jobId.empty())
  {
    bool ok = project->onJobSubmit(this->parameters(), thisResult, logger);
    checkMacro(ok, "Error storing job record");
  }

  // Done
  return thisResult;
}

smtk::operation::Operation::Specification Export::createSpecification()
{
  Specification spec = this->smtk::operation::XMLOperation::createSpecification();

  // Special logic to copy GroupItem named "NERSCSimulation"
  // from "ace3p-submit" attribute defintion to "ace3p-export".
  // This so that we can reuse submit settings in both python
  // and C++ export operations.
  auto exportDefn = spec->findDefinition("ace3p-export");
  if (exportDefn == nullptr)
  {
    smtkErrorMacro(
      this->log(), "Internal Error: failed to find \"ace3p-export\" attribute definition.");
    return spec;
  }

  auto submitDefn = spec->findDefinition("ace3p-submit");
  auto nerscItemDefn = submitDefn->itemDefinition(0);
  if (nerscItemDefn == nullptr)
  {
    smtkErrorMacro(
      this->log(), "Internal Error: failed to find \"ace3p-submit\" attribute definition.");
    return spec;
  }
  submitDefn->removeItemDefinition(nerscItemDefn);
  bool added = exportDefn->addItemDefinition(nerscItemDefn);
  if (!added)
  {
    smtkErrorMacro(
      this->log(), "INTERNAL ERROR: Failed to add NERSCSimulation item to export definition");
  }
  return spec;
}

const char* Export::xmlDescription() const
{
  return Export_xml;
}
} // namespace ace3p
} // namespace simulation
} // namespace smtk
