//=========================================================================
//  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/common/UUID.h"
#include "smtk/io/AttributeWriter.h"
#include "smtk/io/Logger.h"
#include "smtk/operation/Operation.h"
#include "smtk/project/Project.h"
#include "smtk/resource/Manager.h"
#include "smtk/resource/Resource.h"

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

#include <ctime>
#include <iostream>
#include <sstream>
#include <string>

namespace // anonymous namespace
{
boost::filesystem::path projectPath(
  const smtk::project::Project* project,
  std::string subdirectory = std::string())
{
  boost::filesystem::path projectFilePath(project->location());
  boost::filesystem::path projectPath = projectFilePath.parent_path();
  if (subdirectory.empty())
  {
    return projectPath;
  }
  // (else)
  return projectPath / subdirectory;
}

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

} // anonymous namespace

namespace smtk
{
namespace simulation
{
namespace ace3p
{

Project::Project()
  : m_jobsManifest(new JobsManifest)
{
  ;
}

std::size_t Project::numberOfStages() const
{
  return m_stages.size();
}

void Project::addStage(std::shared_ptr<Stage> stage, bool bMakeCurrent)
{
  m_stages.push_back(stage);

  if (bMakeCurrent)
  {
    m_currentStageIndex = m_stages.size() - 1;
  }

  this->setClean(false);
}

bool Project::removeStage(std::size_t index)
{
  if (index < 0 || index >= m_stages.size())
  {
    return false;
  }
  if (m_stages.size() == 1) // not allowed to have 0 stages
  {
    return false;
  }

  if (index == m_currentStageIndex)
  {
    m_currentStageIndex = 0;
  }
  m_stages.erase(m_stages.begin() + index);

  return true;
}

std::size_t Project::currentStageIndex() const
{
  return m_currentStageIndex;
}

void Project::setCurrentStage(int index, bool bMarkUnclean)
{
  if (index < 0 || index >= m_stages.size())
  {
    return;
  }

  m_currentStageIndex = index;

  if (bMarkUnclean)
  {
    this->setClean(false);
  }
}

std::shared_ptr<Stage> Project::stage(std::size_t index) const
{
  if (index < 0 || index >= m_stages.size())
  {
    return nullptr;
  }

  return m_stages[index];
}

std::string Project::stageID(int stageIndex) const
{
  smtk::attribute::ResourcePtr attResource = this->stage(stageIndex)->attributeResource();
  return attResource->id().toString();
}

bool Project::onJobSubmit(
  nlohmann::json& record,
  const std::vector<std::string>& exportFiles,
  smtk::io::Logger& logger)
{
  // Make sure status field is set
  std::string status = record["status"].get<std::string>();
  if (status.empty())
  {
    record["status"] = "created";
  }

  // Make sure submission time is set
  std::string timeString = record["submission_time"].get<std::string>();
  if (timeString.empty())
  {
    std::time_t seconds = std::time(nullptr);
    std::stringstream ss;
    ss << seconds;
    record["submission_time"] = ss.str();
  }

  // Create job folder
  std::string jobId = record["job_id"].get<std::string>();
  boost::filesystem::path startPath(this->jobsDirectory());
  boost::filesystem::path jobPath = startPath / jobId;

  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;
  }
  smtkInfoMacro(logger, "Created local job folder at " << jobPath.string());
  record["local_job_folder"] = jobPath.string();

  // Move export files to job folder
  boost::filesystem::path exportPath(this->exportDirectory());
  for (auto file : exportFiles)
  {
    boost::filesystem::path fromPath(file);
    boost::filesystem::path toPath = jobPath / fromPath.filename();
    boost::filesystem::rename(fromPath, toPath, boost_errcode);
    if (boost_errcode != boost::system::errc::success)
    {
      smtkErrorMacro(
        logger, "Error moving file " << file << " to job directory " << toPath.string());
      return false;
    }
  }

  int stageIndex = this->findStageIndex(record);
  this->addJobRecord(record, stageIndex);
  this->writeJobsManifest();
  return true;
}

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

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

  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;
  }
  smtkInfoMacro(logger, "Created local job folder at " << jobPath.string());

  // 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.jobID("000000"); // future
  jrg.localJobFolder(jobPath.string());

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

  int stageIndex = exportParameters->findInt("stage-index")->value();
  smtk::attribute::ResourcePtr attResource = this->stage(stageIndex)->attributeResource();
  jrg.analysisID(attResource->id().toString());

  smtk::simulation::ace3p::AttributeUtils attUtils;
  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, "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.elapsedTime(0);
  jrg.submissionTime("0");

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

bool Project::addJobRecord(nlohmann::json jRecord, int stageIndex)
{
  auto iter = jRecord.find("job_id");
  if (iter == jRecord.end())
  {
    std::cerr << __FILE__ << ":" << __LINE__ << " "
              << "job record missing job_id: cannot add to project" << std::endl;
    return false;
  }
  m_jobsManifest->insertJobRecord(jRecord);

  std::string jobId = iter->get<std::string>();
  if (stageIndex < 0)
  {
    std::cerr << __FILE__ << ":" << __LINE__ << " "
              << "stage index not found for job " << jobId << std::endl;
  }
  else if (stageIndex >= m_stages.size())
  {
    std::cerr << __FILE__ << ":" << __LINE__ << " "
              << "invalid stage index " << stageIndex << " for job " << jobId << std::endl;
  }
  else
  {
    m_stages[stageIndex]->insertJob(jobId);
  }
  return true;
}

bool Project::removeJobRecord(const std::string& jobId)
{
  // Find Stage instance containing the jobId and remove
  for (auto iter = m_stages.begin(); iter != m_stages.end(); ++iter)
  {
    std::shared_ptr<Stage> stage = *iter;
    if (stage->removeJob(jobId))
    {
      break;
    }
  }

  // Remove job from the manifest
  m_jobsManifest->removeJobRecord(jobId);

  // Save and exit
  this->setClean(false);
  this->writeJobsManifest();
  return true;
}

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;
  }
  bool bChangedUponLoad;
  bool bSuccess = m_jobsManifest->read(path, bChangedUponLoad);
  if (bSuccess && bChangedUponLoad)
  {
    writeJobsManifest(); // TODO - maybe mark something as dirty instead (immediately save
  }                      //        it back out for now)
  return bSuccess;
}

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

std::string Project::projectDirectory() const
{
  return projectPath(this).string();
}

std::string Project::assetsDirectory() const
{
  return projectPath(this, "assets").string();
}

std::string Project::exportDirectory() const
{
  return projectPath(this, "export").string();
}

std::string Project::jobsDirectory() const
{
  return projectPath(this, "jobs").string();
}

std::string Project::resourcesDirectory() const
{
  return projectPath(this, "resources").string();
}

int Project::findStageIndex(smtk::attribute::ResourcePtr attResource) const
{
  // Traverse stages looking for a match
  int i = 0;
  for (auto iter = m_stages.begin(); iter != m_stages.end(); ++iter, ++i)
  {
    std::shared_ptr<Stage> stage = *iter;
    if (stage->attributeResource() == attResource)
    {
      return i;
    }
  }

  return -1; // (not found)
}

int Project::findStageIndex(const nlohmann::json& jobRecord) const
{
  // Check trivial cases
  if (m_stages.size() == 1)
  {
    return 0;
  }
  if (m_stages.size() == 0)
  {
    return -1;
  }

  // Get the attribute resource id from the job record
  auto iter = jobRecord.find("analysis_id");
  if (iter == jobRecord.end())
  {
    std::cerr << __FILE__ << ":" << __LINE__ << " Job record missing analysis_id field."
              << std::endl;
    return -1;
  }

  // Get the attribute resource from the resource manager
  std::string id = *iter;
  smtk::common::UUID uuid(id);
  auto resManager = static_cast<const smtk::resource::Resource*>(this)->manager();
  if (resManager == nullptr)
  {
    return -1;
  }

  auto attResource = resManager->get<smtk::attribute::Resource>(uuid);
  if (attResource == nullptr)
  {
    std::cerr << __FILE__ << ":" << __LINE__ << " No attribute resource with id " << id
              << std::endl;
    return -1;
  }

  return this->findStageIndex(attResource);
}

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