//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/View/FitControl/MinimizerEditor.cpp
//! @brief     Implements class MinimizerEditor.
//!
//! @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/FitControl/MinimizerEditor.h"
#include "Base/Util/Assert.h"
#include "GUI/Model/Job/JobItem.h"
#include "GUI/Model/Mini/MinimizerItems.h"
#include "GUI/Model/Project/ProjectDocument.h"
#include "GUI/Model/Tune/FitSuiteItem.h"
#include "GUI/View/Base/LayoutUtil.h"
#include "GUI/View/Numeric/ComboUtil.h"
#include "GUI/View/Numeric/DSpinBox.h"
#include "GUI/View/Numeric/NumWidgetUtil.h"
#include <QSpinBox>
#include <QStandardItemModel>

MinimizerEditor::MinimizerEditor(QWidget* parent)
    : QWidget(parent)
    , m_container_item(nullptr)
    , m_main_layout(new QFormLayout(this))
{
    setWindowTitle("Minimizer Settings");
    m_main_layout->setSpacing(5);
}

void MinimizerEditor::setJobItem(JobItem* job_item)
{
    ASSERT(job_item);
    setMinContainerItem(job_item->fitSuiteItem()->minimizerContainerItem());
}

void MinimizerEditor::setMinContainerItem(MinimizerContainerItem* container_item)
{
    ASSERT(container_item);

    GUI::Util::Layout::clearLayout(m_main_layout);
    m_updaters.clear();
    m_container_item = container_item;

    if (!m_container_item)
        return;

    createGroupedAlgorithmsCombo();

    auto* w = new QWidget(this);
    m_minimizer_layout = new QFormLayout(w);
    m_main_layout->addRow(w);

    m_main_layout->addRow("Objective metric:",
                          GUI::Util::createComboBox(
                              [this] { return m_container_item->objectiveMetricCombo(); },
                              [this](const QString& t) {
                                  m_container_item->setCurrentObjectiveMetric(t);
                                  gDoc->setModified();
                              },
                              false, &m_updaters,
                              "Objective metric to use for estimating distance between simulated "
                              "and experimental data"));
    m_main_layout->addRow("Norm function:",
                          GUI::Util::createComboBox(
                              [this] { return m_container_item->normFunctionCombo(); },
                              [this](const QString& t) {
                                  m_container_item->setCurrentNormFunction(t);
                                  gDoc->setModified();
                              },
                              false, &m_updaters,
                              "Normalization to use for estimating distance between simulated and "
                              "experimental data"));

    createMimimizerEdits();
    updateUIValues();
}

void MinimizerEditor::createGroupedAlgorithmsCombo()
{
    auto* comboBox = new QComboBox;
    QStringList list = m_container_item->commonAlgorithmCombo().values();

    // list with headers and separators
    QVector<qsizetype> header_indices;
    QVector<qsizetype> non_separator_indices;
    for (const QString& algorithm : list) {
        comboBox->addItem(algorithm);
        if (!m_container_item->algorithmHasMinimizer(algorithm)) {

            comboBox->insertSeparator(comboBox->count() - 1);

            qsizetype header_index = comboBox->count() - 1;
            header_indices.append(header_index);
            non_separator_indices.append(header_index);
            auto* model = qobject_cast<QStandardItemModel*>(comboBox->model());
            QStandardItem* header_item = model->item(header_index);
            header_item->setSelectable(false);

            QFont font(comboBox->font());
            font.setBold(true);
            header_item->setFont(font);

            comboBox->insertSeparator(comboBox->count());
        } else
            non_separator_indices.append(comboBox->count() - 1);
    }
    comboBox->setCurrentText(m_container_item->commonAlgorithmCombo().currentValue());

    // tooltips
    QStringList tooltips = m_container_item->commonAlgorithmCombo().toolTips();
    ASSERT(tooltips.size() == list.size());
    int list_index = 0;
    for (int index : non_separator_indices)
        comboBox->setItemData(index, tooltips.at(list_index++), Qt::ToolTipRole);

    // action
    comboBox->setProperty("previous", comboBox->currentIndex());
    QObject::connect(comboBox, &QComboBox::currentTextChanged, [this, header_indices, comboBox] {
        // skip headers while scrolling
        if (header_indices.contains(comboBox->currentIndex())) {
            int previous_state = comboBox->property("previous").toInt();

            int prev = comboBox->currentIndex() - 2;
            int next = comboBox->currentIndex() + 2;

            QSignalBlocker b(comboBox);
            if (previous_state < comboBox->currentIndex() && next < comboBox->count())
                comboBox->setCurrentIndex(next);
            else if (previous_state > comboBox->currentIndex() && prev >= 0)
                comboBox->setCurrentIndex(prev);
            else
                comboBox->setCurrentIndex(previous_state);
        }
        comboBox->setProperty("previous", comboBox->currentIndex());

        m_container_item->setCurrentCommonAlgorithm(comboBox->currentText());
        createMimimizerEdits();
        gDoc->setModified();
    });

    // update state
    m_updaters << [this, comboBox] {
        QSignalBlocker b(comboBox);
        comboBox->setCurrentText(m_container_item->commonAlgorithmCombo().currentValue());
    };

    m_main_layout->addRow("Algorithm:", comboBox);
}

void MinimizerEditor::createMimimizerEdits()
{
    GUI::Util::Layout::clearLayout(m_minimizer_layout);

    // Minuit2
    if (m_container_item->currentMinimizer() == minimizerTypeToName(MinimizerType::Minuit2))
        createMinuitEdits();

    // GSL MultiMin
    if (m_container_item->currentMinimizer() == minimizerTypeToName(MinimizerType::GSLMultiMin))
        createGSLMultiMinEdits();

    // TMVA Genetic
    if (m_container_item->currentMinimizer() == minimizerTypeToName(MinimizerType::Genetic))
        createTMVAGeneticEdits();

    // GSL Simulated Annealing
    if (m_container_item->currentMinimizer() == minimizerTypeToName(MinimizerType::GSLSimAn))
        createGSLSimulatedAnnealingEdits();

    // GSL Levenberg-Marquardt
    if (m_container_item->currentMinimizer() == minimizerTypeToName(MinimizerType::GSLLMA))
        createGSLLevMarEdits();
}

void MinimizerEditor::createMinuitEdits()
{
    MinuitMinimizerItem* minItem = m_container_item->minimizerItemMinuit();

    m_minimizer_layout->addRow(
        "Strategy:",
        GUI::Util::createIntSpinBox([=] { return minItem->strategy(); },
                                    [=](int v) {
                                        minItem->setStrategy(v);
                                        gDoc->setModified();
                                    },
                                    RealLimits::limited(0, 2),
                                    "Minimization strategy (0-low, 1-medium, 2-high quality)"));

    GUI::Util::addDoubleSpinBoxRow(m_minimizer_layout, minItem->errorDefinition());
    GUI::Util::addDoubleSpinBoxRow(m_minimizer_layout, minItem->tolerance());
    GUI::Util::addDoubleSpinBoxRow(m_minimizer_layout, minItem->precision());

    m_minimizer_layout->addRow("Max func calls:",
                               GUI::Util::createIntSpinBox([=] { return minItem->maxFuncCalls(); },
                                                           [=](int v) {
                                                               minItem->setMaxFuncCalls(v);
                                                               gDoc->setModified();
                                                           },
                                                           RealLimits::nonnegative(),
                                                           "Maximum number of function calls"));
}

void MinimizerEditor::createGSLMultiMinEdits()
{
    GSLMultiMinimizerItem* minItem = m_container_item->minimizerItemGSLMulti();

    m_minimizer_layout->addRow("Max iterations:",
                               GUI::Util::createIntSpinBox([=] { return minItem->maxIterations(); },
                                                           [=](int v) {
                                                               minItem->setMaxIterations(v);
                                                               gDoc->setModified();
                                                           },
                                                           RealLimits::nonnegative(),
                                                           "Maximum number of iterations"));
}

void MinimizerEditor::createTMVAGeneticEdits()
{
    GeneticMinimizerItem* minItem = m_container_item->minimizerItemGenetic();

    GUI::Util::addDoubleSpinBoxRow(m_minimizer_layout, minItem->tolerance());

    m_minimizer_layout->addRow("Max iterations:",
                               GUI::Util::createIntSpinBox([=] { return minItem->maxIterations(); },
                                                           [=](int v) {
                                                               minItem->setMaxIterations(v);
                                                               gDoc->setModified();
                                                           },
                                                           RealLimits::nonnegative(),
                                                           "Maximum number of iterations"));

    m_minimizer_layout->addRow(
        "Population:", GUI::Util::createIntSpinBox([=] { return minItem->populationSize(); },
                                                   [=](int v) {
                                                       minItem->setPopulationSize(v);
                                                       gDoc->setModified();
                                                   },
                                                   RealLimits::nonnegative(), "Population size"));

    m_minimizer_layout->addRow(
        "Random seed:",
        GUI::Util::createIntSpinBox([=] { return minItem->randomSeed(); },
                                    [=](int v) {
                                        minItem->setRandomSeed(v);
                                        gDoc->setModified();
                                    },
                                    RealLimits::limitless(),
                                    "Initialization of pseudorandom number generator"));
}

void MinimizerEditor::createGSLSimulatedAnnealingEdits()
{
    SimAnMinimizerItem* minItem = m_container_item->minimizerItemSimAn();

    m_minimizer_layout->addRow(
        "Max iterations:", GUI::Util::createIntSpinBox([=] { return minItem->maxIterations(); },
                                                       [=](int v) {
                                                           minItem->setMaxIterations(v);
                                                           gDoc->setModified();
                                                       },
                                                       RealLimits::nonnegative(),
                                                       "Number of points to try for each step"));

    m_minimizer_layout->addRow(
        "Iterations at T:",
        GUI::Util::createIntSpinBox([=] { return minItem->iterationsAtEachTemp(); },
                                    [=](int v) {
                                        minItem->setIterationsAtEachTemp(v);
                                        gDoc->setModified();
                                    },
                                    RealLimits::nonnegative(),
                                    "Number of iterations at each temperature"));

    GUI::Util::addDoubleSpinBoxRow(m_minimizer_layout, minItem->stepSize());
    GUI::Util::addDoubleSpinBoxRow(m_minimizer_layout, minItem->boltzmanK());
    GUI::Util::addDoubleSpinBoxRow(m_minimizer_layout, minItem->boltzmanInitT());
    GUI::Util::addDoubleSpinBoxRow(m_minimizer_layout, minItem->boltzmanMu());
    GUI::Util::addDoubleSpinBoxRow(m_minimizer_layout, minItem->boltzmanMinT());
}

void MinimizerEditor::createGSLLevMarEdits()
{
    GSLLMAMinimizerItem* minItem = m_container_item->minimizerItemGSLLMA();

    GUI::Util::addDoubleSpinBoxRow(m_minimizer_layout, minItem->tolerance());

    m_minimizer_layout->addRow("Max iterations:",
                               GUI::Util::createIntSpinBox([=] { return minItem->maxIterations(); },
                                                           [=](int v) {
                                                               minItem->setMaxIterations(v);
                                                               gDoc->setModified();
                                                           },
                                                           RealLimits::nonnegative(),
                                                           "Maximum number of iterations"));
}

void MinimizerEditor::updateUIValues()
{
    for (const auto& updater : m_updaters)
        updater();
}
