//=========================================================================
//  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.
//=========================================================================

// local includes
#include "smtk/simulation/ace3p/qt/qtJobsModel.h"
#include "smtk/simulation/ace3p/JobsManifest.h"
#include "smtk/simulation/ace3p/Project.h"
#include "smtk/simulation/ace3p/qt/qtCumulusJobTracker.h"
#include "smtk/simulation/ace3p/utility/AttributeUtils.h"

// smtk includes
#include "smtk/attribute/Resource.h"
#include "smtk/attribute/StringItem.h"
#include "smtk/project/Project.h"

// Qt includes
#include <QDateTime>
#include <QDebug>
#include <QString>

namespace
{
const QString DateTimeFormat("dd-MMMM-yy  hh:mm");
}

namespace smtk
{
namespace simulation
{
namespace ace3p
{

qtJobsModel::qtJobsModel(QObject* parent)
  : Superclass(parent)
  , m_jobTracker(new qtCumulusJobTracker(this))
{
  // Connect to job status signal from job tracker
  QObject::connect(m_jobTracker, &qtCumulusJobTracker::jobStatus, this, &qtJobsModel::onJobStatus);

  // Relay polling state signal from job tracker to the outside
  QObject::connect(
    m_jobTracker,
    &qtCumulusJobTracker::pollingStateChanged,
    this,
    &qtJobsModel::pollingStateChanged);
}

std::string qtJobsModel::col2field(JobsFields col) const
{
  switch (col)
  {
    case JobsFields::JobName:
      return "job_name";
    case JobsFields::AnalysisType:
      return "analysis";
    case JobsFields::Status:
      return "status";
    case JobsFields::StartTime:
      return "submission_time";
    case JobsFields::Notes:
      return "notes";
    case JobsFields::SlurmID:
      return "slurm_id";
    case JobsFields::Processes:
      return "processes";
    case JobsFields::Nodes:
      return "nodes";
    case JobsFields::Machine:
      return "machine";
    case JobsFields::RemoteDir:
      return "runtime_job_folder";
    case JobsFields::LocalDir:
      return "local_job_folder";
    case JobsFields::InputDir:
      return "runtime_input_folder";
    case JobsFields::CumulusID:
      return "cumulus_id";
    case JobsFields::CumulusFolderID:
      return "cumulus_folder_id";
  }
  return "";
}

QVariant qtJobsModel::headerData(int section, Qt::Orientation orientation, int role) const
{
  if (role != Qt::DisplayRole)
    return QVariant();

  if (orientation == Qt::Horizontal && section < m_nCols)
    return m_headers[section];
  else
    return QStringLiteral("%1").arg(section);
}

int qtJobsModel::rowCount(const QModelIndex& parent) const
{
  if (!m_project)
  {

    return 0;
  }
  return static_cast<int>(m_project->jobsManifest().size());
}

int qtJobsModel::columnCount(const QModelIndex& parent) const
{
  Q_UNUSED(parent);
  return m_nCols;
}

QVariant qtJobsModel::data(const QModelIndex& index, int role) const
{
  if (!m_project)
    return false;
  // default return value
  QVariant ret = QVariant();

  const int row = index.row();
  const int col = index.column();

  const auto jm = m_project->jobsManifest();

  // check if we are on an analysis
  if (row >= jm.size() || col >= m_nCols)
  {
    return ret;
  }

  // get the data out of the Attribute Resource
  if (role == Qt::DisplayRole || role == Qt::EditRole)
  {
    std::string s;
    m_project->getJobRecordField(row, col2field(JobsFields(col)), s);

    if (col == JobsFields::StartTime)
    {
      if (s.empty())
      {
        return ret;
      }

      // Special case - convert to QString to int64 to QDateTime to string
      QString qsTimeStamp = QString::fromStdString(s);
      bool ok;
      qint64 tsSeconds = qsTimeStamp.toLongLong(&ok, 10);
      if (!ok)
      {
        qWarning() << "Invalid startTimeStamp" << qsTimeStamp;
        return ret;
      }
      else if (tsSeconds == 0)
      {
        return ret;
      }
      QDateTime dt = QDateTime::fromSecsSinceEpoch(tsSeconds, Qt::UTC);
      QString text = dt.toString(DateTimeFormat);
      return QVariant(text);
    }

    ret = QVariant(tr(s.c_str()));
  }

  if (role == Qt::TextAlignmentRole)
  {
    ret = QVariant(Qt::AlignCenter);
  }

  return ret;
}

bool qtJobsModel::setData(const QModelIndex& index, const QVariant& value, int role)
{
  const int row = index.row();
  const int col = index.column();
  if (!m_project)
    return false;
  if (role == Qt::EditRole)
  {
    const int row = index.row();
    const int col = index.column();
    std::string str = value.toString().toStdString();
    m_project->setJobRecordField(row, col2field(JobsFields(col)), str);
    emit dataChanged(index, index);
    return true;
  }
  return false;
}

Qt::ItemFlags qtJobsModel::flags(const QModelIndex& index) const
{
  const int col = index.column();

  Qt::ItemFlags defaultFlags = Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable;
  return defaultFlags;
}

void qtJobsModel::populateJobs(smtk::project::ProjectPtr project)
{

  this->beginResetModel();

  emit layoutAboutToBeChanged();
  if (!m_project)
  {
    m_project = smtk::static_pointer_cast<smtk::simulation::ace3p::Project>(project);
  }
  QModelIndex topLeft = this->createIndex(0, 0);
  emit beginInsertRows(topLeft, 0, this->rowCount() - 1);
  emit endInsertRows();
  emit layoutChanged();
  this->endResetModel();
}

void qtJobsModel::enablePolling(bool polling)
{
  qDebug() << __FILE__ << __LINE__ << polling;
  if (polling)
  {
    this->initTrackerJobList();
  }
  m_jobTracker->enablePolling(polling);
}

void qtJobsModel::updateStatus()
{
  qDebug() << __FILE__ << __LINE__;
  this->initTrackerJobList();
  m_jobTracker->pollOnce();
}

void qtJobsModel::onJobStatus(
  const QString& cumulusJobId,
  const QString& status,
  const QString& queueJobId,
  qint64 startTimeStamp)
{
  // Use brute force to find the job in the manifest
  const JobsManifest& jobsManifest = m_project->jobsManifest();
  int jobCount = static_cast<int>(jobsManifest.size());
  std::string inputCid = cumulusJobId.toStdString();
  std::string cid;
  for (int row = 0; row < jobCount; ++row)
  {
    m_project->getJobRecordField(row, "cumulus_id", cid);
    if (cid == inputCid)
    {
      bool changed = false;

      // Get current status and queue id fields to determine if they changed
      std::string currentStatus;
      m_project->getJobRecordField(row, "status", currentStatus);
      if (status != currentStatus.c_str())
      {
        m_project->setJobRecordField(row, "status", status.toStdString());
        changed = true;
      }

      std::string currentQid;
      m_project->getJobRecordField(row, "slurm_id", currentQid);
      if (queueJobId != currentQid.c_str())
      {
        m_project->setJobRecordField(row, "slurm_id", queueJobId.toStdString());
        changed = true;
      }

      std::string inputStart = QString::number(startTimeStamp).toStdString();
      std::string currentStart;
      m_project->getJobRecordField(row, "submission_time", currentStart);
      if (inputStart != currentStart)
      {
        m_project->setJobRecordField(row, "submission_time", inputStart);
        changed = true;
      }

      if (changed)
      {
        // For now, signal that entire row changed
        QModelIndex firstIndex = this->createIndex(row, 0);
        QModelIndex lastIndex = this->createIndex(row, this->columnCount());
        emit this->dataChanged(firstIndex, lastIndex, { Qt::DisplayRole });

        bool wroteManifest = m_project->writeJobsManifest();
        if (!wroteManifest)
        {
          qWarning() << "Failed to write jobs manifest file.";
        }
      }

      break;
    }
  }
}

void qtJobsModel::onJobAdded(const QString& cumulusJobId)
{
  if (!m_project)
  {
    return;
  }
  const JobsManifest& jobsManifest = m_project->jobsManifest();

  // Make sure that the last row of manifest is the new job record
  int index = jobsManifest.getIndexByCumulusID(cumulusJobId.toStdString());
  if (index < 0)
  {
    qWarning() << "Internal Error: onJobAdded(), id" << cumulusJobId << "not found in JobsManifest";
    return;
  }

  // Add row to model
  int row = index;
  this->beginInsertRows(QModelIndex(), row, row);
  this->endInsertRows();

  // Skip jobs generated for local testing
  std::string machine;
  jobsManifest.getField(index, "machine", machine);
  if (machine.empty() || machine == "local-test")
  {
    return;
  }

  m_jobTracker->addNewJob(cumulusJobId);
}

void qtJobsModel::initTrackerJobList()
{
  m_jobTracker->clear();

  // Check all jobs for non-terminal state
  const JobsManifest& jobsManifest = m_project->jobsManifest();
  std::string cumulusId;
  std::string status;
  int jobCount = static_cast<int>(jobsManifest.size());
  for (int i = 0; i < jobCount; ++i)
  {
    jobsManifest.getField(i, "status", status);
    if (status != "complete" && status != "error")
    {
      jobsManifest.getField(i, "cumulus_id", cumulusId);
      m_jobTracker->addJob(QString::fromStdString(cumulusId), false);
    }
  }
}

void qtJobsModel::clearJobs()
{
  if (!m_project)
    return;

  m_project.reset();
  m_project = nullptr;
  emit beginResetModel();
  emit endResetModel();
}

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