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

#include "smtk/simulation/ace3p/JobsManifest.h"
#include "smtk/simulation/ace3p/operations/Write.h"
#include "smtk/simulation/ace3p/utility/AttributeUtils.h"

// SMTK includes
#include "smtk/attribute/Attribute.h"
#include "smtk/attribute/FileItem.h"
#include "smtk/attribute/IntItem.h"
#include "smtk/attribute/Resource.h"
#include "smtk/attribute/ResourceItem.h"
#include "smtk/attribute/SearchStyle.h"
#include "smtk/attribute/StringItem.h"
#include "smtk/attribute/ValueItem.h"
#include "smtk/io/AttributeWriter.h"
#include "smtk/io/Logger.h"
#include "smtk/operation/Operation.h"

#include <boost/filesystem.hpp>
#include <boost/system/error_code.hpp>

#include <iostream>
#include <string>

namespace
{
// Macro to call method on instance. Used for setting fields in JobRecordGenerator
#define AsLambdaMacro(instance, method) [&instance](const std::string& s) { instance.method(s); }

// Copy from smtk ValueItem to job record
void toJobRecord(
  const smtk::attribute::AttributePtr att,
  const std::string& itemName,
  std::function<void(const std::string)> setField,
  smtk::io::Logger& logger)
{
  const auto valueItem = att->findAs<smtk::attribute::ValueItem>(
    itemName, smtk::attribute::SearchStyle::RECURSIVE_ACTIVE);
  if (valueItem == nullptr)
  {
    smtkWarningMacro(logger, "Warning: Did not find attribute ValueItem \"" << itemName << "\".");
  }
  else if (valueItem->isEnabled() && valueItem->isSet())
  {
    setField(valueItem->valueAsString());
  }
}

std::string jobsManifestPath(const smtk::project::Project* project)
{
  std::string location = project->location();
  if (location.empty())
  {
    return std::string();
  }
  boost::filesystem::path projectFilePath(location);
  boost::filesystem::path manifestPath = projectFilePath.parent_path() / "JobsManifest.json";
  return manifestPath.string();
}

} // namespace

namespace smtk
{
namespace simulation
{
namespace ace3p
{

Project::Project() {}

bool Project::onJobSubmit(
  smtk::attribute::AttributePtr exportParameters,
  smtk::attribute::AttributePtr exportResult,
  smtk::io::Logger& logger)
{
  // Get cumulus job id
  auto cumulusJobIdItem = exportResult->findString("CumulusJobId");
  if (!cumulusJobIdItem || !cumulusJobIdItem->isSet() || !cumulusJobIdItem->isValid())
  {
    smtkErrorMacro(logger, "Internal Error: CumulusJobId item missing or not set");
    return false;
  }
  std::string cumulusJobId = cumulusJobIdItem->value();

  // Create job folder
  boost::filesystem::path projectFilePath(this->location());
  boost::filesystem::path projectPath = projectFilePath.parent_path();
  boost::filesystem::path jobPath = projectPath / "jobs" / cumulusJobId;

  boost::system::error_code boost_errcode;
  boost::filesystem::create_directories(jobPath, boost_errcode);
  if (boost_errcode != boost::system::errc::success)
  {
    smtkErrorMacro(
      logger, "Error creating job folder " << jobPath.string() << ": " << boost_errcode.message());
    return false;
  }
  std::cout << "Created local job folder at " << jobPath.string() << std::endl;

  // Copy the ACE3P command file to the job folder
  auto fileItem = exportResult->findFile("OutputFile");
  if (!fileItem || !fileItem->isSet() || !fileItem->isValid())
  {
    smtkErrorMacro(logger, "Internal Error: OutputFile item missing or not set");
    return false;
  }
  boost::filesystem::path ace3pFilePath(fileItem->value());
  if (!boost::filesystem::exists(ace3pFilePath))
  {
    smtkErrorMacro(
      logger, "Internal Error: ace3p input file not found at " << ace3pFilePath.string());
    return false;
  }
  boost::filesystem::path toFilePath = jobPath / ace3pFilePath.filename();
  boost::filesystem::copy_file(ace3pFilePath, toFilePath, boost_errcode);
  if (boost_errcode != boost::system::errc::success)
  {
    smtkErrorMacro(
      logger,
      "Error copying input file " << ace3pFilePath.string() << ": " << boost_errcode.message());
    return false;
  }

  // Write the export spec to the job folder
  boost::filesystem::path specPath = jobPath / "export-spec.smtk";
  smtk::io::AttributeWriter attWriter;
  bool writeErr = attWriter.write(exportParameters->attributeResource(), specPath.string(), logger);
  if (writeErr)
  {
    smtkErrorMacro(logger, "Error writing export parameter file.");
    return false;
  }

  // Create job record
  auto jrg = JobRecordGenerator();
  jrg.slurmID("000000"); // future
  jrg.cumulusID(cumulusJobId);
  jrg.localJobFolder(jobPath.string());

  ::toJobRecord(exportParameters, "JobName", AsLambdaMacro(jrg, jobName), logger);
  ::toJobRecord(exportParameters, "Machine", AsLambdaMacro(jrg, machine), logger);

  smtk::resource::ResourcePtr resource = exportParameters->findResource("analysis")->value();
  jrg.analysisID(resource->id().toString());

  smtk::simulation::ace3p::AttributeUtils attUtils;
  auto attResource = std::dynamic_pointer_cast<smtk::attribute::Resource>(resource);
  auto analysisAtt = attUtils.getAnalysisAtt(attResource);
  if (analysisAtt)
  {
    ::toJobRecord(analysisAtt, "Analysis", AsLambdaMacro(jrg, analysis), logger);
  }
  else
  {
    smtkWarningMacro(logger, "Failed to find analysis item");
  }

  ::toJobRecord(exportParameters, "JobNotes", AsLambdaMacro(jrg, notes), logger);
  ::toJobRecord(exportResult, "CumulusOutputFolder", AsLambdaMacro(jrg, cumulusFolderID), logger);
  ::toJobRecord(exportResult, "NerscJobFolder", AsLambdaMacro(jrg, runtimeJobFolder), logger);
  ::toJobRecord(exportResult, "NerscInputFolder", AsLambdaMacro(jrg, runtimeInputFolder), logger);
  ::toJobRecord(exportParameters, "NumberOfNodes", AsLambdaMacro(jrg, nodes), logger);
  ::toJobRecord(exportParameters, "NumberOfTasks", AsLambdaMacro(jrg, processes), logger);

  // update this continuously via cumulus
  jrg.runtime(0);
  jrg.submissionTime("0");

  this->addJobRecord(jrg.get());
  bool ok = this->writeJobsManifest();
  return ok;
}

void Project::addJobRecord(nlohmann::json job)
{
  m_jobsManifest.addJobRecord(job);
}

void Project::setJobRecordField(int idx, std::string key, std::string value)
{
  m_jobsManifest.setField(idx, key, value);
}

void Project::getJobRecordField(int idx, std::string key, std::string& value)
{
  m_jobsManifest.getField(idx, key, value);
}

bool Project::readJobsManifest()
{
  std::string path = jobsManifestPath(this);
  if (path.empty() || !boost::filesystem::exists(path))
  {
    return false;
  }
  return m_jobsManifest.read(path);
}

bool Project::writeJobsManifest() const
{
  std::string path = jobsManifestPath(this);
  if (path.empty())
  {
    return false;
  }
  return m_jobsManifest.write(path);
}

std::string Project::jobData(int idx, std::string key)
{
  std::string value;
  m_jobsManifest.getField(idx, key, value);
  return value;
}

} // namespace ace3p
} // namespace simulation
} // namespace smtk
