//  ************************************************************************************************
//
//  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/NumWidgetUtil.h"
#include <QSpinBox>
#include <QStandardItemModel>

MinimizerEditor::MinimizerEditor(QWidget* parent)
    : QWidget(parent)
    , m_container_item(nullptr)
    , m_main_layout(new QFormLayout(this))
{
    setWindowTitle(QLatin1String("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* containerItem)
{
    ASSERT(containerItem);

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

    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)",
                                    &m_updaters, true /*easy scroll*/));

    m_minimizer_layout->addRow(
        "ErrorDef factor:",
        GUI::Util::createDoubleSpinBox([=] { return minItem->errorDefinition(); },
                                       [=](double v) {
                                           minItem->setErrorDefinition(v);
                                           gDoc->setModified();
                                       },
                                       &m_updaters,
                                       "Error definition factor for parameter error calculation",
                                       RealLimits::positive()));

    m_minimizer_layout->addRow("Tolerance:",
                               GUI::Util::createDoubleSpinBox(
                                   [=] { return minItem->tolerance(); },
                                   [=](double v) {
                                       minItem->setTolerance(v);
                                       gDoc->setModified();
                                   },
                                   &m_updaters, "Tolerance on the function value at the minimum",
                                   RealLimits::nonnegative()));

    m_minimizer_layout->addRow(
        "Precision:",
        GUI::Util::createDoubleSpinBox([=] { return minItem->precision(); },
                                       [=](double v) {
                                           minItem->setPrecision(v);
                                           gDoc->setModified();
                                       },
                                       &m_updaters, "Relative floating point arithmetic precision",
                                       RealLimits::nonnegative()));

    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",
                                                           &m_updaters, true /*easy scroll*/));
}

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",
                                                           &m_updaters, true /*easy scroll*/));
}

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

    m_minimizer_layout->addRow("Tolerance:",
                               GUI::Util::createDoubleSpinBox(
                                   [=] { return minItem->tolerance(); },
                                   [=](double v) {
                                       minItem->setTolerance(v);
                                       gDoc->setModified();
                                   },
                                   &m_updaters, "Tolerance on the function value at the minimum",
                                   RealLimits::nonnegative()));

    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_updaters, true /*easy scroll*/));

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

    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", &m_updaters,
                                    true /*easy scroll*/));
}

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_updaters, true /*easy scroll*/));

    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", &m_updaters,
                                    true /*easy scroll*/));

    m_minimizer_layout->addRow("Step size:", GUI::Util::createDoubleSpinBox(
                                                 [=] { return minItem->stepSize(); },
                                                 [=](double v) {
                                                     minItem->setStepSize(v);
                                                     gDoc->setModified();
                                                 },
                                                 &m_updaters, "Max step size used in random walk",
                                                 RealLimits::nonnegative()));

    m_minimizer_layout->addRow("k:", GUI::Util::createDoubleSpinBox(
                                         [=] { return minItem->boltzmanK(); },
                                         [=](double v) {
                                             minItem->setBoltzmanK(v);
                                             gDoc->setModified();
                                         },
                                         &m_updaters, "Boltzmann k", RealLimits::nonnegative()));

    m_minimizer_layout->addRow(
        "T init:", GUI::Util::createDoubleSpinBox([=] { return minItem->boltzmanInitT(); },
                                                  [=](double v) {
                                                      minItem->setBoltzmanInitT(v);
                                                      gDoc->setModified();
                                                  },
                                                  &m_updaters, "Boltzmann initial temperature",
                                                  RealLimits::nonnegative()));

    m_minimizer_layout->addRow("mu:", GUI::Util::createDoubleSpinBox(
                                          [=] { return minItem->boltzmanMu(); },
                                          [=](double v) {
                                              minItem->setBoltzmanMu(v);
                                              gDoc->setModified();
                                          },
                                          &m_updaters, "Boltzmann mu", RealLimits::nonnegative()));

    m_minimizer_layout->addRow(
        "T min:", GUI::Util::createDoubleSpinBox([=] { return minItem->boltzmanMinT(); },
                                                 [=](double v) {
                                                     minItem->setBoltzmanMinT(v);
                                                     gDoc->setModified();
                                                 },
                                                 &m_updaters, "Boltzmann minimal temperature",
                                                 RealLimits::nonnegative()));
}

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

    m_minimizer_layout->addRow("Tolerance:",
                               GUI::Util::createDoubleSpinBox(
                                   [=] { return minItem->tolerance(); },
                                   [=](double v) {
                                       minItem->setTolerance(v);
                                       gDoc->setModified();
                                   },
                                   &m_updaters, "Tolerance on the function value at the minimum",
                                   RealLimits::nonnegative()));

    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_updaters, true /*easy scroll*/));
}

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