//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/View/Numeric/DSpinBox.cpp
//! @brief     Implements class DSpinBox.
//!
//! @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/Numeric/DSpinBox.h"
#include "Base/Math/Numeric.h"
#include "Base/Util/Assert.h"
#include "Base/Util/StringUtil.h"
#include "GUI/Model/Descriptor/DoubleProperty.h"
#include "GUI/Model/Project/ProjectDocument.h"
#include <QLineEdit>
#include <QRegularExpression>
#include <QWheelEvent>
#include <iostream>

namespace {

static constexpr double step0 = 0.1;

QString toString(double val, int decimal_points)
{
    if (val == 0)
        return "0";

    QString result = (std::abs(val) >= 10000 || std::abs(val) < 0.1)
                         ? QString::number(val, 'e', decimal_points)
                         : QString::number(val, 'f', decimal_points);

    // suppress ".0" in mantissa; normalize exponent
    return result.replace(QRegularExpression("(\\.?0+)?((e)([\\+]?)([-]?)(0*)([1-9].*))?$"),
                          "\\3\\5\\7");
}

} // namespace


DSpinBox::DSpinBox(DoubleProperty* d)
{
    replaceProperty(d);

    connect(this, &QAbstractSpinBox::editingFinished, [this] {
        ASSERT(m_property);
        setValue(fromDisplay());
    });
}

void DSpinBox::replaceProperty(DoubleProperty* d)
{
    if (m_property)
        disconnect(m_property, &DoubleProperty::setAndNotifyCalled, this, &DSpinBox::updateValue);

    m_property = d;
    if (m_property) {
        setFocusPolicy(Qt::StrongFocus);
        setToolTip(d->tooltip());
        setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);
        lineEdit()->setText(toString(m_property->dVal(), m_property->decimals()));
        connect(d, &DoubleProperty::setAndNotifyCalled, this, &DSpinBox::updateValue,
                Qt::UniqueConnection);
    }
    setReadOnly(!m_property);
    updateValue();
}

void DSpinBox::updateValue()
{
    if (m_property)
        lineEdit()->setText(toString(m_property->dVal(), m_property->decimals()));
    else
        lineEdit()->setText("");
}

QAbstractSpinBox::StepEnabled DSpinBox::stepEnabled() const
{
    return StepUpEnabled | StepDownEnabled;
}

double DSpinBox::fromDisplay() const
{
    double result;
    if (Base::String::to_double(lineEdit()->text().toStdString(), &result))
        return result;
    // invalid input => restore last valid value
    return m_property->dVal();
}

void DSpinBox::setValue(double val)
{
    ASSERT(m_property);
    const double oldval = m_property->dVal();
    val = m_property->limits().clamp(val);
    lineEdit()->setText(toString(val, m_property->decimals()));
    m_property->setDVal(fromDisplay());
    if (m_property->dVal() != oldval) {
        emit valueChanged(m_property->dVal());
        gDoc->setModified();
    }
}

void DSpinBox::wheelEvent(QWheelEvent* event)
{
    if (hasFocus())
        QAbstractSpinBox::wheelEvent(event);
    else
        event->ignore();
}

void DSpinBox::stepBy(int steps)
{
    ASSERT(m_property);
    const double val = m_property->dVal();

    if (m_property->useFixedStep()) {
        setValue(val + m_property->step() * steps);
        return;
    }

    // "step digit" is the orger of magnitude of the step.
    // If it is "1", then we increment/decrement the first digit of the value.
    // If it is "2", then we increment/decrement the second digit of the value and so on.
    const int step_digit = 2;
    const int order_of_mag = Numeric::orderOfMagnitude(val);
    // special cases: if the current value is 100 and step digit is 2, then the diminished value
    // should be not 90, but 99.
    const int correction = (val == std::pow(10, order_of_mag) && steps < 0) ? 1 : 0;
    const double adaptive_step = std::pow(10, order_of_mag - step_digit - correction + 1);
    const double step = (val == 0) ? ::step0 : adaptive_step;
    setValue(val + step * steps);
}
