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

#undef QT_NO_FOREACH // this file contains unported legacy Q_FOREACH uses

#include "abstractdeclarativeinterface_p.h"
#include "abstract3dcontroller_p.h"
#include "qabstract3daxis_p.h"
#include "qvalue3daxis_p.h"
#include "abstract3drenderer_p.h"
#include "qabstract3dinputhandler_p.h"
#include "qtouch3dinputhandler.h"
#include "thememanager_p.h"
#include "q3dtheme_p.h"
#include "qcustom3ditem_p.h"
#include "utils_p.h"
#include <QtCore/QThread>
#include <QtOpenGL/QOpenGLFramebufferObject>
#include <QtCore/QMutexLocker>

QT_BEGIN_NAMESPACE

Abstract3DController::Abstract3DController(QRect initialViewport, Q3DScene *scene,
                                           QObject *parent) :
    QObject(parent),
    m_themeManager(new ThemeManager(this)),
    m_selectionMode(QAbstract3DGraph::SelectionItem),
    m_shadowQuality(QAbstract3DGraph::ShadowQualityMedium),
    m_useOrthoProjection(false),
    m_aspectRatio(2.0),
    m_horizontalAspectRatio(0.0),
    m_optimizationHints(QAbstract3DGraph::OptimizationDefault),
    m_reflectionEnabled(false),
    m_reflectivity(0.5),
    m_locale(QLocale::c()),
    m_scene(scene),
    m_activeInputHandler(0),
    m_axisX(0),
    m_axisY(0),
    m_axisZ(0),
    m_renderer(0),
    m_isDataDirty(true),
    m_isCustomDataDirty(true),
    m_isCustomItemDirty(true),
    m_isSeriesVisualsDirty(true),
    m_renderPending(false),
    m_isPolar(false),
    m_radialLabelOffset(1.0f),
    m_measureFps(false),
    m_numFrames(0),
    m_currentFps(0.0),
    m_clickedType(QAbstract3DGraph::ElementNone),
    m_selectedLabelIndex(-1),
    m_selectedCustomItemIndex(-1),
    m_margin(-1.0)
{
    if (!m_scene)
        m_scene = new Q3DScene;
    m_scene->setParent(this);

    // Set initial theme
    Q3DTheme *defaultTheme = new Q3DTheme(Q3DTheme::ThemeQt);
    defaultTheme->d_ptr->setDefaultTheme(true);
    setActiveTheme(defaultTheme);

    m_scene->d_ptr->setViewport(initialViewport);
    m_scene->activeLight()->setAutoPosition(true);

    // Create initial default input handler
    QAbstract3DInputHandler *inputHandler;
    inputHandler = new QTouch3DInputHandler();
    inputHandler->d_ptr->m_isDefaultHandler = true;
    setActiveInputHandler(inputHandler);
    connect(m_scene->d_ptr.data(), &Q3DScenePrivate::needRender, this,
            &Abstract3DController::emitNeedRender);
}

Abstract3DController::~Abstract3DController()
{
    destroyRenderer();
    delete m_scene;
    delete m_themeManager;
    for (auto item: m_customItems)
        delete item;
    m_customItems.clear();
}

void Abstract3DController::destroyRenderer()
{
    QMutexLocker mutexLocker(&m_renderMutex);
    // Renderer can be in another thread, don't delete it directly in that case
    if (m_renderer && m_renderer->thread() && m_renderer->thread() != this->thread())
        m_renderer->deleteLater();
    else
        delete m_renderer;
    m_renderer = 0;
}

/**
 * @brief setRenderer Sets the renderer to be used. isInitialized returns true from this point onwards.
 * @param renderer Renderer to be used.
 */
void Abstract3DController::setRenderer(Abstract3DRenderer *renderer)
{
    // Note: This function must be called within render mutex
    m_renderer = renderer;

    // If renderer is created in different thread than controller, make sure renderer gets
    // destroyed before the render thread finishes.
    if (renderer->thread() != this->thread()) {
        QObject::connect(renderer->thread(), &QThread::finished, this,
                         &Abstract3DController::destroyRenderer, Qt::DirectConnection);
    }
}

void Abstract3DController::addSeries(QAbstract3DSeries *series)
{
    insertSeries(m_seriesList.size(), series);
}

void Abstract3DController::insertSeries(int index, QAbstract3DSeries *series)
{
    if (series) {
        if (m_seriesList.contains(series)) {
            int oldIndex = m_seriesList.indexOf(series);
            if (index != oldIndex) {
                m_seriesList.removeOne(series);
                if (oldIndex < index)
                    index--;
                m_seriesList.insert(index, series);
            }
        } else {
            int oldSize = m_seriesList.size();
            m_seriesList.insert(index, series);
            series->d_ptr->setController(this);
            QObject::connect(series, &QAbstract3DSeries::visibilityChanged,
                             this, &Abstract3DController::handleSeriesVisibilityChanged);
            series->d_ptr->resetToTheme(*m_themeManager->activeTheme(), oldSize, false);
        }
        if (series->isVisible())
            handleSeriesVisibilityChangedBySender(series);
    }
}

void Abstract3DController::removeSeries(QAbstract3DSeries *series)
{
    if (series && series->d_ptr->m_controller == this) {
        m_seriesList.removeAll(series);
        QObject::disconnect(series, &QAbstract3DSeries::visibilityChanged,
                            this, &Abstract3DController::handleSeriesVisibilityChanged);
        series->d_ptr->setController(0);
        m_isDataDirty = true;
        m_isSeriesVisualsDirty = true;
        emitNeedRender();
    }
}

bool Abstract3DController::hasSeries(QAbstract3DSeries *series)
{
    return m_seriesList.contains(series);
}

QList<QAbstract3DSeries *> Abstract3DController::seriesList()
{
    return m_seriesList;
}

/**
 * @brief synchDataToRenderer Called on the render thread while main GUI thread is blocked before rendering.
 */
void Abstract3DController::synchDataToRenderer()
{
    // Subclass implementations check for renderer validity already, so no need to check here.

    m_renderPending = false;

    // If there are pending queries, handle those first
    if (m_renderer->isGraphPositionQueryResolved())
        handlePendingGraphPositionQuery();

    if (m_renderer->isClickQueryResolved())
        handlePendingClick();

    startRecordingRemovesAndInserts();

    if (m_scene->d_ptr->m_sceneDirty)
        m_renderer->updateScene(m_scene);

    m_renderer->updateTheme(m_themeManager->activeTheme());

    if (m_changeTracker.polarChanged) {
        m_renderer->updatePolar(m_isPolar);
        m_changeTracker.polarChanged = false;
    }

    if (m_changeTracker.radialLabelOffsetChanged) {
        m_renderer->updateRadialLabelOffset(m_radialLabelOffset);
        m_changeTracker.radialLabelOffsetChanged = false;
    }

    if (m_changeTracker.shadowQualityChanged) {
        m_renderer->updateShadowQuality(m_shadowQuality);
        m_changeTracker.shadowQualityChanged = false;
    }

    if (m_changeTracker.selectionModeChanged) {
        m_renderer->updateSelectionMode(m_selectionMode);
        m_changeTracker.selectionModeChanged = false;
    }

    if (m_changeTracker.projectionChanged) {
        m_renderer->m_useOrthoProjection = m_useOrthoProjection;
        m_changeTracker.projectionChanged = false;
    }

    if (m_changeTracker.aspectRatioChanged) {
        m_renderer->updateAspectRatio(float(m_aspectRatio));
        m_changeTracker.aspectRatioChanged = false;
    }

    if (m_changeTracker.horizontalAspectRatioChanged) {
        m_renderer->updateHorizontalAspectRatio(float(m_horizontalAspectRatio));
        m_changeTracker.horizontalAspectRatioChanged = false;
    }

    if (m_changeTracker.optimizationHintChanged) {
        m_renderer->updateOptimizationHint(m_optimizationHints);
        m_changeTracker.optimizationHintChanged = false;
    }

    if (m_changeTracker.reflectionChanged) {
        m_renderer->m_reflectionEnabled = m_reflectionEnabled;
        m_changeTracker.reflectionChanged = false;
    }

    if (m_changeTracker.reflectivityChanged) {
        // Invert value to match functionality to the property description
        m_renderer->m_reflectivity = -(m_reflectivity - 1.0);
        m_changeTracker.reflectivityChanged = false;
    }

    if (m_changeTracker.axisXFormatterChanged) {
        m_changeTracker.axisXFormatterChanged = false;
        if (m_axisX->type() & QAbstract3DAxis::AxisTypeValue) {
            QValue3DAxis *valueAxisX = static_cast<QValue3DAxis *>(m_axisX);
            m_renderer->updateAxisFormatter(QAbstract3DAxis::AxisOrientationX,
                                            valueAxisX->formatter());
        }
    }
    if (m_changeTracker.axisYFormatterChanged) {
        m_changeTracker.axisYFormatterChanged = false;
        if (m_axisY->type() & QAbstract3DAxis::AxisTypeValue) {
            QValue3DAxis *valueAxisY = static_cast<QValue3DAxis *>(m_axisY);
            m_renderer->updateAxisFormatter(QAbstract3DAxis::AxisOrientationY,
                                            valueAxisY->formatter());
        }
    }
    if (m_changeTracker.axisZFormatterChanged) {
        m_changeTracker.axisZFormatterChanged = false;
        if (m_axisZ->type() & QAbstract3DAxis::AxisTypeValue) {
            QValue3DAxis *valueAxisZ = static_cast<QValue3DAxis *>(m_axisZ);
            m_renderer->updateAxisFormatter(QAbstract3DAxis::AxisOrientationZ,
                                            valueAxisZ->formatter());
        }
    }

    if (m_changeTracker.axisXTypeChanged) {
        m_renderer->updateAxisType(QAbstract3DAxis::AxisOrientationX, m_axisX->type());
        m_changeTracker.axisXTypeChanged = false;
    }

    if (m_changeTracker.axisYTypeChanged) {
        m_renderer->updateAxisType(QAbstract3DAxis::AxisOrientationY, m_axisY->type());
        m_changeTracker.axisYTypeChanged = false;
    }

    if (m_changeTracker.axisZTypeChanged) {
        m_renderer->updateAxisType(QAbstract3DAxis::AxisOrientationZ, m_axisZ->type());
        m_changeTracker.axisZTypeChanged = false;
    }

    if (m_changeTracker.axisXTitleChanged) {
        m_renderer->updateAxisTitle(QAbstract3DAxis::AxisOrientationX, m_axisX->title());
        m_changeTracker.axisXTitleChanged = false;
    }

    if (m_changeTracker.axisYTitleChanged) {
        m_renderer->updateAxisTitle(QAbstract3DAxis::AxisOrientationY, m_axisY->title());
        m_changeTracker.axisYTitleChanged = false;
    }

    if (m_changeTracker.axisZTitleChanged) {
        m_renderer->updateAxisTitle(QAbstract3DAxis::AxisOrientationZ, m_axisZ->title());
        m_changeTracker.axisZTitleChanged = false;
    }

    if (m_changeTracker.axisXLabelsChanged) {
        m_renderer->updateAxisLabels(QAbstract3DAxis::AxisOrientationX, m_axisX->labels());
        m_changeTracker.axisXLabelsChanged = false;
    }

    if (m_changeTracker.axisYLabelsChanged) {
        m_renderer->updateAxisLabels(QAbstract3DAxis::AxisOrientationY, m_axisY->labels());
        m_changeTracker.axisYLabelsChanged = false;
    }
    if (m_changeTracker.axisZLabelsChanged) {
        m_renderer->updateAxisLabels(QAbstract3DAxis::AxisOrientationZ, m_axisZ->labels());
        m_changeTracker.axisZLabelsChanged = false;
    }

    if (m_changeTracker.axisXRangeChanged) {
        m_renderer->updateAxisRange(QAbstract3DAxis::AxisOrientationX, m_axisX->min(),
                                    m_axisX->max());
        m_changeTracker.axisXRangeChanged = false;
    }

    if (m_changeTracker.axisYRangeChanged) {
        m_renderer->updateAxisRange(QAbstract3DAxis::AxisOrientationY, m_axisY->min(),
                                    m_axisY->max());
        m_changeTracker.axisYRangeChanged = false;
    }

    if (m_changeTracker.axisZRangeChanged) {
        m_renderer->updateAxisRange(QAbstract3DAxis::AxisOrientationZ, m_axisZ->min(),
                                    m_axisZ->max());
        m_changeTracker.axisZRangeChanged = false;
    }

    if (m_changeTracker.axisXSegmentCountChanged) {
        m_changeTracker.axisXSegmentCountChanged = false;
        if (m_axisX->type() & QAbstract3DAxis::AxisTypeValue) {
            QValue3DAxis *valueAxisX = static_cast<QValue3DAxis *>(m_axisX);
            m_renderer->updateAxisSegmentCount(QAbstract3DAxis::AxisOrientationX,
                                               valueAxisX->segmentCount());
        }
    }

    if (m_changeTracker.axisYSegmentCountChanged) {
        m_changeTracker.axisYSegmentCountChanged = false;
        if (m_axisY->type() & QAbstract3DAxis::AxisTypeValue) {
            QValue3DAxis *valueAxisY = static_cast<QValue3DAxis *>(m_axisY);
            m_renderer->updateAxisSegmentCount(QAbstract3DAxis::AxisOrientationY,
                                               valueAxisY->segmentCount());
        }
    }

    if (m_changeTracker.axisZSegmentCountChanged) {
        m_changeTracker.axisZSegmentCountChanged = false;
        if (m_axisZ->type() & QAbstract3DAxis::AxisTypeValue) {
            QValue3DAxis *valueAxisZ = static_cast<QValue3DAxis *>(m_axisZ);
            m_renderer->updateAxisSegmentCount(QAbstract3DAxis::AxisOrientationZ,
                                               valueAxisZ->segmentCount());
        }
    }

    if (m_changeTracker.axisXSubSegmentCountChanged) {
        m_changeTracker.axisXSubSegmentCountChanged = false;
        if (m_axisX->type() & QAbstract3DAxis::AxisTypeValue) {
            QValue3DAxis *valueAxisX = static_cast<QValue3DAxis *>(m_axisX);
            m_renderer->updateAxisSubSegmentCount(QAbstract3DAxis::AxisOrientationX,
                                                  valueAxisX->subSegmentCount());
        }
    }

    if (m_changeTracker.axisYSubSegmentCountChanged) {
        m_changeTracker.axisYSubSegmentCountChanged = false;
        if (m_axisY->type() & QAbstract3DAxis::AxisTypeValue) {
            QValue3DAxis *valueAxisY = static_cast<QValue3DAxis *>(m_axisY);
            m_renderer->updateAxisSubSegmentCount(QAbstract3DAxis::AxisOrientationY,
                                                  valueAxisY->subSegmentCount());
        }
    }

    if (m_changeTracker.axisZSubSegmentCountChanged) {
        m_changeTracker.axisZSubSegmentCountChanged = false;
        if (m_axisZ->type() & QAbstract3DAxis::AxisTypeValue) {
            QValue3DAxis *valueAxisZ = static_cast<QValue3DAxis *>(m_axisZ);
            m_renderer->updateAxisSubSegmentCount(QAbstract3DAxis::AxisOrientationZ,
                                                  valueAxisZ->subSegmentCount());
        }
    }

    if (m_changeTracker.axisXLabelFormatChanged) {
        m_changeTracker.axisXLabelFormatChanged = false;
        if (m_axisX->type() & QAbstract3DAxis::AxisTypeValue) {
            QValue3DAxis *valueAxisX = static_cast<QValue3DAxis *>(m_axisX);
            m_renderer->updateAxisLabelFormat(QAbstract3DAxis::AxisOrientationX,
                                              valueAxisX->labelFormat());
        }
    }

    if (m_changeTracker.axisYLabelFormatChanged) {
        m_changeTracker.axisYLabelFormatChanged = false;
        if (m_axisY->type() & QAbstract3DAxis::AxisTypeValue) {
            QValue3DAxis *valueAxisY = static_cast<QValue3DAxis *>(m_axisY);
            m_renderer->updateAxisLabelFormat(QAbstract3DAxis::AxisOrientationY,
                                              valueAxisY->labelFormat());
        }
    }

    if (m_changeTracker.axisZLabelFormatChanged) {
        m_changeTracker.axisZLabelFormatChanged = false;
        if (m_axisZ->type() & QAbstract3DAxis::AxisTypeValue) {
            QValue3DAxis *valueAxisZ = static_cast<QValue3DAxis *>(m_axisZ);
            m_renderer->updateAxisLabelFormat(QAbstract3DAxis::AxisOrientationZ,
                                              valueAxisZ->labelFormat());
        }
    }

    if (m_changeTracker.axisXReversedChanged) {
        m_changeTracker.axisXReversedChanged = false;
        if (m_axisX->type() & QAbstract3DAxis::AxisTypeValue) {
            QValue3DAxis *valueAxisX = static_cast<QValue3DAxis *>(m_axisX);
            m_renderer->updateAxisReversed(QAbstract3DAxis::AxisOrientationX,
                                           valueAxisX->reversed());
        }
    }

    if (m_changeTracker.axisYReversedChanged) {
        m_changeTracker.axisYReversedChanged = false;
        if (m_axisY->type() & QAbstract3DAxis::AxisTypeValue) {
            QValue3DAxis *valueAxisY = static_cast<QValue3DAxis *>(m_axisY);
            m_renderer->updateAxisReversed(QAbstract3DAxis::AxisOrientationY,
                                           valueAxisY->reversed());
        }
    }

    if (m_changeTracker.axisZReversedChanged) {
        m_changeTracker.axisZReversedChanged = false;
        if (m_axisZ->type() & QAbstract3DAxis::AxisTypeValue) {
            QValue3DAxis *valueAxisZ = static_cast<QValue3DAxis *>(m_axisZ);
            m_renderer->updateAxisReversed(QAbstract3DAxis::AxisOrientationZ,
                                           valueAxisZ->reversed());
        }
    }

    if (m_changeTracker.axisXLabelAutoRotationChanged) {
        m_renderer->updateAxisLabelAutoRotation(QAbstract3DAxis::AxisOrientationX,
                                                m_axisX->labelAutoRotation());
        m_changeTracker.axisXLabelAutoRotationChanged = false;
    }

    if (m_changeTracker.axisYLabelAutoRotationChanged) {
        m_renderer->updateAxisLabelAutoRotation(QAbstract3DAxis::AxisOrientationY,
                                                m_axisY->labelAutoRotation());
        m_changeTracker.axisYLabelAutoRotationChanged = false;
    }

    if (m_changeTracker.axisZLabelAutoRotationChanged) {
        m_renderer->updateAxisLabelAutoRotation(QAbstract3DAxis::AxisOrientationZ,
                                                m_axisZ->labelAutoRotation());
        m_changeTracker.axisZLabelAutoRotationChanged = false;
    }

    if (m_changeTracker.axisXTitleVisibilityChanged) {
        m_renderer->updateAxisTitleVisibility(QAbstract3DAxis::AxisOrientationX,
                                              m_axisX->isTitleVisible());
        m_changeTracker.axisXTitleVisibilityChanged = false;
    }
    if (m_changeTracker.axisYTitleVisibilityChanged) {
        m_renderer->updateAxisTitleVisibility(QAbstract3DAxis::AxisOrientationY,
                                              m_axisY->isTitleVisible());
        m_changeTracker.axisYTitleVisibilityChanged = false;
    }
    if (m_changeTracker.axisZTitleVisibilityChanged) {
        m_renderer->updateAxisTitleVisibility(QAbstract3DAxis::AxisOrientationZ,
                                              m_axisZ->isTitleVisible());
        m_changeTracker.axisZTitleVisibilityChanged = false;
    }
    if (m_changeTracker.axisXTitleFixedChanged) {
        m_renderer->updateAxisTitleFixed(QAbstract3DAxis::AxisOrientationX,
                                         m_axisX->isTitleFixed());
        m_changeTracker.axisXTitleFixedChanged = false;
    }
    if (m_changeTracker.axisYTitleFixedChanged) {
        m_renderer->updateAxisTitleFixed(QAbstract3DAxis::AxisOrientationY,
                                         m_axisY->isTitleFixed());
        m_changeTracker.axisYTitleFixedChanged = false;
    }
    if (m_changeTracker.axisZTitleFixedChanged) {
        m_renderer->updateAxisTitleFixed(QAbstract3DAxis::AxisOrientationZ,
                                         m_axisZ->isTitleFixed());
        m_changeTracker.axisZTitleFixedChanged = false;
    }

    if (m_changeTracker.marginChanged) {
        m_renderer->updateMargin(float(m_margin));
        m_changeTracker.marginChanged = false;
    }

    if (m_changedSeriesList.size()) {
        m_renderer->modifiedSeriesList(m_changedSeriesList);
        m_changedSeriesList.clear();
    }

    if (m_isSeriesVisualsDirty) {
        m_renderer->updateSeries(m_seriesList);
        m_isSeriesVisualsDirty = false;
    }

    if (m_isDataDirty) {
        // Series list supplied above in updateSeries() is used to access the data,
        // so no data needs to be passed in updateData()
        m_renderer->updateData();
        m_isDataDirty = false;
    }

    if (m_isCustomDataDirty) {
        m_renderer->updateCustomData(m_customItems);
        m_isCustomDataDirty = false;
    }

    if (m_isCustomItemDirty) {
        m_renderer->updateCustomItems();
        m_isCustomItemDirty = false;
    }
}

void Abstract3DController::render(const GLuint defaultFboHandle)
{
    QMutexLocker mutexLocker(&m_renderMutex);

    // If not initialized, do nothing.
    if (!m_renderer)
        return;

    if (m_measureFps) {
        // Measure speed (as milliseconds per frame)
        m_numFrames++;
        int elapsed = m_frameTimer.elapsed();
        if (elapsed >= 1000) {
            m_currentFps = qreal(m_numFrames) * 1000.0 / qreal(elapsed);
            emit currentFpsChanged(m_currentFps);
            m_numFrames = 0;
            m_frameTimer.start();
        }
        // To get meaningful framerate, don't just do render on demand.
        emitNeedRender();
    }

    m_renderer->render(defaultFboHandle);
}

void Abstract3DController::mouseDoubleClickEvent(QMouseEvent *event)
{
    if (m_activeInputHandler)
        m_activeInputHandler->mouseDoubleClickEvent(event);
}

void Abstract3DController::touchEvent(QTouchEvent *event)
{
    if (m_activeInputHandler)
        m_activeInputHandler->touchEvent(event);
}

void Abstract3DController::mousePressEvent(QMouseEvent *event, const QPoint &mousePos)
{
    if (m_activeInputHandler)
        m_activeInputHandler->mousePressEvent(event, mousePos);
}

void Abstract3DController::mouseReleaseEvent(QMouseEvent *event, const QPoint &mousePos)
{
    if (m_activeInputHandler)
        m_activeInputHandler->mouseReleaseEvent(event, mousePos);
}

void Abstract3DController::mouseMoveEvent(QMouseEvent *event, const QPoint &mousePos)
{
    if (m_activeInputHandler)
        m_activeInputHandler->mouseMoveEvent(event, mousePos);
}

#if QT_CONFIG(wheelevent)
void Abstract3DController::wheelEvent(QWheelEvent *event)
{
    if (m_activeInputHandler)
        m_activeInputHandler->wheelEvent(event);
}
#endif

void Abstract3DController::handleThemeColorStyleChanged(Q3DTheme::ColorStyle style)
{
    // Set value for series that have not explicitly set this value
    for (auto series: m_seriesList) {
        if (!series->d_ptr->m_themeTracker.colorStyleOverride) {
            series->setColorStyle(style);
            series->d_ptr->m_themeTracker.colorStyleOverride = false;
        }
    }
    markSeriesVisualsDirty();
}

void Abstract3DController::handleThemeBaseColorsChanged(const QList<QColor> &colors)
{
    int colorIdx = 0;
    // Set value for series that have not explicitly set this value
    for (auto series: m_seriesList) {
        if (!series->d_ptr->m_themeTracker.baseColorOverride) {
            series->setBaseColor(colors.at(colorIdx));
            series->d_ptr->m_themeTracker.baseColorOverride = false;
        }
        if (++colorIdx >= colors.size())
            colorIdx = 0;
    }
    markSeriesVisualsDirty();
}

void Abstract3DController::handleThemeBaseGradientsChanged(const QList<QLinearGradient> &gradients)
{
    int gradientIdx = 0;
    // Set value for series that have not explicitly set this value
    for (auto series: m_seriesList) {
        if (!series->d_ptr->m_themeTracker.baseGradientOverride) {
            series->setBaseGradient(gradients.at(gradientIdx));
            series->d_ptr->m_themeTracker.baseGradientOverride = false;
        }
        if (++gradientIdx >= gradients.size())
            gradientIdx = 0;
    }
    markSeriesVisualsDirty();
}

void Abstract3DController::handleThemeSingleHighlightColorChanged(const QColor &color)
{
    // Set value for series that have not explicitly set this value
    for (auto series: m_seriesList) {
        if (!series->d_ptr->m_themeTracker.singleHighlightColorOverride) {
            series->setSingleHighlightColor(color);
            series->d_ptr->m_themeTracker.singleHighlightColorOverride = false;
        }
    }
    markSeriesVisualsDirty();
}

void Abstract3DController::handleThemeSingleHighlightGradientChanged(
        const QLinearGradient &gradient)
{
    // Set value for series that have not explicitly set this value
    for (auto series: m_seriesList) {
        if (!series->d_ptr->m_themeTracker.singleHighlightGradientOverride) {
            series->setSingleHighlightGradient(gradient);
            series->d_ptr->m_themeTracker.singleHighlightGradientOverride = false;
        }
    }
    markSeriesVisualsDirty();
}

void Abstract3DController::handleThemeMultiHighlightColorChanged(const QColor &color)
{
    // Set value for series that have not explicitly set this value
    for (auto series: m_seriesList) {
        if (!series->d_ptr->m_themeTracker.multiHighlightColorOverride) {
            series->setMultiHighlightColor(color);
            series->d_ptr->m_themeTracker.multiHighlightColorOverride = false;
        }
    }
    markSeriesVisualsDirty();
}

void Abstract3DController::handleThemeMultiHighlightGradientChanged(const QLinearGradient &gradient)
{
    // Set value for series that have not explicitly set this value
    for (auto series: m_seriesList) {
        if (!series->d_ptr->m_themeTracker.multiHighlightGradientOverride) {
            series->setMultiHighlightGradient(gradient);
            series->d_ptr->m_themeTracker.multiHighlightGradientOverride = false;
        }
    }
    markSeriesVisualsDirty();
}

void Abstract3DController::handleThemeTypeChanged(Q3DTheme::Theme theme)
{
    Q_UNUSED(theme);

    if (!m_qml)
        return;

    // Changing theme type is logically equivalent of changing the entire theme
    // object, so reset all attached series to the new theme.
    bool force = m_qml->isReady();
    Q3DTheme *activeTheme = m_themeManager->activeTheme();
    for (int i = 0; i < m_seriesList.size(); i++)
        m_seriesList.at(i)->d_ptr->resetToTheme(*activeTheme, i, force);

    markSeriesVisualsDirty();
}

void Abstract3DController::setAxisX(QAbstract3DAxis *axis)
{
    // Setting null axis will always create new default axis
    if (!axis || axis != m_axisX) {
        setAxisHelper(QAbstract3DAxis::AxisOrientationX, axis, &m_axisX);
        emit axisXChanged(m_axisX);
    }
}

QAbstract3DAxis *Abstract3DController::axisX() const
{
    return m_axisX;
}

void Abstract3DController::setAxisY(QAbstract3DAxis *axis)
{
    // Setting null axis will always create new default axis
    if (!axis || axis != m_axisY) {
        setAxisHelper(QAbstract3DAxis::AxisOrientationY, axis, &m_axisY);
        emit axisYChanged(m_axisY);
    }
}

QAbstract3DAxis *Abstract3DController::axisY() const
{
    return m_axisY;
}

void Abstract3DController::setAxisZ(QAbstract3DAxis *axis)
{
    // Setting null axis will always create new default axis
    if (!axis || axis != m_axisZ) {
        setAxisHelper(QAbstract3DAxis::AxisOrientationZ, axis, &m_axisZ);
        emit axisZChanged(m_axisZ);
    }
}

QAbstract3DAxis *Abstract3DController::axisZ() const
{
    return m_axisZ;
}

void Abstract3DController::addAxis(QAbstract3DAxis *axis)
{
    Q_ASSERT(axis);
    Abstract3DController *owner = qobject_cast<Abstract3DController *>(axis->parent());
    if (owner != this) {
        Q_ASSERT_X(!owner, "addAxis", "Axis already attached to a graph.");
        axis->setParent(this);
    }
    if (!m_axes.contains(axis))
        m_axes.append(axis);
}

void Abstract3DController::releaseAxis(QAbstract3DAxis *axis)
{
    if (axis && m_axes.contains(axis)) {
        // Clear the default status from released default axes
        if (axis->d_ptr->isDefaultAxis())
            axis->d_ptr->setDefaultAxis(false);

        // If the axis is in use, replace it with a temporary one
        switch (axis->orientation()) {
        case QAbstract3DAxis::AxisOrientationX:
            setAxisX(0);
            break;
        case QAbstract3DAxis::AxisOrientationY:
            setAxisY(0);
            break;
        case QAbstract3DAxis::AxisOrientationZ:
            setAxisZ(0);
            break;
        default:
            break;
        }

        m_axes.removeAll(axis);
        axis->setParent(0);
    }
}

QList<QAbstract3DAxis *> Abstract3DController::axes() const
{
    return m_axes;
}

void Abstract3DController::addInputHandler(QAbstract3DInputHandler *inputHandler)
{
    Q_ASSERT(inputHandler);
    Abstract3DController *owner = qobject_cast<Abstract3DController *>(inputHandler->parent());
    if (owner != this) {
        Q_ASSERT_X(!owner, "addInputHandler",
                   "Input handler already attached to another component.");
        inputHandler->setParent(this);
    }

    if (!m_inputHandlers.contains(inputHandler))
        m_inputHandlers.append(inputHandler);
}

void Abstract3DController::releaseInputHandler(QAbstract3DInputHandler *inputHandler)
{
    if (inputHandler && m_inputHandlers.contains(inputHandler)) {
        // Clear the default status from released default input handler
        if (inputHandler->d_ptr->m_isDefaultHandler)
            inputHandler->d_ptr->m_isDefaultHandler = false;

        // If the input handler is in use, remove it
        if (m_activeInputHandler == inputHandler)
            setActiveInputHandler(0);

        m_inputHandlers.removeAll(inputHandler);
        inputHandler->setParent(0);
    }
}

void Abstract3DController::setActiveInputHandler(QAbstract3DInputHandler *inputHandler)
{
    if (inputHandler == m_activeInputHandler)
        return;

    // If existing input handler is the default input handler, delete it
    if (m_activeInputHandler) {
        if (m_activeInputHandler->d_ptr->m_isDefaultHandler) {
            m_inputHandlers.removeAll(m_activeInputHandler);
            delete m_activeInputHandler;
        } else {
            // Disconnect the old input handler
            m_activeInputHandler->setScene(0);
            QObject::disconnect(m_activeInputHandler, 0, this, 0);
        }
    }

    // Assume ownership and connect to this controller's scene
    if (inputHandler)
        addInputHandler(inputHandler);

    m_activeInputHandler = inputHandler;
    if (m_activeInputHandler) {
        m_activeInputHandler->setScene(m_scene);

        // Connect the input handler
        QObject::connect(m_activeInputHandler, &QAbstract3DInputHandler::inputViewChanged, this,
                         &Abstract3DController::handleInputViewChanged);
        QObject::connect(m_activeInputHandler, &QAbstract3DInputHandler::positionChanged, this,
                         &Abstract3DController::handleInputPositionChanged);
    }

    // Notify change of input handler
    emit activeInputHandlerChanged(m_activeInputHandler);
}

QAbstract3DInputHandler* Abstract3DController::activeInputHandler()
{
    return m_activeInputHandler;
}

QList<QAbstract3DInputHandler *> Abstract3DController::inputHandlers() const
{
    return m_inputHandlers;
}

void Abstract3DController::addTheme(Q3DTheme *theme)
{
    m_themeManager->addTheme(theme);
}

void Abstract3DController::releaseTheme(Q3DTheme *theme)
{
    Q3DTheme *oldTheme = m_themeManager->activeTheme();

    m_themeManager->releaseTheme(theme);

    if (oldTheme != m_themeManager->activeTheme())
        emit activeThemeChanged(m_themeManager->activeTheme());
}

QList<Q3DTheme *> Abstract3DController::themes() const
{
    return m_themeManager->themes();
}

void Abstract3DController::setActiveTheme(Q3DTheme *theme, bool force)
{
    if (theme != m_themeManager->activeTheme()) {
        m_themeManager->setActiveTheme(theme);
        m_changeTracker.themeChanged = true;
        // Default theme can be created by theme manager, so ensure we have correct theme
        Q3DTheme *newActiveTheme = m_themeManager->activeTheme();
        // Reset all attached series to the new theme
        for (int i = 0; i < m_seriesList.size(); i++)
            m_seriesList.at(i)->d_ptr->resetToTheme(*newActiveTheme, i, force);
        markSeriesVisualsDirty();
        emit activeThemeChanged(newActiveTheme);
    }
}

Q3DTheme *Abstract3DController::activeTheme() const
{
    return m_themeManager->activeTheme();
}

void Abstract3DController::setSelectionMode(QAbstract3DGraph::SelectionFlags mode)
{
    if (mode != m_selectionMode) {
        m_selectionMode = mode;
        m_changeTracker.selectionModeChanged = true;
        emit selectionModeChanged(mode);
        emitNeedRender();
    }
}

QAbstract3DGraph::SelectionFlags Abstract3DController::selectionMode() const
{
    return m_selectionMode;
}

void Abstract3DController::setShadowQuality(QAbstract3DGraph::ShadowQuality quality)
{
    if (!m_useOrthoProjection)
        doSetShadowQuality(quality);
}

void Abstract3DController::doSetShadowQuality(QAbstract3DGraph::ShadowQuality quality)
{
    if (quality != m_shadowQuality) {
        m_shadowQuality = quality;
        m_changeTracker.shadowQualityChanged = true;
        emit shadowQualityChanged(m_shadowQuality);
        emitNeedRender();
    }
}

QAbstract3DGraph::ShadowQuality Abstract3DController::shadowQuality() const
{
    return m_shadowQuality;
}

void Abstract3DController::setOptimizationHints(QAbstract3DGraph::OptimizationHints hints)
{
    if (hints != m_optimizationHints) {
        m_optimizationHints = hints;
        m_changeTracker.optimizationHintChanged = true;
        m_isDataDirty = true;
        emit optimizationHintsChanged(hints);
        emitNeedRender();
    }
}

QAbstract3DGraph::OptimizationHints Abstract3DController::optimizationHints() const
{
    return m_optimizationHints;
}

bool Abstract3DController::shadowsSupported() const
{
    return !isOpenGLES();
}

bool Abstract3DController::isSlicingActive() const
{
    return m_scene->isSlicingActive();
}

void Abstract3DController::setSlicingActive(bool isSlicing)
{
    m_scene->setSlicingActive(isSlicing);
}

Q3DScene *Abstract3DController::scene()
{
    return m_scene;
}

void Abstract3DController::markDataDirty()
{
    m_isDataDirty = true;

    markSeriesItemLabelsDirty();
    emitNeedRender();
}

void Abstract3DController::markSeriesVisualsDirty()
{
    m_isSeriesVisualsDirty = true;
    emitNeedRender();
}

void Abstract3DController::requestRender(QOpenGLFramebufferObject *fbo)
{
    QMutexLocker mutexLocker(&m_renderMutex);
    m_renderer->render(fbo->handle());
}

int Abstract3DController::addCustomItem(QCustom3DItem *item)
{
    if (!item)
        return -1;

    int index = m_customItems.indexOf(item);

    if (index != -1)
        return index;

    item->setParent(this);
    connect(item->d_ptr.data(), &QCustom3DItemPrivate::needUpdate,
            this, &Abstract3DController::updateCustomItem);
    m_customItems.append(item);
    item->d_ptr->resetDirtyBits();
    m_isCustomDataDirty = true;
    emitNeedRender();
    return m_customItems.size() - 1;
}

void Abstract3DController::deleteCustomItems()
{
    for (auto item: m_customItems)
        delete item;
    m_customItems.clear();
    m_isCustomDataDirty = true;
    emitNeedRender();
}

void Abstract3DController::deleteCustomItem(QCustom3DItem *item)
{
    if (!item)
        return;

    m_customItems.removeOne(item);
    delete item;
    item = 0;
    m_isCustomDataDirty = true;
    emitNeedRender();
}

void Abstract3DController::deleteCustomItem(const QVector3D &position)
{
    // Get the item for the position
    for (auto item: m_customItems) {
        if (item->position() == position)
            deleteCustomItem(item);
    }
}

void Abstract3DController::releaseCustomItem(QCustom3DItem *item)
{
    if (item && m_customItems.contains(item)) {
        disconnect(item->d_ptr.data(), &QCustom3DItemPrivate::needUpdate,
                   this, &Abstract3DController::updateCustomItem);
        m_customItems.removeOne(item);
        item->setParent(0);
        m_isCustomDataDirty = true;
        emitNeedRender();
    }
}

QList<QCustom3DItem *> Abstract3DController::customItems() const
{
    return m_customItems;
}

void Abstract3DController::updateCustomItem()
{
    m_isCustomItemDirty = true;
    emitNeedRender();
}

void Abstract3DController::handleAxisTitleChanged(const QString &title)
{
    Q_UNUSED(title);
    handleAxisTitleChangedBySender(sender());
}

void Abstract3DController::handleAxisTitleChangedBySender(QObject *sender)
{
    if (sender == m_axisX)
        m_changeTracker.axisXTitleChanged = true;
    else if (sender == m_axisY)
        m_changeTracker.axisYTitleChanged = true;
    else if (sender == m_axisZ)
        m_changeTracker.axisZTitleChanged = true;
    else
        qWarning() << __FUNCTION__ << "invoked for invalid axis";

    markSeriesItemLabelsDirty();
    emitNeedRender();
}

void Abstract3DController::handleAxisLabelsChanged()
{
    handleAxisLabelsChangedBySender(sender());
}

void Abstract3DController::handleAxisLabelsChangedBySender(QObject *sender)
{
    if (sender == m_axisX)
        m_changeTracker.axisXLabelsChanged = true;
    else if (sender == m_axisY)
        m_changeTracker.axisYLabelsChanged = true;
    else if (sender == m_axisZ)
        m_changeTracker.axisZLabelsChanged = true;
    else
        qWarning() << __FUNCTION__ << "invoked for invalid axis";

    markSeriesItemLabelsDirty();
    emitNeedRender();
}

void Abstract3DController::handleAxisRangeChanged(float min, float max)
{
    Q_UNUSED(min);
    Q_UNUSED(max);
    handleAxisRangeChangedBySender(sender());
}

void Abstract3DController::handleAxisRangeChangedBySender(QObject *sender)
{
    if (sender == m_axisX) {
        m_isDataDirty = true;
        m_changeTracker.axisXRangeChanged = true;
    } else if (sender == m_axisY) {
        m_isDataDirty = true;
        m_changeTracker.axisYRangeChanged = true;
    } else if (sender == m_axisZ) {
        m_isDataDirty = true;
        m_changeTracker.axisZRangeChanged = true;
    } else {
        qWarning() << __FUNCTION__ << "invoked for invalid axis";
    }
    emitNeedRender();
}

void Abstract3DController::handleAxisSegmentCountChanged(int count)
{
    Q_UNUSED(count);
    handleAxisSegmentCountChangedBySender(sender());
}

void Abstract3DController::handleAxisSegmentCountChangedBySender(QObject *sender)
{
    if (sender == m_axisX)
        m_changeTracker.axisXSegmentCountChanged = true;
    else if (sender == m_axisY)
        m_changeTracker.axisYSegmentCountChanged = true;
    else if (sender == m_axisZ)
        m_changeTracker.axisZSegmentCountChanged = true;
    else
        qWarning() << __FUNCTION__ << "invoked for invalid axis";
    emitNeedRender();
}

void Abstract3DController::handleAxisSubSegmentCountChanged(int count)
{
    Q_UNUSED(count);
    handleAxisSubSegmentCountChangedBySender(sender());
}

void Abstract3DController::handleAxisSubSegmentCountChangedBySender(QObject *sender)
{
    if (sender == m_axisX)
        m_changeTracker.axisXSubSegmentCountChanged = true;
    else if (sender == m_axisY)
        m_changeTracker.axisYSubSegmentCountChanged = true;
    else if (sender == m_axisZ)
        m_changeTracker.axisZSubSegmentCountChanged = true;
    else
        qWarning() << __FUNCTION__ << "invoked for invalid axis";
    emitNeedRender();
}

void Abstract3DController::handleAxisAutoAdjustRangeChanged(bool autoAdjust)
{
    QObject *sender = QObject::sender();
    if (sender != m_axisX && sender != m_axisY && sender != m_axisZ)
        return;

    QAbstract3DAxis *axis = static_cast<QAbstract3DAxis*>(sender);
    handleAxisAutoAdjustRangeChangedInOrientation(axis->orientation(), autoAdjust);
}

void Abstract3DController::handleAxisLabelFormatChanged(const QString &format)
{
    Q_UNUSED(format);
    handleAxisLabelFormatChangedBySender(sender());
}

void Abstract3DController::handleAxisReversedChanged(bool enable)
{
    Q_UNUSED(enable);
    handleAxisReversedChangedBySender(sender());
}

void Abstract3DController::handleAxisFormatterDirty()
{
    handleAxisFormatterDirtyBySender(sender());
}

void Abstract3DController::handleAxisLabelAutoRotationChanged(float angle)
{
    Q_UNUSED(angle);
    handleAxisLabelAutoRotationChangedBySender(sender());
}

void Abstract3DController::handleAxisTitleVisibilityChanged(bool visible)
{
    Q_UNUSED(visible);
    handleAxisTitleVisibilityChangedBySender(sender());
}

void Abstract3DController::handleAxisTitleFixedChanged(bool fixed)
{
    Q_UNUSED(fixed);
    handleAxisTitleFixedChangedBySender(sender());
}

void Abstract3DController::handleInputViewChanged(QAbstract3DInputHandler::InputView view)
{
    // When in automatic slicing mode, input view change to primary disables slice mode
    if (m_selectionMode.testFlag(QAbstract3DGraph::SelectionSlice)
            && view == QAbstract3DInputHandler::InputViewOnPrimary) {
        setSlicingActive(false);
    }

    emitNeedRender();
}

void Abstract3DController::handleInputPositionChanged(const QPoint &position)
{
    Q_UNUSED(position);
    emitNeedRender();
}

void Abstract3DController::handleSeriesVisibilityChanged(bool visible)
{
    Q_UNUSED(visible);

    handleSeriesVisibilityChangedBySender(sender());
}

void Abstract3DController::handleRequestShadowQuality(QAbstract3DGraph::ShadowQuality quality)
{
    setShadowQuality(quality);
}

void Abstract3DController::setMeasureFps(bool enable)
{
    if (m_measureFps != enable) {
        m_measureFps = enable;
        m_currentFps = 0.0;

        if (enable) {
            m_frameTimer.start();
            m_numFrames = -1;
            emitNeedRender();
        }
        emit measureFpsChanged(enable);
    }
}

void Abstract3DController::handleAxisLabelFormatChangedBySender(QObject *sender)
{
    // Label format changing needs to dirty the data so that labels are reset.
    if (sender == m_axisX) {
        m_isDataDirty = true;
        m_changeTracker.axisXLabelFormatChanged = true;
    } else if (sender == m_axisY) {
        m_isDataDirty = true;
        m_changeTracker.axisYLabelFormatChanged = true;
    } else if (sender == m_axisZ) {
        m_isDataDirty = true;
        m_changeTracker.axisZLabelFormatChanged = true;
    } else {
        qWarning() << __FUNCTION__ << "invoked for invalid axis";
    }
    emitNeedRender();
}

void Abstract3DController::handleAxisReversedChangedBySender(QObject *sender)
{
    // Reversing change needs to dirty the data so item positions are recalculated
    if (sender == m_axisX) {
        m_isDataDirty = true;
        m_changeTracker.axisXReversedChanged = true;
    } else if (sender == m_axisY) {
        m_isDataDirty = true;
        m_changeTracker.axisYReversedChanged = true;
    } else if (sender == m_axisZ) {
        m_isDataDirty = true;
        m_changeTracker.axisZReversedChanged = true;
    } else {
        qWarning() << __FUNCTION__ << "invoked for invalid axis";
    }
    emitNeedRender();
}

void Abstract3DController::handleAxisFormatterDirtyBySender(QObject *sender)
{
    // Sender is QValue3DAxisPrivate
    QValue3DAxis *valueAxis = static_cast<QValue3DAxisPrivate *>(sender)->qptr();
    if (valueAxis == m_axisX) {
        m_isDataDirty = true;
        m_changeTracker.axisXFormatterChanged = true;
    } else if (valueAxis == m_axisY) {
        m_isDataDirty = true;
        m_changeTracker.axisYFormatterChanged = true;
    } else if (valueAxis == m_axisZ) {
        m_isDataDirty = true;
        m_changeTracker.axisZFormatterChanged = true;
    } else {
        qWarning() << __FUNCTION__ << "invoked for invalid axis";
    }
    emitNeedRender();
}

void Abstract3DController::handleAxisLabelAutoRotationChangedBySender(QObject *sender)
{
    if (sender == m_axisX)
        m_changeTracker.axisXLabelAutoRotationChanged = true;
    else if (sender == m_axisY)
        m_changeTracker.axisYLabelAutoRotationChanged = true;
    else if (sender == m_axisZ)
        m_changeTracker.axisZLabelAutoRotationChanged = true;
    else
        qWarning() << __FUNCTION__ << "invoked for invalid axis";

    emitNeedRender();
}

void Abstract3DController::handleAxisTitleVisibilityChangedBySender(QObject *sender)
{
    if (sender == m_axisX)
        m_changeTracker.axisXTitleVisibilityChanged = true;
    else if (sender == m_axisY)
        m_changeTracker.axisYTitleVisibilityChanged = true;
    else if (sender == m_axisZ)
        m_changeTracker.axisZTitleVisibilityChanged = true;
    else
        qWarning() << __FUNCTION__ << "invoked for invalid axis";

    emitNeedRender();
}

void Abstract3DController::handleAxisTitleFixedChangedBySender(QObject *sender)
{
    if (sender == m_axisX)
        m_changeTracker.axisXTitleFixedChanged = true;
    else if (sender == m_axisY)
        m_changeTracker.axisYTitleFixedChanged = true;
    else if (sender == m_axisZ)
        m_changeTracker.axisZTitleFixedChanged = true;
    else
        qWarning() << __FUNCTION__ << "invoked for invalid axis";

    emitNeedRender();
}

void Abstract3DController::handleSeriesVisibilityChangedBySender(QObject *sender)
{
    QAbstract3DSeries *series = static_cast<QAbstract3DSeries *>(sender);
    series->d_ptr->m_changeTracker.visibilityChanged = true;

    m_isDataDirty = true;
    m_isSeriesVisualsDirty = true;

    adjustAxisRanges();

    emitNeedRender();
}

void Abstract3DController::markSeriesItemLabelsDirty()
{
    for (int i = 0; i < m_seriesList.size(); i++)
        m_seriesList.at(i)->d_ptr->markItemLabelDirty();
}

bool Abstract3DController::isOpenGLES() const
{
    return Utils::isOpenGLES();
}

void Abstract3DController::setAxisHelper(QAbstract3DAxis::AxisOrientation orientation,
                                         QAbstract3DAxis *axis, QAbstract3DAxis **axisPtr)
{
    // Setting null axis indicates using default axis
    if (!axis)
        axis = createDefaultAxis(orientation);

    // If old axis is default axis, delete it
    QAbstract3DAxis *oldAxis = *axisPtr;
    if (oldAxis) {
        if (oldAxis->d_ptr->isDefaultAxis()) {
            m_axes.removeAll(oldAxis);
            delete oldAxis;
            oldAxis = 0;
        } else {
            // Disconnect the old axis from use
            QObject::disconnect(oldAxis, 0, this, 0);
            oldAxis->d_ptr->setOrientation(QAbstract3DAxis::AxisOrientationNone);
        }
    }

    // Assume ownership
    addAxis(axis);

    // Connect the new axis
    *axisPtr = axis;

    axis->d_ptr->setOrientation(orientation);

    QObject::connect(axis, &QAbstract3DAxis::titleChanged,
                     this, &Abstract3DController::handleAxisTitleChanged);
    QObject::connect(axis, &QAbstract3DAxis::labelsChanged,
                     this, &Abstract3DController::handleAxisLabelsChanged);
    QObject::connect(axis, &QAbstract3DAxis::rangeChanged,
                     this, &Abstract3DController::handleAxisRangeChanged);
    QObject::connect(axis, &QAbstract3DAxis::autoAdjustRangeChanged,
                     this, &Abstract3DController::handleAxisAutoAdjustRangeChanged);
    QObject::connect(axis, &QAbstract3DAxis::labelAutoRotationChanged,
                     this, &Abstract3DController::handleAxisLabelAutoRotationChanged);
    QObject::connect(axis, &QAbstract3DAxis::titleVisibilityChanged,
                     this, &Abstract3DController::handleAxisTitleVisibilityChanged);
    QObject::connect(axis, &QAbstract3DAxis::titleFixedChanged,
                     this, &Abstract3DController::handleAxisTitleFixedChanged);

    if (orientation == QAbstract3DAxis::AxisOrientationX)
        m_changeTracker.axisXTypeChanged = true;
    else if (orientation == QAbstract3DAxis::AxisOrientationY)
        m_changeTracker.axisYTypeChanged = true;
    else if (orientation == QAbstract3DAxis::AxisOrientationZ)
        m_changeTracker.axisZTypeChanged = true;

    handleAxisTitleChangedBySender(axis);
    handleAxisLabelsChangedBySender(axis);
    handleAxisRangeChangedBySender(axis);
    handleAxisAutoAdjustRangeChangedInOrientation(axis->orientation(),
                                                  axis->isAutoAdjustRange());
    handleAxisLabelAutoRotationChangedBySender(axis);
    handleAxisTitleVisibilityChangedBySender(axis);
    handleAxisTitleFixedChangedBySender(axis);

    if (axis->type() & QAbstract3DAxis::AxisTypeValue) {
        QValue3DAxis *valueAxis = static_cast<QValue3DAxis *>(axis);
        QObject::connect(valueAxis, &QValue3DAxis::segmentCountChanged,
                         this, &Abstract3DController::handleAxisSegmentCountChanged);
        QObject::connect(valueAxis, &QValue3DAxis::subSegmentCountChanged,
                         this, &Abstract3DController::handleAxisSubSegmentCountChanged);
        QObject::connect(valueAxis, &QValue3DAxis::labelFormatChanged,
                         this, &Abstract3DController::handleAxisLabelFormatChanged);
        QObject::connect(valueAxis, &QValue3DAxis::reversedChanged,
                         this, &Abstract3DController::handleAxisReversedChanged);
        QObject::connect(valueAxis->dptr(), &QValue3DAxisPrivate::formatterDirty,
                         this, &Abstract3DController::handleAxisFormatterDirty);

        handleAxisSegmentCountChangedBySender(valueAxis);
        handleAxisSubSegmentCountChangedBySender(valueAxis);
        handleAxisLabelFormatChangedBySender(valueAxis);
        handleAxisReversedChangedBySender(valueAxis);
        handleAxisFormatterDirtyBySender(valueAxis->dptr());

        valueAxis->formatter()->setLocale(m_locale);
    }
}

QAbstract3DAxis *Abstract3DController::createDefaultAxis(
        QAbstract3DAxis::AxisOrientation orientation)
{
    Q_UNUSED(orientation);

    // The default default axis is a value axis. If the graph type has a different default axis
    // for some orientation, this function needs to be overridden.
    QAbstract3DAxis *defaultAxis = createDefaultValueAxis();
    return defaultAxis;
}

QValue3DAxis *Abstract3DController::createDefaultValueAxis()
{
    // Default value axis has single segment, empty label format, and auto scaling
    QValue3DAxis *defaultAxis = new QValue3DAxis;
    defaultAxis->d_ptr->setDefaultAxis(true);

    return defaultAxis;
}

QCategory3DAxis *Abstract3DController::createDefaultCategoryAxis()
{
    // Default category axis has no labels
    QCategory3DAxis *defaultAxis = new QCategory3DAxis;
    defaultAxis->d_ptr->setDefaultAxis(true);
    return defaultAxis;
}

void Abstract3DController::startRecordingRemovesAndInserts()
{
    // Default implementation does nothing
}

void Abstract3DController::emitNeedRender()
{
    if (!m_renderPending) {
        emit needRender();
        m_renderPending = true;
    }
}

void Abstract3DController::handlePendingClick()
{
    m_clickedType = m_renderer->clickedType();
    m_selectedLabelIndex = m_renderer->m_selectedLabelIndex;
    m_selectedCustomItemIndex = m_renderer->m_selectedCustomItemIndex;

    // Invalidate query position to indicate the query has been handled, unless another
    // point has been queried.
    if (m_renderer->cachedClickQuery() == m_scene->selectionQueryPosition())
        m_scene->setSelectionQueryPosition(Q3DScene::invalidSelectionPoint());

    m_renderer->clearClickQueryResolved();

    emit elementSelected(m_clickedType);
}

void Abstract3DController::handlePendingGraphPositionQuery()
{
    m_queriedGraphPosition = m_renderer->queriedGraphPosition();

    // Invalidate query position to indicate the query has been handled, unless another
    // point has been queried.
    if (m_renderer->cachedGraphPositionQuery() == m_scene->graphPositionQuery())
        m_scene->setGraphPositionQuery(Q3DScene::invalidSelectionPoint());

    m_renderer->clearGraphPositionQueryResolved();

    emit queriedGraphPositionChanged(m_queriedGraphPosition);
}

int Abstract3DController::selectedLabelIndex() const
{
    int index = m_selectedLabelIndex;
    QAbstract3DAxis *axis = selectedAxis();
    if (axis && axis->labels().size() <= index)
        index = -1;
    return index;
}

QAbstract3DAxis *Abstract3DController::selectedAxis() const
{
    QAbstract3DAxis *axis = 0;
    QAbstract3DGraph::ElementType type = m_clickedType;
    switch (type) {
    case QAbstract3DGraph::ElementAxisXLabel:
        axis = axisX();
        break;
    case QAbstract3DGraph::ElementAxisYLabel:
        axis = axisY();
        break;
    case QAbstract3DGraph::ElementAxisZLabel:
        axis = axisZ();
        break;
    default:
        axis = 0;
        break;
    }

    return axis;
}

int Abstract3DController::selectedCustomItemIndex() const
{
    int index = m_selectedCustomItemIndex;
    if (m_customItems.size() <= index)
        index = -1;
    return index;
}

QCustom3DItem *Abstract3DController::selectedCustomItem() const
{
    QCustom3DItem *item = 0;
    int index = selectedCustomItemIndex();
    if (index >= 0)
        item = m_customItems[index];
    return item;
}

QAbstract3DGraph::ElementType Abstract3DController::selectedElement() const
{
    return m_clickedType;
}

void Abstract3DController::setOrthoProjection(bool enable)
{
    if (enable != m_useOrthoProjection) {
        m_useOrthoProjection = enable;
        m_changeTracker.projectionChanged = true;
        emit orthoProjectionChanged(m_useOrthoProjection);
        // If changed to ortho, disable shadows
        if (m_useOrthoProjection)
            doSetShadowQuality(QAbstract3DGraph::ShadowQualityNone);
        emitNeedRender();
    }
}

bool Abstract3DController::isOrthoProjection() const
{
    return m_useOrthoProjection;
}

void Abstract3DController::setAspectRatio(qreal ratio)
{
    if (m_aspectRatio != ratio && ratio > 0) {
        m_aspectRatio = ratio;
        m_changeTracker.aspectRatioChanged = true;
        emit aspectRatioChanged(m_aspectRatio);
        m_isDataDirty = true;
        emitNeedRender();
    }
}

qreal Abstract3DController::aspectRatio()
{
    return m_aspectRatio;
}

void Abstract3DController::setHorizontalAspectRatio(qreal ratio)
{
    if (m_horizontalAspectRatio != ratio && ratio > 0) {
        m_horizontalAspectRatio = ratio;
        m_changeTracker.horizontalAspectRatioChanged = true;
        emit horizontalAspectRatioChanged(m_horizontalAspectRatio);
        m_isDataDirty = true;
        emitNeedRender();
    }
}

qreal Abstract3DController::horizontalAspectRatio() const
{
    return m_horizontalAspectRatio;
}

void Abstract3DController::setReflection(bool enable)
{
    if (m_reflectionEnabled != enable) {
        m_reflectionEnabled = enable;
        m_changeTracker.reflectionChanged = true;
        emit reflectionChanged(m_reflectionEnabled);
        emitNeedRender();
    }
}

bool Abstract3DController::reflection() const
{
    return m_reflectionEnabled;
}

void Abstract3DController::setReflectivity(qreal reflectivity)
{
    if (m_reflectivity != reflectivity && reflectivity > 0) {
        m_reflectivity = reflectivity;
        m_changeTracker.reflectivityChanged = true;
        emit reflectivityChanged(m_reflectivity);
        emitNeedRender();
    }
}

qreal Abstract3DController::reflectivity() const
{
    return m_reflectivity;
}

void Abstract3DController::setPolar(bool enable)
{
    if (enable != m_isPolar) {
        m_isPolar = enable;
        m_changeTracker.polarChanged = true;
        m_isDataDirty = true;
        emit polarChanged(m_isPolar);
        emitNeedRender();
    }
}

bool Abstract3DController::isPolar() const
{
    return m_isPolar;
}

void Abstract3DController::setRadialLabelOffset(float offset)
{
    if (m_radialLabelOffset != offset) {
        m_radialLabelOffset = offset;
        m_changeTracker.radialLabelOffsetChanged = true;
        emit radialLabelOffsetChanged(m_radialLabelOffset);
        emitNeedRender();
    }
}

float Abstract3DController::radialLabelOffset() const
{
    return m_radialLabelOffset;
}

void Abstract3DController::setLocale(const QLocale &locale)
{
    if (m_locale != locale) {
        m_locale = locale;

        // Value axis formatters need to be updated
        QValue3DAxis *axis = qobject_cast<QValue3DAxis *>(m_axisX);
        if (axis)
            axis->formatter()->setLocale(m_locale);
        axis = qobject_cast<QValue3DAxis *>(m_axisY);
        if (axis)
            axis->formatter()->setLocale(m_locale);
        axis = qobject_cast<QValue3DAxis *>(m_axisZ);
        if (axis)
            axis->formatter()->setLocale(m_locale);
        emit localeChanged(m_locale);
    }
}

QLocale Abstract3DController::locale() const
{
    return m_locale;
}

QVector3D Abstract3DController::queriedGraphPosition() const
{
    return m_queriedGraphPosition;
}

void Abstract3DController::setMargin(qreal margin)
{
    if (m_margin != margin) {
        m_margin = margin;
        m_changeTracker.marginChanged = true;
        emit marginChanged(margin);
        emitNeedRender();
    }
}

qreal Abstract3DController::margin() const
{
    return m_margin;
}


QT_END_NAMESPACE
