//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/View/View/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/View/SimulationView.h"
#include "GUI/Model/Data/DataItem.h"
#include "GUI/Model/File/DatafilesSet.h"
#include "GUI/Model/Job/JobItem.h"
#include "GUI/Model/Job/JobsSet.h"
#include "GUI/Model/Project/ProjectDocument.h"
#include "GUI/Model/Sample/SampleValidator.h"
#include "GUI/Model/Sample/SamplesSet.h"
#include "GUI/Model/Sim/InstrumentsSet.h"
#include "GUI/Model/Sim/SimulationOptionsItem.h"
#include "GUI/View/Info/MessageBox.h"
#include "GUI/View/Loader/PythonScriptWidget.h"
#include "GUI/View/Widget/AppConfig.h"
#include "GUI/View/Widget/GroupBoxes.h"
#include <QButtonGroup>
#include <QFormLayout>
#include <QMessageBox>
#include <QVBoxLayout>
#include <thread>

SimulationView::SimulationView()
    : m_number_of_monte_carlo_points(new QSpinBox)
    , m_number_of_threads_combo(new QComboBox)
{
    auto* layout = new QVBoxLayout;
    setLayout(layout);

    //... Model evaluation settings

    auto* g2 = new StaticGroupBox("Model evaluation settings", this);
    layout->addWidget(g2);
    auto* l2 = new QVBoxLayout;
    g2->body()->setLayout(l2);

    // layer material
    auto* b_lm = new QButtonGroup;
    m_ambient_layer_radio = new QRadioButton("R&T computed for layer material as given");
    b_lm->addButton(m_ambient_layer_radio);
    l2->addWidget(m_ambient_layer_radio);
    m_average_layer_radio = new QRadioButton("R&T computed for averaged layer material");
    b_lm->addButton(m_average_layer_radio);
    l2->addWidget(m_average_layer_radio);
    l2->addSpacerItem(new QSpacerItem(10, 10));

    // also specular peak?
    m_include_specular_check = new QCheckBox("Also compute specular peak");
    l2->addWidget(m_include_specular_check);
    l2->addSpacerItem(new QSpacerItem(10, 10));

    //... Advanced model evaluation settings

    auto* g2_1 =
        new CollapsibleGroupBox("Advanced options", this, optionsItem()->expandAdvancedOptions);
    l2->addWidget(g2_1);
    auto* l2_1 = new QVBoxLayout;
    g2_1->body()->setLayout(l2_1);

    // analytical vs MC
    auto* b_mc = new QButtonGroup;
    m_analytical_radio = new QRadioButton("Deterministic computation at pixel center");
    b_mc->addButton(m_analytical_radio);
    l2_1->addWidget(m_analytical_radio);
    m_monte_carlo_radio = new QRadioButton("Monte-Carlo computation; number of points per pixel: ");
    b_mc->addButton(m_monte_carlo_radio);
    auto* l_mc2 = new QHBoxLayout;
    l2_1->addLayout(l_mc2);
    l_mc2->addWidget(m_monte_carlo_radio);

    l_mc2->addWidget(m_number_of_monte_carlo_points);
    l_mc2->addStretch(1);
    l2_1->addSpacerItem(new QSpacerItem(10, 10));

    // mesocrystal: finite Fourier sum vs real sum
    auto* b_meso = new QButtonGroup;
    m_real_sum = new QRadioButton("Sum over mesocrystal lattice points (exact but possibly slow)");
    b_meso->addButton(m_real_sum);
    l2_1->addWidget(m_real_sum);
    m_fourier_sum = new QRadioButton("Fourier sum with cutoff radius factor: ");
    b_meso->addButton(m_fourier_sum);
    auto* l_meso2 = new QHBoxLayout;
    l2_1->addLayout(l_meso2);
    l_meso2->addWidget(m_fourier_sum);

    m_meso_radius_factor = new QDoubleSpinBox;
    m_meso_radius_factor->setFocusPolicy(Qt::StrongFocus);
    m_meso_radius_factor->setMinimum(0);
    m_meso_radius_factor->setMaximum(1000);
    m_meso_radius_factor->setDecimals(2);
    m_meso_radius_factor->setSingleStep(0.1);
    l_meso2->addWidget(m_meso_radius_factor);
    l_meso2->addStretch(1);
    l2_1->addSpacerItem(new QSpacerItem(10, 10));

    //... Execution settings

    auto* g3 = new StaticGroupBox("Execution settings", this);
    layout->addWidget(g3);
    auto* l3 = new QVBoxLayout;
    g3->body()->setLayout(l3);

    // immediately/background?
    auto* b_exec = new QButtonGroup;
    m_run_policy_immediately_radio = new QRadioButton("Run immediately");
    b_exec->addButton(m_run_policy_immediately_radio);
    l3->addWidget(m_run_policy_immediately_radio);
    m_run_policy_background_radio = new QRadioButton("Run in background");
    b_exec->addButton(m_run_policy_background_radio);
    l3->addWidget(m_run_policy_background_radio);
    l3->addSpacerItem(new QSpacerItem(10, 10));

    // number of threads
    auto* f3 = new QFormLayout;

    m_number_of_threads_combo->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
    f3->addRow("Number of threads", m_number_of_threads_combo);
    l3->addLayout(f3);
    l3->addSpacerItem(new QSpacerItem(10, 10));

    auto* l_launchers = new QHBoxLayout;
    l3->addLayout(l_launchers);
    m_simulate_button = new QPushButton("Run simulation");
    l_launchers->addWidget(m_simulate_button);
    m_export_to_py_script_button = new QPushButton("Export to Python Script");
    l_launchers->addWidget(m_export_to_py_script_button);
    l_launchers->addStretch(1);

    layout->addStretch(1);

    //... Set data

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

    //... Connect

    connect(m_simulate_button, &QPushButton::clicked, this, &SimulationView::simulate);
    connect(m_export_to_py_script_button, &QPushButton::clicked, this,
            &SimulationView::exportPythonScript);

    connect(m_run_policy_immediately_radio, &QRadioButton::toggled,
            [this] { updateStateFromUI(); });

    connect(m_average_layer_radio, &QRadioButton::toggled, [this] { updateStateFromUI(); });

    connect(m_number_of_threads_combo, &QComboBox::currentIndexChanged,
            [this] { updateStateFromUI(); });

    connect(m_analytical_radio, &QRadioButton::toggled, [this] {
        updateStateFromUI();
        updateEnabling();
    });
    connect(m_monte_carlo_radio, &QRadioButton::toggled, [this] {
        updateStateFromUI();
        updateEnabling();
    });
    connect(m_number_of_monte_carlo_points, &QSpinBox::valueChanged,
            [this] { updateStateFromUI(); });

    connect(m_real_sum, &QRadioButton::toggled, [this] {
        updateStateFromUI();
        updateEnabling();
    });
    connect(m_fourier_sum, &QRadioButton::toggled, [this] {
        updateStateFromUI();
        updateEnabling();
    });
    connect(m_meso_radius_factor, &QDoubleSpinBox::valueChanged, [this] { updateStateFromUI(); });

    connect(m_include_specular_check, &QCheckBox::toggled, [this] { updateStateFromUI(); });
}

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

void SimulationView::writeOptionsToUI()
{
    // -- options group
    QSignalBlocker b1(m_run_policy_immediately_radio);
    QSignalBlocker b2(m_run_policy_background_radio);
    optionsItem()->runImmediately() ? m_run_policy_immediately_radio->setChecked(true)
                                    : m_run_policy_background_radio->setChecked(true);

    QSignalBlocker b3(m_average_layer_radio);
    QSignalBlocker b4(m_ambient_layer_radio);
    optionsItem()->useAverageMaterials() ? m_average_layer_radio->setChecked(true)
                                         : m_ambient_layer_radio->setChecked(true);


    // -- advanced options group
    QSignalBlocker b5(m_analytical_radio);
    QSignalBlocker b6(m_monte_carlo_radio);
    optionsItem()->useAnalytical() ? m_analytical_radio->setChecked(true)
                                   : m_monte_carlo_radio->setChecked(true);
    QSignalBlocker b7(m_number_of_monte_carlo_points);
    m_number_of_monte_carlo_points->setValue(optionsItem()->numberOfMonteCarloPoints());


    QSignalBlocker b8(m_real_sum);
    QSignalBlocker b9(m_fourier_sum);
    optionsItem()->useFastMesocrystalCalc() ? m_fourier_sum->setChecked(true)
                                            : m_real_sum->setChecked(true);
    QSignalBlocker b10(m_meso_radius_factor);
    m_meso_radius_factor->setValue(optionsItem()->mesocrystalCutoff());


    QSignalBlocker b11(m_number_of_threads_combo);
    m_number_of_threads_combo->setCurrentIndex(
        m_number_of_threads_combo->findData(optionsItem()->numberOfThreads()));

    QSignalBlocker b12(m_include_specular_check);
    m_include_specular_check->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();
    gDoc->setModified();
}

void SimulationView::simulate()
{
    readOptionsFromUI();
    if (!gDoc->instruments()->currentItem()) {
        GUI::Message::warning("Simulate", "Cannot launch simulation as no instrument is selected");
        return;
    }
    if (!gDoc->samples()->currentItem()) {
        GUI::Message::warning("Simulate", "Cannot launch simulation as no sample is selected");
        return;
    }
    auto* job_item = new JobItem(gDoc->samples()->currentItem(), gDoc->instruments()->currentItem(),
                                 gDoc->datafiles()->currentItem(), optionsItem());
    JobsSet* jobs = gDoc->jobsRW();
    jobs->addJobItem(job_item);
    jobs->runJob(job_item);
    gDoc->setModified();
}

void SimulationView::exportPythonScript()
{
    readOptionsFromUI();
    auto* pythonWidget = new PythonScriptWidget(gApp->mainWindow);
    pythonWidget->show();
    pythonWidget->raise();
    pythonWidget->generatePythonScript(gDoc->samples()->currentItem(),
                                       gDoc->instruments()->currentItem(), *optionsItem(),
                                       gDoc->validProjectDir());
}

void SimulationView::readOptionsFromUI()
{
    optionsItem()->setRunImmediately(m_run_policy_immediately_radio->isChecked());
    optionsItem()->setNumberOfThreads(m_number_of_threads_combo->currentData().toInt());

    optionsItem()->setUseAverageMaterials(m_average_layer_radio->isChecked());
    optionsItem()->setIncludeSpecularPeak(m_include_specular_check->isChecked());

    if (m_analytical_radio->isChecked())
        optionsItem()->setUseAnalytical();
    else
        optionsItem()->setUseMonteCarloIntegration(m_number_of_monte_carlo_points->value());

    if (m_real_sum->isChecked())
        optionsItem()->setFastMesocrystalCalc(false);
    else {
        optionsItem()->setFastMesocrystalCalc(true);
        optionsItem()->setMesocrystalCutoff(m_meso_radius_factor->value());
    }
}

void SimulationView::updateEnabling()
{
    m_number_of_monte_carlo_points->setEnabled(m_monte_carlo_radio->isChecked());
    m_meso_radius_factor->setEnabled(m_fourier_sum->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));
}

SimulationOptionsItem* SimulationView::optionsItem() const
{
    return gDoc->simulationOptionsRW();
}
