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

#include "GUI/View/Toplevel/SimulationView.h"
#include "GUI/Model/Data/DataItem.h"
#include "GUI/Model/Device/InstrumentItems.h"
#include "GUI/Model/Device/RealItem.h"
#include "GUI/Model/Job/JobItem.h"
#include "GUI/Model/Model/JobModel.h"
#include "GUI/Model/Model/RealModel.h"
#include "GUI/Model/Project/ProjectDocument.h"
#include "GUI/Model/Sample/SampleItem.h"
#include "GUI/Model/Sample/SampleValidator.h"
#include "GUI/Support/Data/SimulationOptionsItem.h"
#include "GUI/View/Project/ProjectManager.h"
#include "GUI/View/Tool/Globals.h"
#include "GUI/View/Tool/GroupBoxCollapser.h"
#include "GUI/View/Toplevel/PythonScriptWidget.h"
#include "ui_SimulationView.h"
#include <QButtonGroup>
#include <QMessageBox>
#include <thread>

SimulationView::SimulationView(QWidget* parent, ProjectDocument* document)
    : QWidget(parent)
    , m_ui(new Ui::SimulationView)
    , m_document(document)
{
    m_ui->setupUi(this);

    GroupBoxCollapser::installIntoGroupBox(m_ui->groupBox_3);
    GroupBoxCollapser::installIntoGroupBox(m_ui->groupBox_2);
    GroupBoxCollapser::installIntoGroupBox(m_ui->groupBox);

    auto* buttonGroup = new QButtonGroup(this);
    buttonGroup->addButton(m_ui->runPolicyImmediatelyRadio);
    buttonGroup->addButton(m_ui->runPolicyBackgroundRadio);

    auto* computationButtonGroup = new QButtonGroup(this);
    computationButtonGroup->addButton(m_ui->analyticalRadio);
    computationButtonGroup->addButton(m_ui->monteCarloRadio);

    buttonGroup = new QButtonGroup(this);
    buttonGroup->addButton(m_ui->ambientLayerRadio);
    buttonGroup->addButton(m_ui->averageLayerRadio);

    // -- fill combo for "number of threads"
    const int nthreads = static_cast<int>(std::thread::hardware_concurrency());
    m_ui->numberOfThreadsCombo->addItem(QString("Max (%1 threads)").arg(nthreads), nthreads);
    for (int i = nthreads - 1; i > 1; i--)
        m_ui->numberOfThreadsCombo->addItem(QString("%1 threads").arg(i), i);
    m_ui->numberOfThreadsCombo->addItem("1 thread", 1);

    updateFunctionalityNarrowing();

    connect(m_ui->instrumentCombo, &QComboBox::currentTextChanged, [this] { updateStateFromUI(); });
    connect(m_ui->sampleCombo, &QComboBox::currentTextChanged, [this] { updateStateFromUI(); });
    connect(m_ui->realDataCombo, &QComboBox::currentTextChanged, [this] { updateStateFromUI(); });

    connect(m_ui->simulateButton, &QPushButton::clicked, this, &SimulationView::simulate);
    connect(m_ui->exportToPyScriptButton, &QPushButton::clicked, this,
            &SimulationView::exportPythonScript);
    connect(computationButtonGroup, &QButtonGroup::buttonClicked, this,
            &SimulationView::updateEnabling);

    connect(m_ui->runPolicyImmediatelyRadio, &QRadioButton::toggled,
            [this]() { updateStateFromUI(); });

    connect(m_ui->analyticalRadio, &QRadioButton::toggled, [this]() { updateStateFromUI(); });

    connect(m_ui->averageLayerRadio, &QRadioButton::toggled, [this]() { updateStateFromUI(); });

    connect(m_ui->numberOfThreadsCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
            [this]() { updateStateFromUI(); });

    connect(m_ui->numberOfMonteCarloPoints, QOverload<int>::of(&QSpinBox::valueChanged),
            [this]() { updateStateFromUI(); });

    connect(m_ui->includeSpecularCheck, &QCheckBox::toggled, [this]() { updateStateFromUI(); });

    connect(m_document, &ProjectDocument::modifiedStateChanged, this,
            &SimulationView::updateFunctionalityNarrowing);
}

void SimulationView::showEvent(QShowEvent*)
{
    writeOptionsToUI();
}

void SimulationView::writeOptionsToUI()
{
    QSignalBlocker b1(m_ui->runPolicyImmediatelyRadio);
    QSignalBlocker b2(m_ui->analyticalRadio);
    QSignalBlocker b3(m_ui->averageLayerRadio);
    QSignalBlocker b4(m_ui->numberOfThreadsCombo);
    QSignalBlocker b5(m_ui->numberOfMonteCarloPoints);
    QSignalBlocker b6(m_ui->includeSpecularCheck);

    // -- selection group
    updateSelection(m_ui->instrumentCombo, m_document->instrumentModel()->instrumentNames(),
                    optionsItem()->selectedInstrumentIndex());
    updateSelection(m_ui->sampleCombo, m_document->sampleModel()->sampleNames(),
                    optionsItem()->selectedSampleIndex());
    updateSelection(m_ui->realDataCombo, m_document->realModel()->realItemNames(),
                    optionsItem()->selectedDataIndex(), true);

    // -- options group
    optionsItem()->runImmediately() ? m_ui->runPolicyImmediatelyRadio->setChecked(true)
                                    : m_ui->runPolicyBackgroundRadio->setChecked(true);

    optionsItem()->useAnalytical() ? m_ui->analyticalRadio->setChecked(true)
                                   : m_ui->monteCarloRadio->setChecked(true);

    optionsItem()->useAverageMaterials() ? m_ui->averageLayerRadio->setChecked(true)
                                         : m_ui->ambientLayerRadio->setChecked(true);

    m_ui->numberOfThreadsCombo->setCurrentIndex(
        m_ui->numberOfThreadsCombo->findData(optionsItem()->numberOfThreads()));
    m_ui->numberOfMonteCarloPoints->setValue(optionsItem()->numberOfMonteCarloPoints());
    m_ui->includeSpecularCheck->setChecked(optionsItem()->includeSpecularPeak());

    updateEnabling();
}

void SimulationView::hideEvent(QHideEvent*)
{
    // Storing the options has do be done here because on a showEvent the values are set again from
    // the SimulationOptionsItem (=> keep values when switching views)
    readOptionsFromUI();
}

void SimulationView::updateStateFromUI()
{
    readOptionsFromUI();
    m_document->setModified();
}

void SimulationView::simulate()
{
    readOptionsFromUI();
    if (const QString msg = validateSimulationSetup(true); !msg.isEmpty()) {
        QMessageBox::warning(this, "Cannot run the job",
                             "Cannot run the job with current settings\n\n" + msg);
        return;
    }
    JobModel* jobModel = m_document->jobModel();
    JobItem* jobItem = jobModel->addJobItem(selectedSampleItem(), selectedInstrumentItem(),
                                            selectedRealItem(), *optionsItem());
    jobModel->runJob(jobItem);
    gProjectDocument.value()->setModified();
}

void SimulationView::exportPythonScript()
{
    readOptionsFromUI();
    if (const QString msg = validateSimulationSetup(false); !msg.isEmpty()) {
        QMessageBox::warning(this, "Cannot export to Python",
                             "Cannot export to Python with current settings\n\n" + msg);
        return;
    }
    auto* pythonWidget = new PythonScriptWidget(GUI::Global::mainWindow);
    pythonWidget->show();
    pythonWidget->raise();
    pythonWidget->generatePythonScript(*selectedSampleItem(), *selectedInstrumentItem(),
                                       *optionsItem(), ProjectManager::instance()->projectDir());
}

void SimulationView::readOptionsFromUI()
{
    optionsItem()->setSelectedInstrumentIndex(m_ui->instrumentCombo->currentIndex());
    optionsItem()->setSelectedSampleIndex(m_ui->sampleCombo->currentIndex());
    optionsItem()->setSelectedDataIndex(m_ui->realDataCombo->currentIndex());

    optionsItem()->setRunImmediately(m_ui->runPolicyImmediatelyRadio->isChecked());
    optionsItem()->setNumberOfThreads(m_ui->numberOfThreadsCombo->currentData().toInt());
    if (m_ui->analyticalRadio->isChecked())
        optionsItem()->setUseAnalytical();
    else
        optionsItem()->setUseMonteCarloIntegration(m_ui->numberOfMonteCarloPoints->value());
    optionsItem()->setUseAverageMaterials(m_ui->averageLayerRadio->isChecked());
    optionsItem()->setIncludeSpecularPeak(m_ui->includeSpecularCheck->isChecked());
}

void SimulationView::updateEnabling()
{
    m_ui->numberOfMonteCarloPoints->setEnabled(m_ui->monteCarloRadio->isChecked());
}

void SimulationView::updateSelection(QComboBox* comboBox, QStringList itemList, int currentIndex,
                                     bool allowNone)
{
    QSignalBlocker b(comboBox);
    const QString previousItem = comboBox->currentText();

    comboBox->clear();
    comboBox->setEnabled(!itemList.isEmpty());

    if (itemList.isEmpty()) {
        comboBox->addItem("Not yet defined");
        return;
    }

    if (allowNone)
        itemList.prepend("None");

    comboBox->addItems(itemList);
    if (currentIndex > 0 && currentIndex < itemList.count())
        comboBox->setCurrentIndex(currentIndex);
    else if (itemList.contains(previousItem))
        comboBox->setCurrentIndex(itemList.indexOf(previousItem));
}

QString SimulationView::validateSimulationSetup(bool validateRealData) const
{
    QString messages;
    const auto append = [&](const QString& m) { messages.append("- " + m + "\n"); };

    if (!selectedSampleItem())
        append("No sample selected");
    else {
        SampleValidator sampleValidator;
        if (!sampleValidator.isValidSample(selectedSampleItem()))
            append(sampleValidator.getValidationMessage());
    }

    if (!selectedInstrumentItem())
        append("No instrument selected");

    if (validateRealData && selectedRealItem() && selectedInstrumentItem()
        && !selectedInstrumentItem()->alignedWith(selectedRealItem()))
        append("The experimental data does not fit in the selected instrument. Try linking "
               "them in Import Tab.");

    if (selectedRealItem()
        && (!selectedRealItem()->dataItem() || !selectedRealItem()->dataItem()->c_field()))
        append("The experimental data does not contain values.");
    return messages;
}

void SimulationView::updateFunctionalityNarrowing()
{
    m_ui->instrumentCombo->setVisible(!m_document->singleInstrumentMode());
    m_ui->instrumentLabel->setVisible(!m_document->singleInstrumentMode());
}

SimulationOptionsItem* SimulationView::optionsItem() const
{
    return m_document->simulationOptionsItem();
}

const SampleItem* SimulationView::selectedSampleItem() const
{
    return m_document->sampleModel()->sampleItems().value(m_ui->sampleCombo->currentIndex(),
                                                          nullptr);
}

const InstrumentItem* SimulationView::selectedInstrumentItem() const
{
    return m_document->instrumentModel()->instrumentItems().value(
        m_ui->instrumentCombo->currentIndex(), nullptr);
}

const RealItem* SimulationView::selectedRealItem() const
{
    return m_document->realModel()->realItems().value(m_ui->realDataCombo->currentIndex() - 1,
                                                      nullptr); // -1: "None"
}
