/**
 *
 * @file src/render/RenderLayout.cpp
 *
 * @copyright 2008-2024 Bordeaux INP, CNRS (LaBRI UMR 5800), Inria,
 *                      Univ. Bordeaux. All rights reserved.
 *
 * @author Camille Ordronneau
 *
 * @date 2024-07-17
 */
/*!
 *\file RanderLayout.cpp
 *\brief This is a class containing a render area and the widgets relative to this area
 */

#include "RenderLayout.hpp"
#include "Render_abstract.hpp"
/* -- */
#include <QHBoxLayout>
#include <QLabel>
#include <QWidget>
#include <QSplitter>
/* -- */
#include "trace/Trace.hpp"

RenderLayout::RenderLayout(Interface_graphic *interface_graphic, QWidget *parent_widget, QVBoxLayout *parent_layout, Render_windowed *render_area) :
    QObject(),
    _interface_graphic(interface_graphic),
    _slider_x_position(0), _slider_y_position(0) {

    // In the following code Qt objects are allocated using new
    // Qt desallocate automatically these object

    // The root is a vertival splitter between
    // - on top a render area and y scroll bar
    // - at the bottom the x scroll bar and zoom boxes
    QSplitter *splitter = new QSplitter(parent_widget);
    splitter->setOrientation(Qt::Vertical);
    splitter->setHandleWidth(5);
    splitter->setStyleSheet(QString::fromUtf8("QSplitter::handle{background: #FFFFFF;}"));

    // There is first a horizontal layout composed of :
    // - on the left a render area
    // - on the right a vertival scroll bar
    // A QLayout can't be directly put inside a splitter
    // it needs to be inside a QWidget
    QWidget *render_scroll_widget = new QWidget(splitter);
    QHBoxLayout *render_scroll_layout = new QHBoxLayout(render_scroll_widget);

    _render_area = render_area;
    render_scroll_layout->addWidget(_render_area->get_render_widget());

    _scroll_bar_y = new QScrollBar(render_scroll_widget);
    _scroll_bar_y->setOrientation(Qt::Vertical);
    _scroll_bar_y->setMaximum(0);
    _scroll_bar_y->setPageStep(_SCROLL_PAGE_STEP);
    render_scroll_layout->addWidget(_scroll_bar_y);
    render_scroll_layout->setContentsMargins(0, 0, 0, 0);
    render_scroll_layout->setSpacing(0);

    splitter->addWidget(render_scroll_widget);

    // There is then a horizontal layout composed of several elements
    QWidget *scroll_widget = new QWidget(splitter);
    // We can't only set the y value of the maximum size so the x value is also set
    // 16777215 = 2^24 - 1 is the default Qt value for this x field
    scroll_widget->setMaximumSize(QSize(16777215, 35));
    QHBoxLayout *scroll_widget_layout = new QHBoxLayout(scroll_widget);
    scroll_widget_layout->setContentsMargins(-1, 2, -1, 4);

    // The Scale Containers Label
    QLabel *scale_constainer_label = new QLabel(scroll_widget);
    scale_constainer_label->setText(QString::fromUtf8("Scale containers: "));
    scroll_widget_layout->addWidget(scale_constainer_label);

    // The Scale Container Slider
    _scale_container_slider = new QSlider(scroll_widget);
    QSizePolicy sizePolicy1(QSizePolicy::Minimum, QSizePolicy::Fixed);
    sizePolicy1.setHorizontalStretch(0);
    sizePolicy1.setVerticalStretch(0);
    sizePolicy1.setHeightForWidth(_scale_container_slider->sizePolicy().hasHeightForWidth());
    _scale_container_slider->setSizePolicy(sizePolicy1);
    _scale_container_slider->setValue(20);
    _scale_container_slider->setOrientation(Qt::Horizontal);
    scroll_widget_layout->addWidget(_scale_container_slider);

    // A vertical line separator
    QFrame *line = new QFrame(scroll_widget);
    line->setFrameShape(QFrame::VLine);
    line->setFrameShadow(QFrame::Sunken);
    scroll_widget_layout->addWidget(line);

    // A horizontal scroll bar
    _scroll_bar_x = new QScrollBar(scroll_widget);
    QSizePolicy sizePolicy2(QSizePolicy::Expanding, QSizePolicy::Fixed);
    sizePolicy2.setHorizontalStretch(0);
    sizePolicy2.setVerticalStretch(0);
    sizePolicy2.setHeightForWidth(_scroll_bar_x->sizePolicy().hasHeightForWidth());
    _scroll_bar_x->setSizePolicy(sizePolicy2);
    _scroll_bar_x->setOrientation(Qt::Horizontal);
    _scroll_bar_x->setMaximum(0);
    _scroll_bar_x->setPageStep(_SCROLL_PAGE_STEP);
    scroll_widget_layout->addWidget(_scroll_bar_x);

    // A vertical line separator
    line = new QFrame(scroll_widget);
    line->setFrameShape(QFrame::VLine);
    line->setFrameShadow(QFrame::Raised);
    scroll_widget_layout->addWidget(line);

    // A label for x zoom box
    QLabel *zoom_label_x = new QLabel(scroll_widget);
    zoom_label_x->setText(QString::fromUtf8("Zoom X: "));
    scroll_widget_layout->addWidget(zoom_label_x);

    // A x zoom box
    _zoom_box_x = new QSpinBox(scroll_widget);
    QFont font;
    font.setItalic(true);
    _zoom_box_x->setFont(font);
    _zoom_box_x->setAlignment(Qt::AlignRight | Qt::AlignTrailing | Qt::AlignVCenter);
    _zoom_box_x->setMinimum(Render_abstract::_MIN_X_ZOOM_VALUE * 100);
    _zoom_box_x->setMaximum(Render_abstract::_MAX_ZOOM_VALUE * 100);
    _zoom_box_x->setValue(100);
    _zoom_box_x->setStepType(QAbstractSpinBox::AdaptiveDecimalStepType);
    scroll_widget_layout->addWidget(_zoom_box_x);

    // A "%" label
    QLabel *zoom_label_x_percent = new QLabel(scroll_widget);
    zoom_label_x_percent->setText(QString::fromUtf8("%"));
    scroll_widget_layout->addWidget(zoom_label_x_percent);

    // A vertical line separator
    line = new QFrame(scroll_widget);
    line->setFrameShape(QFrame::VLine);
    line->setFrameShadow(QFrame::Raised);
    scroll_widget_layout->addWidget(line);

    // A label for y zoom box
    QLabel *zoom_label_y = new QLabel(scroll_widget);
    zoom_label_y->setText(QString::fromUtf8("Zoom Y: "));
    scroll_widget_layout->addWidget(zoom_label_y);

    // A y zoom box
    _zoom_box_y = new QSpinBox(scroll_widget);
    _zoom_box_y->setFont(font);
    _zoom_box_y->setAlignment(Qt::AlignRight | Qt::AlignTrailing | Qt::AlignVCenter);
    _zoom_box_y->setMinimum(100);
    _zoom_box_y->setMaximum(Render_abstract::_MAX_ZOOM_VALUE * 100);
    _zoom_box_y->setStepType(QAbstractSpinBox::AdaptiveDecimalStepType);
    scroll_widget_layout->addWidget(_zoom_box_y);

    // A "%" label
    QLabel *zoom_label_y_percent = new QLabel(scroll_widget);
    zoom_label_y_percent->setText(QString::fromUtf8("%"));
    scroll_widget_layout->addWidget(zoom_label_y_percent);

    splitter->addWidget(scroll_widget);

    parent_layout->addWidget(splitter);

    // connect signals to slots
    connect(_scale_container_slider, &QSlider::valueChanged, this, &RenderLayout::scale_container_value_changed);

    connect(_scroll_bar_x, &QScrollBar::valueChanged, this, &RenderLayout::scroll_x_value_changed);
    connect(_scroll_bar_x, &QScrollBar::sliderPressed, this, [=]() { _slider_x_position = _scroll_bar_x->sliderPosition(); });
    connect(_scroll_bar_y, &QScrollBar::valueChanged, this, &RenderLayout::scroll_y_value_changed);
    connect(_scroll_bar_y, &QScrollBar::sliderPressed, this, [=]() { _slider_y_position = _scroll_bar_y->sliderPosition(); });

    connect(_zoom_box_x, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &RenderLayout::zoom_box_x_value_changed);
    connect(_zoom_box_y, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &RenderLayout::zoom_box_y_value_changed);

    _x_factor_virtual_to_real = 100;
    _y_factor_virtual_to_real = 100;

    _trace = nullptr;
}

RenderLayout::~RenderLayout() {
    if (nullptr != _trace) {
        delete _trace;
    }
}

QSlider *RenderLayout::get_scale_container_slider() {
    return _scale_container_slider;
}

QSpinBox *RenderLayout::get_zoom_box_x() {
    return _zoom_box_x;
}

QSpinBox *RenderLayout::get_zoom_box_y() {
    return _zoom_box_y;
}

QScrollBar *RenderLayout::get_scroll_bar_x() {
    return _scroll_bar_x;
}

QScrollBar *RenderLayout::get_scroll_bar_y() {
    return _scroll_bar_y;
}

Render_windowed *RenderLayout::get_render_area() {
    return _render_area;
}

void RenderLayout::set_trace(Trace *trace) {
    _trace = trace;
}

Trace *RenderLayout::get_trace() {
    return _trace;
}

void RenderLayout::delete_trace() {
    if (nullptr != _trace) {
        delete _trace;
        _trace = nullptr;
    }
}

void RenderLayout::set_zoom_box_x_value(const int &new_value) {
    // This object prevents signals (especially when the value change from the UI or from the code)
    // to be intercepted by _ui_zoom_box_x and avoids an infinite loop.
    // The signal is blocked during the lifetime of the blocker object, so the scope of this function.
    const QSignalBlocker blocker(_zoom_box_x);
    _zoom_box_x->setValue(new_value);
}

void RenderLayout::set_zoom_box_y_value(const int &new_value) {
    // This object prevents signals (especially when the value change from the UI or from the code)
    // to be intercepted by _ui_zoom_box_y and avoids an infinite loop.
    // The signal is blocked during the lifetime of the blocker object, so the scope of this function.
    const QSignalBlocker blocker(_zoom_box_y);
    _zoom_box_y->setValue(new_value);
}

void RenderLayout::zoom_box_x_value_changed(const int &new_value) {
    _render_area->replace_scale_x((Element_pos)new_value / 100);
}

void RenderLayout::zoom_box_y_value_changed(const int &new_value) {
    _render_area->replace_scale_y((Element_pos)new_value / 100);
}

void RenderLayout::scroll_x_value_changed(int new_value) {
    /**
     * There are two known bugs
     * - If the scroll bar is past the left,
     *      If it is moved a bit to the right, then to the left
     *      the scroll bar will continue to move right.
     *      This is due to the way Qt handle continuous calls to valueChanged
     *      If the mouse is move 100 units (to the right) then -20 units (to the left)
     *      First a call to valueChanged(100) happens then a call to valueChanged(80)
     *      The signal is called with the sum of all mouse movement
     *      This causes problems like here if we manually change the slide value between mouse moves
     *
     *  - The second bug is caused by the same thing
     *      If the bar is too far right and is moved slowly to the left
     *      it will jump a bit to the left once it recovers its normal size
     */

    // x scale from percentage to float value
    const Element_pos x_scale_factor = (float)_zoom_box_x->value() / 100;
    // At scale 1, slider can't move -> maximum == 0
    // At scale 2, slider is half the size of the entire range -> maximum == _SCROLL_PAGE_STEP
    // This interpolates the default maximum value
    const int x_default_max = std::max((x_scale_factor - 1) * _SCROLL_PAGE_STEP, 0.f);
    const int &margin = _render_area->get_scroll_margin_x();

    // Default behaviour when slider is not past the left nor the right
    if (_scroll_bar_x->maximum() == x_default_max) {
        _render_area->replace_translate_x(((Element_pos)new_value / _SCROLL_PAGE_STEP) * (2 * margin));
        _interface_graphic->update_interval_select();
        return;
    }

    // Check is slider is past the left
    bool is_slider_on_left = (_slider_x_position == 0);

    // Case where slider is past the left
    if (is_slider_on_left) {
        // Reduce the maximum to enlarge the slide bar
        int new_maximum = std::max(x_default_max, _scroll_bar_x->maximum() - new_value);
        _scroll_bar_x->setMaximum(new_maximum);

        // Set the new position to 0 without triggering valueChanged signal handler
        bool last_block_state = _scroll_bar_x->blockSignals(true);
        _scroll_bar_x->setValue(0);
        _slider_x_position = 0;
        _scroll_bar_x->blockSignals(last_block_state);

        // Move the view
        // margin_coeff goes from 0 to 1 and count how much the slider is too far left
        const float margin_coeff = (float)(_scroll_bar_x->maximum() - x_default_max) / (_SCROLL_PAGE_STEP * x_scale_factor);
        const Element_pos x_value = -margin_coeff * margin;
        _render_area->replace_translate_x(x_value);
        _interface_graphic->update_interval_select();
        return;
    }

    // Case where scroll bar is past the right
    // Reduce the maximum to enlarge the slide bar
    int new_maximum = std::max(x_default_max, new_value);
    _scroll_bar_x->setMaximum(new_maximum);

    // Set the new position to 0 without triggering valueChanged signal handler
    bool last_block_state = _scroll_bar_x->blockSignals(true);
    _scroll_bar_x->setValue(_scroll_bar_x->maximum());
    _slider_x_position = _scroll_bar_x->maximum();
    _scroll_bar_x->blockSignals(last_block_state);

    // Move the view
    // margin_coeff goes from 0 to 1 and count how much the slider is too far right
    const float margin_coeff = (_scroll_bar_x->maximum() - x_default_max) / (_SCROLL_PAGE_STEP * x_scale_factor);
    const Element_pos x_value = (margin_coeff + std::max(x_scale_factor - 1, 0.f) * 2) * margin;
    _render_area->replace_translate_x(x_value);
    _interface_graphic->update_interval_select();
    return;
}

void RenderLayout::scroll_y_value_changed(int new_value) {
    // y scale from percentage to float value
    const Element_pos y_scale_factor = (float)_zoom_box_y->value() / 100;
    // At scale 1, slider can't move -> maximum == 0
    // At scale 2, slider is half the size of the entire range -> maximum == _SCROLL_PAGE_STEP
    // This interpolates the default maximum value
    const int y_default_max = (y_scale_factor - 1) * _SCROLL_PAGE_STEP;
    const int &margin = _render_area->get_scroll_margin_y();

    // Default behaviour when slider is not past the left nor the right
    if (_scroll_bar_y->maximum() == y_default_max) {
        _render_area->replace_translate_y(((Element_pos)new_value / _SCROLL_PAGE_STEP) * (2 * margin));
        return;
    }

    // Check is slider is past the left
    bool is_slider_on_left = (_slider_y_position == 0);

    // Case where slider is past the left
    if (is_slider_on_left) {
        // Reduce the maximum to enlarge the slide bar
        int new_maximum = std::max(y_default_max, _scroll_bar_y->maximum() - new_value);
        _scroll_bar_y->setMaximum(new_maximum);

        // Set the new position to 0 without triggering valueChanged signal handler
        bool last_block_state = _scroll_bar_y->blockSignals(true);
        _scroll_bar_y->setValue(0);
        _slider_y_position = 0;
        _scroll_bar_y->blockSignals(last_block_state);

        // Move the view
        // margin_coeff goes from 0 to 1 and count how much the slider is too far left
        const float margin_coeff = (float)(_scroll_bar_y->maximum() - y_default_max) / (_SCROLL_PAGE_STEP * y_scale_factor);
        const Element_pos y_value = -margin_coeff * margin;
        _render_area->replace_translate_y(y_value);
        return;
    }

    // Case where scroll bar is past the right
    // Reduce the maximum to enlarge the slide bar
    int new_maximum = std::max(y_default_max, new_value);
    _scroll_bar_y->setMaximum(new_maximum);

    // Set the new position to 0 without triggering valueChanged signal handler
    bool last_block_state = _scroll_bar_y->blockSignals(true);
    _scroll_bar_y->setValue(_scroll_bar_y->maximum());
    _slider_y_position = _scroll_bar_y->maximum();
    _scroll_bar_y->blockSignals(last_block_state);

    // Move the view
    // margin_coeff goes from 0 to 1 and count how much the slider is too far right
    const float margin_coeff = (_scroll_bar_y->maximum() - y_default_max) / (_SCROLL_PAGE_STEP * y_scale_factor);
    const Element_pos y_value = (margin_coeff + ((y_scale_factor - 1) * 2)) * margin;
    _render_area->replace_translate_y(y_value);
    return;
}

void RenderLayout::refresh_scroll_bars(const Element_pos &x_value, const Element_pos &y_value) {
    /**
     * The slide bar is composed of two part
     * - The slider of size pageStep
     * - The empty space of size maximum
     * Here the slider size changes by changing the maximum
     *
     * For a visual explanation see :
     * https://doc.qt.io/qt-6/images/qscrollbar-values.png
     */

    // Blocks signals to avoid infinite loop because
    // changing the scroll bar changes the render
    // and changing the render changes the scroll bar
    bool last_block_state = _scroll_bar_x->blockSignals(true);

    // Change x scroll bar
    const int &margin_x = _render_area->get_scroll_margin_x();
    const Element_pos x_scale_factor = (float)_zoom_box_x->value() / 100;
    const int x_default_max = std::max((x_scale_factor - 1) * _SCROLL_PAGE_STEP, 0.f);
    const Element_pos x_translation_range = std::max((x_scale_factor - 1) * 2 * margin_x, 0.f);

    if (x_value < 0) { // If the view is too far left, resize down the scroll bar
        const float margin_coeff = (-x_value / margin_x);
        _scroll_bar_x->setMaximum(x_default_max + _SCROLL_PAGE_STEP * margin_coeff * x_scale_factor);
        _scroll_bar_x->setValue(0);
    }
    else if (x_value > x_translation_range) { // If the view is too far right, resize down the scroll bar
        const float margin_coeff = (x_value - x_translation_range) / margin_x;
        _scroll_bar_x->setMaximum(x_default_max + _SCROLL_PAGE_STEP * margin_coeff * x_scale_factor);
        _scroll_bar_x->setValue(_scroll_bar_x->maximum());
    }
    else { // Default behaviour
        _scroll_bar_x->setMaximum(x_default_max);
        _scroll_bar_x->setValue(x_default_max * (x_value / x_translation_range));
    }
    _scroll_bar_x->blockSignals(last_block_state);

    // Change y scroll bar
    last_block_state = _scroll_bar_y->blockSignals(true);
    const int &margin_y = _render_area->get_scroll_margin_y();
    const Element_pos y_scale_factor = (float)_zoom_box_y->value() / 100;
    const int y_default_max = (y_scale_factor - 1) * _SCROLL_PAGE_STEP;
    const Element_pos y_translation_range = (y_scale_factor - 1) * 2 * margin_y;

    if (y_value < 0) { // If the view is too far left, resize down the scroll bar
        const float margin_coeff = (-y_value / margin_y);
        _scroll_bar_y->setMaximum(y_default_max + _SCROLL_PAGE_STEP * margin_coeff * y_scale_factor);
        _scroll_bar_y->setValue(0);
    }
    else if (y_value > y_translation_range) { // If the view is too far right, resize down the scroll bar
        const float margin_coeff = (y_value - y_translation_range) / margin_y;
        _scroll_bar_y->setMaximum(y_default_max + _SCROLL_PAGE_STEP * margin_coeff * y_scale_factor);
        _scroll_bar_y->setValue(_scroll_bar_y->maximum());
    }
    else { // Default behaviour
        _scroll_bar_y->setMaximum(y_default_max);
        _scroll_bar_y->setValue(y_default_max * (y_value / y_translation_range));
    }

    _scroll_bar_y->blockSignals(last_block_state);
    _interface_graphic->update_interval_select();
}

void RenderLayout::scale_container_value_changed(const int &new_value) {
    _render_area->change_container_scale_x(new_value);
}
