//  ************************************************************************************************
//
//  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/IO/PythonExport.h"
#include "GUI/View/Info/MessageBox.h"
#include "GUI/View/Job/Simulate.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()
{
    auto* layout = new QVBoxLayout;
    setLayout(layout);

    //... All settings

    m_settings = new CollapsibleGroupBox("Settings", this, optionsItem()->expandOptions);
    layout->addWidget(m_settings);
    auto* s_l = new QVBoxLayout;
    m_settings->body()->setLayout(s_l);

    //... Model evaluation settings

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

    // layer material
    m_use_avge_material_check =
        new QCheckBox("Use averaged scattering-length density for layers with particles");
    m_use_avge_material_check->setToolTip(
        "If checked, then the scattering-length density (SLD) for each layer or slice is computed\n"
        " by averaging the SLD of the layer material and the SLD of any embedded particle.\n"
        " If unchecked, only the SLD of the layer material is taken into account");
    l2->addWidget(m_use_avge_material_check);
    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

    m_advanced_settings =
        new CollapsibleGroupBox("Advanced options", this, optionsItem()->expandAdvancedOptions);
    l2->addWidget(m_advanced_settings);
    auto* l2_1 = new QVBoxLayout;
    m_advanced_settings->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);

    m_number_of_monte_carlo_points = new QSpinBox;
    m_number_of_monte_carlo_points->setMaximum(1 << 15);
    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);
    s_l->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 = new QComboBox;
    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));

    //... Run simulation

    auto* g4 = new StaticGroupBox("Simulation setup", this);
    layout->addWidget(g4);
    auto* l4 = new QVBoxLayout;
    g4->body()->setLayout(l4);

    auto* l_setup = new QGridLayout;
    l_setup->setAlignment(Qt::AlignLeft);
    l4->addLayout(l_setup);

    l_setup->addWidget(new QLabel("Simulation setup:"), 1, 0, 1, 1, Qt::AlignCenter);
    l_setup->addWidget(new QLabel("Instrument:"), 0, 1, 1, 1, Qt::AlignCenter);
    l_setup->addWidget(new QLabel("Sample:"), 0, 3, 1, 1, Qt::AlignCenter);

    m_use_dataset = new QCheckBox("Data:");
    l_setup->addWidget(m_use_dataset, 0, 5, 1, 1, Qt::AlignCenter);

    QFont names_font;
    names_font.setBold(true);

    m_current_instrument_name = new QLabel;
    m_current_instrument_name->setFont(names_font);
    l_setup->addWidget(m_current_instrument_name, 1, 1, 1, 1, Qt::AlignCenter);

    l_setup->addWidget(new QLabel("+"), 1, 2, 1, 1, Qt::AlignCenter);

    m_current_sample_name = new QLabel;
    m_current_sample_name->setFont(names_font);
    l_setup->addWidget(m_current_sample_name, 1, 3, 1, 1, Qt::AlignCenter);

    l_setup->addWidget(new QLabel("+"), 1, 4, 1, 1, Qt::AlignCenter);

    m_current_data_name = new QLabel;
    m_current_data_name->setFont(names_font);
    l_setup->addWidget(m_current_data_name, 1, 5, 1, 1, Qt::AlignCenter);

    // launch buttons
    l4->addSpacing(20);
    auto* l_launchers = new QHBoxLayout;
    l4->addLayout(l_launchers);
    m_simulate_button = new QPushButton("Run simulation");
    m_simulate_button->setFixedSize(110, 50);
    l_launchers->addWidget(m_simulate_button);
    l_launchers->addSpacing(15);
    m_export_to_py_script_button = new QPushButton("Export to Python script");
    m_export_to_py_script_button->setFixedSize(160, 50);
    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);

    updateOptions();

    //... Connect subwidgets

    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] {
        optionsItem()->setRunImmediately(m_run_policy_immediately_radio->isChecked());
        gDoc->setModified();
    });

    connect(m_use_avge_material_check, &QCheckBox::toggled, [this] {
        optionsItem()->setUseAverageMaterials(m_use_avge_material_check->isChecked());
        gDoc->setModified();
    });

    connect(m_number_of_threads_combo, &QComboBox::currentIndexChanged, [this] {
        optionsItem()->setNumberOfThreads(m_number_of_threads_combo->currentData().toInt());
        gDoc->setModified();
    });

    connect(m_analytical_radio, &QRadioButton::toggled, [this] {
        if (m_analytical_radio->isChecked())
            optionsItem()->setUseAnalytical();
        else
            optionsItem()->setUseMonteCarloIntegration(m_number_of_monte_carlo_points->value());
        gDoc->setModified();
        updateEnabling();
    });
    connect(m_number_of_monte_carlo_points, &QSpinBox::valueChanged, [this] {
        ASSERT(m_monte_carlo_radio->isChecked());
        optionsItem()->setUseMonteCarloIntegration(m_number_of_monte_carlo_points->value());
        gDoc->setModified();
    });

    connect(m_real_sum, &QRadioButton::toggled, [this] {
        if (m_real_sum->isChecked())
            optionsItem()->setFastMesocrystalCalc(false);
        else
            optionsItem()->setFastMesocrystalCalc(true);
        gDoc->setModified();
        updateEnabling();
    });
    connect(m_meso_radius_factor, &QDoubleSpinBox::valueChanged, [this] {
        ASSERT(m_fourier_sum->isChecked());
        optionsItem()->setMesocrystalCutoff(m_meso_radius_factor->value());
        gDoc->setModified();
    });

    connect(m_include_specular_check, &QCheckBox::toggled, [this] {
        optionsItem()->setIncludeSpecularPeak(m_include_specular_check->isChecked());
        gDoc->setModified();
    });

    connect(m_use_dataset, &QCheckBox::toggled, [this] {
        optionsItem()->setUseDataset(m_use_dataset->isChecked());
        gDoc->setModified();
        updateEnabling();
    });

    //... Connect external change of state
    connect(gDoc.get(), &ProjectDocument::documentOpened, this, &SimulationView::updateOptions);
    connect(gDoc->datafiles(), &DatafilesSet::setChanged, this, &SimulationView::updateOptions);
    connect(gDoc->instruments(), &InstrumentsSet::setChanged, this, &SimulationView::updateOptions);
    connect(gDoc->samples(), &SamplesSet::setChanged, this, &SimulationView::updateOptions);
}

void SimulationView::updateOptions()
{
    // -- options group
    m_settings->setExpanded(optionsItem()->expandOptions);

    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_use_avge_material_check);
    m_use_avge_material_check->setChecked(optionsItem()->useAverageMaterials());

    // -- advanced options group
    m_advanced_settings->setExpanded(optionsItem()->expandAdvancedOptions);

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

    // -- simuilation setup
    m_current_instrument_name->setText(gDoc->instruments()->currentItem()
                                           ? gDoc->instruments()->currentItem()->name()
                                           : "--------------------");

    m_current_sample_name->setText(gDoc->samples()->currentItem()
                                       ? gDoc->samples()->currentItem()->name()
                                       : "--------------------");

    m_current_data_name->setText(gDoc->datafiles()->currentItem()
                                     ? gDoc->datafiles()->currentItem()->name()
                                     : "--------------------");

    m_use_dataset->setChecked(optionsItem()->useDataset());

    updateEnabling();
}

void SimulationView::simulate()
{
    auto* instrument = gDoc->instruments()->currentItem();
    auto* sample = gDoc->samples()->currentItem();
    auto* dataset = optionsItem()->useDataset() ? gDoc->datafiles()->currentItem() : nullptr;

    if (!instrument) {
        GUI::Message::warning("Simulate", "Cannot launch simulation as no instrument is selected");
        return;
    }
    if (!sample) {
        GUI::Message::warning("Simulate", "Cannot launch simulation as no sample is selected");
        return;
    }
    if (dataset && instrument->is<DepthprobeInstrumentItem>()) {
        GUI::Message::warning("Simulate", "The Depthprobe instrument does not require a dataset");
        return;
    }
    if (dataset && !instrument->alignedWith(dataset)) {
        GUI::Message::warning("Simulate", "The dataset does not fit the instrument");
        return;
    }
    auto* job_item = new JobItem(sample, instrument, dataset, optionsItem());
    gDoc->jobsRW()->addJobItem(job_item);
    GUI::Sim::simulate(job_item, gDoc->jobsRW());
    gDoc->setModified();
}

void SimulationView::exportPythonScript()
{
    if (!gDoc->instruments()->currentItem()) {
        GUI::Message::warning("Export", "Cannot save script as no instrument is selected");
        return;
    }
    if (!gDoc->samples()->currentItem()) {
        GUI::Message::warning("Export", "Cannot save script as no sample is selected");
        return;
    }
    IO::Py::saveScript(gDoc->samples()->currentItem(), gDoc->instruments()->currentItem(),
                       optionsItem());
}

void SimulationView::updateEnabling()
{
    m_number_of_monte_carlo_points->setEnabled(m_monte_carlo_radio->isChecked());
    m_meso_radius_factor->setEnabled(m_fourier_sum->isChecked());
    m_current_data_name->setEnabled(m_use_dataset->isChecked());
}

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