// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

#include "graphs3d/utils/qgraphs3dlogging_p.h"
#include "private/qquick3drepeater_p.h"
#include "q3dscene.h"
#include "qabstractdataproxy.h"
#include "qcategory3daxis_p.h"
#include "qgraphs3dlogging_p.h"
#include "qgraphsinputhandler_p.h"
#include "qquickgraphssurface_p.h"
#include "qquickgraphstexturedata_p.h"
#include "qsurface3dseries.h"
#include "qsurface3dseries_p.h"
#include "qsurfacedataproxy_p.h"
#include "qvalue3daxis_p.h"
#include "utils_p.h"

#include <QtQuick3D/private/qquick3dcustommaterial_p.h>
#include <QtQuick3D/private/qquick3dprincipledmaterial_p.h>

#include <QtQuick/qquickitemgrabresult.h>
#include <qtgraphs_tracepoints_p.h>

QT_BEGIN_NAMESPACE

Q_TRACE_PREFIX(qtgraphs,
                   "QT_BEGIN_NAMESPACE" \
                   "class QQuickGraphsSurface;" \
                   "class QSurface3DSeries;" \
                   "QT_END_NAMESPACE"
               )

Q_TRACE_POINT(qtgraphs, QGraphs3DSurfaceModelUpdate_entry, void *model);
Q_TRACE_POINT(qtgraphs, QGraphs3DSurfaceModelUpdate_exit);

Q_TRACE_POINT(qtgraphs, QGraphs3DSurfacePointSelectionUpdate_entry, void *model);
Q_TRACE_POINT(qtgraphs, QGraphs3DSurfacePointSelectionUpdate_exit);

Q_TRACE_POINT(qtgraphs, QGraphs3DSurfaceDoPicking_entry, float posX, float posY);
Q_TRACE_POINT(qtgraphs, QGraphs3DSurfaceDoPicking_exit);

Q_TRACE_POINT(qtgraphs, QGraphs3DSurfaceDoRayPicking_entry, float originX, float originY,
              float originZ, float directionX, float directionY, float directionZ);
Q_TRACE_POINT(qtgraphs, QGraphs3DSurfaceDoRayPicking_exit);

Q_TRACE_POINT(qtgraphs, QGraphs3DSurfaceCreateSliceView_entry);
Q_TRACE_POINT(qtgraphs, QGraphs3DSurfaceCreateSliceView_exit);

Q_TRACE_POINT(qtgraphs, QGraphs3DSurfaceCreateOffscreenSliceView_entry, int index, int requestedIndex, int sliceType);
Q_TRACE_POINT(qtgraphs, QGraphs3DSurfaceCreateOffscreenSliceView_exit);

Q_TRACE_POINT(qtgraphs, QGraphs3DSurfaceAddModel_entry, QSurface3DSeries *series);
Q_TRACE_POINT(qtgraphs, QGraphs3DSurfaceAddModel_exit);

Q_TRACE_POINT(qtgraphs, QGraphs3DSurfaceAddSliceModel_entry);
Q_TRACE_POINT(qtgraphs, QGraphs3DSurfaceAddSliceModel_exit);

Q_TRACE_POINT(qtgraphs, QGraphs3DSurfaceAddFillModel_entry);
Q_TRACE_POINT(qtgraphs, QGraphs3DSurfaceAddFillModel_exit);

/*!
 * \qmltype Surface3D
 * \inherits GraphsItem3D
 * \inqmlmodule QtGraphs
 * \ingroup graphs_qml_3D
 * \brief Describes the usage of the 3D surface graph.
 *
 * This type enables developers to render surface plots in 3D with Qt Quick.
 *
 * You will need to import the Qt Graphs module to use this type:
 *
 * \snippet doc_src_qmlgraphs.cpp 0
 *
 * After that you can use Surface3D in your qml files:
 *
 * \snippet doc_src_qmlgraphs.cpp 3
 *
 * See \l{Surface Graph Gallery} for more thorough usage example.
 *
 * \sa Surface3DSeries, ItemModelSurfaceDataProxy, Bars3D, Scatter3D,
 * {Qt Graphs C++ Classes for 3D}
 */

/*!
 * \qmlproperty Value3DAxis Surface3D::axisX
 * The active x-axis.
 *
 * If an axis is not given, a temporary default axis with no labels and an
 * automatically adjusting range is created.
 * This temporary axis is destroyed if another axis is explicitly set to the
 * same orientation.
 */

/*!
 * \qmlproperty Value3DAxis Surface3D::axisY
 * The active y-axis.
 *
 * If an axis is not given, a temporary default axis with no labels and an
 * automatically adjusting range is created.
 * This temporary axis is destroyed if another axis is explicitly set to the
 * same orientation.
 */

/*!
 * \qmlproperty Value3DAxis Surface3D::axisZ
 * The active z-axis.
 *
 * If an axis is not given, a temporary default axis with no labels and an
 * automatically adjusting range is created.
 * This temporary axis is destroyed if another axis is explicitly set to the
 * same orientation.
 */

/*!
 * \qmlproperty Surface3DSeries Surface3D::selectedSeries
 * \readonly
 *
 * The selected series or null. If \l {GraphsItem3D::selectionMode}{selectionMode}
 * has the \c MultiSeries flag set, this property holds the series
 * which owns the selected point.
 */

/*!
 * \qmlproperty list<Surface3DSeries> Surface3D::seriesList
 * \qmldefault
 * This property holds the series of the graph.
 * By default, this property contains an empty list.
 * To set the series, either use the addSeries() function or define them as
 * children of the graph.
 */

/*!
 * \qmlproperty bool Surface3D::flipHorizontalGrid
 *
 * In some use cases the horizontal axis grid is mostly covered by the surface,
 * so it can be more useful to display the horizontal axis grid on top of the
 * graph rather than on the bottom. A typical use case for this is showing 2D
 * spectrograms using orthoGraphic projection with a top-down viewpoint.
 *
 * If \c{false}, the horizontal axis grid and labels are drawn on the horizontal
 * background of the graph.
 * If \c{true}, the horizontal axis grid and labels are drawn on the opposite
 * side of the graph from the horizontal background.
 * Defaults to \c{false}.
 */

/*!
 * \qmlmethod void Surface3D::addSeries(Surface3DSeries series)
 * Adds the \a series to the graph.
 * \sa GraphsItem3D::hasSeries()
 */

/*!
 * \qmlmethod void Surface3D::removeSeries(Surface3DSeries series)
 * Removes the \a series from the graph.
 * \sa GraphsItem3D::hasSeries()
 */

/*!
 * \qmlmethod void Surface3D::removeSeries(Surface3DSeries series)
 * Removes the \a series from the graph.
 * \sa GraphsItem3D::hasSeries()
 */

/*!
 * \qmlmethod void Surface3D::renderSliceToImage(int index, int requestedIndex, QtGraphs3D::SliceCaptureType sliceType, QUrl filePath)
 * \since 6.10
 *
 * Exports a 2d slice from series at \a index and saves the result to an image
 * at a specified \a filePath.
 * To export all series, set \a index to -1.
 * The exported slice includes lines of row or column, which is defined by
 * \a sliceType at a given \a requestedIndex.
 */

/*!
 * \qmlsignal Surface3D::axisXChanged(ValueAxis3D axis)
 *
 * This signal is emitted when axisX changes to \a axis.
 */

/*!
 * \qmlsignal Surface3D::axisYChanged(ValueAxis3D axis)
 *
 * This signal is emitted when axisY changes to \a axis.
 */

/*!
 * \qmlsignal Surface3D::axisZChanged(ValueAxis3D axis)
 *
 * This signal is emitted when axisZ changes to \a axis.
 */

/*!
 * \qmlsignal Surface3D::selectedSeriesChanged(Surface3DSeries series)
 *
 * This signal is emitted when selectedSeries changes to \a series.
 */

/*!
 * \qmlsignal Surface3D::flipHorizontalGridChanged(bool flip)
 *
 * This signal is emitted when flipHorizontalGrid changes to \a flip.
 */

QQuickGraphsSurface::QQuickGraphsSurface(QQuickItem *parent)
    : QQuickGraphsItem(parent)
{
    m_graphType = QAbstract3DSeries::SeriesType::Surface;
    setAxisX(0);
    setAxisY(0);
    setAxisZ(0);
    setAcceptedMouseButtons(Qt::AllButtons);
    clearSelection();
}

QQuickGraphsSurface::~QQuickGraphsSurface()
{
    for (const auto &model : std::as_const(m_model))
        delete model;
    if (m_grabresult)
        delete m_grabresult;
}

void QQuickGraphsSurface::setAxisX(QValue3DAxis *axis)
{
    QQuickGraphsItem::setAxisX(axis);
}

QValue3DAxis *QQuickGraphsSurface::axisX() const
{
    return static_cast<QValue3DAxis *>(QQuickGraphsItem::axisX());
}

void QQuickGraphsSurface::setAxisY(QValue3DAxis *axis)
{
    QQuickGraphsItem::setAxisY(axis);
}

QValue3DAxis *QQuickGraphsSurface::axisY() const
{
    return static_cast<QValue3DAxis *>(QQuickGraphsItem::axisY());
}

void QQuickGraphsSurface::setAxisZ(QValue3DAxis *axis)
{
    QQuickGraphsItem::setAxisZ(axis);
}

QValue3DAxis *QQuickGraphsSurface::axisZ() const
{
    return static_cast<QValue3DAxis *>(QQuickGraphsItem::axisZ());
}

void QQuickGraphsSurface::handleShadingChanged()
{
    auto series = static_cast<QSurface3DSeries *>(sender());
    for (auto model : std::as_const(m_model)) {
        if (model->series == series) {
            updateModel(model);
            break;
        }
    }
}

void QQuickGraphsSurface::handleWireframeColorChanged()
{
    for (auto model : std::as_const(m_model)) {
        QQmlListReference gridMaterialRef(model->gridModel, "materials");
        auto gridMaterial = gridMaterialRef.at(0);
        QColor gridColor = model->series->wireframeColor();
        gridMaterial->setProperty("gridColor", gridColor);

        if (sliceView()) {
            QQmlListReference gridMaterialRef(model->sliceGridModel, "materials");
            auto gridMaterial = static_cast<QQuick3DPrincipledMaterial *>(gridMaterialRef.at(0));
            gridMaterial->setBaseColor(gridColor);
        }
    }
}

void QQuickGraphsSurface::handleMeshTypeChanged(QAbstract3DSeries::Mesh mesh)
{
    QSurface3DSeries *sender = qobject_cast<QSurface3DSeries *>(QObject::sender());
    changePointerMeshTypeForSeries(mesh, sender);
    if (sliceView())
        changeSlicePointerMeshTypeForSeries(mesh, sender);
}

void QQuickGraphsSurface::changePointerMeshTypeForSeries(QAbstract3DSeries::Mesh mesh,
                                                         QSurface3DSeries *series)
{
    changePointerForSeries(getMeshFileName(mesh, series), series);
}

void QQuickGraphsSurface::changeSlicePointerMeshTypeForSeries(QAbstract3DSeries::Mesh mesh,
                                                              QSurface3DSeries *series)
{
    changeSlicePointerForSeries(getMeshFileName(mesh, series), series);
}

void QQuickGraphsSurface::handleLightingModeChanged()
{
    auto series = static_cast<QSurface3DSeries *>(QObject::sender());
    for (auto model : std::as_const(m_model)) {
        if (model->series == series) {
            updateMaterial(model);
            break;
        }
    }
}

QString QQuickGraphsSurface::getMeshFileName(QAbstract3DSeries::Mesh mesh,
                                             QSurface3DSeries *series) const
{
    QString fileName = {};
    switch (mesh) {
    case QAbstract3DSeries::Mesh::Sphere:
        fileName = QStringLiteral("defaultMeshes/sphereMesh");
        break;
    case QAbstract3DSeries::Mesh::Bar:
    case QAbstract3DSeries::Mesh::Cube:
        fileName = QStringLiteral("defaultMeshes/barMesh");
        break;
    case QAbstract3DSeries::Mesh::Pyramid:
        fileName = QStringLiteral("defaultMeshes/pyramidMesh");
        break;
    case QAbstract3DSeries::Mesh::Cone:
        fileName = QStringLiteral("defaultMeshes/coneMesh");
        break;
    case QAbstract3DSeries::Mesh::Cylinder:
        fileName = QStringLiteral("defaultMeshes/cylinderMesh");
        break;
    case QAbstract3DSeries::Mesh::BevelBar:
    case QAbstract3DSeries::Mesh::BevelCube:
        fileName = QStringLiteral("defaultMeshes/bevelBarMesh");
        break;
    case QAbstract3DSeries::Mesh::UserDefined:
        fileName = series->userDefinedMesh();
        break;
    default:
        fileName = QStringLiteral("defaultMeshes/sphereMesh");
    }
    return fileName;
}

void QQuickGraphsSurface::handlePointerChanged(const QString &filename)
{
    QSurface3DSeries *sender = qobject_cast<QSurface3DSeries *>(QObject::sender());
    changePointerForSeries(filename, sender);
    changeSlicePointerForSeries(filename, sender);
}

void QQuickGraphsSurface::changePointerForSeries(const QString &filename, QSurface3DSeries *series)
{
    if (!filename.isEmpty()) {
        // Selection pointer
        QQuick3DNode *parent = graphNode();

        QQuick3DPrincipledMaterial *pointerMaterial = nullptr;
        QQuick3DModel *pointer = m_selectionPointers.value(series);

        if (pointer) {
            // Retrieve the already created material
            QQmlListReference materialRef(pointer, "materials");
            pointerMaterial = qobject_cast<QQuick3DPrincipledMaterial *>(materialRef.at(0));
            // Delete old model
            delete pointer;
        } else {
            // No pointer yet; create material for it
            pointerMaterial = new QQuick3DPrincipledMaterial();
            pointerMaterial->setParent(this);
            pointerMaterial->setBaseColor(theme()->singleHighlightColor());
        }
        // Create new pointer
        pointer = new QQuick3DModel();
        pointer->setParent(parent);
        pointer->setParentItem(parent);
        pointer->setSource(QUrl(filename));
        pointer->setScale(QVector3D(0.05f, 0.05f, 0.05f));
        // Insert to the map
        m_selectionPointers.insert(series, pointer);
        // Set material
        QQmlListReference materialRef(pointer, "materials");
        materialRef.append(pointerMaterial);
    }
}

void QQuickGraphsSurface::changeSlicePointerForSeries(const QString &filename,
                                                      QSurface3DSeries *series)
{
    if (!filename.isEmpty()) {
        // Slice selection pointer
        QQuick3DNode *sliceParent = sliceView()->scene();

        QQuick3DPrincipledMaterial *slicePointerMaterial = nullptr;
        QQuick3DModel *slicePointer = m_sliceSelectionPointers.value(series);

        if (slicePointer) {
            // Retrieve the already created material
            QQmlListReference sliceMaterialRef(slicePointer, "materials");
            slicePointerMaterial = qobject_cast<QQuick3DPrincipledMaterial *>(
                sliceMaterialRef.at(0));
            // Delete old model
            delete slicePointer;
        } else {
            // No pointer yet; create material for it
            slicePointerMaterial = new QQuick3DPrincipledMaterial();
            slicePointerMaterial->setParent(sliceParent);
            slicePointerMaterial->setBaseColor(theme()->singleHighlightColor());
        }
        // Create new pointer
        slicePointer = new QQuick3DModel();
        slicePointer->setParent(sliceParent);
        slicePointer->setParentItem(sliceParent);
        slicePointer->setSource(QUrl(filename));
        slicePointer->setScale(QVector3D(0.05f, 0.05f, 0.05f));
        // Insert to the map
        m_sliceSelectionPointers.insert(series, slicePointer);
        // Set material
        QQmlListReference sliceMaterialRef(slicePointer, "materials");
        sliceMaterialRef.append(slicePointerMaterial);
    }
}

void QQuickGraphsSurface::handleFlipHorizontalGridChanged(bool flip)
{
    updateLabels();
    emit flipHorizontalGridChanged(flip);
    setFlipHorizontalGridChanged(false);
}

void QQuickGraphsSurface::adjustAxisRanges()
{
    QValue3DAxis *valueAxisX = static_cast<QValue3DAxis *>(m_axisX);
    QValue3DAxis *valueAxisY = static_cast<QValue3DAxis *>(m_axisY);
    QValue3DAxis *valueAxisZ = static_cast<QValue3DAxis *>(m_axisZ);
    bool adjustX = (valueAxisX && valueAxisX->isAutoAdjustRange());
    bool adjustY = (valueAxisY && valueAxisY->isAutoAdjustRange());
    bool adjustZ = (valueAxisZ && valueAxisZ->isAutoAdjustRange());
    bool first = true;

    if (adjustX || adjustY || adjustZ) {
        float minValueX = 0.0f;
        float maxValueX = 0.0f;
        float minValueY = 0.0f;
        float maxValueY = 0.0f;
        float minValueZ = 0.0f;
        float maxValueZ = 0.0f;
        qsizetype seriesCount = m_seriesList.size();
        for (int series = 0; series < seriesCount; series++) {
            const QSurface3DSeries *surfaceSeries = static_cast<QSurface3DSeries *>(
                m_seriesList.at(series));
            const QSurfaceDataProxy *proxy = surfaceSeries->dataProxy();
            if (surfaceSeries->isVisible() && proxy) {
                QVector3D minLimits;
                QVector3D maxLimits;
                proxy->d_func()->limitValues(minLimits,
                                             maxLimits,
                                             valueAxisX,
                                             valueAxisY,
                                             valueAxisZ);
                if (adjustX) {
                    if (first) {
                        // First series initializes the values
                        minValueX = minLimits.x();
                        maxValueX = maxLimits.x();
                    } else {
                        minValueX = qMin(minValueX, minLimits.x());
                        maxValueX = qMax(maxValueX, maxLimits.x());
                    }
                }
                if (adjustY) {
                    if (first) {
                        // First series initializes the values
                        minValueY = minLimits.y();
                        maxValueY = maxLimits.y();
                    } else {
                        minValueY = qMin(minValueY, minLimits.y());
                        maxValueY = qMax(maxValueY, maxLimits.y());
                    }
                }
                if (adjustZ) {
                    if (first) {
                        // First series initializes the values
                        minValueZ = minLimits.z();
                        maxValueZ = maxLimits.z();
                    } else {
                        minValueZ = qMin(minValueZ, minLimits.z());
                        maxValueZ = qMax(maxValueZ, maxLimits.z());
                    }
                }
                first = false;
            }
        }

        static const float adjustmentRatio = 20.0f;
        static const float defaultAdjustment = 1.0f;

        if (adjustX) {
            // If all points at same coordinate, need to default to some valid range
            float adjustment = 0.0f;
            if (minValueX == maxValueX) {
                if (adjustZ) {
                    // X and Z are linked to have similar unit size, so choose the valid
                    // range based on it
                    if (minValueZ == maxValueZ)
                        adjustment = defaultAdjustment;
                    else
                        adjustment = qAbs(maxValueZ - minValueZ) / adjustmentRatio;
                } else {
                    if (valueAxisZ)
                        adjustment = qAbs(valueAxisZ->max() - valueAxisZ->min()) / adjustmentRatio;
                    else
                        adjustment = defaultAdjustment;
                }
            }
            valueAxisX->d_func()->setRange(minValueX - adjustment, maxValueX + adjustment, true);
        }
        if (adjustY) {
            // If all points at same coordinate, need to default to some valid range
            // Y-axis unit is not dependent on other axes, so simply adjust +-1.0f
            float adjustment = 0.0f;
            if (minValueY == maxValueY)
                adjustment = defaultAdjustment;
            valueAxisY->d_func()->setRange(minValueY - adjustment, maxValueY + adjustment, true);
        }
        if (adjustZ) {
            // If all points at same coordinate, need to default to some valid range
            float adjustment = 0.0f;
            if (minValueZ == maxValueZ) {
                if (adjustX) {
                    // X and Z are linked to have similar unit size, so choose the valid
                    // range based on it
                    if (minValueX == maxValueX)
                        adjustment = defaultAdjustment;
                    else
                        adjustment = qAbs(maxValueX - minValueX) / adjustmentRatio;
                } else {
                    if (valueAxisX)
                        adjustment = qAbs(valueAxisX->max() - valueAxisX->min()) / adjustmentRatio;
                    else
                        adjustment = defaultAdjustment;
                }
            }
            valueAxisZ->d_func()->setRange(minValueZ - adjustment, maxValueZ + adjustment, true);
        }
    }
}

void QQuickGraphsSurface::handleArrayReset()
{
    QSurface3DSeries *series;
    if (qobject_cast<QSurfaceDataProxy *>(sender()))
        series = static_cast<QSurfaceDataProxy *>(sender())->series();
    else
        series = static_cast<QSurface3DSeries *>(sender());

    if (series->isVisible()) {
        adjustAxisRanges();
        setDataDirty(true);
    }
    if (!m_changedSeriesList.contains(series))
        m_changedSeriesList.append(series);

    // Clear selection unless still valid
    if (m_selectedPoint != invalidSelectionPosition())
        setSelectedPoint(m_selectedPoint, m_selectedSeries, false);
    series->d_func()->markItemLabelDirty();
    emitNeedRender();
}

void QQuickGraphsSurface::handleFlatShadingSupportedChange(bool supported)
{
    // Handle renderer flat surface support indicator signal. This happens exactly
    // once per renderer.
    if (m_flatShadingSupported != supported) {
        m_flatShadingSupported = supported;
        // Emit the change for all added surfaces
        for (QAbstract3DSeries *series : std::as_const(m_seriesList)) {
            QSurface3DSeries *surfaceSeries = static_cast<QSurface3DSeries *>(series);
            emit surfaceSeries->flatShadingSupportedChanged(m_flatShadingSupported);
        }
    }
}

void QQuickGraphsSurface::handleRowsChanged(qsizetype startIndex, qsizetype count)
{
    QSurface3DSeries *series = static_cast<QSurfaceDataProxy *>(QObject::sender())->series();
    qsizetype oldChangeCount = m_changedRows.size();
    if (!oldChangeCount)
        m_changedRows.reserve(count);

    int selectedRow = m_selectedPoint.x();
    for (qsizetype i = 0; i < count; i++) {
        bool newItem = true;
        qsizetype candidate = startIndex + i;
        for (qsizetype j = 0; j < oldChangeCount; j++) {
            const ChangeRow &oldChangeItem = m_changedRows.at(j);
            if (oldChangeItem.row == candidate && series == oldChangeItem.series) {
                newItem = false;
                break;
            }
        }
        if (newItem) {
            ChangeRow newChangeItem = {series, candidate};
            m_changedRows.append(newChangeItem);
            if (series == m_selectedSeries && selectedRow == candidate)
                series->d_func()->markItemLabelDirty();
        }
    }
    if (count) {
        m_changeTracker.rowsChanged = true;
        setDataDirty(true);

        if (series->isVisible())
            adjustAxisRanges();
        emitNeedRender();
    }
}

void QQuickGraphsSurface::handleItemChanged(qsizetype rowIndex, qsizetype columnIndex)
{
    QSurfaceDataProxy *sender = static_cast<QSurfaceDataProxy *>(QObject::sender());
    QSurface3DSeries *series = sender->series();

    bool newItem = true;
    QPoint candidate((int(rowIndex)), (int(columnIndex)));
    for (ChangeItem item : std::as_const(m_changedItems)) {
        if (item.point == candidate && item.series == series) {
            newItem = false;
            break;
        }
    }
    if (newItem) {
        ChangeItem newItem = {series, candidate};
        m_changedItems.append(newItem);
        m_changeTracker.itemChanged = true;
        setDataDirty(true);

        if (series == m_selectedSeries && m_selectedPoint == candidate)
            series->d_func()->markItemLabelDirty();

        if (series->isVisible())
            adjustAxisRanges();
        emitNeedRender();
    }
}

void QQuickGraphsSurface::handleRowsAdded(qsizetype startIndex, qsizetype count)
{
    Q_UNUSED(startIndex);
    Q_UNUSED(count);
    QSurface3DSeries *series = static_cast<QSurfaceDataProxy *>(sender())->series();
    if (series->isVisible()) {
        adjustAxisRanges();
        setDataDirty(true);
    }
    if (!m_changedSeriesList.contains(series))
        m_changedSeriesList.append(series);
    emitNeedRender();
}

void QQuickGraphsSurface::handleRowsInserted(qsizetype startIndex, qsizetype count)
{
    Q_UNUSED(startIndex);
    Q_UNUSED(count);
    QSurface3DSeries *series = static_cast<QSurfaceDataProxy *>(sender())->series();
    if (series == m_selectedSeries) {
        // If rows inserted to selected series before the selection, adjust the selection
        int selectedRow = m_selectedPoint.x();
        if (startIndex <= selectedRow) {
            selectedRow += count;
            setSelectedPoint(QPoint(selectedRow, m_selectedPoint.y()), m_selectedSeries, false);
        }
    }

    if (series->isVisible()) {
        adjustAxisRanges();
        setDataDirty(true);
    }
    if (!m_changedSeriesList.contains(series))
        m_changedSeriesList.append(series);

    emitNeedRender();
}

void QQuickGraphsSurface::handleRowsRemoved(qsizetype startIndex, qsizetype count)
{
    Q_UNUSED(startIndex);
    Q_UNUSED(count);
    QSurface3DSeries *series = static_cast<QSurfaceDataProxy *>(sender())->series();
    if (series == m_selectedSeries) {
        // If rows removed from selected series before the selection, adjust the selection
        int selectedRow = m_selectedPoint.x();
        if (startIndex <= selectedRow) {
            if ((startIndex + count) > selectedRow)
                selectedRow = -1; // Selected row removed
            else
                selectedRow -= count; // Move selected row down by amount of rows removed

            setSelectedPoint(QPoint(selectedRow, m_selectedPoint.y()), m_selectedSeries, false);
        }
    }

    if (series->isVisible()) {
        adjustAxisRanges();
        setDataDirty(true);
    }
    if (!m_changedSeriesList.contains(series))
        m_changedSeriesList.append(series);

    emitNeedRender();
}

QPoint QQuickGraphsSurface::invalidSelectionPosition()
{
    static QPoint invalidSelectionPoint(-1, -1);
    return invalidSelectionPoint;
}

void QQuickGraphsSurface::setSelectedPoint(const QPoint position,
                                           QSurface3DSeries *series,
                                           bool enterSlice)
{
    // If the selection targets non-existent point, clear selection instead.
    QPoint pos = position;
    m_selectionDirty = true;

    // Series may already have been removed, so check it before setting the selection.
    if (!m_seriesList.contains(series))
        series = 0;

    const QSurfaceDataProxy *proxy = 0;
    if (series)
        proxy = series->dataProxy();

    if (!proxy)
        pos = invalidSelectionPosition();

    if (pos != invalidSelectionPosition()) {
        qsizetype maxRow = proxy->rowCount() - 1;
        qsizetype maxCol = proxy->columnCount() - 1;

        if (pos.y() < 0 || pos.y() > maxRow || pos.x() < 0 || pos.x() > maxCol)
            pos = invalidSelectionPosition();
    }

    if (selectionMode().testFlag(QtGraphs3D::SelectionFlag::Slice)) {
        bool previousSliceActive = isSlicingActive();
        if (pos == invalidSelectionPosition() || !series->isVisible()) {
            scene()->setSlicingActive(false);
        } else {
            // If the selected point is outside data window, or there is no selected
            // point, disable slicing
            float axisMinX = m_axisX->min();
            float axisMaxX = m_axisX->max();
            float axisMinZ = m_axisZ->min();
            float axisMaxZ = m_axisZ->max();

            QSurfaceDataItem item = series->dataArray().at(pos.y()).at(pos.x());
            if (item.x() < axisMinX || item.x() > axisMaxX || item.z() < axisMinZ
                || item.z() > axisMaxZ) {
                scene()->setSlicingActive(false);
            } else if (enterSlice) {
                scene()->setSlicingActive(true);
            }

        }
        if (previousSliceActive != isSlicingActive())
            setSliceActivatedChanged(true);

        emitNeedRender();
    }

    if (pos != m_selectedPoint || series != m_selectedSeries) {
        bool seriesChanged = (series != m_selectedSeries);
        m_selectedPoint = pos;
        m_selectedSeries = series;
        m_changeTracker.selectedPointChanged = true;

        // Clear selection from other series and finally set new selection to the
        // specified series
        for (QAbstract3DSeries *otherSeries : std::as_const(m_seriesList)) {
            QSurface3DSeries *surfaceSeries = static_cast<QSurface3DSeries *>(otherSeries);
            if (surfaceSeries != m_selectedSeries)
                surfaceSeries->d_func()->setSelectedPoint(invalidSelectionPosition());
        }
        if (m_selectedSeries)
            m_selectedSeries->d_func()->setSelectedPoint(m_selectedPoint);

        if (seriesChanged)
            emit selectedSeriesChanged(m_selectedSeries);

        auto hasSeries = [series](SurfaceModel *model){ return model->series == series; };
        auto hasVertex = [pos](SurfaceVertex vertex) {
            return vertex.coord == pos;
        };

        auto it = std::find_if(m_model.constBegin(), m_model.constEnd(), hasSeries);
        if (it != m_model.constEnd()) {
            SurfaceModel *model = *it;
            auto vIt = std::find_if(model->vertices.constBegin(), model->vertices.constEnd(), hasVertex);
            if (vIt != model->vertices.constEnd()) {
                model->selectedVertex = *vIt;
                model->picked = true;
            }
        }
        emitNeedRender();
    }
}

void QQuickGraphsSurface::setSelectionMode(QtGraphs3D::SelectionFlags mode)
{
    // Currently surface only supports row and column modes when also slicing
    if ((mode.testFlag(QtGraphs3D::SelectionFlag::Row)
         || mode.testFlag(QtGraphs3D::SelectionFlag::Column))
        && !mode.testFlag(QtGraphs3D::SelectionFlag::Slice)) {
        qCWarning(lcProperties3D, "%s unsupported selection mode",
                  qUtf8Printable(QLatin1String(__FUNCTION__)));
        return;
    } else if (mode.testFlag(QtGraphs3D::SelectionFlag::Slice)
               && (mode.testFlag(QtGraphs3D::SelectionFlag::Row)
                   == mode.testFlag(QtGraphs3D::SelectionFlag::Column))) {
        qCWarning(lcProperties3D, "%s must specify one of either row or column selection mode"
                 "in conjunction with slicing mode.", qUtf8Printable(QLatin1String(__FUNCTION__)));
    } else {
        QtGraphs3D::SelectionFlags oldMode = selectionMode();

        QQuickGraphsItem::setSelectionMode(mode);

        if (mode != oldMode) {
            // Refresh selection upon mode change to ensure slicing is correctly
            // updated according to series the visibility.
            setSelectedPoint(m_selectedPoint, m_selectedSeries, true);

            // Special case: Always deactivate slicing when changing away from slice
            // automanagement, as this can't be handled in setSelectedBar.
            if (!mode.testFlag(QtGraphs3D::SelectionFlag::Slice)
                && oldMode.testFlag(QtGraphs3D::SelectionFlag::Slice)) {
                scene()->setSlicingActive(false);
            }
        }
    }
}

void QQuickGraphsSurface::handleAxisAutoAdjustRangeChangedInOrientation(
    QAbstract3DAxis::AxisOrientation orientation, bool autoAdjust)
{
    Q_UNUSED(orientation);
    Q_UNUSED(autoAdjust);

    adjustAxisRanges();
}

void QQuickGraphsSurface::handleAxisRangeChangedBySender(QObject *sender)
{
    QQuickGraphsItem::handleAxisRangeChangedBySender(sender);

    // Update selected point - may be moved offscreen
    setSelectedPoint(m_selectedPoint, m_selectedSeries, false);
}

void QQuickGraphsSurface::handleSeriesVisibilityChangedBySender(QObject *sender)
{
    QQuickGraphsItem::handleSeriesVisibilityChangedBySender(sender);

    setSeriesVisibilityDirty(true);
    // Visibility changes may require disabling slicing,
    // so just reset selection to ensure everything is still valid.
    setSelectedPoint(m_selectedPoint, m_selectedSeries, false);
}

void QQuickGraphsSurface::handleItemLabelVisibleChangedBySender(bool visible, QObject *sender)
{
    auto series = static_cast<QSurface3DSeries *>(sender);

    if (series == m_selectedSeries) {
        itemLabel()->setVisible(visible);
        if (auto label = sliceItemLabel(); label && isSlicingActive())
            label->setVisible(visible);
    }
}

void QQuickGraphsSurface::setFlipHorizontalGrid(bool flip)
{
    if (m_flipHorizontalGrid != flip) {
        m_flipHorizontalGrid = flip;
        m_changeTracker.flipHorizontalGridChanged = true;
        emit flipHorizontalGridChanged(flip);
        emitNeedRender();
    }
}

bool QQuickGraphsSurface::flipHorizontalGrid() const
{
    return m_flipHorizontalGrid;
}

bool QQuickGraphsSurface::isFlatShadingSupported()
{
    return m_flatShadingSupported;
}

QList<QSurface3DSeries *> QQuickGraphsSurface::surfaceSeriesList()
{
    QList<QSurface3DSeries *> surfaceSeriesList;
    for (QAbstract3DSeries *abstractSeries : std::as_const(m_seriesList)) {
        QSurface3DSeries *surfaceSeries = qobject_cast<QSurface3DSeries *>(abstractSeries);
        if (surfaceSeries)
            surfaceSeriesList.append(surfaceSeries);
    }

    return surfaceSeriesList;
}

void QQuickGraphsSurface::updateSurfaceTexture(QSurface3DSeries *series)
{
    m_changeTracker.surfaceTextureChanged = true;

    if (!m_changedTextures.contains(series))
        m_changedTextures.append(series);

    emitNeedRender();
}

QQmlListProperty<QSurface3DSeries> QQuickGraphsSurface::seriesList()
{
    return QQmlListProperty<QSurface3DSeries>(this,
                                              this,
                                              &QQuickGraphsSurface::appendSeriesFunc,
                                              &QQuickGraphsSurface::countSeriesFunc,
                                              &QQuickGraphsSurface::atSeriesFunc,
                                              &QQuickGraphsSurface::clearSeriesFunc);
}

void QQuickGraphsSurface::appendSeriesFunc(QQmlListProperty<QSurface3DSeries> *list,
                                           QSurface3DSeries *series)
{
    reinterpret_cast<QQuickGraphsSurface *>(list->data)->addSeries(series);
}

qsizetype QQuickGraphsSurface::countSeriesFunc(QQmlListProperty<QSurface3DSeries> *list)
{
    return reinterpret_cast<QQuickGraphsSurface *>(list->data)->surfaceSeriesList().size();
}

QSurface3DSeries *QQuickGraphsSurface::atSeriesFunc(QQmlListProperty<QSurface3DSeries> *list,
                                                    qsizetype index)
{
    return reinterpret_cast<QQuickGraphsSurface *>(list->data)->surfaceSeriesList().at(index);
}

void QQuickGraphsSurface::clearSeriesFunc(QQmlListProperty<QSurface3DSeries> *list)
{
    QQuickGraphsSurface *declSurface = reinterpret_cast<QQuickGraphsSurface *>(list->data);
    QList<QSurface3DSeries *> realList = declSurface->surfaceSeriesList();
    qsizetype count = realList.size();
    for (qsizetype i = 0; i < count; i++)
        declSurface->removeSeries(realList.at(i));
}

void QQuickGraphsSurface::addSeries(QSurface3DSeries *series)
{
    Q_ASSERT(series && series->type() == QAbstract3DSeries::SeriesType::Surface);

    QQuickGraphsItem::addSeriesInternal(series);

    QSurface3DSeries *surfaceSeries = static_cast<QSurface3DSeries *>(series);
    if (surfaceSeries->selectedPoint() != invalidSelectionPosition())
        setSelectedPoint(surfaceSeries->selectedPoint(), surfaceSeries, false);

    if (!surfaceSeries->texture().isNull())
        updateSurfaceTexture(surfaceSeries);

    if (isReady())
        addModel(series);
}

void QQuickGraphsSurface::removeSeries(QSurface3DSeries *series)
{
    bool wasVisible = (series && series->d_func()->m_graph == this && series->isVisible());

    QQuickGraphsItem::removeSeriesInternal(series);

    if (m_selectedSeries == series)
        setSelectedPoint(invalidSelectionPosition(), 0, false);

    if (wasVisible)
        adjustAxisRanges();

    series->setParent(this); // Reparent as removing will leave series parentless
    for (int i = 0; i < m_model.size();) {
        if (m_model[i]->series == series) {
            m_model[i]->model->deleteLater();
            m_model[i]->gridModel->deleteLater();
            m_model[i]->fillModel->deleteLater();
            m_model[i]->heights.clear();
            m_fillDirty.remove(m_model.at(i));
            if (sliceView()) {
                m_model[i]->sliceModel->deleteLater();
                m_model[i]->sliceGridModel->deleteLater();
            }
            delete m_model[i];
            m_model.removeAt(i);
        } else {
            ++i;
        }
    }
}

void QQuickGraphsSurface::clearSelection()
{
    setSelectedPoint(invalidSelectionPosition(), 0, false);
    for (auto model : std::as_const(m_model))
        model->picked = false;
}

void QQuickGraphsSurface::handleAxisXChanged(QAbstract3DAxis *axis)
{
    emit axisXChanged(static_cast<QValue3DAxis *>(axis));
}

void QQuickGraphsSurface::handleAxisYChanged(QAbstract3DAxis *axis)
{
    emit axisYChanged(static_cast<QValue3DAxis *>(axis));
}

void QQuickGraphsSurface::handleAxisZChanged(QAbstract3DAxis *axis)
{
    emit axisZChanged(static_cast<QValue3DAxis *>(axis));
}

void QQuickGraphsSurface::componentComplete()
{
    QQuickGraphsItem::componentComplete();

    auto serieslist = surfaceSeriesList();
    for (auto series : std::as_const(serieslist)) {
        addModel(series);
        changePointerMeshTypeForSeries(series->mesh(), series);
    }

    graphsInputHandler()->setGraphsItem(this);
    qCDebug(lcGraphs3D, " QQuickGraphsSurface::componentComplete");
}

void QQuickGraphsSurface::synchData()
{
    qCDebug(lcGraphs3D, "%s start syncing", qUtf8Printable(QLatin1String(__FUNCTION__)));

    if (isFlipHorizontalGridChanged()) {
        setHorizontalFlipFactor(flipHorizontalGrid() ? -1 : 1);
        handleFlipHorizontalGridChanged(flipHorizontalGrid());
    }

    QQuickGraphsItem::synchData();

    if (isSelectedPointChanged()) {
        if (selectionMode().testFlag(QtGraphs3D::SelectionFlag::Item))
            updateSelectedPoint();
        setSelectedPointChanged(false);
    }

    if (isSurfaceTextureChanged()) {
        if (!isChangedTexturesEmpty()) {
            for (auto model : std::as_const(m_model)) {
                if (hasSeriesToChangeTexture(model->series))
                    updateMaterial(model);
            }
        }
        setSurfaceTextureChanged(false);
    }

    if (gridLineType() == QtGraphs3D::GridLineType::Shader) {
        if (!m_topGrid) {
            //add horizontal top grid
            QUrl topGridUrl = QUrl(QStringLiteral(":/defaultMeshes/barMeshFull"));
            m_topGrid = new QQuick3DModel();
            m_topGridScale = new QQuick3DNode();
            m_topGridRotation = new QQuick3DNode();

            m_topGridScale->setParent(graphNode());
            m_topGridScale->setParentItem(graphNode());

            m_topGridRotation->setParent(m_topGridScale);
            m_topGridRotation->setParentItem(m_topGridScale);

            m_topGrid->setObjectName("Top Grid");
            m_topGrid->setParent(m_topGridRotation);
            m_topGrid->setParentItem(m_topGridRotation);

            m_topGrid->setSource(topGridUrl);
            m_topGrid->setPickable(false);
        }
        auto min = qMin(scaleWithBackground().x() + backgroundScaleMargin().x(),
                        scaleWithBackground().z() + backgroundScaleMargin().z());
        m_topGridScale->setScale(QVector3D(scaleWithBackground().x() + backgroundScaleMargin().x(),
                                           min * gridOffset(),
                                           scaleWithBackground().z() + backgroundScaleMargin().z()));
        m_topGridScale->setPosition(
            QVector3D(0.0f, scaleWithBackground().y() + backgroundScaleMargin().y(), 0.0f));

        m_topGrid->setVisible(m_flipHorizontalGrid);
        QQmlListReference materialsRefF(m_topGrid, "materials");
        QQmlListReference bbRef(background(), "materials");
        QQuick3DCustomMaterial *bgMatFloor;
        if (!materialsRefF.size() && bbRef.size()) {
            bgMatFloor = static_cast<QQuick3DCustomMaterial *>(bbRef.at(0));
            materialsRefF.append(bgMatFloor);
            bgMatFloor->setProperty("gridOnTop", m_flipHorizontalGrid);
        } else if (materialsRefF.size()) {
            bgMatFloor = static_cast<QQuick3DCustomMaterial *>(materialsRefF.at(0));
            bgMatFloor->setProperty("gridOnTop", m_flipHorizontalGrid);
        }
    }

    qCDebug(lcGraphs3D, "%s end syncing", qUtf8Printable(QLatin1String(__FUNCTION__)));
}

void QQuickGraphsSurface::updateGraph()
{
    for (auto model : std::as_const(m_model)) {
        bool seriesVisible = model->series->isVisible();
        if (isSeriesVisibilityDirty()) {
            if (!seriesVisible) {
                model->model->setVisible(seriesVisible);
                model->gridModel->setVisible(seriesVisible);
                if (sliceView()) {
                    model->sliceModel->setVisible(seriesVisible);
                    model->sliceGridModel->setVisible(seriesVisible);

                    if (m_selectedSeries == model->series) {
                        clearSelection();
                        setSliceActivatedChanged(true);
                        m_selectionDirty = !seriesVisible;
                    }
                }
                continue;
            }
        }

        if (model->model->visible() != seriesVisible)
            model->model->setVisible(seriesVisible);

        model->gridModel->setVisible(
            model->series->drawMode().testFlag(QSurface3DSeries::DrawWireframe) && seriesVisible);
        if (model->series->drawMode().testFlag(QSurface3DSeries::DrawSurface))
            model->model->setLocalOpacity(1.f);
        else
            model->model->setLocalOpacity(.0f);

        if (sliceView() && sliceView()->isVisible()) {
            model->sliceGridModel->setVisible(
                model->series->drawMode().testFlag(QSurface3DSeries::DrawWireframe));
            if (model->series->drawMode().testFlag(QSurface3DSeries::DrawSurface))
                model->sliceModel->setLocalOpacity(1.f);
            else
                model->sliceModel->setLocalOpacity(.0f);
        }
        updateMaterial(model);
    }

    setSeriesVisibilityDirty(false);
    if (isDataDirty() || isSeriesVisualsDirty()) {
        if (hasChangedSeriesList()) {
            handleChangedSeries();
        } else {
            for (auto model : std::as_const(m_model)) {
                bool visible = model->series->isVisible();
                if (visible)
                    updateModel(model);
            }
        }

        if (isSliceEnabled()) {
            if (!sliceView())
                createSliceView();

            if (sliceView()->isVisible()) {
                if (!m_selectedSeries) {
                    m_selectionDirty = true;
                    setSliceActivatedChanged(true);
                }
                toggleSliceGraph();
            }
        }

        setDataDirty(false);
        setSeriesVisualsDirty(false);
    }

    if (selectionMode().testFlag(QtGraphs3D::SelectionFlag::Item))
        updateSelectedPoint();
}

void QQuickGraphsSurface::calculateSceneScalingFactors()
{
    float scaleX, scaleY, scaleZ;
    float marginH, marginV;

    if (margin() < 0.0f) {
        marginH = .1f;
        marginV = .1f;
    } else {
        marginH = margin();
        marginV = margin();
    }

    if (isPolar()) {
        float polarMargin = calculatePolarBackgroundMargin();
        marginH = qMax(marginH, polarMargin);
    }
    float hAspectRatio;
    if (isPolar())
        hAspectRatio = 1.0f;
    else
        hAspectRatio = horizontalAspectRatio();

    QSizeF areaSize;
    if (qFuzzyIsNull(hAspectRatio)) {
        areaSize.setHeight(axisZ()->max() - axisZ()->min());
        areaSize.setWidth(axisX()->max() - axisX()->min());
    } else {
        areaSize.setHeight(1.0);
        areaSize.setWidth(hAspectRatio);
    }

    float horizontalMaxDimension;
    if (aspectRatio() > 2.0f) {
        horizontalMaxDimension = 2.0f;
        scaleY = 2.0f / aspectRatio();
    } else {
        horizontalMaxDimension = aspectRatio();
        scaleY = 1.0f;
    }

    if (isPolar())
        m_polarRadius = horizontalMaxDimension;

    float scaleFactor = qMax(areaSize.width(), areaSize.height());
    scaleX = horizontalMaxDimension * areaSize.width() / scaleFactor;
    scaleZ = horizontalMaxDimension * areaSize.height() / scaleFactor;

    setScale(QVector3D(scaleX, scaleY, scaleZ));
    setScaleWithBackground(QVector3D(scaleX, scaleY, scaleZ));
    setBackgroundScaleMargin(QVector3D(marginH, marginV, marginH));
}

void QQuickGraphsSurface::handleChangedSeries()
{
    auto changedSeries = changedSeriesList();
    for (auto series : std::as_const(changedSeries)) {
        for (auto model : std::as_const(m_model)) {
            if (model->series == series) {
                updateModel(model);
            }
        }
    }
}

inline static float getDataValue(const QSurfaceDataArray &array, bool searchRow, qsizetype index)
{
    if (searchRow)
        return array.at(0).at(index).x();
    else
        return array.at(index).at(0).z();
}

inline static int binarySearchArray(const QSurfaceDataArray &array,
                                    qsizetype maxIndex,
                                    float limitValue,
                                    bool searchRow,
                                    bool lowBound,
                                    bool ascending)
{
    qsizetype min = 0;
    qsizetype max = maxIndex;
    qsizetype mid = 0;
    qsizetype retVal;

    while (max >= min) {
        mid = (min + max) / 2;
        float arrayValue = getDataValue(array, searchRow, mid);
        if (arrayValue == limitValue)
            return int(mid);
        if (ascending) {
            if (arrayValue < limitValue)
                min = mid + 1;
            else
                max = mid - 1;
        } else {
            if (arrayValue > limitValue)
                min = mid + 1;
            else
                max = mid - 1;
        }
    }

    if (lowBound == ascending) {
        if (mid > max)
            retVal = mid;
        else
            retVal = min;
    } else {
        if (mid > max)
            retVal = max;
        else
            retVal = mid;
    }

    if (retVal < 0 || retVal > maxIndex) {
        retVal = -1;
    } else if (lowBound) {
        if (getDataValue(array, searchRow, retVal) < limitValue)
            retVal = -1;
    } else {
        if (getDataValue(array, searchRow, retVal) > limitValue)
            retVal = -1;
    }
    return int(retVal);
}

QRect QQuickGraphsSurface::calculateSampleSpace(SurfaceModel *model)
{
    QRect sampleSpace;
    const QSurfaceDataArray &array = model->series->dataArray();
    if (array.size() > 0) {
        if (array.size() >= 1 && array.at(0).size() >= 1) {
            const qsizetype maxRow = array.size() - 1;
            const qsizetype maxColumn = array.at(0).size() - 1;

            const bool ascendingX = array.at(0).at(0).x() < array.at(0).at(maxColumn).x();
            const bool ascendingZ = array.at(0).at(0).z() < array.at(maxRow).at(0).z();

            // Check if Z is filled before X. If it is, or there's something else that is fishy,
            // print out a warning about incorrectly formed data.
            bool incorrectDataFormat = false;
            qreal val = array.at(0).at(0).z();
            qreal step = array.at(1).at(0).z() - array.at(0).at(0).z();
            if (maxRow > 1) {
                if ((val + step * maxRow == array.at(maxRow).at(0).z() && !ascendingZ)
                    || (val - step * maxRow == array.at(maxRow).at(0).z() && ascendingZ)) {
                    incorrectDataFormat = true;
                }
            }
            val = array.at(0).at(0).x();
            step = array.at(0).at(1).x() - array.at(0).at(0).x();
            if (maxColumn > 1) {
                if ((val + step * maxColumn == array.at(0).at(maxColumn).x() && !ascendingX)
                    || (val - step * maxColumn == array.at(0).at(maxColumn).x() && ascendingX)) {
                    incorrectDataFormat = true;
                }
            }

            if (incorrectDataFormat) {
                qCWarning(lcProperties3D,
                          "Data might be in an incorrect format. If the graph looks wrong or "
                          "is displayed only partially, verify that rows are filled first, "
                          "and columns after.");
            }

            if (model->ascendingX != ascendingX) {
                setIndexDirty(true);
                model->ascendingX = ascendingX;
            }
            if (model->ascendingZ != ascendingZ) {
                setIndexDirty(true);
                model->ascendingZ = ascendingZ;
            }

            int idx = binarySearchArray(array, maxColumn, axisX()->min(), true, true, ascendingX);
            if (idx != -1) {
                if (ascendingX)
                    sampleSpace.setLeft(idx);
                else
                    sampleSpace.setRight(idx);
            } else {
                sampleSpace.setWidth(-1);
                return sampleSpace;
            }

            idx = binarySearchArray(array, maxColumn, axisX()->max(), true, false, ascendingX);
            if (idx != -1) {
                if (ascendingX)
                    sampleSpace.setRight(idx);
                else
                    sampleSpace.setLeft(idx);
            } else {
                sampleSpace.setWidth(-1); // to indicate nothing needs to be shown
                return sampleSpace;
            }

            idx = binarySearchArray(array, maxRow, axisZ()->min(), false, true, ascendingZ);
            if (idx != -1) {
                if (ascendingZ)
                    sampleSpace.setTop(idx);
                else
                    sampleSpace.setBottom(idx);
            } else {
                sampleSpace.setWidth(-1); // to indicate nothing needs to be shown
                return sampleSpace;
            }

            idx = binarySearchArray(array, maxRow, axisZ()->max(), false, false, ascendingZ);
            if (idx != -1) {
                if (ascendingZ)
                    sampleSpace.setBottom(idx);
                else
                    sampleSpace.setTop(idx);
            } else {
                sampleSpace.setWidth(-1); // to indicate nothing needs to be shown
                return sampleSpace;
            }
        }
    }
    return sampleSpace;
}

void QQuickGraphsSurface::updateModel(SurfaceModel *model)
{
    const QSurfaceDataArray &array = model->series->dataArray();

    if (!array.isEmpty()) {
        Q_TRACE(QGraphs3DSurfaceModelUpdate_entry, static_cast<void *>(model));
        qsizetype rowCount = array.size();
        qsizetype columnCount = array.at(0).size();

        const qsizetype maxSize = 4096; // maximum texture size
        columnCount = qMin(maxSize, columnCount);
        rowCount = qMin(maxSize, rowCount);

        bool lineData = (rowCount == 1 || columnCount == 1);

        if (model->rowCount != rowCount) {
            model->rowCount = rowCount;
            setIndexDirty(true);
        }
        if (model->columnCount != columnCount) {
            model->columnCount = columnCount;
            setIndexDirty(true);
        }

        bool dimensionsChanged = false;
        QRect sampleSpace = calculateSampleSpace(model);
        if (sampleSpace != model->sampleSpace) {
            dimensionsChanged = true;
            model->sampleSpace = sampleSpace;
        }
        int rowStart = sampleSpace.top();
        int columnStart = sampleSpace.left();
        int rowLimit = sampleSpace.bottom() + 1;
        int columnLimit = sampleSpace.right() + 1;

        QPoint selC = model->selectedVertex.coord;
        selC.setX(qMin(selC.x(), int(columnCount) - 1));
        selC.setY(qMin(selC.y(), int(rowCount) - 1));
        QVector3D selP = array.at(selC.y()).at(selC.x()).position();

        bool pickOutOfRange = false;
        if (selP.x() < axisX()->min() || selP.x() > axisX()->max() || selP.z() < axisZ()->min()
            || selP.z() > axisZ()->max()) {
            pickOutOfRange = true;
        }

        if (m_isIndexDirty || pickOutOfRange) {
            model->selectedVertex = SurfaceVertex();
            if (sliceView() && sliceView()->isVisible() && model->series == m_selectedSeries) {
                setSlicingActive(false);
                setSliceActivatedChanged(true);
                m_selectionDirty = true;
            }
        }
        qsizetype totalSize = rowCount * columnCount * 2;
        float uvX = 1.0f / float(columnCount - 1);
        float uvY = 1.0f / float(rowCount - 1);

        bool flatShading = model->series->shading() == QSurface3DSeries::Shading::Flat;

        QVector3D boundsMin = QVector3D();
        QVector3D boundsMax = QVector3D();

        model->heights.clear();
        model->heights.reserve(totalSize);

        QQmlListReference materialRef(model->model, "materials");
        auto material = materialRef.at(0);
        QVariant heightInputAsVariant = material->property("height");
        QQuick3DShaderUtilsTextureInput *heightInput
            = heightInputAsVariant.value<QQuick3DShaderUtilsTextureInput *>();
        QQuick3DTexture *heightMap = heightInput->texture();
        QQuick3DTextureData *heightMapData = nullptr;
        if (!heightMap) {
            heightMap = new QQuick3DTexture();
            heightMap->setParent(this);
            heightMap->setHorizontalTiling(QQuick3DTexture::ClampToEdge);
            heightMap->setVerticalTiling(QQuick3DTexture::ClampToEdge);
            heightMap->setMinFilter(QQuick3DTexture::Nearest);
            heightMap->setMagFilter(QQuick3DTexture::Nearest);
            heightMapData = new QQuick3DTextureData();
            heightMapData->setSize(QSize(sampleSpace.width(), sampleSpace.height()));
            heightMapData->setFormat(QQuick3DTextureData::RGBA32F);
            heightMapData->setParent(heightMap);
            heightMapData->setParentItem(heightMap);
        } else {
            heightMapData = heightMap->textureData();
            if (dimensionsChanged)
                heightMapData->setSize(QSize(sampleSpace.width(), sampleSpace.height()));
        }

        if (heightMapData->size().width() < 1 || heightMapData->size().height() < 1) {
            heightMapData->setTextureData(QByteArray());
            heightMap->setTextureData(heightMapData);
            heightInput->setTexture(heightMap);
            model->heightTexture = heightMap;
            Q_TRACE(QGraphs3DSurfaceModelUpdate_exit);
            return;
        }

        material->setProperty("xDiff", 1.0f / float(sampleSpace.width() - 1));
        material->setProperty("yDiff", 1.0f / float(sampleSpace.height() - 1));
        material->setProperty("flatShading", flatShading);
        material->setProperty("graphHeight", scaleWithBackground().y());
        material->setProperty("uvOffset", QVector2D(columnStart, rowStart));
        material->setProperty("size", QVector2D(sampleSpace.width(), sampleSpace.height()));
        material->setProperty("vertCount", QVector2D(columnCount, rowCount));
        material->setProperty("flipU", !model->ascendingX);
        material->setProperty("flipV", !model->ascendingZ);
        material->setProperty("fill", model->series->drawMode().testFlag(QSurface3DSeries::DrawFilledSurface));
        material->setProperty("lineData", lineData);
        for (int i = 0; i < m_seriesList.size(); i++) {
            if (m_seriesList.at(i) == model->series)
                material->setProperty("order", i);
        }

        model->vertices.clear();
        model->vertices.reserve(totalSize);

        for (int i = rowStart; i < rowLimit; i++) {
            const QSurfaceDataRow &row = array.at(i);
            for (int j = columnStart; j < columnLimit; j++) {
                QVector3D pos = getNormalizedVertex(row.at(j), isPolar(), false);

                float alpha = 1.0f;
                for (int axis = 0; axis < 3; axis++) {
                    if (qIsNaN(pos[axis])) {
                        pos[axis] = 0;
                        alpha = .0f;
                    }
                }
                model->heights.push_back(QVector4D(pos, alpha));
                SurfaceVertex vertex;
                vertex.position = pos;
                vertex.uv = QVector2D(j * uvX, i * uvY);
                vertex.coord = QPoint(j, i);
                model->vertices.push_back(vertex);
                if (!qIsNaN(pos.y()) && !qIsInf(pos.y())) {
                    if (boundsMin.isNull()) {
                        boundsMin = pos;
                    } else {
                        boundsMin = QVector3D(qMin(boundsMin.x(), pos.x()),
                                              qMin(boundsMin.y(), pos.y()),
                                              qMin(boundsMin.z(), pos.z()));
                    }
                }
                if (boundsMax.isNull()) {
                    boundsMax = pos;
                } else {
                    boundsMax = QVector3D(qMax(boundsMax.x(), pos.x()),
                                          qMax(boundsMax.y(), pos.y()),
                                          qMax(boundsMax.z(), pos.z()));
                }
            }
        }

        if (model->boundsMin != boundsMin || model->boundsMax != boundsMax) {
            model->boundsMin = boundsMin;
            model->boundsMax = boundsMax;
            float range = boundsMax.y() - boundsMin.y();
            material->setProperty("gradientMin", -(boundsMin.y() / range));
            material->setProperty("gradientHeight", 1.0f / range);
        }
        QByteArray heightData = QByteArray(reinterpret_cast<char *>(model->heights.data()),
                                           model->heights.size() * sizeof(QVector4D));
        heightMapData->setTextureData(heightData);
        heightMap->setTextureData(heightMapData);
        heightInput->setTexture(heightMap);
        model->heightTexture = heightMap;

        if (m_isIndexDirty) {
            QVector<SurfaceVertex> vertices;
            for (int i = 0; i < rowCount; i++) {
                QSurfaceDataRow row = array.at(i);
                for (int j = 0; j < columnCount; j++) {
                    SurfaceVertex vertex;
                    QVector3D pos = getNormalizedVertex(row.at(j), isPolar(), false);

                    for (int axis = 0; axis < 3; axis++) {
                        if (qIsNaN(pos[axis]))
                            pos[axis] = 0;
                    }
                    vertex.position = pos;
                    float uStep = model->ascendingX ? j * uvX : 1 - (j * uvX);
                    float vStep = model->ascendingZ ? i * uvY : 1 - (i * uvY);

                    vertex.uv = QVector2D(uStep, vStep);
                    vertex.coord = QPoint(j, i);
                    vertices.push_back(vertex);
                }
            }
            if (lineData) {
                createLineIndices(model, rowCount);
                model->model->geometry()->setPrimitiveType(QQuick3DGeometry::PrimitiveType::LineStrip);

            } else {
                createIndices(model, columnCount, rowCount);
                model->model->geometry()->setPrimitiveType(QQuick3DGeometry::PrimitiveType::Triangles);
            }

            auto geometry = model->model->geometry();
            QByteArray vertexBuffer(reinterpret_cast<char *>(vertices.data()),
                                    vertices.size() * sizeof(SurfaceVertex));
            geometry->setVertexData(vertexBuffer);
            QByteArray indexBuffer(reinterpret_cast<char *>(model->indices.data()),
                                   model->indices.size() * sizeof(quint32));
            geometry->setIndexData(indexBuffer);
            geometry->setBounds(boundsMin, boundsMax);
            geometry->update();

            createGridlineIndices(model, 0, 0, columnCount, rowCount);
            auto gridGeometry = model->gridModel->geometry();
            gridGeometry->setVertexData(vertexBuffer);
            QByteArray gridIndexBuffer(reinterpret_cast<char *>(model->gridIndices.data()),
                                       model->gridIndices.size() * sizeof(quint32));
            gridGeometry->setIndexData(gridIndexBuffer);
            gridGeometry->setBounds(boundsMin, boundsMax);
            gridGeometry->update();
            m_fillDirty[model] = true;
            m_isIndexDirty = false;
        }
        QQmlListReference gridMaterialRef(model->gridModel, "materials");
        auto gridMaterial = gridMaterialRef.at(0);
        QVariant gridHeightInputAsVariant = gridMaterial->property("height");
        QQuick3DShaderUtilsTextureInput *gridHeightInput
            = gridHeightInputAsVariant.value<QQuick3DShaderUtilsTextureInput *>();
        gridHeightInput->setTexture(heightMap);
        QColor gridColor = model->series->wireframeColor();
        gridMaterial->setProperty("gridColor", gridColor);
        gridMaterial->setProperty("range", QVector2D(sampleSpace.width(), sampleSpace.height()));
        gridMaterial->setProperty("vertices", QVector2D(columnCount, rowCount));
        gridMaterial->setProperty("graphHeight", scaleWithBackground().y());
        gridMaterial->setProperty("fill", model->series->drawMode().testFlag(QSurface3DSeries::DrawFilledSurface));

        qCDebug(lcGraphs3D) << "surface info"
            << "\n vertices:" << model->vertices.count()
            << "\n indices:" << model->indices.count()
            << "\n dataArray dimensions:" << QVector2D(model->series->dataArray().size(), model->series->dataArray().at(0).size())
            << "\n plane size:" << QVector2D(sampleSpace.width(), sampleSpace.height())
            << "\n vertCount:" << QVector2D(columnCount, rowCount);


        if (lineData)
            updateLineFill(model);
        else
            updateFill(model);
        Q_TRACE(QGraphs3DSurfaceModelUpdate_exit);
    }
    updateMaterial(model);
    updateSelectedPoint();
}

void QQuickGraphsSurface::updateFill(SurfaceModel *model)
{
    bool fillVisible = model->series->drawMode().testFlag(QSurface3DSeries::DrawFilledSurface);
    if (m_fillDirty[model] && fillVisible) {
        QVector<QVector<SurfaceVertex>> sideVertsList(4);
        qsizetype rowCount = model->rowCount;
        qsizetype colCount = model->columnCount;

        float uvX = 1.0f / float(colCount - 1);
        float uvY = 1.0f / float(rowCount - 1);
        for (int i = 0; i < rowCount; i++) {
            for (int j = 0; j < colCount; j++) {
                SurfaceVertex vertex;
                QVector3D pos = QVector3D(0, 0, 0);
                vertex.position = pos;

                vertex.uv = QVector2D(j * uvX, i * uvY);
                vertex.coord = QPoint(j, i);
                float base = -scaleWithBackground().y();

                auto addVerts = [base](SurfaceVertex vertex,
                                       QVector<SurfaceVertex> &sideVerts,
                                       QVector2D uvOffset,
                                       bool rev = false) {
                    // add small offset to uv to distinguish them in the shaders
                    vertex.uv += uvOffset * 0.1f;
                    SurfaceVertex botVert = vertex;
                    botVert.position.setY(base);
                    botVert.uv += uvOffset;
                    if (rev) {
                        sideVerts.push_front(botVert);
                        sideVerts.push_front(vertex);
                    } else {
                        sideVerts.push_back(vertex);
                        sideVerts.push_back(botVert);
                    }
                };

                if (i == 0) { //Bottom row
                    QVector<SurfaceVertex> &sideVerts = sideVertsList[0];
                    addVerts(vertex, sideVerts, QVector2D(0.0f, -0.1f));
                } else if (j == colCount - 1) {
                    QVector<SurfaceVertex> &sideVerts = sideVertsList[1];
                    addVerts(vertex, sideVerts, QVector2D(0.1f, 0.0f));
                } else if (i == rowCount - 1) {
                    QVector<SurfaceVertex> &sideVerts = sideVertsList[2];
                    addVerts(vertex, sideVerts, QVector2D(0.0f, 0.1f), true);
                } else if (j == 0) {
                    QVector<SurfaceVertex> &sideVerts = sideVertsList[3];
                    addVerts(vertex, sideVerts, QVector2D(-0.1f, 0.0f), true);
                }
            }
        }

        QVector<SurfaceVertex> sideVerts;
        for (const auto &side : std::as_const(sideVertsList)) {
            for (auto vert : side)
                sideVerts.append(vert);
        }
        auto sideGeom = model->fillModel->geometry();
        QByteArray fillVertBuffer(reinterpret_cast<char *>(sideVerts.data()),
                                  sideVerts.size() * sizeof(SurfaceVertex));
        sideGeom->setVertexData(fillVertBuffer);
        sideGeom->setBounds(model->boundsMin, model->boundsMax);
        sideGeom->update();

        QVector<quint32> indices;
        int totVerts = (rowCount * 2 + colCount * 2) - 4;
        int idxCount = 6 * (totVerts);
        indices.reserve(idxCount);

        //side 1
        for (int i = 0; i < totVerts - 1; i++) {
            quint32 start = i * 2;
            indices.push_back(start + 2);
            indices.push_back(start);
            indices.push_back(start + 1);

            indices.push_back(start + 2);
            indices.push_back(start + 1);
            indices.push_back(start + 3);
        }

        //Connecting triangles
        quint32 start = (totVerts - 1) * 2;
        indices.push_back(0);
        indices.push_back(start);
        indices.push_back(start + 1);

        indices.push_back(0);
        indices.push_back(start + 1);
        indices.push_back(1);

        QByteArray indexBuffer(reinterpret_cast<char *>(indices.data()),
                               indices.size() * sizeof(quint32));

        auto geometry = model->fillModel->geometry();
        geometry->setIndexData(indexBuffer);

        m_fillDirty[model] = false;
    }

    bool surfaceVisible = model->series->drawMode().testFlag(QSurface3DSeries::DrawSurface)
                          && model->series->isVisible();
    model->fillModel->setVisible(
        model->series->drawMode().testFlag(QSurface3DSeries::DrawFilledSurface) && surfaceVisible);
}

void QQuickGraphsSurface::updateLineFill(SurfaceModel *model)
{
    bool fillVisible = model->series->drawMode().testFlag(QSurface3DSeries::DrawFilledSurface);
    if (m_fillDirty[model] && fillVisible) {
        qsizetype rowCount = model->rowCount;
        qsizetype colCount = model->columnCount;

        bool oneRow = rowCount == 1;

        float uvX = 1.0f / qMax(float(colCount - 1), 1.0f);
        float uvY = 1.0f / qMax(float(rowCount - 1), 1.0f);
        QVector<SurfaceVertex> vertices;
        for (int i = 0; i < rowCount; i++) {
            for (int j = 0; j < colCount; j++) {
                SurfaceVertex vertex;
                QVector3D pos = QVector3D(0, 0, 0);
                vertex.position = pos;

                vertex.uv = QVector2D(j * uvX, i * uvY);
                vertex.coord = QPoint(j, i);
                float base = -scaleWithBackground().y();

                QVector2D uvOffset;
                if (oneRow)
                     uvOffset = QVector2D(0.0f, -0.1f);
                else
                     uvOffset = QVector2D(-0.1f, 0.0f);
                vertex.uv += uvOffset * 0.1f;

                SurfaceVertex botVert = vertex;
                botVert.position.setY(base);
                botVert.uv += uvOffset;

                vertices.push_back(vertex);
                vertices.push_back(botVert);

            }
        }
        QVector<quint32> indices;

        int totVerts = oneRow? colCount : rowCount;
        int idxCount = 6 * (totVerts);
        indices.reserve(idxCount);

        for (int i = 0; i < totVerts - 1; i++) {
            quint32 start = i * 2 ;
            indices.push_back(start + 2);
            indices.push_back(start);
            indices.push_back(start + 1);

            indices.push_back(start + 2);
            indices.push_back(start + 1);
            indices.push_back(start + 3);
        }

        QByteArray indexBuffer(reinterpret_cast<char *>(indices.data()),
                               indices.size() * sizeof(quint32));

        auto sideGeom = model->fillModel->geometry();
        QByteArray fillVertBuffer(reinterpret_cast<char *>(vertices.data()),
                                  vertices.size() * sizeof(SurfaceVertex));
        sideGeom->setVertexData(fillVertBuffer);
        sideGeom->setBounds(model->boundsMin, model->boundsMax);
        sideGeom->setIndexData(indexBuffer);
        sideGeom->update();
        m_fillDirty[model] = false;

    }

    bool surfaceVisible = model->series->drawMode().testFlag(QSurface3DSeries::DrawSurface)
        && model->series->isVisible();
    model->fillModel->setVisible(
        model->series->drawMode().testFlag(QSurface3DSeries::DrawFilledSurface) && surfaceVisible);
}

void QQuickGraphsSurface::updateMaterial(SurfaceModel *model)
{
    QQmlListReference materialRef(model->model, "materials");

    QQuick3DCustomMaterial *material = qobject_cast<QQuick3DCustomMaterial *>(materialRef.at(0));

    if (!material) {
        material = createQmlCustomMaterial(QStringLiteral(":/materials/SurfaceMaterial"));
        model->customMaterial = material;
    }

    bool textured = !(model->series->texture().isNull() && model->series->textureFile().isEmpty());
    material->setProperty("textured", textured);

    if (isSeriesVisualsDirty()) {
        switch (model->series->colorStyle()) {
        case (QGraphsTheme::ColorStyle::ObjectGradient):
            material->setProperty("colorStyle", 0);
            break;
        case (QGraphsTheme::ColorStyle::RangeGradient):
            material->setProperty("colorStyle", 1);
            break;
        case (QGraphsTheme::ColorStyle::Uniform):
            material->setProperty("colorStyle", 2);
            material->setProperty("uniformColor", model->series->baseColor());
            break;
        }

        bool flatShading = model->series->shading() == QSurface3DSeries::Shading::Flat;

        QVariant textureInputAsVariant = material->property("custex");
        QQuick3DShaderUtilsTextureInput *textureInput
            = textureInputAsVariant.value<QQuick3DShaderUtilsTextureInput *>();
        auto textureData = static_cast<QQuickGraphsTextureData *>(model->gradientTexture->textureData());
        textureData->createGradient(model->series->baseGradient());
        textureInput->setTexture(model->gradientTexture);

        QVariant heightInputAsVariant = material->property("height");
        QQuick3DShaderUtilsTextureInput *heightInput
            = heightInputAsVariant.value<QQuick3DShaderUtilsTextureInput *>();
        heightInput->setTexture(model->heightTexture);
        material->setParent(model->model);
        material->setParentItem(model->model);
        material->setCullMode(QQuick3DMaterial::NoCulling);
        material->setProperty("flatShading", flatShading);
        material->setProperty("shaded",
                              model->series->lightingMode()
                                  == QAbstract3DSeries::LightingMode::Shaded);

        // Not textured, clear transparency flag
        if (!textured)
            model->texture->textureData()->setHasTransparency(false);
    }

    if (textured) {
        QQuick3DShaderUtilsTextureInput *texInput = material->property("baseColor")
                                                        .value<QQuick3DShaderUtilsTextureInput *>();
        if (!texInput->texture()) {
            QQuick3DTexture *texture = new QQuick3DTexture();
            texture->setParent(material);
            texture->setParentItem(material);
            texInput->setTexture(texture);
        }
        if (!texInput->texture()->hasSourceData() || isSeriesVisualsDirty()) {
            if (!model->series->textureFile().isEmpty()) {
                texInput->texture()->setSource(QUrl::fromLocalFile(model->series->textureFile()));
                // Check for transparency
                model->texture->textureData()->setHasTransparency(
                    Utils::imageHasTransparency(model->series->texture()));
            } else if (!model->series->texture().isNull()) {
                QImage image = model->series->texture();
                // Check for transparency
                bool hasTransparency = Utils::imageHasTransparency(image);
                image.convertTo(QImage::Format_RGBA32FPx4);
                auto textureData = static_cast<QQuickGraphsTextureData *>(
                    model->texture->textureData());
                textureData->setHasTransparency(hasTransparency);
                textureData->setFormat(QQuick3DTextureData::RGBA32F);
                textureData->setSize(image.size());
                textureData->setTextureData(
                    QByteArray(reinterpret_cast<const char *>(image.bits()), image.sizeInBytes()));
                texInput->texture()->setTextureData(textureData);
                texInput->texture()->setVerticalTiling(QQuick3DTexture::ClampToEdge);
                texInput->texture()->setHorizontalTiling(QQuick3DTexture::ClampToEdge);
            } else {
                texInput->texture()->setSource(QUrl());
            }
        }
    }
    material->setProperty("rootScale", rootNode()->scale().y() * scaleWithBackground().y());

    bool colorHasTransparency = false;
    if (model->series->colorStyle() == QGraphsTheme::ColorStyle::Uniform)
        colorHasTransparency = model->series->baseColor().alphaF() < 1.0;
    else
        colorHasTransparency = model->gradientTexture->textureData()->hasTransparency();

    bool textureHasTransparency = model->texture->textureData()->hasTransparency();

    material->setProperty("hasTransparency", colorHasTransparency || textureHasTransparency);
    material->update();
}

QVector3D QQuickGraphsSurface::getNormalizedVertex(const QSurfaceDataItem &data,
                                                   bool polar,
                                                   bool flipXZ)
{
    Q_UNUSED(flipXZ);

    QValue3DAxis *axisXValue = static_cast<QValue3DAxis *>(axisX());
    QValue3DAxis *axisYValue = static_cast<QValue3DAxis *>(axisY());
    QValue3DAxis *axisZValue = static_cast<QValue3DAxis *>(axisZ());

    bool xReversed = axisXValue->reversed();
    bool yReversed = axisYValue->reversed();
    bool zReversed = axisZValue->reversed();

    float normalizedX = xReversed ? 1.f - axisXValue->positionAt(data.x())
                                  : axisXValue->positionAt(data.x());
    float normalizedY;
    float normalizedZ = zReversed ? 1.f - axisZValue->positionAt(data.z())
                                  : axisZValue->positionAt(data.z());

    // TODO : Need to handle, flipXZ

    float scale, translate;
    if (polar) {
        float angle = normalizedX * M_PI * 2.0f;
        float radius = normalizedZ * this->scaleWithBackground().z();
        normalizedX = radius * qSin(angle) * 1.0f;
        normalizedZ = -(radius * qCos(angle)) * 1.0f;
    } else {
        scale = translate = this->scaleWithBackground().x();
        normalizedX = normalizedX * scale * 2.0f - translate;
        scale = translate = this->scaleWithBackground().z();
        normalizedZ = normalizedZ * -scale * 2.0f + translate;
    }
    scale = translate = this->scale().y();
    float yval = (yReversed ? axisYValue->max() - data.y() : data.y());
    normalizedY = axisYValue->positionAt(yval) * scale * 2.0f - translate;
    return QVector3D(normalizedX, normalizedY, normalizedZ);
}

void QQuickGraphsSurface::toggleSliceGraph()
{
    if (m_selectionDirty)
        QQuickGraphsItem::toggleSliceGraph();

    setSelectedPointChanged(true);

    if (!sliceView()->isVisible())
        return;

    QPointF worldCoord;
    for (auto model : std::as_const(m_model)) {
        if (model->picked) {
            QPoint coords = model->selectedVertex.coord;
            worldCoord = mapCoordsToWorldSpace(model, coords);
        }
    }

    for (auto model : std::as_const(m_model)) {
        bool visible = model->series->isVisible();

        model->sliceModel->setVisible(visible);
        model->sliceGridModel->setVisible(visible);

        if (!selectionMode().testFlag(QtGraphs3D::SelectionFlag::MultiSeries) && !model->picked) {
            model->sliceModel->setVisible(false);
            model->sliceGridModel->setVisible(false);
            continue;
        } else {
            model->sliceGridModel->setVisible(
                model->series->drawMode().testFlag(QSurface3DSeries::DrawWireframe));
            if (model->series->drawMode().testFlag(QSurface3DSeries::DrawSurface))
                model->sliceModel->setLocalOpacity(1.f);
            else
                model->sliceModel->setLocalOpacity(.0f);
        }

        QVector<SurfaceVertex> selectedSeries;

        QRect sampleSpace = model->sampleSpace;
        int rowStart = sampleSpace.top();
        int columnStart = sampleSpace.left();
        int rowEnd = sampleSpace.bottom() + 1;
        int columnEnd = sampleSpace.right() + 1;
        int rowCount = sampleSpace.height();
        int columnCount = sampleSpace.width();

        QPoint coord;
        if (model->picked)
            coord = model->selectedVertex.coord;
        else
            coord = mapCoordsToSampleSpace(model, worldCoord);

        int indexCount = 0;
        const QSurfaceDataArray &array = model->series->dataArray();
        const qsizetype maxRow = array.size() - 1;
        const qsizetype maxColumn = array.at(0).size() - 1;
        const bool ascendingX = array.at(0).at(0).x() < array.at(0).at(maxColumn).x();
        const bool ascendingZ = array.at(0).at(0).z() < array.at(maxRow).at(0).z();
        if (selectionMode().testFlag(QtGraphs3D::SelectionFlag::Row) && coord.y() != -1) {
            selectedSeries.reserve(columnCount * 2);
            QVector<SurfaceVertex> list;
            QSurfaceDataRow row = array.at(coord.y());
            for (int i = columnStart; i < columnEnd; i++) {
                int index = ascendingX ? i : columnEnd - i + columnStart - 1;
                QVector3D pos = getNormalizedVertex(row.at(index), false, false);
                SurfaceVertex vertex;
                vertex.position = pos;
                vertex.position.setY(vertex.position.y() - .025f);
                vertex.position.setZ(.0f);
                selectedSeries.append(vertex);
                vertex.position.setY(vertex.position.y() + .05f);
                list.append(vertex);
            }
            selectedSeries.append(list);
            indexCount = columnCount - 1;
        }

        if (selectionMode().testFlag(QtGraphs3D::SelectionFlag::Column) && coord.x() != -1) {
            selectedSeries.reserve(rowCount * 2);
            QVector<SurfaceVertex> list;
            for (int i = rowStart; i < rowEnd; i++) {
                int index = ascendingZ ? i : rowEnd - i + rowStart - 1;
                QVector3D pos = getNormalizedVertex(array.at(index).at(coord.x()), false, false);
                SurfaceVertex vertex;
                vertex.position = pos;
                vertex.position.setX(-vertex.position.z());
                vertex.position.setY(vertex.position.y() - .025f);
                vertex.position.setZ(0);
                selectedSeries.append(vertex);
                vertex.position.setY(vertex.position.y() + .05f);
                list.append(vertex);
            }
            selectedSeries.append(list);
            indexCount = rowCount - 1;

            QQmlListReference materialRef(model->sliceModel, "materials");
            auto material = materialRef.at(0);
            material->setProperty("isColumn", true);
        }

        QVector<quint32> indices;
        indices.reserve(indexCount * 6);
        for (int i = 0; i < indexCount; i++) {
            indices.push_back(i + 1);
            indices.push_back(i + indexCount + 1);
            indices.push_back(i);
            indices.push_back(i + indexCount + 2);
            indices.push_back(i + indexCount + 1);
            indices.push_back(i + 1);
        }

        auto geometry = model->sliceModel->geometry();
        QByteArray vertexBuffer(reinterpret_cast<char *>(selectedSeries.data()),
                                selectedSeries.size() * sizeof(SurfaceVertex));
        geometry->setVertexData(vertexBuffer);
        QByteArray indexBuffer(reinterpret_cast<char *>(indices.data()),
                               indices.size() * sizeof(quint32));
        geometry->setIndexData(indexBuffer);
        geometry->update();

        geometry = model->sliceGridModel->geometry();
        geometry->setVertexData(vertexBuffer);

        QVector<quint32> gridIndices;
        gridIndices.reserve(indexCount * 4);
        for (int i = 0; i < indexCount; i++) {
            gridIndices.push_back(i);
            gridIndices.push_back(i + indexCount + 1);

            gridIndices.push_back(i);
            gridIndices.push_back(i + 1);
        }
        QByteArray gridIndexBuffer(reinterpret_cast<char *>(gridIndices.data()),
                                   gridIndices.size() * sizeof(quint32));
        geometry->setIndexData(gridIndexBuffer);
        geometry->update();

        QQmlListReference gridMaterialRef(model->sliceGridModel, "materials");
        auto gridMaterial = static_cast<QQuick3DPrincipledMaterial *>(gridMaterialRef.at(0));
        QColor gridColor = model->series->wireframeColor();
        gridMaterial->setBaseColor(gridColor);

        updateSelectedPoint();
    }
}

QPointF QQuickGraphsSurface::mapCoordsToWorldSpace(SurfaceModel *model, QPointF coords)
{
    const QSurfaceDataArray &array = model->series->dataArray();
    QSurfaceDataItem item = array.at(coords.y()).at(coords.x());
    return QPointF(item.x(), item.z());
}

QPoint QQuickGraphsSurface::mapCoordsToSampleSpace(SurfaceModel *model, QPointF coords)
{
    const QSurfaceDataArray &array = model->series->dataArray();
    qsizetype maxRow = array.size() - 1;
    qsizetype maxCol = array.at(0).size() - 1;
    const bool ascendingX = array.at(0).at(0).x() < array.at(0).at(maxCol).x();
    const bool ascendingZ = array.at(0).at(0).z() < array.at(maxRow).at(0).z();
    qsizetype botX = ascendingX ? 0 : maxCol;
    qsizetype botZ = ascendingZ ? 0 : maxRow;
    qsizetype topX = ascendingX ? maxCol : 0;
    qsizetype topZ = ascendingZ ? maxRow : 0;

    QPoint point(-1, -1);

    QSurfaceDataItem bottomLeft = array.at(botZ).at(botX);
    QSurfaceDataItem topRight = array.at(topZ).at(topX);

    QPointF pointBL(bottomLeft.x(), bottomLeft.z());
    QPointF pointTR(topRight.x(), topRight.z());

    QPointF pointF = coords - pointBL;
    QPointF span = pointTR - pointBL;
    QPointF step = QPointF(span.x() / float(maxCol), span.y() / float(maxRow));
    QPoint sample = QPoint((pointF.x() + (step.x() / 2.0)) / step.x(),
                           (pointF.y() + (step.y() / 2.0)) / step.y());

    if (bottomLeft.x() <= coords.x() && topRight.x() >= coords.x())
        point.setX(ascendingX ? sample.x() : int(maxCol) - sample.x());

    if (bottomLeft.z() <= coords.y() && topRight.z() >= coords.y())
        point.setY(ascendingZ ? sample.y() : int(maxRow) - sample.y());
    return point;
}

void QQuickGraphsSurface::createIndices(SurfaceModel *model, qsizetype columnCount, qsizetype rowCount)
{
    qsizetype endX = columnCount - 1;
    qsizetype endY = rowCount - 1;

    qsizetype indexCount = 6 * endX * endY;
    QVector<quint32> *indices = &model->indices;

    indices->clear();
    indices->reserve(indexCount);

    qsizetype rowEnd = endY * columnCount;
    for (qsizetype row = 0; row < rowEnd; row += columnCount) {
        for (qsizetype j = 0; j < endX; j++) {
            indices->push_back(int(row + j + 1));
            indices->push_back(int(row + columnCount + j));
            indices->push_back(int(row + j));

            indices->push_back(int(row + columnCount + j + 1));
            indices->push_back(int(row + columnCount + j));
            indices->push_back(int(row + j + 1));
        }
    }
}

void QQuickGraphsSurface::createLineIndices(SurfaceModel *model, qsizetype pointCount)
{
    model->indices.clear();
    model->gridIndices.reserve(pointCount);
    for (int i = 0; i < pointCount; i++)
        model->indices.push_back(i);
}

void QQuickGraphsSurface::createGridlineIndices(SurfaceModel *model, qsizetype x, qsizetype y, qsizetype endX, qsizetype endY)
{
    qsizetype columnCount = model->columnCount;
    qsizetype rowCount = model->rowCount;

    if (endX >= columnCount)
        endX = columnCount - 1;
    if (endY >= rowCount)
        endY = rowCount - 1;
    if (x > endX)
        x = endX - 1;
    if (y > endY)
        y = endY - 1;

    qsizetype nColumns = endX - x + 1;
    qsizetype nRows = endY - y + 1;

    qsizetype gridIndexCount = 2 * nColumns * (nRows - 1) + 2 * nRows * (nColumns - 1);
    model->gridIndices.clear();
    model->gridIndices.reserve(gridIndexCount);

    for (qsizetype i = y, row = columnCount * y; i <= endY; i++, row += columnCount) {
        for (qsizetype j = x; j < endX; j++) {
            model->gridIndices.push_back(int(row + j));
            model->gridIndices.push_back(int(row + j + 1));
        }
    }
    for (qsizetype i = y, row = columnCount * y; i < endY; i++, row += columnCount) {
        for (qsizetype j = x; j <= endX; j++) {
            model->gridIndices.push_back(int(row + j));
            model->gridIndices.push_back(int(row + j + columnCount));
        }
    }
}

bool QQuickGraphsSurface::doPicking(QPointF position)
{
    if (!QQuickGraphsItem::doPicking(position))
        return false;
    Q_TRACE(QGraphs3DSurfaceDoPicking_entry, position.x(), position.y());

    SurfaceModel *pickedModel = nullptr;
    const QVector3D rayOrigin = rootNode()->mapPositionFromScene(mapTo3DScene(QVector3D(position.x(), position.y(), 0)));
    const QVector3D rayEnd = rootNode()->mapPositionFromScene(mapTo3DScene(QVector3D(position.x(), position.y(), 1)));
    const QVector3D rayDir = (rayEnd - rayOrigin).normalized();
    QVector3D pickedPos = pickSurfaces(rayOrigin, rayDir, pickedModel);

    if (!selectionMode().testFlag(QtGraphs3D::SelectionFlag::None)) {
        if (!sliceView() && selectionMode().testFlag(QtGraphs3D::SelectionFlag::Slice))
            createSliceView();

        bool inBounds = qAbs(pickedPos.y()) < scaleWithBackground().y();
        if (!pickedPos.isNull() && inBounds) {

            bool inRange = qAbs(pickedPos.x()) < scaleWithBackground().x()
                           && qAbs(pickedPos.z()) < scaleWithBackground().z();

            if (!pickedPos.isNull() && inRange) {
                for (auto model : std::as_const(m_model)) {
                    if (!model->series->isVisible()) {
                        model->picked = false;
                        continue;
                    }

                    model->picked = (model == pickedModel);

                    SurfaceVertex selectedVertex;
                    qreal min = std::numeric_limits<qreal>::max();

                    for (auto vertex : std::as_const(model->vertices)) {
                        QVector3D pos = vertex.position;
                        float dist = pickedPos.distanceToPoint(pos);
                        if (dist < min) {
                            min = dist;
                            selectedVertex = vertex;
                        }
                    }
                    model->selectedVertex = selectedVertex;
                    if (!selectedVertex.position.isNull() && model->picked) {
                        setSelectedPoint(selectedVertex.coord, model->series, true);
                        qCDebug(lcInput3D) << "pick results:"
                            << "\n instance position:" << selectedVertex.position
                            << "\n picked vertices coords:" << selectedVertex.coord
                            << "\n picked vertices values:" << model->series->dataProxy()->itemAt(selectedVertex.coord).position();
                    }
                }
            }
        } else {
            clearSelection();
            for (auto model : std::as_const(m_model))
                model->picked = false;
        }
    }
    Q_TRACE(QGraphs3DSurfaceDoPicking_exit);
    return true;
}

bool QQuickGraphsSurface::doRayPicking(QVector3D origin, QVector3D direction)
{
    if (!QQuickGraphsItem::doRayPicking(origin, direction))
        return false;

    Q_TRACE(QGraphs3DSurfaceDoRayPicking_entry, origin.x(), origin.y(), origin.z(), direction.x(),
            direction.y(), direction.z());

    SurfaceModel *pickedModel = nullptr;
    QVector3D pickedPos = pickSurfaces(origin, direction, pickedModel);

    if (!selectionMode().testFlag(QtGraphs3D::SelectionFlag::None)) {
        if (!sliceView() && selectionMode().testFlag(QtGraphs3D::SelectionFlag::Slice))
            createSliceView();

        bool inBounds = qAbs(pickedPos.y()) < scaleWithBackground().y();
        if (!pickedPos.isNull() && inBounds) {

            bool inRange = qAbs(pickedPos.x()) < scaleWithBackground().x()
                           && qAbs(pickedPos.z()) < scaleWithBackground().z();

            if (!pickedPos.isNull() && inRange) {
                for (auto model : std::as_const(m_model)) {
                    if (!model->series->isVisible()) {
                        model->picked = false;
                        continue;
                    }
                    model->picked = (model == pickedModel);

                    SurfaceVertex selectedVertex;
                    qreal min = std::numeric_limits<qreal>::max();

                    for (auto vertex : std::as_const(model->vertices)) {
                        QVector3D pos = vertex.position;
                        float dist = pickedPos.distanceToPoint(pos);
                        if (dist < min) {
                            min = dist;
                            selectedVertex = vertex;
                        }
                    }
                    model->selectedVertex = selectedVertex;
                    if (!selectedVertex.position.isNull() && model->picked) {
                        setSelectedPoint(selectedVertex.coord, model->series, true);
                        qCDebug(lcInput3D) << "pick results:"
                            << "\n instance position:" << selectedVertex.position
                            << "\n picked vertices coords:" << selectedVertex.coord
                            << "\n picked vertices values:" << model->series->dataProxy()->itemAt(selectedVertex.coord).position();
                    }
                }
            }
        } else {
            clearSelection();
            for (auto model : std::as_const(m_model))
                model->picked = false;
        }
    }
    Q_TRACE(QGraphs3DSurfaceDoRayPicking_exit);
    return true;
}

QVector3D QQuickGraphsSurface::pickSurfaces(QVector3D rayOrigin,
                                            QVector3D rayDir,
                                            SurfaceModel *&pickedModel)
{
    qreal closestDistance = std::numeric_limits<qreal>::max();
    qreal closestLineDistance = std::numeric_limits<qreal>::max();

    QVector3D pickedPosition;

    QVector3D pickedLinePosition;
    SurfaceModel *pickedLineModel = nullptr;

    for (auto model : m_model) {
        qsizetype columnCount = model->sampleSpace.width();
        qsizetype rowCount = model->sampleSpace.height();
        bool lineData = (rowCount == 1 || columnCount == 1);

        if (!model->series->isVisible())
            continue;

        if (lineData) {
            QVector3D linePos;
            for (auto vertex : model->vertices) {
                QVector3D v = vertex.position - rayOrigin;
                qreal t = QVector3D::dotProduct(v, rayDir);
                QVector3D closestRayPos = rayOrigin + rayDir * t;
                qreal distance = closestRayPos.distanceToPoint(vertex.position);
                if (distance < closestLineDistance) {
                    linePos = vertex.position;
                    closestLineDistance = distance;
                }
            }
            // check distance against reasonable pick radius
            /*After testing, 0.05 gives a good error margin for
            picking, without casuing false positives
            from clicks for surrounding points */

            qreal pickRadius = 0.05f;
            if (closestLineDistance < pickRadius) {
                pickedLinePosition = linePos;
                pickedLineModel = model;
            }
            continue;
        }

        if (!intersectWithAABB(model->boundsMin, model->boundsMax, rayOrigin, rayDir))
            continue;

        qsizetype endX = columnCount - 1;
        qsizetype endY = rowCount - 1;

        qsizetype rowEnd = endY * columnCount;
        for (qsizetype row = 0; row < rowEnd; row += columnCount) {
            for (qsizetype j = 0; j < endX; j++) {
                std::array<QVector3D, 3> triangle;
                triangle[0] = model->heights.at(int(row + j + 1)).toVector3D();
                triangle[1] = model->heights.at(int(row + columnCount + j)).toVector3D();
                triangle[2] = model->heights.at(int(row + j)).toVector3D();
                QVector3D intersection = triangleIntersection(rayOrigin, rayDir, triangle);
                if (!intersection.isNull()) {
                    qreal distance = rayOrigin.distanceToPoint(intersection);
                    if (distance < closestDistance) {
                        pickedPosition = intersection;
                        pickedModel = model;
                        closestDistance = distance;
                    }
                }

                triangle[0] = model->heights.at(int(row + columnCount + j + 1)).toVector3D();
                triangle[1] = model->heights.at(int(row + columnCount + j)).toVector3D();
                triangle[2] = model->heights.at(int(row + j + 1)).toVector3D();
                intersection = triangleIntersection(rayOrigin, rayDir, triangle);
                if (!intersection.isNull()) {
                    qreal distance = rayOrigin.distanceToPoint(intersection);
                    if (distance < closestDistance) {
                        pickedPosition = intersection;
                        pickedModel = model;
                        closestDistance = distance;
                    }
                }
            }
        }
    }
    // compare line and surface positions
    if (!pickedLinePosition.isNull() && pickedLineModel) {
        qreal lineDistance = rayOrigin.distanceToPoint(pickedLinePosition);
        if (lineDistance < closestDistance) {
            pickedPosition = pickedLinePosition;
            pickedModel = pickedLineModel;
        }
    }
    return pickedPosition;
}

// Möller-Trumbore ray-triangle intersection
QVector3D QQuickGraphsSurface::triangleIntersection(QVector3D origin,
                                                    QVector3D dir,
                                                    const std::array<QVector3D, 3> &triangle)
{
    constexpr qreal epsilon = std::numeric_limits<float>::epsilon();

    const QVector3D edge1 = triangle[1] - triangle[0];
    const QVector3D edge2 = triangle[2] - triangle[0];

    // Compute the vector P as the cross product of the ray direction and edge2
    const QVector3D P = QVector3D::crossProduct(dir, edge2);

    // Compute the determinant
    const float determinant = QVector3D::dotProduct(edge1, P);

    QVector3D Q;
    qreal u;
    qreal v;

    if (determinant > epsilon) {
        // Compute the vector T from the ray origin to the first vertex of the triangle
        const QVector3D T = origin - triangle[0];

        // Calculate coordinate u and test bounds
        u = QVector3D::dotProduct(T, P);
        if (u < 0.0f || u > determinant)
            return QVector3D();

        // Compute the vector Q as the cross product of vector T and edge1
        Q = QVector3D::crossProduct(T, edge1);

        // Calculate coordinate v and test bounds
        v = QVector3D::dotProduct(dir, Q);
        if (v < 0.0f || ((u + v) > determinant))
            return QVector3D();
    } else if (determinant < -epsilon) { //Backfaces
        // Compute the vector T from the ray origin to the first vertex of the triangle
        const QVector3D T = origin - triangle[0];

        // Calculate coordinate u and test bounds
        u = QVector3D::dotProduct(T, P);
        if (u > 0.0f || u < determinant)
            return QVector3D();

        // Compute the vector Q as the cross product of vector T and edge1
        Q = QVector3D::crossProduct(T, edge1);

        // Calculate coordinate v and test bounds
        v = QVector3D::dotProduct(dir, Q);
        if (v > 0.0f || ((u + v) < determinant))
            return QVector3D();
    } else {
        // Ray is parallel to the plane of the triangle
        return QVector3D();
    }

    const float invDeterminant = 1.0f / determinant;

    // Calculate the value of t, the parameter of the intersection point along the ray
    const float t = QVector3D::dotProduct(edge2, Q) * invDeterminant;

    if (t > epsilon)
        return origin + dir * t;

    return QVector3D();
}

bool QQuickGraphsSurface::intersectWithAABB(QVector3D boundsMin,
                                            QVector3D boundsMax,
                                            QVector3D rayOrigin,
                                            QVector3D rayDir)
{
    float tmax = std::numeric_limits<float>::max();
    float tmin = std::numeric_limits<float>::min();
    float origin;

    for (int axis = 0; axis < 3; ++axis) {
        origin = rayOrigin[axis];
        const bool zeroDir = rayDir[axis] == 0;

        // Pickray is roughly parallel to the plane of the slab
        // so, if the origin is not in the range, we have no intersection
        if (zeroDir && (origin < boundsMin[axis] || origin > boundsMax[axis]))
            return false;

        if (!zeroDir) {
            float t1 = (boundsMin[axis] - origin) / rayDir[axis];
            float t2 = (boundsMax[axis] - origin) / rayDir[axis];

            tmin = qMax(tmin, qMin(t1, t2));
            tmax = qMin(tmax, qMax(t1, t2));
        }
    }

    return tmin < tmax;
}

void QQuickGraphsSurface::updateSelectedPoint()
{
    bool labelVisible = false;

    auto list = surfaceSeriesList();
    for (auto series : std::as_const(list)) {
        // If the pointer and its instancing do not exist yet (as will happen in widget case),
        // we must create them
        if (!m_selectionPointers.value(series))
            changePointerMeshTypeForSeries(series->mesh(), series);

        if (sliceView() && !m_sliceSelectionPointers.value(series))
            changeSlicePointerMeshTypeForSeries(series->mesh(), series);
    }

    for (auto i = m_selectionPointers.keyValueBegin(); i != m_selectionPointers.keyValueEnd(); i++)
        i->second->setVisible(false);

    if (sliceView() && sliceView()->isVisible()) {
        for (auto i = m_sliceSelectionPointers.keyValueBegin();
             i != m_sliceSelectionPointers.keyValueEnd();
             i++) {
            i->second->setVisible(false);
        }
    }

    QPointF worldCoord;
    for (auto model : std::as_const(m_model)) {
        if (model->picked) {
            QPoint coords = model->selectedVertex.coord;
            worldCoord = mapCoordsToWorldSpace(model, coords);
        }
    }
    for (auto model : std::as_const(m_model)) {
        Q_TRACE(QGraphs3DSurfacePointSelectionUpdate_entry,static_cast<void *>( model));

        if ((!selectionMode().testFlag(QtGraphs3D::SelectionFlag::MultiSeries) && !model->picked)
            || model->selectedVertex.position.isNull()) {
            Q_TRACE(QGraphs3DSurfacePointSelectionUpdate_exit);
            continue;
        }
        QPoint selectedCoord;
        if (model->picked)
            selectedCoord = model->selectedVertex.coord;
        else
            selectedCoord = mapCoordsToSampleSpace(model, worldCoord);
        if (selectedCoord.x() == -1 || selectedCoord.y() == -1) {
            Q_TRACE(QGraphs3DSurfacePointSelectionUpdate_exit);
            continue;
        }

        const QSurfaceDataItem &dataPos
            = model->series->dataArray().at(selectedCoord.y()).at(selectedCoord.x());
        QVector3D pos = getNormalizedVertex(dataPos, isPolar(), false);

        SurfaceVertex selectedVertex;
        selectedVertex.position = pos;
        selectedVertex.coord = model->selectedVertex.coord;
        if (model->series->isVisible() && !selectedVertex.position.isNull()
            && selectionMode().testFlag(QtGraphs3D::SelectionFlag::Item)) {
            m_selectionPointers.value(model->series)->setPosition(selectedVertex.position);
            m_selectionPointers.value(model->series)->setVisible(true);
            QVector3D slicePosition = getNormalizedVertex(dataPos, false, false);
            if (sliceView() && sliceView()->isVisible()) {
                if (selectionMode().testFlag(QtGraphs3D::SelectionFlag::Column))
                    slicePosition.setX(-slicePosition.z());
                slicePosition.setZ(.0f);
                m_sliceSelectionPointers.value(model->series)->setPosition(slicePosition);
                m_sliceSelectionPointers.value(model->series)->setVisible(true);
            }
            if (model->picked) {
                QVector3D labelPosition = selectedVertex.position;
                QString label = model->series->itemLabel();
                setSelectedPoint(selectedVertex.coord, model->series, false);

                updateItemLabel(labelPosition);
                itemLabel()->setProperty("labelText", label);
                if (!label.compare(QString(hiddenLabelTag)))
                    itemLabel()->setVisible(false);
                labelVisible = model->series->isItemLabelVisible();
                if (sliceView() && sliceView()->isVisible())
                    updateSliceItemLabel(label, slicePosition);
            }
        }
        Q_TRACE(QGraphs3DSurfacePointSelectionUpdate_exit);
    }
    setItemSelected(m_selectedSeries != nullptr);
    itemLabel()->setVisible(labelVisible);
    if (sliceView() && sliceView()->isVisible())
        sliceItemLabel()->setVisible(labelVisible);
}

void QQuickGraphsSurface::addModel(QSurface3DSeries *series)
{
    Q_TRACE_SCOPE(QGraphs3DSurfaceAddModel, series);
    auto parent = graphNode();
    bool visible = series->isVisible();

    auto model = new QQuick3DModel();
    model->setParent(parent);
    model->setParentItem(parent);
    model->setObjectName(QStringLiteral("SurfaceModel"));
    model->setVisible(visible);
    if (selectionMode().testFlag(QtGraphs3D::SelectionFlag::None))
        model->setPickable(false);
    else
        model->setPickable(true);

    auto geometry = new QQuick3DGeometry();
    geometry->setParent(model);
    geometry->setStride(sizeof(SurfaceVertex));
    geometry->setPrimitiveType(QQuick3DGeometry::PrimitiveType::Triangles);
    geometry->addAttribute(QQuick3DGeometry::Attribute::PositionSemantic,
                           0,
                           QQuick3DGeometry::Attribute::F32Type);
    geometry->addAttribute(QQuick3DGeometry::Attribute::TexCoord0Semantic,
                           sizeof(QVector3D),
                           QQuick3DGeometry::Attribute::F32Type);
    geometry->addAttribute(QQuick3DGeometry::Attribute::IndexSemantic,
                           0,
                           QQuick3DGeometry::Attribute::U32Type);
    model->setGeometry(geometry);

    model->setCastsShadows(false); //Disable shadows as they render incorrectly

    QQuick3DTexture *texture = new QQuick3DTexture();
    texture->setHorizontalTiling(QQuick3DTexture::ClampToEdge);
    texture->setVerticalTiling(QQuick3DTexture::ClampToEdge);
    QQuickGraphsTextureData *textureData = new QQuickGraphsTextureData();
    textureData->setParent(texture);
    textureData->setParentItem(texture);
    texture->setTextureData(textureData);

    QQuick3DTexture *gradientTexture = new QQuick3DTexture();
    gradientTexture->setHorizontalTiling(QQuick3DTexture::ClampToEdge);
    gradientTexture->setVerticalTiling(QQuick3DTexture::ClampToEdge);
    QQuickGraphsTextureData *gradientTextureData = new QQuickGraphsTextureData();
    textureData->setParent(gradientTexture);
    textureData->setParentItem(gradientTexture);
    gradientTexture->setTextureData(gradientTextureData);

    QQmlListReference materialRef(model, "materials");

    QQuick3DCustomMaterial *customMaterial = createQmlCustomMaterial(
        QStringLiteral(":/materials/SurfaceMaterial"));

    customMaterial->setParent(model);
    customMaterial->setParentItem(model);
    customMaterial->setCullMode(QQuick3DMaterial::NoCulling);
    QVariant textureInputAsVariant = customMaterial->property("custex");
    QQuick3DShaderUtilsTextureInput *textureInput = textureInputAsVariant
                                                        .value<QQuick3DShaderUtilsTextureInput *>();
    textureInput->setTexture(texture);

    texture->setParent(customMaterial);

    materialRef.append(customMaterial);

    auto gridModel = new QQuick3DModel();
    gridModel->setParent(parent);
    gridModel->setParentItem(parent);
    gridModel->setObjectName(QStringLiteral("SurfaceModel"));
    gridModel->setVisible(visible);
    gridModel->setDepthBias(1.0f);
    auto gridGeometry = new QQuick3DGeometry();
    gridGeometry->setParent(this);
    gridGeometry->setStride(sizeof(SurfaceVertex));
    gridGeometry->setPrimitiveType(QQuick3DGeometry::PrimitiveType::Lines);
    gridGeometry->addAttribute(QQuick3DGeometry::Attribute::PositionSemantic,
                               0,
                               QQuick3DGeometry::Attribute::F32Type);
    gridGeometry->addAttribute(QQuick3DGeometry::Attribute::TexCoord0Semantic,
                               sizeof(QVector3D),
                               QQuick3DGeometry::Attribute::F32Type);
    gridGeometry->addAttribute(QQuick3DGeometry::Attribute::IndexSemantic,
                               0,
                               QQuick3DGeometry::Attribute::U32Type);
    gridModel->setGeometry(gridGeometry);
    gridModel->setCastsShadows(false);
    QQmlListReference gridMaterialRef(gridModel, "materials");
    auto gridMaterial = createQmlCustomMaterial(QStringLiteral(":/materials/GridSurfaceMaterial"));
    gridMaterial->setParent(gridModel);
    gridMaterial->setParentItem(gridModel);
    gridMaterialRef.append(gridMaterial);

    SurfaceModel *surfaceModel = new SurfaceModel();
    surfaceModel->model = model;
    surfaceModel->gridModel = gridModel;
    surfaceModel->series = series;
    surfaceModel->texture = texture;
    surfaceModel->gradientTexture = gradientTexture;
    surfaceModel->customMaterial = customMaterial;

    m_model.push_back(surfaceModel);

    connect(series,
            &QSurface3DSeries::shadingChanged,
            this,
            &QQuickGraphsSurface::handleShadingChanged);
    connect(series,
            &QSurface3DSeries::wireframeColorChanged,
            this,
            &QQuickGraphsSurface::handleWireframeColorChanged);
    connect(series,
            &QSurface3DSeries::userDefinedMeshChanged,
            this,
            &QQuickGraphsSurface::handlePointerChanged);
    connect(series,
            &QSurface3DSeries::meshChanged,
            this,
            &QQuickGraphsSurface::handleMeshTypeChanged);
    if (sliceView())
        addSliceModel(surfaceModel);

    addFillModel(surfaceModel);
}

void QQuickGraphsSurface::addFillModel(SurfaceModel *model)
{
    Q_TRACE_SCOPE(QGraphs3DSurfaceAddFillModel);
    auto parent = graphNode();

    auto fillModel = new QQuick3DModel();
    fillModel->setParent(parent);
    fillModel->setParentItem(model->model);
    fillModel->setObjectName(QStringLiteral("FillModel"));
    fillModel->setVisible(true);
    fillModel->setPickable(false);

    auto geometry = new QQuick3DGeometry();
    geometry->setParent(fillModel);
    geometry->setStride(sizeof(SurfaceVertex));
    geometry->setPrimitiveType(QQuick3DGeometry::PrimitiveType::Triangles);
    geometry->addAttribute(QQuick3DGeometry::Attribute::PositionSemantic,
                           0,
                           QQuick3DGeometry::Attribute::F32Type);
    geometry->addAttribute(QQuick3DGeometry::Attribute::TexCoord0Semantic,
                           sizeof(QVector3D),
                           QQuick3DGeometry::Attribute::F32Type);
    geometry->addAttribute(QQuick3DGeometry::Attribute::IndexSemantic,
                           0,
                           QQuick3DGeometry::Attribute::U32Type);
    fillModel->setGeometry(geometry);

    fillModel->setCastsShadows(false); //Disable shadows as they render incorrectly

    QQmlListReference materialRef(fillModel, "materials");

    auto *customMaterial = model->customMaterial;
    materialRef.append(customMaterial);

    model->fillModel = fillModel;
    m_fillDirty[model] = false;
}

void QQuickGraphsSurface::createSliceView()
{
    setSliceOrthoProjection(true);
    QQuickGraphsItem::createSliceView();
    Q_TRACE_SCOPE(QGraphs3DSurfaceCreateSliceView);

    for (auto surfaceModel : std::as_const(m_model)) {
        addSliceModel(surfaceModel);
        changeSlicePointerMeshTypeForSeries(surfaceModel->series->mesh(), surfaceModel->series);
    }
}

QQuick3DViewport *
QQuickGraphsSurface::createOffscreenSliceView(int index, int requestedIndex,
                                              QtGraphs3D::SliceCaptureType sliceType)
{
    QQuick3DViewport *sliceView = QQuickGraphsItem::createOffscreenSliceView(sliceType);
    Q_TRACE_SCOPE(QGraphs3DSurfaceCreateOffscreenSliceView, index, requestedIndex,
                  static_cast<int>(sliceType));

    const bool isRow = (sliceType == QtGraphs3D::SliceCaptureType::RowImage);
    const bool isColumn = (sliceType == QtGraphs3D::SliceCaptureType::ColumnImage);

    int modelIndex = 0;
    for (const auto &model : std::as_const(m_model)) {
        if (index > 0 && modelIndex++ != index)
            continue;

        QRect sampleSpace = model->sampleSpace;
        int rowStart = sampleSpace.top();
        int columnStart = sampleSpace.left();
        int rowEnd = sampleSpace.bottom() + 1;
        int columnEnd = sampleSpace.right() + 1;
        int rowCount = sampleSpace.height();
        int columnCount = sampleSpace.width();

        QVector<SurfaceVertex> selectedSeries;
        int indexCount = 0;
        const QSurfaceDataArray &array = model->series->dataArray();
        const qsizetype maxRow = array.size() - 1;
        const qsizetype maxColumn = array.at(0).size() - 1;
        const bool ascendingX = array.at(0).at(0).x() < array.at(0).at(maxColumn).x();
        const bool ascendingZ = array.at(0).at(0).z() < array.at(maxRow).at(0).z();

        if (requestedIndex < 0 || requestedIndex >= maxRow || requestedIndex >= maxColumn) {
            qCWarning(lcGraphsSurface3D, "The index is out of range. The render stops.");
            sliceView->setVisible(false);
            sliceView->deleteLater();
            return nullptr;
        }

        if (isRow && requestedIndex != -1) {
            selectedSeries.reserve(columnCount * 2);
            QVector<SurfaceVertex> list;
            QSurfaceDataRow row = array.at(requestedIndex);
            for (int i = columnStart; i < columnEnd; i++) {
                int index = ascendingX ? i : columnEnd - i + columnStart - 1;
                QVector3D pos = getNormalizedVertex(row.at(index), false, false);
                SurfaceVertex vertex;
                vertex.position = pos;
                vertex.position.setY(vertex.position.y() - .025f);
                vertex.position.setZ(.0f);
                selectedSeries.append(vertex);
                vertex.position.setY(vertex.position.y() + .05f);
                list.append(vertex);
            }
            selectedSeries.append(list);
            indexCount = columnCount - 1;
        }

        if (isColumn && requestedIndex != -1) {
            selectedSeries.reserve(rowCount * 2);
            QVector<SurfaceVertex> list;
            for (int i = rowStart; i < rowEnd; i++) {
                int index = ascendingZ ? i : rowEnd - i + rowStart - 1;
                QVector3D pos =
                        getNormalizedVertex(array.at(index).at(requestedIndex), false, false);
                SurfaceVertex vertex;
                vertex.position = pos;
                vertex.position.setX(-vertex.position.z());
                vertex.position.setY(vertex.position.y() - .025f);
                vertex.position.setZ(0);
                selectedSeries.append(vertex);
                vertex.position.setY(vertex.position.y() + .05f);
                list.append(vertex);
            }
            selectedSeries.append(list);
            indexCount = rowCount - 1;
        }

        QVector<quint32> indices;
        indices.reserve(indexCount * 6);
        for (int i = 0; i < indexCount; i++) {
            indices.push_back(i + 1);
            indices.push_back(i + indexCount + 1);
            indices.push_back(i);
            indices.push_back(i + indexCount + 2);
            indices.push_back(i + indexCount + 1);
            indices.push_back(i + 1);
        }

        auto surfaceModel = new QQuick3DModel();
        surfaceModel->setParent(sliceView->scene());
        surfaceModel->setParentItem(sliceView->scene());

        auto geometry = new QQuick3DGeometry();
        geometry->setParent(surfaceModel);
        geometry->setParentItem(surfaceModel);
        geometry->setStride(sizeof(SurfaceVertex));
        geometry->setPrimitiveType(QQuick3DGeometry::PrimitiveType::Triangles);
        geometry->addAttribute(QQuick3DGeometry::Attribute::PositionSemantic, 0,
                               QQuick3DGeometry::Attribute::F32Type);
        geometry->addAttribute(QQuick3DGeometry::Attribute::TexCoord0Semantic, sizeof(QVector3D),
                               QQuick3DGeometry::Attribute::F32Type);
        geometry->addAttribute(QQuick3DGeometry::Attribute::IndexSemantic, 0,
                               QQuick3DGeometry::Attribute::U32Type);
        QByteArray vertexBuffer(reinterpret_cast<char *>(selectedSeries.data()),
                                selectedSeries.size() * sizeof(SurfaceVertex));
        geometry->setVertexData(vertexBuffer);
        QByteArray indexBuffer(reinterpret_cast<char *>(indices.data()),
                               indices.size() * sizeof(quint32));
        geometry->setIndexData(indexBuffer);
        surfaceModel->setGeometry(geometry);

        QQmlListReference materialRef(surfaceModel, "materials");
        auto material = createQmlCustomMaterial(QStringLiteral(":/materials/SurfaceSliceMaterial"));
        material->setCullMode(QQuick3DMaterial::NoCulling);
        material->setProperty("isColumn", isColumn);
        QVariant textureInputAsVariant = material->property("custex");
        QQuick3DShaderUtilsTextureInput *textureInput =
                textureInputAsVariant.value<QQuick3DShaderUtilsTextureInput *>();
        QQuick3DTexture *texture = model->texture;
        textureInput->setTexture(texture);
        materialRef.append(material);

        if (model->series->drawMode().testFlag(QSurface3DSeries::DrawSurface))
            surfaceModel->setLocalOpacity(1.f);
        else
            surfaceModel->setLocalOpacity(.0f);

        if (model->series->drawMode().testFlag(QSurface3DSeries::DrawWireframe)) {
            QVector<quint32> gridIndices;
            gridIndices.reserve(indexCount * 4);
            for (int i = 0; i < indexCount; i++) {
                gridIndices.push_back(i);
                gridIndices.push_back(i + indexCount + 1);

                gridIndices.push_back(i);
                gridIndices.push_back(i + 1);
            }
            QQuick3DModel *gridModel = new QQuick3DModel();
            gridModel->setParent(sliceView->scene());
            gridModel->setParentItem(sliceView->scene());
            gridModel->setDepthBias(1.0f);
            QQuick3DGeometry *gridGeometry = new QQuick3DGeometry();
            gridGeometry->setParent(gridModel);
            gridGeometry->setStride(sizeof(SurfaceVertex));
            gridGeometry->setPrimitiveType(QQuick3DGeometry::PrimitiveType::Lines);
            gridGeometry->addAttribute(QQuick3DGeometry::Attribute::PositionSemantic, 0,
                                       QQuick3DGeometry::Attribute::F32Type);
            gridGeometry->addAttribute(QQuick3DGeometry::Attribute::IndexSemantic, 0,
                                       QQuick3DGeometry::Attribute::U32Type);
            QByteArray gridIndexBuffer(reinterpret_cast<char *>(gridIndices.data()),
                                       gridIndices.size() * sizeof(quint32));
            gridGeometry->setVertexData(vertexBuffer);
            gridGeometry->setIndexData(gridIndexBuffer);
            gridGeometry->update();
            gridModel->setGeometry(gridGeometry);
            QQmlListReference gridMaterialRef(gridModel, "materials");
            QQuick3DPrincipledMaterial *gridMaterial = new QQuick3DPrincipledMaterial();
            gridMaterial->setParent(gridModel);
            gridMaterial->setLighting(QQuick3DPrincipledMaterial::NoLighting);
            gridMaterial->setParent(gridModel);
            QColor gridColor = model->series->wireframeColor();
            gridMaterial->setBaseColor(gridColor);
            gridMaterialRef.append(gridMaterial);
        }
    }

    return sliceView;
}

void QQuickGraphsSurface::renderSliceToImage(int index,
                                             int requestedIndex,
                                             QtGraphs3D::SliceCaptureType sliceType)
{
    QQuick3DViewport *sliceView = createOffscreenSliceView(index, requestedIndex, sliceType);

    if (!m_grabresult)
        m_grabresult = new QImage();

    if (sliceView) {
        QSharedPointer<QQuickItemGrabResult> grabbed = sliceView->grabToImage();
        connect(grabbed.data(), &QQuickItemGrabResult::ready, this, [&, grabbed, sliceView]() {
            sliceView->setVisible(false);
            sliceView->deleteLater();
            *m_grabresult = grabbed.data()->image();
            emit sliceImageChanged(*m_grabresult);
        });
    }
}

void QQuickGraphsSurface::renderSliceToImage(int index, int requestedIndex,
                                             QtGraphs3D::SliceCaptureType sliceType,
                                             const QUrl &filePath)
{
    QQuick3DViewport *sliceView = createOffscreenSliceView(index, requestedIndex, sliceType);

    if (!sliceView)
        return;

    if (filePath.isEmpty()) {
        qCWarning(lcGraphsSurface3D, "Save path is not defined.");
        sliceView->setVisible(false);
        sliceView->deleteLater();
        return;
    }

    QSharedPointer<QQuickItemGrabResult> grabbed = sliceView->grabToImage();
    connect(grabbed.data(), &QQuickItemGrabResult::ready, this, [grabbed, sliceView, filePath]() {
        if (!grabbed.data()->saveToFile(filePath))
            qCWarning(lcGraphsSurface3D, "Saving requested slice view to image failed");
        sliceView->setVisible(false);
        sliceView->deleteLater();
    });
}

void QQuickGraphsSurface::updateSliceItemLabel(const QString &label, QVector3D position)
{
    QQuickGraphsItem::updateSliceItemLabel(label, position);

    QFontMetrics fm(theme()->labelFont());
    float textPadding = 12.0f;
    float labelHeight = fm.height() + textPadding;
    float labelWidth = fm.horizontalAdvance(label) + textPadding;
    sliceItemLabel()->setProperty("labelWidth", labelWidth);
    sliceItemLabel()->setProperty("labelHeight", labelHeight);
    QVector3D labelPosition = position;
    labelPosition.setZ(.1f);
    labelPosition.setY(position.y() + .05f);
    sliceItemLabel()->setPosition(labelPosition);
    sliceItemLabel()->setProperty("labelText", label);
    if (!label.compare(QString(hiddenLabelTag)))
        sliceItemLabel()->setVisible(false);
}

void QQuickGraphsSurface::updateSelectionMode(QtGraphs3D::SelectionFlags mode)
{
    checkSliceEnabled();
    bool validSlice = mode.testFlag(QtGraphs3D::SelectionFlag::Slice)
                      && m_selectedPoint != invalidSelectionPosition();
    if (sliceView() && sliceView()->isVisible()) {
        if (validSlice) {
            toggleSliceGraph();
        } else {
            m_selectionDirty = true;
            setSliceActivatedChanged(true);
        }
    } else if (validSlice) {
        m_selectionDirty = true;
        setSliceActivatedChanged(true);
    }

    setSeriesVisualsDirty(true);
    itemLabel()->setVisible(false);
    if (sliceView() && sliceView()->isVisible())
        sliceItemLabel()->setVisible(false);
}

void QQuickGraphsSurface::addSliceModel(SurfaceModel *model)
{
    Q_TRACE_SCOPE(QGraphs3DSurfaceAddSliceModel);
    QQuick3DViewport *sliceParent = nullptr;

    sliceParent = sliceView();

    auto surfaceModel = new QQuick3DModel();
    surfaceModel->setParent(sliceParent->scene());
    surfaceModel->setParentItem(sliceParent->scene());
    surfaceModel->setVisible(model->series->isVisible());

    auto geometry = new QQuick3DGeometry();
    geometry->setParent(surfaceModel);
    geometry->setParentItem(surfaceModel);
    geometry->setStride(sizeof(SurfaceVertex));
    geometry->setPrimitiveType(QQuick3DGeometry::PrimitiveType::Triangles);
    geometry->addAttribute(QQuick3DGeometry::Attribute::PositionSemantic,
                           0,
                           QQuick3DGeometry::Attribute::F32Type);
    geometry->addAttribute(QQuick3DGeometry::Attribute::TexCoord0Semantic,
                           sizeof(QVector3D),
                           QQuick3DGeometry::Attribute::F32Type);
    geometry->addAttribute(QQuick3DGeometry::Attribute::IndexSemantic,
                           0,
                           QQuick3DGeometry::Attribute::U32Type);
    surfaceModel->setGeometry(geometry);

    QQmlListReference materialRef(surfaceModel, "materials");
    auto material = createQmlCustomMaterial(QStringLiteral(":/materials/SurfaceSliceMaterial"));
    material->setCullMode(QQuick3DMaterial::NoCulling);
    QVariant textureInputAsVariant = material->property("custex");
    QQuick3DShaderUtilsTextureInput *textureInput = textureInputAsVariant
                                                        .value<QQuick3DShaderUtilsTextureInput *>();
    QQuick3DTexture *texture = model->texture;
    textureInput->setTexture(texture);
    materialRef.append(material);

    model->sliceModel = surfaceModel;

    QQuick3DModel *gridModel = new QQuick3DModel();
    gridModel->setParent(sliceParent->scene());
    gridModel->setParentItem(sliceParent->scene());
    gridModel->setVisible(model->series->isVisible());
    gridModel->setDepthBias(1.0f);
    QQuick3DGeometry *gridGeometry = new QQuick3DGeometry();
    gridGeometry->setParent(gridModel);
    gridGeometry->setStride(sizeof(SurfaceVertex));
    gridGeometry->setPrimitiveType(QQuick3DGeometry::PrimitiveType::Lines);
    gridGeometry->addAttribute(QQuick3DGeometry::Attribute::PositionSemantic,
                               0,
                               QQuick3DGeometry::Attribute::F32Type);
    gridGeometry->addAttribute(QQuick3DGeometry::Attribute::IndexSemantic,
                               0,
                               QQuick3DGeometry::Attribute::U32Type);
    gridModel->setGeometry(gridGeometry);
    QQmlListReference gridMaterialRef(gridModel, "materials");
    QQuick3DPrincipledMaterial *gridMaterial = new QQuick3DPrincipledMaterial();
    gridMaterial->setParent(gridModel);
    gridMaterial->setLighting(QQuick3DPrincipledMaterial::NoLighting);
    gridMaterial->setParent(gridModel);
    gridMaterialRef.append(gridMaterial);

    model->sliceGridModel = gridModel;
}

void QQuickGraphsSurface::updateSingleHighlightColor()
{
    auto list = surfaceSeriesList();
    for (auto series : std::as_const(list)) {
        QQmlListReference pMaterialRef(m_selectionPointers.value(series), "materials");
        auto pmat = qobject_cast<QQuick3DPrincipledMaterial *>(pMaterialRef.at(0));
        if (pmat)
            pmat->setBaseColor(theme()->singleHighlightColor());
        if (sliceView()) {
            QQmlListReference spMaterialRef(m_sliceSelectionPointers.value(series), "materials");
            auto spmat = qobject_cast<QQuick3DPrincipledMaterial *>(spMaterialRef.at(0));
            spmat->setBaseColor(theme()->singleHighlightColor());
        }
    }
}

void QQuickGraphsSurface::updateLightStrength()
{
    for (auto model : std::as_const(m_model)) {
        QQmlListReference materialRef(model->model, "materials");
        QQuick3DCustomMaterial *material = qobject_cast<QQuick3DCustomMaterial *>(materialRef.at(0));
        material->setProperty("specularBrightness", lightStrength() * 0.05);
    }
}

void QQuickGraphsSurface::handleThemeTypeChange()
{
    for (auto model : std::as_const(m_model))
        updateMaterial(model);
}

QT_END_NAMESPACE
