//=========================================================================
//  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/qt/qtJobsWidget.h"
#include "smtk/simulation/ace3p/qt/qtJobsModel.h"
#include "smtk/simulation/ace3p/qt/ui_qtJobsWidget.h"

#include "smtk/newt/qtDownloadFolderRequester.h"
#include "smtk/newt/qtNewtInterface.h"
#include "smtk/simulation/ace3p/Project.h"
#include "smtk/simulation/ace3p/Stage.h"
#include "smtk/simulation/ace3p/qt/qtProjectRuntime.h"

#include "smtk/attribute/Resource.h"

#include <iostream>

#include <QAction>
#include <QClipboard>
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QFileDialog>
#include <QMessageBox>
#include <QNetworkReply>
#include <QPushButton>
#include <QSet>
#include <QVariant>

#include <set>
#include <string>

namespace
{
// ACE3P codes that write mode files to their results folder
const std::set<std::string> ModeFileTypes = { "Omega3P", "S3P", "T3P", "TEM3P" };

// ACE3P codes that we currently provide visualization
const std::set<std::string> VizTypes = { "Omega3P", "S3P", "T3P", "Track3P", "TEM3P" };
} // namespace

namespace smtk
{
namespace simulation
{
namespace ace3p
{

qtJobsWidget::qtJobsWidget(QWidget* parentWidget)
  : QWidget(parentWidget)
  , ui(new Ui::qtJobsWidget)
  , m_jobs_model(new qtJobsModel(this))
  , m_downloadRequester(new ::newt::qtDownloadFolderRequester(this))
  , m_proxyModel(new QSortFilterProxyModel(this))
{
  // initialize the UI
  this->ui->setupUi(this);

#if 1
  this->ui->m_addJobButton->hide();
#endif

  // the proxy model is used to show/hide Jobs based on the active Stage
  m_proxyModel->setSourceModel(m_jobs_model);

  // setup jobs table
  this->ui->m_jobsTable->setModel(m_proxyModel);
  this->ui->m_jobsTable->setSelectionBehavior(QAbstractItemView::SelectRows);
  this->ui->m_jobsTable->setSelectionMode(QAbstractItemView::SingleSelection);
  this->ui->m_jobsTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
  this->ui->m_jobsTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);

  // windows only stylesheet declaration to get the horizontal header cells to properly draw their
  //    border lines
  if (QSysInfo::windowsVersion() == QSysInfo::WV_WINDOWS10)
  {
    this->ui->m_jobsTable->horizontalHeader()->setStyleSheet("QHeaderView::section{"
                                                             "border-top:0px solid #D8D8D8;"
                                                             "border-left:0px solid #D8D8D8;"
                                                             "border-right:1px solid #D8D8D8;"
                                                             "border-bottom: 1px solid #D8D8D8;"
                                                             "background-color:white;"
                                                             "}"
                                                             /*
      "QTableCornerButton::section{"
        "border-top:0px solid #D8D8D8;"
        "border-left:0px solid #D8D8D8;"
        "border-right:1px solid #D8D8D8;"
        "border-bottom: 1px solid #D8D8D8;"
        "background-color:white;"
      "}"
      */
    );
  }

  // we use the JobsModel to fill the details panel, but we only want a few fields in the table
  for (int i = 4; i < m_jobs_model->columnCount(); ++i)
  {
    this->ui->m_jobsTable->hideColumn(i);
  }

  // hide the jobs details panel when first opened
  this->ui->m_JobsDetails->hide();

  // connect selection model to details section
  auto selection = this->ui->m_jobsTable->selectionModel();
  QObject::connect(
    selection,
    &QItemSelectionModel::selectionChanged,
    this,
    &qtJobsWidget::toggleDetailsVisibility);

  // connect job name field to job model setData
  QObject::connect(
    this->ui->m_job_name_field, &QLineEdit::textEdited, this, &qtJobsWidget::jobNameChanged);

  QObject::connect(
    this->ui->m_notes_field, &QPlainTextEdit::textChanged, this, &qtJobsWidget::notesChanged);

  // Connect polling widgets to qtJobsModel
  QObject::connect(
    this->ui->m_updateStatusButton,
    &QPushButton::clicked,
    m_jobs_model,
    &qtJobsModel::updateStatus);

  QObject::connect(
    this->ui->m_enablePollingCheckBox,
    &QCheckBox::stateChanged,
    m_jobs_model,
    &qtJobsModel::enablePolling);

  QObject::connect(m_jobs_model, &qtJobsModel::pollingStateChanged, [this](bool polling) {
    QString text = polling ? "ON" : "Off";
    this->ui->m_pollingStateLabel->setText(text);
  });

  QObject::connect(
    this->ui->m_download_remote_dir, &QPushButton::clicked, this, &qtJobsWidget::downloadJob);

  QObject::connect(
    this->ui->m_remote_dir_button,
    &QPushButton::clicked,
    this,
    &qtJobsWidget::onNavigateRemoteClicked);

  QObject::connect(
    this->ui->m_input_dir_button,
    &QPushButton::clicked,
    this,
    &qtJobsWidget::onNavigateInputClicked);

  QObject::connect(
    this->ui->m_load_job_button, &QPushButton::clicked, this, &qtJobsWidget::onLoadJobClicked);

  // connect copy remote dir button to system clipboard
  QObject::connect(this->ui->m_copy_remote_path, &QPushButton::clicked, [this]() {
    QString path = this->ui->m_remote_dir->text();
    auto clipboard = QGuiApplication::clipboard();
    clipboard->setText(path);
    qInfo() << "Remote Directory path copied.";
  });

  // connect copy download dir button to system clipboard
  QObject::connect(this->ui->m_copy_download_path, &QPushButton::clicked, [this]() {
    QString path = this->ui->m_local_dir->text();
    auto clipboard = QGuiApplication::clipboard();
    clipboard->setText(path);
    qInfo() << "Download Directory path copied.";
  });

  // connect copy input dir button to system clipboard
  QObject::connect(this->ui->m_copy_input_path, &QPushButton::clicked, [this]() {
    QString path = this->ui->m_input_dir->text();
    auto clipboard = QGuiApplication::clipboard();
    clipboard->setText(path);
    qInfo() << "Remote Input Directory path copied.";
  });

  // connect remote vis button
  QObject::connect(
    this->ui->m_load_remote_button,
    &QPushButton::clicked,
    this,
    &qtJobsWidget::onLoadJobRemoteClicked);

  // Initialize polling widgets
  this->enablePollingWidgets();

  // Listen for user logged-in signal
  auto newt = ::newt::qtNewtInterface::instance();
  if (!newt->isLoggedIn())
  {
    QObject::connect(newt, &newt::qtNewtInterface::loginComplete, [this]() {
      this->enablePollingWidgets();
      const QItemSelection selection = this->ui->m_jobsTable->selectionModel()->selection();
      this->toggleDetailsVisibility(selection);
    });
  }

  // Listen for job-downloader signals
  QObject::connect(
    m_downloadRequester, &::newt::qtDownloadFolderRequester::downloadComplete, [this]() {
      this->onJobDownloaded(m_downloadJobId);
    });
  QObject::connect(
    m_downloadRequester,
    &::newt::qtDownloadFolderRequester::progressMessage,
    [](const QString& msg) { qInfo() << msg; });
  QObject::connect(
    m_downloadRequester, &::newt::qtDownloadFolderRequester::errorMessage, [](const QString& msg) {
      qWarning() << msg;
    });
}

void qtJobsWidget::setProject(smtk::project::ProjectPtr project)
{
  if (project)
  {
    m_jobs_model->populateJobs(project);
    this->enablePollingWidgets();
  }
}

void qtJobsWidget::onJobAdded(const QString& jobId)
{
  m_jobs_model->onJobAdded(jobId);
  this->enablePollingWidgets();

  // Enable polling
  this->ui->m_enablePollingCheckBox->setChecked(true);
}

void qtJobsWidget::onJobOverwritten(const QString& jobId)
{
  m_jobs_model->onJobOverwritten(jobId);
}

void qtJobsWidget::toggleDetailsVisibility(const QItemSelection& selected)
{
  // get the list of selected items
  auto selectedList = selected.indexes();

  // if the selection is empty, hide the details panel and return
  if (!selectedList.size())
  {
    this->ui->m_JobsDetails->hide();
    return;
  }

  // fill in all the fields in the details panel
  const int jobIndex =
    m_proxyModel->mapToSource(m_proxyModel->index(selectedList[0].row(), 0)).row();
  QModelIndex index;
  QVariant fieldData;

  index = m_jobs_model->index(jobIndex, qtJobsModel::Machine);
  fieldData = m_jobs_model->data(index, Qt::DisplayRole);
  this->ui->m_machine->setText(fieldData.toString());

  index = m_jobs_model->index(jobIndex, qtJobsModel::Nodes);
  fieldData = m_jobs_model->data(index, Qt::DisplayRole);
  this->ui->m_nodes->setText(fieldData.toString());

  index = m_jobs_model->index(jobIndex, qtJobsModel::Status);
  fieldData = m_jobs_model->data(index, Qt::DisplayRole);
  QString statusEntry = fieldData.toString();
  this->ui->m_job_status->setText(statusEntry);

  index = m_jobs_model->index(jobIndex, qtJobsModel::AnalysisType);
  fieldData = m_jobs_model->data(index, Qt::DisplayRole);
  QString analysis = fieldData.toString();
  this->ui->m_analysis_type->setText(analysis);

  // Check for input folder
  index = m_jobs_model->index(jobIndex, qtJobsModel::InputDir);
  fieldData = m_jobs_model->data(index, Qt::DisplayRole);
  bool inputVisible = !(fieldData.toString().isEmpty());
  this->ui->m_input_dir->setVisible(inputVisible);
  this->ui->m_input_dir_button->setVisible(inputVisible);
  this->ui->m_input_dir_label->setVisible(inputVisible);
  this->ui->m_copy_input_path->setVisible(inputVisible);

  // Check if we should show & enable the SLACTools loader buttons
  this->ui->m_load_job_button->setVisible(false);
  this->ui->m_load_remote_button->setVisible(false);

  // Enable viz buttons for "most" analysis results
  if (::VizTypes.find(analysis.toStdString()) != ::VizTypes.end())
  {
    this->ui->m_load_job_button->setVisible(true);
    this->ui->m_load_remote_button->setVisible(true);

    bool enableLocal = statusEntry == "downloaded";
    this->ui->m_load_job_button->setEnabled(enableLocal);
    bool enableRemote = (statusEntry == "complete") || (statusEntry == "downloaded");
    this->ui->m_load_remote_button->setEnabled(enableRemote);
  }

  // Check if we should enable download and file browser buttons
  bool isLoggedIn = newt::qtNewtInterface::instance()->isLoggedIn();
  this->ui->m_input_dir_button->setEnabled(isLoggedIn);
  this->ui->m_remote_dir_button->setEnabled(isLoggedIn);
  bool isJobFinished =
    smtk::simulation::ace3p::JobsManifest::isJobFinished(statusEntry.toStdString());
  this->ui->m_download_remote_dir->setEnabled(isLoggedIn && isJobFinished);

  index = m_jobs_model->index(jobIndex, qtJobsModel::Notes);
  fieldData = m_jobs_model->data(index, Qt::DisplayRole);
  this->ui->m_notes_field->setPlainText(fieldData.toString());

  index = m_jobs_model->index(jobIndex, qtJobsModel::RemoteDir);
  fieldData = m_jobs_model->data(index, Qt::DisplayRole);
  this->ui->m_remote_dir->setText(fieldData.toString());

  index = m_jobs_model->index(jobIndex, qtJobsModel::LocalDir);
  fieldData = m_jobs_model->data(index, Qt::DisplayRole);
  this->ui->m_local_dir->setText(fieldData.toString());

  index = m_jobs_model->index(jobIndex, qtJobsModel::InputDir);
  fieldData = m_jobs_model->data(index, Qt::DisplayRole);
  this->ui->m_input_dir->setText(fieldData.toString());

  index = m_jobs_model->index(jobIndex, qtJobsModel::Processes);
  fieldData = m_jobs_model->data(index, Qt::DisplayRole);
  this->ui->m_processes->setText(fieldData.toString());

  index = m_jobs_model->index(jobIndex, qtJobsModel::JobID);
  fieldData = m_jobs_model->data(index, Qt::DisplayRole);
  this->ui->m_job_id->setText(fieldData.toString());

  index = m_jobs_model->index(jobIndex, qtJobsModel::JobName);
  fieldData = m_jobs_model->data(index, Qt::DisplayRole);
  this->ui->m_job_name_field->setText(fieldData.toString());

  this->ui->m_JobsDetails->show();
}

void qtJobsWidget::jobNameChanged(const QString& text)
{
  // get index of current job
  auto selection = this->ui->m_jobsTable->selectionModel();
  QModelIndexList rowList = selection->selectedRows(qtJobsModel::JobName);
  this->m_jobs_model->setData(rowList[0], text);
}

void qtJobsWidget::notesChanged()
{
  // get index of current job
  auto selection = this->ui->m_jobsTable->selectionModel();
  QModelIndexList rowList = selection->selectedRows(qtJobsModel::Notes);
  this->m_jobs_model->setData(rowList[0], this->ui->m_notes_field->toPlainText());
}

void qtJobsWidget::pollingCheckBoxStateChanged(bool checked)
{
  m_jobs_model->enablePolling(checked);
  this->ui->m_updateStatusButton->setEnabled(!checked);
}

/////////////////// TODO - delete later, temporary code for creating a testing project
void qtJobsWidget::on_m_addJobButton_clicked()
{
  m_jobs_model->addTestJob();
  onStageSelected(m_activeStageIndex);
}
///////////////////

bool qtJobsWidget::enablePollingWidgets()
{
  bool enable = (m_jobs_model->rowCount() > 0) && newt::qtNewtInterface::instance()->isLoggedIn();

  // Set polling widget states
  this->ui->m_enablePollingCheckBox->setEnabled(enable);
  this->ui->m_pollingStateLabel->setEnabled(enable);
  this->ui->m_updateStatusButton->setEnabled(
    enable && !this->ui->m_enablePollingCheckBox->isChecked());
  if (!enable)
  {
    this->ui->m_pollingStateLabel->setText("Off");
  }

  return enable;
}

void qtJobsWidget::onProjectClosed()
{
  this->m_jobs_model->clearJobs();
  this->ui->m_JobsDetails->hide();
  this->enablePollingWidgets();
}

void qtJobsWidget::onNavigateRemoteClicked()
{
  QString dir = this->ui->m_remote_dir->text();
  emit requestNavigateDir(dir);
}

void qtJobsWidget::onNavigateInputClicked()
{
  QString dir = this->ui->m_input_dir->text();
  emit requestNavigateDir(dir);
}

void qtJobsWidget::onStageSelected(int stageIndex)
{
  m_activeStageIndex = stageIndex;

  auto project = qtProjectRuntime::instance()->ace3pProject();
  if (project == nullptr)
  {
    qWarning() << "Internal error - null project pointer.";
    return;
  }

  std::shared_ptr<Stage> stage = project->stage(m_activeStageIndex);
  if (stage == nullptr)
  {
    qWarning() << "Internal error - null stage pointer.";
    return;
  }

  // turn on filtering of Jobs (based on StageID)
  std::string stageId = stage->attributeResource()->id().toString();
  m_proxyModel->setFilterFixedString(QString::fromStdString(stageId));
  m_proxyModel->setFilterKeyColumn(m_jobs_model->filterKeyColumn());
}

void qtJobsWidget::onLoadJobClicked()
{
  QString jobId = this->ui->m_job_id->text();
  emit this->requestLoadJob(jobId, false);
}

void qtJobsWidget::onLoadJobRemoteClicked()
{
  QString jobId = this->ui->m_job_id->text();
  emit this->requestLoadJob(jobId, true);
}

void qtJobsWidget::downloadJob()
{
  QString jobId = this->ui->m_job_id->text();
  auto project = qtProjectRuntime::instance()->ace3pProject();

  int manifestIndex = project->jobsManifest()->findIndex(jobId.toStdString());
  if (manifestIndex < 0)
  {
    qWarning() << "Internal Error: id" << jobId << "not found in jobs manifest";
    return;
  }
  // qDebug() << __FILE__ << __LINE__ << jobId << manifestIndex;

  std::string status;
  project->getJobRecordField(manifestIndex, "status", status);
  if (!smtk::simulation::ace3p::JobsManifest::isJobFinished(status))
  {
    QString msg = QString("Sorry, download is not supported for job state %1").arg(status.c_str());
    QMessageBox::information(this, "Unsupported Request", msg);
    return;
  }

  std::string machine;
  project->getJobRecordField(manifestIndex, "machine", machine);

  // Setup local folder for download
  std::string resultsSubfolder;
  project->getJobRecordField(manifestIndex, "results_subfolder", resultsSubfolder);
  QString localResultsPath = QString("%1/%2/download/%3")
                               .arg(project->jobsDirectory().c_str())
                               .arg(jobId)
                               .arg(resultsSubfolder.c_str());

  // check if the job has been downloaded. present a popup if so
  QDir dir(localResultsPath);
  if (dir.exists())
  {
    QMessageBox msgBox(this);
    msgBox.setText("This job has already been downloaded.");
    msgBox.setInformativeText(
      "Do you want to download this job? This may overwrite existing files.");
    QPushButton* continueButton = msgBox.addButton(tr("Continue"), QMessageBox::AcceptRole);
    msgBox.setDefaultButton(continueButton);
    QPushButton* cancelButton = msgBox.addButton(tr("Cancel"), QMessageBox::RejectRole);

    msgBox.exec();
    if (msgBox.clickedButton() == cancelButton)
    {
      qInfo() << "Job download canceled.";
      return;
    }
  }

  // Get path to remote results
  std::string remoteJobFolder;
  project->getJobRecordField(manifestIndex, "runtime_job_folder", remoteJobFolder);
  std::string remoteResultsFolder = remoteJobFolder + "/" + resultsSubfolder;
  QString remoteResultsPath = QString::fromStdString(remoteResultsFolder);

  // Start download
  m_downloadJobId = jobId;

  // Check for model (.ncdf) file
  std::string ncdf_filename;
  project->getJobRecordField(manifestIndex, "runtime_mesh_filename", ncdf_filename);
  if (!ncdf_filename.empty())
  {
    // See if we need to download it
    QString localMeshPath =
      QString("%1/%2").arg(project->assetsDirectory().c_str()).arg(ncdf_filename.c_str());
    if (dir.exists(localMeshPath))
    {
      QString msg = QString("Using model file at %1").arg(localMeshPath);
      qDebug() << msg;
    }
    else
    {
      std::string remote_mesh_path = remoteJobFolder + "/" + ncdf_filename;
      QString remoteMeshPath = QString::fromStdString(remote_mesh_path);
      this->downloadFile(QString::fromStdString(machine), remoteMeshPath, localMeshPath);
    }
  }

  // Download mode files for relevant analysis codes
  std::string analysis;
  project->getJobRecordField(manifestIndex, "analysis", analysis);
  if (::ModeFileTypes.find(analysis) != ::ModeFileTypes.end())
  {
    m_downloadRequester->startDownload(
      localResultsPath, QString::fromStdString(machine), remoteResultsPath, false, "*.mod");
  }
  else if (analysis == "Track3P")
  {
    // mode files are in the input folder
    std::string input_folder;
    project->getJobRecordField(manifestIndex, "runtime_input_folder", input_folder);
    remoteResultsPath = QString::fromStdString(input_folder);
    m_downloadRequester->startDownload(
      localResultsPath, QString::fromStdString(machine), remoteResultsPath, false, "*.mod");
  }
  else
  {
    // Download results directory
    qInfo() << "Downloading results directory" << remoteResultsPath;
    m_downloadRequester->startDownload(
      localResultsPath, QString::fromStdString(machine), remoteResultsPath);
  }
}

void qtJobsWidget::downloadFile(
  const QString& machine,
  const QString& remotePath,
  const QString& localPath)
{
  // Open QFile for local file
  QFile* file = new QFile(localPath);
  if (!file->open(QIODevice::WriteOnly)) // truncates any existing file
  {
    delete file;
    QString msg = QString("Unable to open local file for writing at %1").arg(localPath);
    QMessageBox::critical(this, "Error Opening Local File", msg);
    return;
  }

  qDebug() << "Requesting download" << remotePath;
  auto newt = ::newt::qtNewtInterface::instance();
  QNetworkReply* reply = newt->requestFileDownload(machine, remotePath);
  QObject::connect(
    reply, &QNetworkReply::readyRead, [this, reply, file]() { file->write(reply->readAll()); });

  QObject::connect(reply, &QNetworkReply::finished, [this, reply, file]() {
    file->close();

    // Check for errors
    int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
    if (statusCode != 200)
    {
      QString msg =
        QString("HTTP ERROR Downloading to file %1: %2").arg(file->fileName()).arg(statusCode);
      QMessageBox::critical(this, "Download Error", msg);
      file->remove();
    }
    else if (reply->error() != QNetworkReply::NoError)
    {
      QString msg =
        QString("ERROR Downloading to file %1: %2").arg(file->fileName()).arg(reply->error());
      QMessageBox::critical(this, "Download Error", msg);
      file->remove();
    }

    delete file;
    reply->deleteLater();
  });
}

void qtJobsWidget::onJobDownloaded(const QString& jobId)
{
  qInfo() << "Download complete";

  auto project = qtProjectRuntime::instance()->ace3pProject();
  if (project == nullptr)
  {
    qWarning() << "No project loaded for download job" << jobId;
    return;
  }

  std::shared_ptr<JobsManifest> manifest = project->jobsManifest();
  int index = manifest->findIndex(jobId.toStdString());
  if (index < 0)
  {
    qWarning() << "Job" << jobId << "not found in project" << project->name().c_str();
    return;
  }

  // Update jobs manifest
  project->setJobRecordField(index, "status", "downloaded");
  project->writeJobsManifest();

  // Check if the job is currently selected
  auto selection = this->ui->m_jobsTable->selectionModel();
  QModelIndexList rowList = selection->selectedRows(qtJobsModel::JobID);
  if (rowList.isEmpty())
  {
    return;
  }
  QString selectedId = rowList[0].data().toString();
  if (selectedId != jobId)
  {
    return;
  }

  // notify model to update
  QModelIndex qIndex = this->m_jobs_model->index(index, qtJobsModel::Status);
  this->ui->m_jobsTable->update(qIndex);

  // Check analysis to see if we should enable SLACTools button
  std::string analysis;
  manifest->getField(index, "analysis", analysis);
  bool stEnable = ::VizTypes.find(analysis) != ::VizTypes.end();
  this->ui->m_load_job_button->setEnabled(stEnable);
}

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