/******************************************************************************

  This source file is part of the Avogadro project.

  Copyright 2013 Kitware, Inc.

  This source code is released under the New BSD License, (the "License").

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.

******************************************************************************/

#include "molequeuewidget.h"
#include "ui_molequeuewidget.h"

#include "molequeuemanager.h"

#include <QtWidgets/QMessageBox>

#include <QtCore/QTimer>

#include <limits>

namespace Avogadro {
namespace MoleQueue {

using ::MoleQueue::JobObject;
using ::MoleQueue::Client;

const unsigned int MoleQueueWidget::InvalidMoleQueueId(
  std::numeric_limits<unsigned int>::max());

MoleQueueWidget::MoleQueueWidget(QWidget* parent_)
  : QWidget(parent_), m_ui(new Ui::MoleQueueWidget), m_jobState("Unknown"),
    m_requestId(-1), m_moleQueueId(InvalidMoleQueueId)
{
  m_ui->setupUi(this);

  connect(m_ui->refreshProgramsButton, SIGNAL(clicked()),
          SLOT(refreshPrograms()));

  MoleQueueManager& mqManager = MoleQueueManager::instance();
  m_ui->queueListView->setModel(&mqManager.queueListModel());

  if (mqManager.connectIfNeeded())
    mqManager.requestQueueList();
}

MoleQueueWidget::~MoleQueueWidget()
{
  delete m_ui;
}

JobObject& MoleQueueWidget::jobTemplate()
{
  return m_jobTemplate;
}

const JobObject& MoleQueueWidget::jobTemplate() const
{
  return m_jobTemplate;
}

void MoleQueueWidget::setJobTemplate(const JobObject& job)
{
  m_jobTemplate = job;

  m_ui->numberOfCores->setValue(job.value("numberOfCores", 1).toInt());
  m_ui->cleanRemoteFiles->setChecked(
    job.value("cleanRemoteFiles", false).toBool());
  m_ui->hideFromGui->setChecked(job.value("hideFromGui", false).toBool());
  m_ui->popupOnStateChange->setChecked(
    job.value("popupOnStateChange", false).toBool());
}

void MoleQueueWidget::refreshPrograms()
{
  MoleQueueManager& mqManager = MoleQueueManager::instance();
  if (!mqManager.connectIfNeeded()) {
    QMessageBox::information(this, tr("Cannot connect to MoleQueue"),
                             tr("Cannot connect to MoleQueue server. Please "
                                "ensure that it is running and try again."));
    return;
  }
  mqManager.requestQueueList();
}

int MoleQueueWidget::submitJobRequest()
{
  m_submissionError.clear();
  m_jobState = "Unknown";
  m_requestId = -1;
  m_moleQueueId = InvalidMoleQueueId;

  MoleQueueManager& mqManager = MoleQueueManager::instance();
  if (!mqManager.connectIfNeeded())
    return -1;

  MoleQueue::JobObject job(configuredJob());
  if (job.queue().isEmpty()) // if the queue is not set, the job config failed.
    return -1;

  m_requestId = mqManager.client().submitJob(job);
  if (m_requestId >= 0) {
    listenForJobSubmitReply();
    listenForJobStateChange();
  } else {
    m_submissionError = tr("Client failed to submit job to MoleQueue.");
    // Single shot ensures that this signal is emitted after control returns
    // to the event loop
    QTimer::singleShot(0, this, SIGNAL(jobSubmitted(false)));
  }
  return m_requestId;
}

void MoleQueueWidget::showAndSelectProgram(const QString& programName)
{
  MoleQueueManager& mqManager = MoleQueueManager::instance();
  setProperty("selectProgramName", programName);

  connect(&mqManager, SIGNAL(queueListUpdated()),
          SLOT(showAndSelectProgramHandler()));

  if (mqManager.connectIfNeeded())
    mqManager.requestQueueList();
}

bool MoleQueueWidget::openOutput() const
{
  return m_ui->openOutput->isChecked();
}

bool MoleQueueWidget::requestJobLookup()
{
  MoleQueueManager& mqManager = MoleQueueManager::instance();
  if (m_moleQueueId != InvalidMoleQueueId && mqManager.connectIfNeeded()) {
    listenForLookupJobReply();
    int reqId = mqManager.client().lookupJob(m_moleQueueId);
    setProperty("lookupJobRequestId", reqId);
    return true;
  }
  return false;
}

MoleQueue::JobObject MoleQueueWidget::configuredJob() const
{
  MoleQueueManager& mqManager = MoleQueueManager::instance();

  // get queue/program
  QModelIndexList sel(m_ui->queueListView->selectionModel()->selectedIndexes());
  if (sel.size() < 1) {
    QMessageBox::information(parentWidget(), tr("No program selected."),
                             tr("Please select the target program from the "
                                "\"Queue and Program\" list."));
    return MoleQueue::JobObject();
  }

  QString queue;
  QString program;
  if (!mqManager.queueListModel().lookupProgram(sel.first(), queue, program)) {
    QMessageBox::critical(parentWidget(), tr("Internal error."),
                          tr("Unable to resolve program selection. This is "
                             "a bug."));
    return MoleQueue::JobObject();
  }

  MoleQueue::JobObject job(m_jobTemplate);
  job.setQueue(queue);
  job.setProgram(program);
  job.setValue("numberOfCores", m_ui->numberOfCores->value());
  job.setValue("cleanRemoteFiles", m_ui->cleanRemoteFiles->isChecked());
  job.setValue("hideFromGui", m_ui->hideFromGui->isChecked());
  job.setValue("popupOnStateChange", m_ui->popupOnStateChange->isChecked());

  return job;
}

void MoleQueueWidget::setBatchMode(bool batch)
{
  m_ui->openOutput->setHidden(batch);
  m_ui->openOutputLabel->setHidden(batch);
}

bool MoleQueueWidget::batchMode() const
{
  return m_ui->openOutput->isHidden();
}

void MoleQueueWidget::showAndSelectProgramHandler()
{
  MoleQueueManager& mqManager = MoleQueueManager::instance();
  const QString program(property("selectProgramName").toString());
  setProperty("selectProgramName", QVariant());
  disconnect(&mqManager, SIGNAL(queueListUpdated()), this,
             SLOT(showAndSelectProgramHandler()));

  // Get all program nodes that match the name
  QModelIndexList list(mqManager.queueListModel().findProgramIndices(program));

  // Expand the corresponding queues
  foreach (const QModelIndex& mi, list)
    m_ui->queueListView->expand(mi.parent());

  // Select the first entry
  if (!list.isEmpty()) {
    m_ui->queueListView->selectionModel()->select(
      list.first(), QItemSelectionModel::ClearAndSelect);
    m_ui->queueListView->scrollTo(list.first());
  }
}

void MoleQueueWidget::onLookupJobReply(int reqId, const QJsonObject& result)
{
  QVariant reqIdVariant(property("lookupJobRequestId"));
  bool ok;
  int myReqId = reqIdVariant.toInt(&ok);
  if (ok && reqId == myReqId) {
    setProperty("lookupJobRequestId", QVariant());
    listenForLookupJobReply(false);
    MoleQueue::JobObject job;
    job.fromJson(result);
    emit jobUpdated(job);
  }
}

void MoleQueueWidget::onSubmissionSuccess(int localId, unsigned int mqId)
{
  if (localId != m_requestId)
    return;

  listenForJobSubmitReply(false);
  m_moleQueueId = mqId;
  emit jobSubmitted(true);
}

void MoleQueueWidget::onSubmissionFailure(int localId, unsigned int,
                                          const QString& error)
{
  if (localId != m_requestId)
    return;

  listenForJobSubmitReply(false);
  m_submissionError = error;
  emit jobSubmitted(false);
}

void MoleQueueWidget::onJobStateChange(unsigned int mqId, const QString&,
                                       const QString& newState)
{
  if (mqId != m_moleQueueId)
    return;

  m_jobState = newState;

  if (m_jobState == QLatin1String("Finished")) {
    listenForJobStateChange(false);
    emit jobFinished(true);
  } else if (m_jobState == QLatin1String("Error") ||
             m_jobState == QLatin1String("Canceled")) {
    listenForJobStateChange(false);
    emit jobFinished(false);
  }
}

void MoleQueueWidget::listenForLookupJobReply(bool listen)
{
  Client& mqClient(MoleQueueManager::instance().client());
  if (listen) {
    connect(&mqClient, SIGNAL(lookupJobResponse(int, QJsonObject)), this,
            SLOT(onLookupJobReply(int, QJsonObject)));
  } else {
    disconnect(&mqClient, SIGNAL(lookupJobResponse(int, QJsonObject)), this,
               SLOT(onLookupJobReply(int, QJsonObject)));
  }
}

void MoleQueueWidget::listenForJobSubmitReply(bool listen)
{
  MoleQueue::Client& mqClient(MoleQueueManager::instance().client());

  if (listen) {
    connect(&mqClient, SIGNAL(submitJobResponse(int, uint)), this,
            SLOT(onSubmissionSuccess(int, uint)));
    connect(&mqClient, SIGNAL(errorReceived(int, uint, QString)), this,
            SLOT(onSubmissionFailure(int, uint, QString)));
  } else {
    disconnect(&mqClient, SIGNAL(submitJobResponse(int, uint)), this,
               SLOT(onSubmissionSuccess(int, uint)));
    disconnect(&mqClient, SIGNAL(errorReceived(int, uint, QString)), this,
               SLOT(onSubmissionFailure(int, uint, QString)));
  }
}

void MoleQueueWidget::listenForJobStateChange(bool listen)
{
  MoleQueue::Client& mqClient(MoleQueueManager::instance().client());

  if (listen) {
    connect(&mqClient, SIGNAL(jobStateChanged(uint, QString, QString)), this,
            SLOT(onJobStateChange(uint, QString, QString)));
  } else {
    disconnect(&mqClient, SIGNAL(jobStateChanged(uint, QString, QString)), this,
               SLOT(onJobStateChange(uint, QString, QString)));
  }
}

bool MoleQueueWidget::programSelected()
{
  QModelIndexList sel(m_ui->queueListView->selectionModel()->selectedIndexes());
  return sel.size() > 0;
}

} // namespace MoleQueue
} // namespace Avogadro
