//  ************************************************************************************************
//
//  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/Job/Simulate.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();
}

} // 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] { onCancel(m_list_view->selectionModel()->selectedIndexes()); });
    addAction(m_cancel_action);

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

    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);

    connect(gDoc.get(), &ProjectDocument::documentAboutToReopen, m_list_view->selectionModel(),
            &QItemSelectionModel::clearSelection);
    connect(gDoc.get(), &ProjectDocument::documentOpened, this, &JobsListing::restoreSelection);

    updateActions();

    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::selectNewJob(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());
    else
        gDoc->jobsRW()->setCurrentIndex(-1);

    emit selectedJobsChanged(selectedJobItems());
    emit gDoc->jobs()->jobPlotContextChanged();
}

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())
        if (!GUI::Sim::simulate(m_model->jobItemForIndex(index), gDoc->jobsRW()))
            break;
    gDoc->setModified();
}

void JobsListing::onCancel(const QModelIndexList indexes)
{
    for (const QModelIndex& index : indexes)
        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 rowToSelect = indexes.front().row();
    selectAlternativeItem(rowToSelect);

    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.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())
        return;

    QModelIndex lastUsedIndex = m_model->index(lastUsed, 0, QModelIndex());
    m_list_view->selectionModel()->select(lastUsedIndex, QItemSelectionModel::ClearAndSelect);
    m_list_view->selectionModel()->setCurrentIndex(lastUsedIndex,
                                                   QItemSelectionModel::ClearAndSelect);
}

void JobsListing::selectAlternativeItem(int lastSelectedRow)
{
    if (m_list_view->selectionModel()->hasSelection() || !m_model->rowCount())
        return;

    QModelIndex rowToSelect = m_model->index(m_model->rowCount() - 1, 0, QModelIndex());
    if (lastSelectedRow >= 0 && lastSelectedRow < m_model->rowCount())
        rowToSelect = m_model->index(lastSelectedRow, 0, QModelIndex());
    m_list_view->selectionModel()->select(rowToSelect, QItemSelectionModel::ClearAndSelect);
}
