//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/View/Fit/MinimizerSettingsWidget.cpp
//! @brief     Implements class MinimizerSettingsWidget
//!
//! @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/Fit/MinimizerSettingsWidget.h"
#include "Base/Util/Assert.h"
#include "GUI/Model/Job/FitSuiteItem.h"
#include "GUI/Model/Project/ProjectDocument.h"
#include "GUI/View/Numeric/NumWidgetUtil.h"
#include "GUI/View/Tool/LayoutUtil.h"
#include <QStandardItemModel>

MinimizerSettingsWidget::MinimizerSettingsWidget(QWidget* parent)
    : QWidget(parent)
    , m_containerItem(nullptr)
{
    setWindowTitle(QLatin1String("Minimizer Settings"));

    m_mainLayout = new QFormLayout(this);
    m_mainLayout->setContentsMargins(8, 8, 8, 8);
    m_mainLayout->setSpacing(5);
}

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

void MinimizerSettingsWidget::setMinContainerItem(MinimizerContainerItem* containerItem)
{
    ASSERT(containerItem);

    GUI::Util::Layout::clearLayout(m_mainLayout);
    m_updaters.clear();
    m_containerItem = containerItem;

    if (!m_containerItem)
        return;

    createGroupedAlgorithmsCombo();

    auto* w = new QWidget(this);
    m_minimizerLayout = new QFormLayout(w);
    m_minimizerLayout->setContentsMargins(10, 8, 0, 8);
    m_mainLayout->addRow(w);

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

    createMimimizerEdits();
    updateUIValues();
}

void MinimizerSettingsWidget::createGroupedAlgorithmsCombo()
{
    QComboBox* comboBox = new QComboBox;
    QStringList list = m_containerItem->commonAlgorithmCombo().values();

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

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

            qsizetype header_index = comboBox->count() - 1;
            header_indices.append(header_index);
            non_separator_indices.append(header_index);
            QStandardItemModel* 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_containerItem->commonAlgorithmCombo().currentValue());

    // tooltips
    QStringList tooltips = m_containerItem->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_containerItem->setCurrentCommonAlgorithm(comboBox->currentText());
        createMimimizerEdits();
        gProjectDocument.value()->setModified();
    });

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

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

void MinimizerSettingsWidget::createMimimizerEdits()
{
    GUI::Util::Layout::clearLayout(m_minimizerLayout);

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

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

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

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

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

void MinimizerSettingsWidget::createMinuitEdits()
{
    MinuitMinimizerItem* minItem = m_containerItem->minimizerItemMinuit();

    m_minimizerLayout->addRow(
        "Strategy:",
        GUI::Util::createIntSpinbox([=] { return minItem->strategy(); },
                                    [=](int v) {
                                        minItem->setStrategy(v);
                                        gProjectDocument.value()->setModified();
                                    },
                                    RealLimits::limited(0, 2),
                                    "Minimization strategy (0-low, 1-medium, 2-high quality)",
                                    &m_updaters, true /*easy scroll*/));

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

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

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

    m_minimizerLayout->addRow(
        "Max func calls:",
        GUI::Util::createIntSpinbox([=] { return minItem->maxFuncCalls(); },
                                    [=](int v) {
                                        minItem->setMaxFuncCalls(v);
                                        gProjectDocument.value()->setModified();
                                    },
                                    RealLimits::nonnegative(), "Maximum number of function calls",
                                    &m_updaters, true /*easy scroll*/));
}

void MinimizerSettingsWidget::createGSLMultiMinEdits()
{
    GSLMultiMinimizerItem* minItem = m_containerItem->minimizerItemGSLMulti();

    m_minimizerLayout->addRow(
        "Max iterations:",
        GUI::Util::createIntSpinbox([=] { return minItem->maxIterations(); },
                                    [=](int v) {
                                        minItem->setMaxIterations(v);
                                        gProjectDocument.value()->setModified();
                                    },
                                    RealLimits::nonnegative(), "Maximum number of iterations",
                                    &m_updaters, true /*easy scroll*/));
}

void MinimizerSettingsWidget::createTMVAGeneticEdits()
{
    GeneticMinimizerItem* minItem = m_containerItem->minimizerItemGenetic();

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

    m_minimizerLayout->addRow(
        "Max iterations:",
        GUI::Util::createIntSpinbox([=] { return minItem->maxIterations(); },
                                    [=](int v) {
                                        minItem->setMaxIterations(v);
                                        gProjectDocument.value()->setModified();
                                    },
                                    RealLimits::nonnegative(), "Maximum number of iterations",
                                    &m_updaters, true /*easy scroll*/));

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

    m_minimizerLayout->addRow("Random seed:", GUI::Util::createIntSpinbox(
                                                  [=] { return minItem->randomSeed(); },
                                                  [=](int v) {
                                                      minItem->setRandomSeed(v);
                                                      gProjectDocument.value()->setModified();
                                                  },
                                                  RealLimits::limitless(),
                                                  "Initialization of pseudorandom number generator",
                                                  &m_updaters, true /*easy scroll*/));
}

void MinimizerSettingsWidget::createGSLSimulatedAnnealingEdits()
{
    SimAnMinimizerItem* minItem = m_containerItem->minimizerItemSimAn();

    m_minimizerLayout->addRow(
        "Max iterations:", GUI::Util::createIntSpinbox([=] { return minItem->maxIterations(); },
                                                       [=](int v) {
                                                           minItem->setMaxIterations(v);
                                                           gProjectDocument.value()->setModified();
                                                       },
                                                       RealLimits::nonnegative(),
                                                       "Number of points to try for each step",
                                                       &m_updaters, true /*easy scroll*/));

    m_minimizerLayout->addRow(
        "Iterations at T:",
        GUI::Util::createIntSpinbox([=] { return minItem->iterationsAtEachTemp(); },
                                    [=](int v) {
                                        minItem->setIterationsAtEachTemp(v);
                                        gProjectDocument.value()->setModified();
                                    },
                                    RealLimits::nonnegative(),
                                    "Number of iterations at each temperature", &m_updaters,
                                    true /*easy scroll*/));

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

    m_minimizerLayout->addRow("k:", GUI::Util::createDoubleSpinbox(
                                        [=] { return minItem->boltzmanK(); },
                                        [=](double v) {
                                            minItem->setBoltzmanK(v);
                                            gProjectDocument.value()->setModified();
                                        },
                                        &m_updaters, "Boltzmann k", RealLimits::nonnegative()));

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

    m_minimizerLayout->addRow("mu:", GUI::Util::createDoubleSpinbox(
                                         [=] { return minItem->boltzmanMu(); },
                                         [=](double v) {
                                             minItem->setBoltzmanMu(v);
                                             gProjectDocument.value()->setModified();
                                         },
                                         &m_updaters, "Boltzmann mu", RealLimits::nonnegative()));

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

void MinimizerSettingsWidget::createGSLLevMarEdits()
{
    GSLLMAMinimizerItem* minItem = m_containerItem->minimizerItemGSLLMA();

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

    m_minimizerLayout->addRow(
        "Max iterations:",
        GUI::Util::createIntSpinbox([=] { return minItem->maxIterations(); },
                                    [=](int v) {
                                        minItem->setMaxIterations(v);
                                        gProjectDocument.value()->setModified();
                                    },
                                    RealLimits::nonnegative(), "Maximum number of iterations",
                                    &m_updaters, true /*easy scroll*/));
}

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