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

namespace {

static constexpr double step0 = 0.1;

QString toString(double val, int decimal_points = 6)
{
    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


DoubleSpinBox::DoubleSpinBox(DoubleProperty* d)
    : m_step(::step0)
{
    replaceProperty(d);

    connect(this, &QAbstractSpinBox::editingFinished, [this] {
        ASSERT(m_property);
        setValue(fromDisplay());
        m_old_dir = 0;
        m_step = ::step0;
    });
    // setSingleStep(m_property->step());
}

void DoubleSpinBox::replaceProperty(DoubleProperty* d)
{
    disconnect(m_property);
    m_property = d;
    if (m_property) {
        setFocusPolicy(Qt::StrongFocus);
        setToolTip(d->tooltip());
        setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);
        lineEdit()->setText(toString(m_property->value()));
        connect(d, &DoubleProperty::setAndNotifyCalled, this, &DoubleSpinBox::updateValue);
        //		[this] { updateValue(); });
    }
    setReadOnly(!m_property);
    updateValue();
}

void DoubleSpinBox::updateValue()
{
    if (m_property)
        lineEdit()->setText(toString(m_property->value()));
    else
        lineEdit()->setText("");
}

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

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

void DoubleSpinBox::setValue(double val)
{
    ASSERT(m_property);
    const double oldval = m_property->value();
    val = m_property->limits().clamp(val);
    lineEdit()->setText(toString(val));
    m_property->setValue(fromDisplay());
    if (m_property->value() != oldval)
        emit valueChanged(m_property->value());
}

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

void DoubleSpinBox::stepBy(int steps)
{
    ASSERT(m_property);

    const int new_dir = steps > 0 ? +1 : -1;
    if (m_old_dir == new_dir)
        m_step = std::min(m_step * (std::abs(steps) == 1 ? 1.2 : 2.) * (1 + m_step), 9.);
    else if (m_old_dir == -new_dir)
        m_step = std::abs(steps) == 1 ? std::max(m_step / 9, 1e-6) : m_step;

    const double fac = (steps > 0) ^ (m_property->value() < 0) ? 1 + m_step : 1 / (1 + m_step);
    const int decimals = std::min(6, std::max(2, (int)(2 - std::log10(m_step))));

    // std::cout << "DEBUG steps=" << steps << " m_s=" << m_step << " fac=" << fac
    //	      << " dec=" << decimals << " old=" << m_property->value()
    //	      << " new=" << m_property->value() * fac
    //	      << " rnd=" << Numeric::round_decimal(m_property->value() * fac, decimals)
    //	      << std::endl;
    setValue(Numeric::round_decimal(m_property->value() * fac, decimals));

    m_old_dir = new_dir;
}
