//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/View/ParEdit/ParameterTuningDelegate.cpp
//! @brief     Implements class ParameterTuningDelegate.
//!
//! @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/ParEdit/ParameterTuningDelegate.h"
#include "GUI/Model/Par/ParameterTreeItems.h"
#include "GUI/Model/Project/ProjectDocument.h"
#include "GUI/View/ParEdit/ParSpinBox.h"
#include <QHBoxLayout>

namespace {

const double maximum_doublespin_value = std::numeric_limits<double>::max();
const double minimum_doublespin_value = std::numeric_limits<double>::lowest();

} // namespace

//  ************************************************************************************************
//  member class TuningData
//  ************************************************************************************************

ParameterTuningDelegate::TuningData::TuningData()
    : m_smin(0)
    , m_smax(100)
    , m_rmin(0.0)
    , m_rmax(0.0)
    , m_range_factor(100.0)
{
}

void ParameterTuningDelegate::TuningData::setRangeFactor(double range_factor)
{
    m_range_factor = range_factor;
}

void ParameterTuningDelegate::TuningData::setItemLimits(const RealLimits& item_limits)
{
    m_item_limits = item_limits;
}

int ParameterTuningDelegate::TuningData::value_to_slider(double value)
{
    double dr(0);
    if (value == 0.0)
        dr = 1.0 * m_range_factor / 100.;
    else
        dr = std::abs(value) * m_range_factor / 100.;
    m_rmin = value - dr;
    m_rmax = value + dr;

    if (m_item_limits.hasLowerLimit() && m_rmin < m_item_limits.min())
        m_rmin = m_item_limits.min();

    if (m_item_limits.hasUpperLimit() && m_rmax > m_item_limits.max())
        m_rmax = m_item_limits.max();

    double result = m_smin + (value - m_rmin) * (m_smax - m_smin) / (m_rmax - m_rmin);
    return static_cast<int>(result);
}

double ParameterTuningDelegate::TuningData::slider_to_value(int slider) const
{
    return m_rmin + (slider - m_smin) * (m_rmax - m_rmin) / (m_smax - m_smin);
}

double ParameterTuningDelegate::TuningData::step() const
{
    return (m_rmax - m_rmin) / (m_smax - m_smin);
}

//  ************************************************************************************************
//  class ParameterTuningDelegate
//  ************************************************************************************************

ParameterTuningDelegate::ParameterTuningDelegate(QObject* parent)
    : QItemDelegate(parent)
    , m_value_column(1)
    , m_slider(nullptr)
    , m_value_box(nullptr)
    , m_content_layout(nullptr)
    , m_current_item(nullptr)
    , m_is_read_only(false)
{
}

ParameterTuningDelegate::~ParameterTuningDelegate() = default;

void ParameterTuningDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option,
                                    const QModelIndex& index) const
{
    if (index.column() == m_value_column) {
        if (!index.parent().isValid())
            return;

        QVariant valueAsVariant = index.model()->data(index, Qt::EditRole);
        if (valueAsVariant.isValid() && valueAsVariant.type() == QVariant::Double) {
            double value = valueAsVariant.toDouble();
            QString text = ParSpinBox::toString(value, 3);

            QStyleOptionViewItem myOption = option;
            // myOption.displayAlignment = Qt::AlignLeft | Qt::AlignVCenter;

            drawDisplay(painter, myOption, myOption.rect, text);
            drawFocus(painter, myOption, myOption.rect);
            return;
        }
    }
    QItemDelegate::paint(painter, option, index);
}

QWidget* ParameterTuningDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option,
                                               const QModelIndex& index) const
{
    //... Special cases

    if (m_is_read_only)
        return nullptr;

    if (index.column() != m_value_column)
        return QItemDelegate::createEditor(parent, option, index);

    if (!index.parent().isValid())
        return nullptr;

    QVariant data = index.model()->data(index, Qt::EditRole);
    if (!data.isValid())
        return nullptr;

    auto* item = static_cast<QObject*>(index.internalPointer());
    m_current_item = dynamic_cast<ParameterItem*>(item);
    if (!m_current_item)
        return nullptr;

    //... Retrieve data

    double value = data.toDouble();
    RealLimits limits = m_current_item->limitsOfLink();
    m_tuning_info.setItemLimits(limits);
    m_tuning_info.value_to_slider(value);

    //... Construct return widget

    auto* result = new QWidget(parent);
    m_content_layout = new QHBoxLayout(result);
    m_content_layout->setContentsMargins(0, 0, 0, 0);

    //... Spin box

    m_value_box = new ParSpinBox;
    m_content_layout->addWidget(m_value_box);
    m_value_box->setKeyboardTracking(false);
    m_value_box->setFixedWidth(105);
    m_value_box->setDecimals(m_current_item->decimalsOfLink());
    m_value_box->setSingleStep(m_tuning_info.step());
    m_value_box->setMinimum(limits.hasLowerLimit() ? limits.min() : minimum_doublespin_value);
    m_value_box->setMaximum(limits.hasUpperLimit() ? limits.max() : maximum_doublespin_value);
    m_value_box->setValue(value);

    connect(m_value_box, &ParSpinBox::valueChanged, this,
            &ParameterTuningDelegate::editorValueChanged);

    //... Slider

    m_slider = new QSlider(Qt::Horizontal);
    m_content_layout->addWidget(m_slider);
    m_slider->setFocusPolicy(Qt::StrongFocus);
    m_slider->setTickPosition(QSlider::NoTicks);
    m_slider->setTickInterval(1);
    m_slider->setSingleStep(1);
    m_slider->setRange(m_tuning_info.m_smin, m_tuning_info.m_smax);

    updateSlider(value);

    //... Done

    return result;
}

void ParameterTuningDelegate::updateSlider(double value) const
{
    disconnect(m_slider, &QSlider::valueChanged, this,
               &ParameterTuningDelegate::sliderValueChanged);

    m_slider->setValue(m_tuning_info.value_to_slider(value));

    connect(m_slider, &QSlider::valueChanged, this, &ParameterTuningDelegate::sliderValueChanged,
            Qt::UniqueConnection);
}

void ParameterTuningDelegate::sliderValueChanged(int position)
{
    disconnect(m_value_box, &ParSpinBox::valueChanged, this,
               &ParameterTuningDelegate::editorValueChanged);

    double value = m_tuning_info.slider_to_value(position);
    m_value_box->setValue(value);

    connect(m_value_box, &ParSpinBox::valueChanged, this,
            &ParameterTuningDelegate::editorValueChanged, Qt::UniqueConnection);
    emitSignals(value);
}

void ParameterTuningDelegate::editorValueChanged(double value)
{
    disconnect(m_slider, &QSlider::valueChanged, this,
               &ParameterTuningDelegate::sliderValueChanged);

    updateSlider(value);

    connect(m_slider, &QSlider::valueChanged, this, &ParameterTuningDelegate::sliderValueChanged,
            Qt::UniqueConnection);
    emitSignals(value);
}

void ParameterTuningDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const
{
    if (index.column() != m_value_column)
        QItemDelegate::setEditorData(editor, index);
    // else using custom widget, nothing to be done here
}

void ParameterTuningDelegate::setModelData(QWidget* editor, QAbstractItemModel* model,
                                           const QModelIndex& index) const
{
    if (index.column() == m_value_column)
        model->setData(index, m_value_box->value());
    else
        QItemDelegate::setModelData(editor, model, index);
}

void ParameterTuningDelegate::emitSignals(double value)
{
    if (m_current_item) {
        m_current_item->propagateValueToLink(value);
        emit currentLinkChanged(m_current_item);
    }
    gDoc->setModified();
}

void ParameterTuningDelegate::setSliderRangeFactor(int value)
{
    m_tuning_info.setRangeFactor(double(value));
}

void ParameterTuningDelegate::setReadOnly(bool isReadOnly)
{
    m_is_read_only = isReadOnly;
}
