//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/View/Job/JobsListing.cpp
//! @brief     Implements class JobsListing.
//!
//! @homepage  http://www.bornagainproject.org
//! @license   GNU General Public License v3 or higher (see COPYING)
//! @copyright Forschungszentrum Jülich GmbH 2018
//! @authors   Scientific Computing Group at MLZ (see CITATION, AUTHORS)
//
//  ************************************************************************************************

#include "GUI/View/Job/JobsListing.h"
#include "Base/Util/Assert.h"
#include "GUI/Model/Data/Data2DItem.h"
#include "GUI/Model/Job/BatchInfo.h"
#include "GUI/Model/Job/JobStatus.h"
#include "GUI/Model/Job/JobsSet.h"
#include "GUI/Model/Project/ProjectDocument.h"
#include "GUI/View/Job/JobProgressDelegate.h"
#include "GUI/View/Job/JobsQModel.h"
#include "GUI/View/Setup/ActionFactory.h"
#include "GUI/View/Widget/StyledToolbar.h"
#include <QVBoxLayout>

namespace {

//! compare function for sorting indexes according to row descending
bool row_descending(const QModelIndex& idx1, const QModelIndex& idx2)
{
    return idx1.row() > idx2.row();
}

//! compare function for sorting indexes according to row asscending
bool row_ascending(const QModelIndex& idx1, const QModelIndex& idx2)
{
    return idx1.row() < idx2.row();
}

} // namespace

//  ------------------------------------------------------------------------------------------------
//  public member functions
//  ------------------------------------------------------------------------------------------------

JobsListing::JobsListing(QWidget* parent, Qt::WindowFlags f)
    : QWidget(parent, f)
    , m_list_view(new QListView(this))
    , m_progress_delegate(new JobProgressDelegate(this))
    , m_model(new JobsQModel(this))
{
    auto* layout = new QVBoxLayout(this);
    layout->setContentsMargins(0, 0, 0, 0);

    m_run_action = new QAction("Run", this);
    m_run_action->setIcon(QIcon(":/images/play.svg"));
    m_run_action->setToolTip("Run currently selected jobs");
    connect(m_run_action, &QAction::triggered, this, &JobsListing::onRun);
    addAction(m_run_action);

    m_cancel_action = new QAction("Stop", this);
    m_cancel_action->setIcon(QIcon(":/images/stop.svg"));
    m_cancel_action->setToolTip("Stop currently selected jobs");
    connect(m_cancel_action, &QAction::triggered, this, &JobsListing::onCancel);
    addAction(m_cancel_action);

    m_remove_action = ActionFactory::createRemoveAction("job");
    connect(m_remove_action, &QAction::triggered, this, &JobsListing::onRemove);
    addAction(m_remove_action);

    m_equalize_menu = new QMenu("Equalize selected plots", this);

    QToolBar* toolbar = new StyledToolbar(this);
    toolbar->setMinimumSize(toolbar->minimumHeight(), toolbar->minimumHeight());
    toolbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
    toolbar->addAction(m_run_action);
    toolbar->addAction(m_cancel_action);
    toolbar->addAction(m_remove_action);
    layout->addWidget(toolbar);

    m_list_view->setSelectionMode(QAbstractItemView::ExtendedSelection);

    connect(m_progress_delegate, &JobProgressDelegate::cancelButtonClicked, this,
            &JobsListing::onCancel);
    m_list_view->setItemDelegate(m_progress_delegate);
    layout->addWidget(m_list_view);

    m_list_view->setModel(m_model);

    setContextMenuPolicy(Qt::CustomContextMenu);
    connect(this, &QWidget::customContextMenuRequested, this, &JobsListing::showContextMenu);

    connect(m_list_view->selectionModel(), &QItemSelectionModel::selectionChanged, this,
            &JobsListing::onItemSelectionChanged);
    connect(m_model, &QAbstractListModel::dataChanged, this, &JobsListing::onJobsDataChanged);

    updateActions();
    restoreSelection();

    setMinimumWidth(10);
}

QVector<JobItem*> JobsListing::selectedJobItems() const
{
    QVector<JobItem*> result;
    for (const QModelIndex& index : m_list_view->selectionModel()->selectedIndexes())
        result.push_back(m_model->jobItemForIndex(index));
    return result;
}

void JobsListing::selectJob(JobItem* job)
{
    QModelIndex idx = m_model->indexForJob(job);
    QModelIndexList selected = m_list_view->selectionModel()->selectedIndexes();

    // Already selected, but we still will emit the signal to notify widgets.
    // To handle the case, when the job was selected before it completed (and some stack widgets
    // were refusing to show the content for non-complete job).
    if (selected.size() == 1 && selected.front() == idx) {
        emit selectedJobsChanged({job});
        return;
    }

    m_list_view->selectionModel()->select(idx, QItemSelectionModel::ClearAndSelect);
}

//  ------------------------------------------------------------------------------------------------
//  private slots
//  ------------------------------------------------------------------------------------------------

void JobsListing::onItemSelectionChanged()
{
    updateActions();

    QModelIndexList selected = m_list_view->selectionModel()->selectedIndexes();
    if (selected.size() == 1)
        gDoc->jobsRW()->setCurrentIndex(selected.first().row());

    emit selectedJobsChanged(selectedJobItems());
}

void JobsListing::onJobsDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight)
{
    // currently only single items change, not ranges; thus ranges are not supported
    ASSERT(topLeft == bottomRight);

    if (m_list_view->selectionModel()->isSelected(topLeft))
        updateActions();
}

void JobsListing::onRun()
{
    for (const QModelIndex& index : m_list_view->selectionModel()->selectedIndexes())
        m_model->launchJob(index);
    gDoc->setModified();
}

void JobsListing::onCancel()
{
    for (const QModelIndex& index : m_list_view->selectionModel()->selectedIndexes())
        m_model->cancelJob(index);
    gDoc->setModified();
}

void JobsListing::onRemove()
{
    QModelIndexList indexes = m_list_view->selectionModel()->selectedIndexes();
    ASSERT(!indexes.isEmpty());
    std::sort(indexes.begin(), indexes.end(), row_descending);
    for (const QModelIndex& index : indexes)
        m_model->removeJob(index);

    int lastSelectedRow = indexes.front().row();
    ensureItemSelected(lastSelectedRow);

    gDoc->setModified();
}

void JobsListing::equalizeSelectedToJob(JobItem* srcJob)
{
    QModelIndexList indexes = m_list_view->selectionModel()->selectedIndexes();

    Data2DItem* srcData = srcJob->data2DItem();
    if (!srcData)
        return;

    for (const QModelIndex& index : indexes) {
        JobItem* job = m_model->jobItemForIndex(index);
        if (job != srcJob) {
            if (Data2DItem* di = job->data2DItem()) {
                di->setXrange(srcData->lowerX(), srcData->upperX());
                di->setYrange(srcData->lowerY(), srcData->upperY());
                di->setZrange(srcData->lowerZ(), srcData->upperZ());
            }
        }
    }
    gDoc->setModified();
}

void JobsListing::showContextMenu(const QPoint&)
{
    QMenu menu(this);
    menu.addAction(m_run_action);
    menu.addAction(m_cancel_action);
    menu.addAction(m_remove_action);
    menu.addSeparator();

    m_equalize_menu->clear();
    QModelIndexList indexes = m_list_view->selectionModel()->selectedIndexes();
    if (indexes.size() > 1) {
        std::sort(indexes.begin(), indexes.end(), row_ascending);
        for (const QModelIndex& index : indexes) {
            JobItem* job = m_model->jobItemForIndex(index);
            QAction* action =
                m_equalize_menu->addAction(QString("to ").append(job->batchInfo()->jobName()));
            connect(action, &QAction::triggered, [this, job] { equalizeSelectedToJob(job); });
        }
        m_equalize_menu->setEnabled(true);
    } else
        m_equalize_menu->setEnabled(false);
    menu.addMenu(m_equalize_menu);
    menu.exec(QCursor::pos());
}

//  ------------------------------------------------------------------------------------------------
//  private member functions
//  ------------------------------------------------------------------------------------------------

void JobsListing::updateActions()
{
    QModelIndexList indexes = m_list_view->selectionModel()->selectedIndexes();

    struct IsRunningOrFitting {
        JobsQModel* m_model;
        IsRunningOrFitting(JobsQModel* model)
            : m_model(model)
        {
        }
        bool operator()(const QModelIndex& i) const
        {
            JobItem* job = m_model->jobItemForIndex(i);
            ASSERT(job);
            return isActive(job->batchInfo()->status());
        }
    };

    bool none_running = std::none_of(indexes.begin(), indexes.end(), IsRunningOrFitting(m_model));
    bool all_running = std::all_of(indexes.begin(), indexes.end(), IsRunningOrFitting(m_model));
    bool nonempty = !indexes.empty();
    m_run_action->setEnabled(nonempty && none_running);
    m_cancel_action->setEnabled(nonempty && all_running);
    m_remove_action->setEnabled(nonempty && none_running);
}

void JobsListing::restoreSelection()
{
    int lastUsed = gDoc->jobs()->currentIndex();
    if (lastUsed >= 0 && lastUsed < m_model->rowCount()) {
        QModelIndex lastUsedIndex = m_model->index(lastUsed, 0, QModelIndex());
        m_list_view->selectionModel()->select(lastUsedIndex, QItemSelectionModel::ClearAndSelect);
    } else
        ensureItemSelected();
}

void JobsListing::ensureItemSelected(int lastSelectedRow)
{
    if (!m_list_view->selectionModel()->hasSelection() && m_model->rowCount()) {
        QModelIndex last = m_model->index(m_model->rowCount() - 1, 0, QModelIndex());
        if (lastSelectedRow >= 0 && lastSelectedRow < m_model->rowCount())
            last = m_model->index(lastSelectedRow, 0, QModelIndex());
        m_list_view->selectionModel()->select(last, QItemSelectionModel::ClearAndSelect);
    }
}
