//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/View/Fit/FitParameterWidget.cpp
//! @brief     Implements class FitParameterWidget.
//!
//! @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/Fit/FitParameterWidget.h"
#include "Base/Util/Assert.h"
#include "GUI/Model/Job/BatchInfo.h"
#include "GUI/Model/Job/JobItem.h"
#include "GUI/Model/Job/JobStatus.h"
#include "GUI/Model/Par/ParameterTreeItems.h"
#include "GUI/Model/Project/ProjectDocument.h"
#include "GUI/Model/Tune/FitParameterContainerItem.h"
#include "GUI/Model/Tune/FitSuiteItem.h"
#include "GUI/View/Base/CustomEventFilters.h"
#include "GUI/View/Fit/FitparQModel.h"
#include "GUI/View/Info/OverlayLabelController.h"
#include "GUI/View/ParEdit/FitParameterDelegate.h"
#include "GUI/View/Tuning/ParameterTuningWidget.h"
#include <QAction>
#include <QHeaderView>
#include <QMenu>
#include <QVBoxLayout>
#include <memory>

FitParameterWidget::FitParameterWidget()
    : m_tree_view(new QTreeView)
    , m_tuning_widget(nullptr)
    , m_create_fit_par_action(nullptr)
    , m_remove_from_fit_par_action(nullptr)
    , m_remove_fit_par_action(nullptr)
    , m_keyboard_filter(new DeleteEventFilter(this))
    , m_info_label(new OverlayLabelController(this))
{
    auto* layout = new QVBoxLayout(this);
    layout->setContentsMargins(0, 0, 0, 0);
    layout->setSpacing(0);
    layout->addWidget(m_tree_view);
    init_actions();

    m_tree_view->setSelectionMode(QAbstractItemView::ExtendedSelection);
    m_tree_view->setSelectionBehavior(QAbstractItemView::SelectRows);
    m_tree_view->setContextMenuPolicy(Qt::CustomContextMenu);
    m_tree_view->setItemDelegate(new FitParameterDelegate(this));
    m_tree_view->setDragEnabled(true);
    m_tree_view->setDragDropMode(QAbstractItemView::DragDrop);
    m_tree_view->installEventFilter(m_keyboard_filter);
    m_tree_view->setAlternatingRowColors(true);
    m_tree_view->setStyleSheet("alternate-background-color: #EFF0F1;");
    m_tree_view->header()->setSectionResizeMode(QHeaderView::Stretch);
    m_tree_view->setEditTriggers(QAbstractItemView::AllEditTriggers);

    connect(m_tree_view, &QTreeView::customContextMenuRequested, this,
            &FitParameterWidget::onFitParameterTreeContextMenu);

    m_info_label->setArea(m_tree_view);
    m_info_label->setText("Drop parameter(s) to fit here");
}

void FitParameterWidget::setJobItem(JobItem* job_item)
{
    ASSERT(job_item);
    m_job_item = job_item;
    connect(m_job_item, &QObject::destroyed, this, &FitParameterWidget::onJobDestroyed,
            Qt::UniqueConnection);
    init_fit_model();
}

//! Sets ParameterTuningWidget to be able to provide it with context menu and steer
//! its behaviour in the course of fit settings or fit running

void FitParameterWidget::setParameterTuningWidget(ParameterTuningWidget* tuningWidget)
{
    if (tuningWidget == m_tuning_widget)
        return;

    m_tuning_widget = tuningWidget;
    if (!m_tuning_widget)
        return;

    connect(m_tuning_widget, &ParameterTuningWidget::itemContextMenuRequest, this,
            &FitParameterWidget::onTuningWidgetContextMenu, Qt::UniqueConnection);
    connect(tuningWidget, &QObject::destroyed, this, &FitParameterWidget::onTunerDestroyed,
            Qt::UniqueConnection);
}

//! Creates context menu for ParameterTuningWidget

void FitParameterWidget::onTuningWidgetContextMenu(const QPoint& point)
{
    ASSERT(m_job_item);

    if (isFitting(m_job_item->batchInfo()->status())) {
        setActionsEnabled(false);
        return;
    }

    m_remove_from_fit_par_action->setEnabled(canRemoveFromFitParameters());
    m_create_fit_par_action->setEnabled(canCreateFitParameter());

    QMenu menu;
    menu.addAction(m_create_fit_par_action);
    QMenu* addToFitParMenu = menu.addMenu("Add to existing fit parameter");
    addToFitParMenu->setEnabled(true);

    const bool allow_one_fit_parameter_to_have_more_than_one_link = true;
    if (allow_one_fit_parameter_to_have_more_than_one_link) {
        QStringList fitParNames = fitContainerItem()->fitParameterNames();
        if (fitParNames.isEmpty() || !canCreateFitParameter())
            addToFitParMenu->setEnabled(false);
        for (int i = 0; i < fitParNames.count(); ++i) {
            auto* action = new QAction(QString("to ").append(fitParNames.at(i)), addToFitParMenu);
            connect(action, &QAction::triggered, [this, i] { onAddToFitParAction(i); });
            addToFitParMenu->addAction(action);
        }
    }
    menu.addSeparator();
    menu.addAction(m_remove_from_fit_par_action);

    menu.exec(point);
    setActionsEnabled(true);
}

//! Creates context menu for the tree with fit parameters

void FitParameterWidget::onFitParameterTreeContextMenu(const QPoint& point)
{
    ASSERT(m_job_item);

    if (isFitting(m_job_item->batchInfo()->status())) {
        setActionsEnabled(false);
        return;
    }
    if (fitContainerItem()->isEmpty())
        return;

    QMenu menu;
    menu.addAction(m_remove_fit_par_action);
    menu.exec(m_tree_view->viewport()->mapToGlobal(point));
    setActionsEnabled(true);
}

//! Propagates selection form the tree with fit parameters to the tuning widget

void FitParameterWidget::onFitParametersSelectionChanged(const QItemSelection& selection)
{
    ASSERT(m_job_item);

    if (selection.indexes().isEmpty())
        return;

    for (auto index : selection.indexes()) {
        m_tuning_widget->selectionModel()->clearSelection();
        QObject* item = m_fit_parameter_model->itemForIndex(index);
        if (auto* fitLinkItem = dynamic_cast<FitParameterLinkItem*>(item->parent())) {
            QString link = fitLinkItem->link();
            m_tuning_widget->makeSelected(
                m_job_item->parameterContainerItem()->findParameterItem(link));
        }
    }
}

void FitParameterWidget::updateView()
{
    m_tree_view->update();
}

//! Creates fit parameters for all selected ParameterItem's in tuning widget

void FitParameterWidget::onCreateFitParAction()
{
    for (auto* item : m_tuning_widget->selectedParameterItems())
        if (!fitContainerItem()->fitParameterItem(item))
            fitContainerItem()->createFitParameter(item);
}

//! All ParameterItem's selected in tuning widget will be removed from link section of
//! corresponding fitParameterItem.

void FitParameterWidget::onRemoveFromFitParAction()
{
    for (auto* item : m_tuning_widget->selectedParameterItems())
        fitContainerItem()->removeLink(item);

    for (auto* item : emptyFitParameterItems())
        fitContainerItem()->removeFitParameter(item);

    emit fitContainerItem() -> fitItemChanged();
}

//! All selected FitParameterItem's of FitParameterItemLink's will be removed

void FitParameterWidget::onRemoveFitParAction()
{
    // retrieve both, selected FitParameterItem and FitParameterItemLink
    QStringList linksToRemove = selectedFitParameterLinks();
    QVector<FitParameterItem*> itemsToRemove = selectedFitParameterItems();

    for (const auto& link : linksToRemove)
        for (auto* fitParItem : fitContainerItem()->fitParameterItems())
            fitParItem->removeLink(link);

    // remove parameters that lost their links
    itemsToRemove = itemsToRemove + emptyFitParameterItems();

    for (auto* item : itemsToRemove)
        fitContainerItem()->removeFitParameter(item);

    emit fitContainerItem() -> fitItemChanged();
}

//! Add all selected parameters to fitParameter with given index

void FitParameterWidget::onAddToFitParAction(int ipar)
{
    const QString fitParName = fitContainerItem()->fitParameterNames().at(ipar);
    for (auto* item : m_tuning_widget->selectedParameterItems())
        fitContainerItem()->addToFitParameter(item, fitParName);
}

void FitParameterWidget::onFitparQModelChange()
{
    spanParameters();
    updateInfoLabel();
}

void FitParameterWidget::init_actions()
{
    m_create_fit_par_action = new QAction("Create fit parameter", this);
    connect(m_create_fit_par_action, &QAction::triggered, this,
            &FitParameterWidget::onCreateFitParAction);

    m_remove_from_fit_par_action = new QAction("Remove from fit parameters", this);
    connect(m_remove_from_fit_par_action, &QAction::triggered, this,
            &FitParameterWidget::onRemoveFromFitParAction);

    m_remove_fit_par_action = new QAction("Remove fit parameter", this);
    connect(m_remove_fit_par_action, &QAction::triggered, this,
            &FitParameterWidget::onRemoveFitParAction);

    connect(m_keyboard_filter, &DeleteEventFilter::removeItem, this,
            &FitParameterWidget::onRemoveFitParAction);
}

//! Initializes FitparQModel and its tree.

void FitParameterWidget::init_fit_model()
{
    ASSERT(m_job_item);

    m_tree_view->setModel(nullptr);

    m_fit_parameter_model = std::make_unique<FitparQModel>(fitContainerItem(), m_job_item);
    m_tree_view->setModel(m_fit_parameter_model.get());

    connect(m_fit_parameter_model.get(), &FitparQModel::dataChanged, this,
            &FitParameterWidget::onFitparQModelChange, Qt::UniqueConnection);
    connect(m_fit_parameter_model.get(), &FitparQModel::modelReset, this,
            &FitParameterWidget::onFitparQModelChange, Qt::UniqueConnection);

    connect(fitContainerItem(), &FitParameterContainerItem::fitItemChanged, gDoc.get(),
            &ProjectDocument::setModified, Qt::UniqueConnection);

    onFitparQModelChange();

    connect(m_tree_view->selectionModel(), &QItemSelectionModel::selectionChanged, this,
            &FitParameterWidget::onFitParametersSelectionChanged, Qt::UniqueConnection);
}

FitParameterContainerItem* FitParameterWidget::fitContainerItem() const
{
    ASSERT(m_job_item);
    ASSERT(m_job_item->fitSuiteItem());
    return m_job_item->fitSuiteItem()->fitParameterContainerItem();
}

//! Returns true if tuning widget contains selected ParameterItem's which can be used to create
//! a fit parameter (i.e. it is not linked with some fit parameter already).

bool FitParameterWidget::canCreateFitParameter()
{
    QVector<ParameterItem*> selected = m_tuning_widget->selectedParameterItems();
    for (auto* item : selected) {
        if (fitContainerItem()->fitParameterItem(item) == nullptr)
            return true;
    }
    return false;
}

//! Returns true if tuning widget contains selected ParameterItem's which can be removed from
//! fit parameters.

bool FitParameterWidget::canRemoveFromFitParameters()
{
    QVector<ParameterItem*> selected = m_tuning_widget->selectedParameterItems();
    for (auto* item : selected) {
        if (fitContainerItem()->fitParameterItem(item))
            return true;
    }
    return false;
}

//! Enables/disables all context menu actions.

void FitParameterWidget::setActionsEnabled(bool value)
{
    m_create_fit_par_action->setEnabled(value);
    m_remove_from_fit_par_action->setEnabled(value);
    m_remove_fit_par_action->setEnabled(value);
}

//! Returns list of FitParameterItem's currently selected in FitParameterItem tree

QVector<FitParameterItem*> FitParameterWidget::selectedFitParameterItems()
{
    QVector<FitParameterItem*> result;
    QModelIndexList indexes = m_tree_view->selectionModel()->selectedIndexes();
    for (auto index : indexes) {
        auto* item = static_cast<QObject*>(index.internalPointer());
        if (auto* fitParItem = dynamic_cast<FitParameterItem*>(item))
            result.push_back(fitParItem);
    }
    return result;
}

//! Returns list of FitParameterItem's which doesn't have any links attached.

QVector<FitParameterItem*> FitParameterWidget::emptyFitParameterItems()
{
    QVector<FitParameterItem*> result;
    for (auto* fitParItem : fitContainerItem()->fitParameterItems())
        if (fitParItem->linkItems().empty())
            result.push_back(fitParItem);

    return result;
}

//! Returns links of FitParameterLink's item selected in FitParameterItem tree

QStringList FitParameterWidget::selectedFitParameterLinks()
{
    QStringList result;
    QModelIndexList indexes = m_tree_view->selectionModel()->selectedIndexes();
    for (QModelIndex index : indexes) {
        auto* item = static_cast<QObject*>(index.internalPointer());
        if (auto* linkItem = dynamic_cast<LinkItem*>(item))
            result.push_back(linkItem->link());
    }
    return result;
}

//! Makes first column in FitParameterItem's tree related to ParameterItem link occupy whole space.

void FitParameterWidget::spanParameters()
{
    m_tree_view->expandAll();
    for (int i = 0; i < m_fit_parameter_model->rowCount(QModelIndex()); i++) {
        QModelIndex parameter = m_fit_parameter_model->index(i, 0, QModelIndex());
        if (!parameter.isValid())
            break;
        int childRowCount = m_fit_parameter_model->rowCount(parameter);
        if (childRowCount > 0) {
            for (int j = 0; j < childRowCount; j++)
                m_tree_view->setFirstColumnSpanned(j, parameter, true);
        }
    }
}

//! Places overlay label on top of tree view, if there is no fit parameters
void FitParameterWidget::updateInfoLabel()
{
    if (!m_job_item)
        return;

    m_info_label->setShown(fitContainerItem()->isEmpty());
}

void FitParameterWidget::onJobDestroyed()
{
    m_job_item = nullptr;
    m_fit_parameter_model.reset(nullptr);
    m_tree_view->setModel(nullptr);
}

void FitParameterWidget::onTunerDestroyed()
{
    m_tuning_widget = nullptr;
}
