//=========================================================================
//  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/Stage.h"
#include "smtk/simulation/ace3p/qt/qtNewtJobTracker.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 <QCoreApplication>
#include <QDateTime>
#include <QDebug>
#include <QFont>
#include <QString>

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

namespace smtk
{
namespace simulation
{
namespace ace3p
{

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

  // Relay polling state signal from job tracker to the outside
  QObject::connect(
    m_jobTracker, &qtNewtJobTracker::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::JobID:
      return "job_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::StageID:
      return "analysis_id";
    case JobsFields::ACDTool_Task:
      return "acdtool_task";
  }
  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];
  }

  return QAbstractItemModel::headerData(section, orientation, role);
}

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 variant = QVariant();

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

  auto jm = m_project->jobsManifest();

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

  // handle the hidden sorting column differently (as it's not actually part of the data model)
  if (col == JobsFields::StageID)
  {
    // we can use the UUID of the attribute resources for the stage id
    // this is stored in the job records as the analysis_id field
    std::string stageID;
    if (m_project->jobsManifest()->getField(row, "analysis_id", stageID))
    {
      return QVariant(stageID.c_str());
    }

    return variant; // backup return for failure cases
  }

  // 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 "ACDTool", instead display ACDTool_Task
    if (col == JobsFields::AnalysisType && s == "ACDTool")
    {
      m_project->getJobRecordField(row, col2field(JobsFields::ACDTool_Task), s);
    }

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

      // 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 variant;
      }
      else if (tsSeconds == 0)
      {
        return variant;
      }
      QDateTime dt = QDateTime::fromSecsSinceEpoch(tsSeconds, Qt::UTC);
      QString text = dt.toString(DateTimeFormat);
      return QVariant(text);
    }

    variant = QVariant(s.c_str());
  }

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

  return variant;
}

bool qtJobsModel::setData(const QModelIndex& index, const QVariant& value, int role)
{
  // handle the hidden sorting column differently (as it's not actually part of the data model)
  if (index.column() == JobsFields::StageID)
  {
    return false;
  }

  const int row = index.row();
  const int col = index.column();
  if (!m_project)
    return false;
  if (role == Qt::EditRole)
  {
    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
{
  Qt::ItemFlags defaultFlags = Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable;
  return defaultFlags;
}

void qtJobsModel::populateJobs(smtk::project::ProjectPtr project)
{
  if (m_project != nullptr)
  {
    qWarning() << "Internal warning: m_project not null" << __FILE__ << __LINE__;
  }

  this->beginResetModel();

#if 0
  // PLEASE DONT DELETE THIS CODE WITHOUT CHECKING WITH JOHN TOURTELLOTT
  // It was used as a UI workaround on Corey's macOS machine
  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();
#endif
  m_project = smtk::static_pointer_cast<smtk::simulation::ace3p::Project>(project);

  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)
{
  // Find the job in the manifest
  std::shared_ptr<JobsManifest> jobsManifest = m_project->jobsManifest();
  int jobIndex = jobsManifest->findIndex(queueJobId.toStdString());
  if (jobIndex < 0)
  {
    qWarning() << "Unrecognized job id" << queueJobId;
    return;
  }
  int row = jobIndex;
  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, "job_id", currentQid);
  if (queueJobId != currentQid.c_str())
  {
    m_project->setJobRecordField(row, "job_id", queueJobId.toStdString());
    changed = true;
  }

  std::string currentStart;
  m_project->getJobRecordField(row, "submission_time", currentStart);
  if (currentStart.empty() || (currentStart == "0"))
  {
    std::string inputStart = QString::number(startTimeStamp).toStdString();
    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() - 1);
    emit this->dataChanged(firstIndex, lastIndex);

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

/////////////////// TODO - delete later, temporary code for creating a testing project
#include "smtk/simulation/ace3p/testing/cxx/randomJobCreator.h"
void qtJobsModel::addTestJob()
{
  nlohmann::json jobRecord = randomJob();

  // hack in the correct StageID
  int stageIndex = static_cast<int>(m_project->currentStageIndex());
  std::string stageID = m_project->stage(stageIndex)->attributeResource()->id().toString();
  jobRecord["analysis_id"] = stageID;

  this->beginInsertRows(QModelIndex(), 0, 0);
  bool done = m_project->addJobRecord(jobRecord, m_project->currentStageIndex());
  qDebug() << "addTestJob" << jobRecord["job_name"].get<std::string>().c_str() << "to stage"
           << m_project->currentStageIndex() << done;
  this->endInsertRows();

  m_project->setClean(false);

  for (int i = 0; i < m_project->numberOfStages(); i++)
  {
    qDebug() << "Stage" << i << "number of jobs" << m_project->stage(i)->jobIds().size();
  }
}
///////////////////

void qtJobsModel::onJobAdded(const QString& jobId)
{
  if (!m_project)
  {
    qWarning() << "Internal Error: project is null" << __FILE__ << __LINE__;
    return;
  }
  std::shared_ptr<JobsManifest> jobsManifest = m_project->jobsManifest();

  // Get the job's index in the manifest
  int index = jobsManifest->findIndex(jobId.toStdString());
  if (index < 0)
  {
    qWarning() << "Internal Error: onJobAdded(), id" << jobId << "not found in JobsManifest";
    return;
  }

  // Skip jobs generated for local testing
  std::string machine;
  jobsManifest->getField(index, "machine", machine);
  if (machine.empty() || machine == "local-test")
  {
    qDebug() << "Ignoring job" << jobId << "with machine field:" << QString::fromStdString(machine)
             << __FILE__ << __LINE__;
    return;
  }

  // New jobs are inserted at the head of the jobs manifest, so insert row 0 into model
  this->beginInsertRows(QModelIndex(), 0, 0);

  // macOS builds don't always update their views when jobs are added.
  // Dont know whether this is a compiler optimization or runtime behavior,
  // but processing events between begin/end seems to fix this behavior.
  QCoreApplication::processEvents();

  this->endInsertRows();

  m_jobTracker->addNewJob(jobId);
}

void qtJobsModel::onJobOverwritten(const QString& jobId)
{
  if (!m_project)
  {
    return;
  }

  int row = m_project->jobsManifest()->findIndex(jobId.toStdString());
  if (row < 0)
  {
    qWarning() << "Did not find job" << jobId << "in the project.";
    return;
  }

  qInfo() << "Removing job record" << jobId;
  this->beginRemoveRows(QModelIndex(), row, row);
  m_project->removeJobRecord(jobId.toStdString());
  this->endRemoveRows();
}

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

  // Check all jobs for non-terminal state
  std::shared_ptr<JobsManifest> jobsManifest = m_project->jobsManifest();
  std::string jobId;
  std::string status;
  int jobCount = static_cast<int>(jobsManifest->size());
  for (int i = 0; i < jobCount; ++i)
  {
    jobsManifest->getField(i, "status", status);
    if (!JobsManifest::isJobFinished(status))
    {
      jobsManifest->getField(i, "job_id", jobId);
      m_jobTracker->addJob(QString::fromStdString(jobId), false);
    }
  }
}

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

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

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