// 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 "abstract3drenderer_p.h"
#include "texturehelper_p.h"
#include "q3dcamera_p.h"
#include "q3dtheme_p.h"
#include "qvalue3daxisformatter_p.h"
#include "shaderhelper_p.h"
#include "qcustom3ditem_p.h"
#include "qcustom3dlabel_p.h"
#include "qcustom3dvolume_p.h"
#include "scatter3drenderer_p.h"

#include <QtCore/qmath.h>
#include <QtGui/QOffscreenSurface>
#include <QtCore/QThread>

QT_BEGIN_NAMESPACE

// Defined in shaderhelper.cpp
extern void discardDebugMsgs(QtMsgType type, const QMessageLogContext &context, const QString &msg);

const qreal doublePi(M_PI * 2.0);
const int polarGridRoundness(64);
const qreal polarGridAngle(doublePi / qreal(polarGridRoundness));
const float polarGridAngleDegrees(float(360.0 / qreal(polarGridRoundness)));
const qreal polarGridHalfAngle(polarGridAngle / 2.0);

Abstract3DRenderer::Abstract3DRenderer(Abstract3DController *controller)
    : QObject(0),
      m_hasNegativeValues(false),
      m_cachedTheme(new Q3DTheme()),
      m_drawer(new Drawer(m_cachedTheme)),
      m_cachedShadowQuality(QAbstract3DGraph::ShadowQualityMedium),
      m_autoScaleAdjustment(1.0f),
      m_cachedSelectionMode(QAbstract3DGraph::SelectionNone),
      m_cachedOptimizationHint(QAbstract3DGraph::OptimizationDefault),
      m_textureHelper(0),
      m_depthTexture(0),
      m_cachedScene(new Q3DScene()),
      m_selectionDirty(true),
      m_selectionState(SelectNone),
      m_devicePixelRatio(1.0f),
      m_selectionLabelDirty(true),
      m_clickResolved(false),
      m_graphPositionQueryPending(false),
      m_graphPositionQueryResolved(false),
      m_clickedSeries(0),
      m_clickedType(QAbstract3DGraph::ElementNone),
      m_selectedLabelIndex(-1),
      m_selectedCustomItemIndex(-1),
      m_selectionLabelItem(0),
      m_visibleSeriesCount(0),
      m_customItemShader(0),
      m_volumeTextureShader(0),
      m_volumeTextureLowDefShader(0),
      m_volumeTextureSliceShader(0),
      m_volumeSliceFrameShader(0),
      m_labelShader(0),
      m_cursorPositionShader(0),
      m_cursorPositionFrameBuffer(0),
      m_cursorPositionTexture(0),
      m_useOrthoProjection(false),
      m_xFlipped(false),
      m_yFlipped(false),
      m_zFlipped(false),
      m_yFlippedForGrid(false),
      m_backgroundObj(0),
      m_gridLineObj(0),
      m_labelObj(0),
      m_positionMapperObj(0),
      m_graphAspectRatio(2.0f),
      m_graphHorizontalAspectRatio(0.0f),
      m_polarGraph(false),
      m_radialLabelOffset(1.0f),
      m_polarRadius(2.0f),
      m_xRightAngleRotation(QQuaternion::fromAxisAndAngle(1.0f, 0.0f, 0.0f, 90.0f)),
      m_yRightAngleRotation(QQuaternion::fromAxisAndAngle(0.0f, 1.0f, 0.0f, 90.0f)),
      m_zRightAngleRotation(QQuaternion::fromAxisAndAngle(0.0f, 0.0f, 1.0f, 90.0f)),
      m_xRightAngleRotationNeg(QQuaternion::fromAxisAndAngle(1.0f, 0.0f, 0.0f, -90.0f)),
      m_yRightAngleRotationNeg(QQuaternion::fromAxisAndAngle(0.0f, 1.0f, 0.0f, -90.0f)),
      m_zRightAngleRotationNeg(QQuaternion::fromAxisAndAngle(0.0f, 0.0f, 1.0f, -90.0f)),
      m_xFlipRotation(QQuaternion::fromAxisAndAngle(1.0f, 0.0f, 0.0f, -180.0f)),
      m_zFlipRotation(QQuaternion::fromAxisAndAngle(0.0f, 0.0f, 1.0f, -180.0f)),
      m_requestedMargin(-1.0f),
      m_vBackgroundMargin(0.1f),
      m_hBackgroundMargin(0.1f),
      m_scaleXWithBackground(0.0f),
      m_scaleYWithBackground(0.0f),
      m_scaleZWithBackground(0.0f),
      m_oldCameraTarget(QVector3D(2000.0f, 2000.0f, 2000.0f)), // Just random invalid target
      m_reflectionEnabled(false),
      m_reflectivity(0.5),
#if !QT_CONFIG(opengles2)
      m_funcs_2_1(0),
#endif
      m_context(0),
      m_isOpenGLES(true)

{
    initializeOpenGLFunctions();
    m_isOpenGLES = Utils::isOpenGLES();
#if !QT_CONFIG(opengles2)
    if (!m_isOpenGLES) {
        // Discard warnings about deprecated functions
        QtMessageHandler handler = qInstallMessageHandler(discardDebugMsgs);

        m_funcs_2_1 = new QOpenGLFunctions_2_1;
        if (m_funcs_2_1)
            m_funcs_2_1->initializeOpenGLFunctions();

        // Restore original message handler
        qInstallMessageHandler(handler);

        if (!m_funcs_2_1)
            qFatal("OpenGL version is too low, at least 2.1 is required");
    }
#endif
    QObject::connect(m_drawer, &Drawer::drawerChanged, this, &Abstract3DRenderer::updateTextures);
    QObject::connect(this, &Abstract3DRenderer::needRender, controller,
                     &Abstract3DController::needRender, Qt::QueuedConnection);
    QObject::connect(this, &Abstract3DRenderer::requestShadowQuality, controller,
                     &Abstract3DController::handleRequestShadowQuality, Qt::QueuedConnection);
}

Abstract3DRenderer::~Abstract3DRenderer()
{
    contextCleanup();
    delete m_drawer;
    delete m_cachedScene;
    delete m_cachedTheme;
    delete m_selectionLabelItem;
    delete m_customItemShader;
    delete m_volumeTextureShader;
    delete m_volumeTextureLowDefShader;
    delete m_volumeSliceFrameShader;
    delete m_volumeTextureSliceShader;
    delete m_labelShader;
    delete m_cursorPositionShader;

    for (auto cache: m_renderCacheList) {
        cache->cleanup(m_textureHelper);
        delete cache;
    }
    m_renderCacheList.clear();

    for (auto item: m_customRenderCache) {
        GLuint texture = item->texture();
        m_textureHelper->deleteTexture(&texture);
        delete item;
    }
    m_customRenderCache.clear();

    ObjectHelper::releaseObjectHelper(this, m_backgroundObj);
    ObjectHelper::releaseObjectHelper(this, m_gridLineObj);
    ObjectHelper::releaseObjectHelper(this, m_labelObj);
    ObjectHelper::releaseObjectHelper(this, m_positionMapperObj);

    if (m_textureHelper) {
        m_textureHelper->deleteTexture(&m_depthTexture);
        m_textureHelper->deleteTexture(&m_cursorPositionTexture);
        delete m_textureHelper;
    }

    m_axisCacheX.clearLabels();
    m_axisCacheY.clearLabels();
    m_axisCacheZ.clearLabels();

#if !QT_CONFIG(opengles2)
    delete m_funcs_2_1;
#endif
}

void Abstract3DRenderer::contextCleanup()
{
    if (QOpenGLContext::currentContext())
        m_textureHelper->glDeleteFramebuffers(1, &m_cursorPositionFrameBuffer);
}

void Abstract3DRenderer::initializeOpenGL()
{
    m_context = QOpenGLContext::currentContext();

    // Set OpenGL features
    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LESS);
    glEnable(GL_CULL_FACE);
    glCullFace(GL_BACK);

#if !QT_CONFIG(opengles2)
    if (!m_isOpenGLES) {
        glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
        glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
        glHint(GL_POLYGON_SMOOTH_HINT, GL_NICEST);
    }
#endif

    m_textureHelper = new TextureHelper();
    m_drawer->initializeOpenGL();

    axisCacheForOrientation(QAbstract3DAxis::AxisOrientationX).setDrawer(m_drawer);
    axisCacheForOrientation(QAbstract3DAxis::AxisOrientationY).setDrawer(m_drawer);
    axisCacheForOrientation(QAbstract3DAxis::AxisOrientationZ).setDrawer(m_drawer);

    initLabelShaders(QStringLiteral(":/shaders/vertexLabel"),
                     QStringLiteral(":/shaders/fragmentLabel"));

    initCursorPositionShaders(QStringLiteral(":/shaders/vertexPosition"),
                              QStringLiteral(":/shaders/fragmentPositionMap"));

    loadLabelMesh();
    loadPositionMapperMesh();

    QObject::connect(m_context.data(), &QOpenGLContext::aboutToBeDestroyed,
                     this, &Abstract3DRenderer::contextCleanup);
}

void Abstract3DRenderer::render(const GLuint defaultFboHandle)
{
    if (defaultFboHandle) {
        glDepthMask(true);
        glEnable(GL_DEPTH_TEST);
        glDepthFunc(GL_LESS);
        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glDisable(GL_BLEND); // For QtQuick2 blending is enabled by default, but we don't want it to be
    }

    // Clear the graph background to the theme color
    glViewport(m_viewport.x(),
               m_viewport.y(),
               m_viewport.width(),
               m_viewport.height());
    glScissor(m_viewport.x(),
              m_viewport.y(),
              m_viewport.width(),
              m_viewport.height());
    glEnable(GL_SCISSOR_TEST);
    QVector4D clearColor = Utils::vectorFromColor(m_cachedTheme->windowColor());
    glClearColor(clearColor.x(), clearColor.y(), clearColor.z(), 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
    glDisable(GL_SCISSOR_TEST);
}

void Abstract3DRenderer::updateSelectionState(SelectionState state)
{
    m_selectionState = state;
}

void Abstract3DRenderer::initGradientShaders(const QString &vertexShader,
                                             const QString &fragmentShader)
{
    // Do nothing by default
    Q_UNUSED(vertexShader);
    Q_UNUSED(fragmentShader);
}

void Abstract3DRenderer::initStaticSelectedItemShaders(const QString &vertexShader,
                                                       const QString &fragmentShader,
                                                       const QString &gradientVertexShader,
                                                       const QString &gradientFragmentShader)
{
    // Do nothing by default
    Q_UNUSED(vertexShader);
    Q_UNUSED(fragmentShader);
    Q_UNUSED(gradientVertexShader);
    Q_UNUSED(gradientFragmentShader);
}

void Abstract3DRenderer::initCustomItemShaders(const QString &vertexShader,
                                               const QString &fragmentShader)
{
    delete m_customItemShader;
    m_customItemShader = new ShaderHelper(this, vertexShader, fragmentShader);
    m_customItemShader->initialize();
}

void Abstract3DRenderer::initVolumeTextureShaders(const QString &vertexShader,
                                                  const QString &fragmentShader,
                                                  const QString &fragmentLowDefShader,
                                                  const QString &sliceShader,
                                                  const QString &sliceFrameVertexShader,
                                                  const QString &sliceFrameShader)
{

    delete m_volumeTextureShader;
    m_volumeTextureShader = new ShaderHelper(this, vertexShader, fragmentShader);
    m_volumeTextureShader->initialize();

    delete m_volumeTextureLowDefShader;
    m_volumeTextureLowDefShader = new ShaderHelper(this, vertexShader, fragmentLowDefShader);
    m_volumeTextureLowDefShader->initialize();

    delete m_volumeTextureSliceShader;
    m_volumeTextureSliceShader = new ShaderHelper(this, vertexShader, sliceShader);
    m_volumeTextureSliceShader->initialize();

    delete m_volumeSliceFrameShader;
    m_volumeSliceFrameShader = new ShaderHelper(this, sliceFrameVertexShader, sliceFrameShader);
    m_volumeSliceFrameShader->initialize();
}

void Abstract3DRenderer::initLabelShaders(const QString &vertexShader, const QString &fragmentShader)
{
    delete m_labelShader;
    m_labelShader = new ShaderHelper(this, vertexShader, fragmentShader);
    m_labelShader->initialize();
}

void Abstract3DRenderer::initCursorPositionShaders(const QString &vertexShader,
                                                   const QString &fragmentShader)
{
    // Init the shader
    delete m_cursorPositionShader;
    m_cursorPositionShader = new ShaderHelper(this, vertexShader, fragmentShader);
    m_cursorPositionShader->initialize();
}

void Abstract3DRenderer::initCursorPositionBuffer()
{
    m_textureHelper->deleteTexture(&m_cursorPositionTexture);
    m_textureHelper->glDeleteFramebuffers(1, &m_cursorPositionFrameBuffer);
    m_cursorPositionFrameBuffer = 0;

    if (m_primarySubViewport.size().isEmpty())
        return;

    m_cursorPositionTexture =
            m_textureHelper->createCursorPositionTexture(m_primarySubViewport.size(),
                                                         m_cursorPositionFrameBuffer);
}

void Abstract3DRenderer::updateTheme(Q3DTheme *theme)
{
    // Synchronize the controller theme with renderer
    bool updateDrawer = theme->d_ptr->sync(*m_cachedTheme->d_ptr);

    if (updateDrawer)
        m_drawer->setTheme(m_cachedTheme);
}

void Abstract3DRenderer::updateScene(Q3DScene *scene)
{
    m_viewport = scene->d_ptr->glViewport();
    m_secondarySubViewport = scene->d_ptr->glSecondarySubViewport();

    if (m_primarySubViewport != scene->d_ptr->glPrimarySubViewport()) {
        // Resize of primary subviewport means resizing shadow and selection buffers
        m_primarySubViewport = scene->d_ptr->glPrimarySubViewport();
        handleResize();
    }

    if (m_devicePixelRatio != scene->devicePixelRatio()) {
        m_devicePixelRatio = scene->devicePixelRatio();
        handleResize();
    }

    QPoint logicalPixelPosition = scene->selectionQueryPosition();
    m_inputPosition = QPoint(logicalPixelPosition.x() * m_devicePixelRatio,
                             logicalPixelPosition.y() * m_devicePixelRatio);

    QPoint logicalGraphPosition = scene->graphPositionQuery();
    m_graphPositionQuery = QPoint(logicalGraphPosition.x() * m_devicePixelRatio,
                                  logicalGraphPosition.y() * m_devicePixelRatio);

    // Synchronize the renderer scene to controller scene
    scene->d_ptr->sync(*m_cachedScene->d_ptr);

    updateCameraViewport();

    if (Q3DScene::invalidSelectionPoint() == logicalPixelPosition) {
        updateSelectionState(SelectNone);
    } else {
        if (scene->isSlicingActive()) {
            if (scene->isPointInPrimarySubView(logicalPixelPosition))
                updateSelectionState(SelectOnOverview);
            else if (scene->isPointInSecondarySubView(logicalPixelPosition))
                updateSelectionState(SelectOnSlice);
            else
                updateSelectionState(SelectNone);
        } else {
            updateSelectionState(SelectOnScene);
        }
    }

    if (Q3DScene::invalidSelectionPoint() != logicalGraphPosition)
        m_graphPositionQueryPending = true;

    // Queue up another render when we have a query that needs resolving.
    // This is needed because QtQuick scene graph can sometimes do a sync without following it up
    // with a render.
    if (m_graphPositionQueryPending || m_selectionState != SelectNone)
        emit needRender();
}

void Abstract3DRenderer::updateTextures()
{
    m_axisCacheX.updateTextures();
    m_axisCacheY.updateTextures();
    m_axisCacheZ.updateTextures();
}

void Abstract3DRenderer::reInitShaders()
{
    if (!m_isOpenGLES) {
        if (m_cachedShadowQuality > QAbstract3DGraph::ShadowQualityNone) {
            if (m_cachedOptimizationHint.testFlag(QAbstract3DGraph::OptimizationStatic)
                    && qobject_cast<Scatter3DRenderer *>(this)) {
                initGradientShaders(QStringLiteral(":/shaders/vertexShadow"),
                                    QStringLiteral(":/shaders/fragmentShadow"));
                initStaticSelectedItemShaders(QStringLiteral(":/shaders/vertexShadow"),
                                              QStringLiteral(":/shaders/fragmentShadowNoTex"),
                                              QStringLiteral(":/shaders/vertexShadow"),
                                              QStringLiteral(":/shaders/fragmentShadowNoTexColorOnY"));
                initShaders(QStringLiteral(":/shaders/vertexShadowNoMatrices"),
                            QStringLiteral(":/shaders/fragmentShadowNoTex"));
            } else {
                initGradientShaders(QStringLiteral(":/shaders/vertexShadow"),
                                    QStringLiteral(":/shaders/fragmentShadowNoTexColorOnY"));
                initShaders(QStringLiteral(":/shaders/vertexShadow"),
                            QStringLiteral(":/shaders/fragmentShadowNoTex"));
            }
            initBackgroundShaders(QStringLiteral(":/shaders/vertexShadow"),
                                  QStringLiteral(":/shaders/fragmentShadowNoTex"));
            initCustomItemShaders(QStringLiteral(":/shaders/vertexShadow"),
                                  QStringLiteral(":/shaders/fragmentShadow"));
        } else {
            if (m_cachedOptimizationHint.testFlag(QAbstract3DGraph::OptimizationStatic)
                    && qobject_cast<Scatter3DRenderer *>(this)) {
                initGradientShaders(QStringLiteral(":/shaders/vertexTexture"),
                                    QStringLiteral(":/shaders/fragmentTexture"));
                initStaticSelectedItemShaders(QStringLiteral(":/shaders/vertex"),
                                              QStringLiteral(":/shaders/fragment"),
                                              QStringLiteral(":/shaders/vertex"),
                                              QStringLiteral(":/shaders/fragmentColorOnY"));
                initShaders(QStringLiteral(":/shaders/vertexNoMatrices"),
                            QStringLiteral(":/shaders/fragment"));
            } else {
                initGradientShaders(QStringLiteral(":/shaders/vertex"),
                                    QStringLiteral(":/shaders/fragmentColorOnY"));
                initShaders(QStringLiteral(":/shaders/vertex"),
                            QStringLiteral(":/shaders/fragment"));
            }
            initBackgroundShaders(QStringLiteral(":/shaders/vertex"),
                                  QStringLiteral(":/shaders/fragment"));
            initCustomItemShaders(QStringLiteral(":/shaders/vertexTexture"),
                                  QStringLiteral(":/shaders/fragmentTexture"));
        }
        initVolumeTextureShaders(QStringLiteral(":/shaders/vertexTexture3D"),
                                 QStringLiteral(":/shaders/fragmentTexture3D"),
                                 QStringLiteral(":/shaders/fragmentTexture3DLowDef"),
                                 QStringLiteral(":/shaders/fragmentTexture3DSlice"),
                                 QStringLiteral(":/shaders/vertexPosition"),
                                 QStringLiteral(":/shaders/fragment3DSliceFrames"));
    } else  {
        if (m_cachedOptimizationHint.testFlag(QAbstract3DGraph::OptimizationStatic)
                && qobject_cast<Scatter3DRenderer *>(this)) {
            initGradientShaders(QStringLiteral(":/shaders/vertexTexture"),
                                QStringLiteral(":/shaders/fragmentTextureES2"));
            initStaticSelectedItemShaders(QStringLiteral(":/shaders/vertex"),
                                          QStringLiteral(":/shaders/fragmentES2"),
                                          QStringLiteral(":/shaders/vertex"),
                                          QStringLiteral(":/shaders/fragmentColorOnYES2"));
            initShaders(QStringLiteral(":/shaders/vertexNoMatrices"),
                        QStringLiteral(":/shaders/fragmentES2"));
        } else {
            initGradientShaders(QStringLiteral(":/shaders/vertex"),
                                QStringLiteral(":/shaders/fragmentColorOnYES2"));
            initShaders(QStringLiteral(":/shaders/vertex"),
                        QStringLiteral(":/shaders/fragmentES2"));
        }
        initBackgroundShaders(QStringLiteral(":/shaders/vertex"),
                              QStringLiteral(":/shaders/fragmentES2"));
        initCustomItemShaders(QStringLiteral(":/shaders/vertexTexture"),
                              QStringLiteral(":/shaders/fragmentTextureES2"));
    }
}

void Abstract3DRenderer::handleShadowQualityChange()
{
    reInitShaders();

    if (m_cachedScene->activeLight()->isAutoPosition()
            || m_cachedShadowQuality > QAbstract3DGraph::ShadowQualityNone) {
        m_cachedScene->d_ptr->setLightPositionRelativeToCamera(defaultLightPos);
        emit needRender();
    }
    if (m_isOpenGLES && m_cachedShadowQuality != QAbstract3DGraph::ShadowQualityNone) {
        emit requestShadowQuality(QAbstract3DGraph::ShadowQualityNone);
        qWarning("Shadows are not yet supported for OpenGL ES2");
        m_cachedShadowQuality = QAbstract3DGraph::ShadowQualityNone;
    }
}

void Abstract3DRenderer::updateSelectionMode(QAbstract3DGraph::SelectionFlags mode)
{
    m_cachedSelectionMode = mode;
    m_selectionDirty = true;
}

void Abstract3DRenderer::updateAspectRatio(float ratio)
{
    m_graphAspectRatio = ratio;
    for (auto cache: m_renderCacheList)
        cache->setDataDirty(true);
}

void Abstract3DRenderer::updateHorizontalAspectRatio(float ratio)
{
    m_graphHorizontalAspectRatio = ratio;
    for (auto cache: m_renderCacheList)
        cache->setDataDirty(true);
}

void Abstract3DRenderer::updatePolar(bool enable)
{
    m_polarGraph = enable;
    for (auto cache: m_renderCacheList)
        cache->setDataDirty(true);
}

void Abstract3DRenderer::updateRadialLabelOffset(float offset)
{
    m_radialLabelOffset = offset;
}

void Abstract3DRenderer::updateMargin(float margin)
{
    m_requestedMargin = margin;
}

void Abstract3DRenderer::updateOptimizationHint(QAbstract3DGraph::OptimizationHints hint)
{
    m_cachedOptimizationHint = hint;
    for (auto cache: m_renderCacheList)
        cache->setDataDirty(true);
}

void Abstract3DRenderer::handleResize()
{
    if (m_primarySubViewport.width() == 0 || m_primarySubViewport.height() == 0)
        return;

    // Recalculate zoom
    calculateZoomLevel();

    // Re-init selection buffer
    initSelectionBuffer();

    // Re-init depth buffer
    updateDepthBuffer();

    initCursorPositionBuffer();
}

void Abstract3DRenderer::calculateZoomLevel()
{
    // Calculate zoom level based on aspect ratio
    GLfloat div;
    GLfloat zoomAdjustment;
    div = qMin(m_primarySubViewport.width(), m_primarySubViewport.height());
    zoomAdjustment = defaultRatio
            * ((m_primarySubViewport.width() / div)
               / (m_primarySubViewport.height() / div));
    m_autoScaleAdjustment = qMin(zoomAdjustment, 1.0f); // clamp to 1.0f
}

void Abstract3DRenderer::updateAxisType(QAbstract3DAxis::AxisOrientation orientation,
                                        QAbstract3DAxis::AxisType type)
{
    axisCacheForOrientation(orientation).setType(type);
}

void Abstract3DRenderer::updateAxisTitle(QAbstract3DAxis::AxisOrientation orientation,
                                         const QString &title)
{
    axisCacheForOrientation(orientation).setTitle(title);
}

void Abstract3DRenderer::updateAxisLabels(QAbstract3DAxis::AxisOrientation orientation,
                                          const QStringList &labels)
{
    axisCacheForOrientation(orientation).setLabels(labels);
}

void Abstract3DRenderer::updateAxisRange(QAbstract3DAxis::AxisOrientation orientation,
                                         float min, float max)
{
    AxisRenderCache &cache = axisCacheForOrientation(orientation);
    cache.setMin(min);
    cache.setMax(max);

    for (auto cache: m_renderCacheList)
        cache->setDataDirty(true);
}

void Abstract3DRenderer::updateAxisSegmentCount(QAbstract3DAxis::AxisOrientation orientation,
                                                int count)
{
    AxisRenderCache &cache = axisCacheForOrientation(orientation);
    cache.setSegmentCount(count);
}

void Abstract3DRenderer::updateAxisSubSegmentCount(QAbstract3DAxis::AxisOrientation orientation,
                                                   int count)
{
    AxisRenderCache &cache = axisCacheForOrientation(orientation);
    cache.setSubSegmentCount(count);
}

void Abstract3DRenderer::updateAxisLabelFormat(QAbstract3DAxis::AxisOrientation orientation,
                                               const QString &format)
{
    axisCacheForOrientation(orientation).setLabelFormat(format);
}

void Abstract3DRenderer::updateAxisReversed(QAbstract3DAxis::AxisOrientation orientation,
                                            bool enable)
{
    axisCacheForOrientation(orientation).setReversed(enable);
    for (auto cache: m_renderCacheList)
        cache->setDataDirty(true);
}

void Abstract3DRenderer::updateAxisFormatter(QAbstract3DAxis::AxisOrientation orientation,
                                             QValue3DAxisFormatter *formatter)
{
    AxisRenderCache &cache = axisCacheForOrientation(orientation);
    if (cache.ctrlFormatter() != formatter) {
        delete cache.formatter();
        cache.setFormatter(formatter->createNewInstance());
        cache.setCtrlFormatter(formatter);
    }
    formatter->d_ptr->populateCopy(*(cache.formatter()));
    cache.markPositionsDirty();

    for (auto cache: m_renderCacheList)
        cache->setDataDirty(true);
}

void Abstract3DRenderer::updateAxisLabelAutoRotation(QAbstract3DAxis::AxisOrientation orientation,
                                                     float angle)
{
    AxisRenderCache &cache = axisCacheForOrientation(orientation);
    if (cache.labelAutoRotation() != angle)
        cache.setLabelAutoRotation(angle);
}

void Abstract3DRenderer::updateAxisTitleVisibility(QAbstract3DAxis::AxisOrientation orientation,
                                                   bool visible)
{
    AxisRenderCache &cache = axisCacheForOrientation(orientation);
    if (cache.isTitleVisible() != visible)
        cache.setTitleVisible(visible);
}

void Abstract3DRenderer::updateAxisTitleFixed(QAbstract3DAxis::AxisOrientation orientation,
                                              bool fixed)
{
    AxisRenderCache &cache = axisCacheForOrientation(orientation);
    if (cache.isTitleFixed() != fixed)
        cache.setTitleFixed(fixed);
}

void Abstract3DRenderer::modifiedSeriesList(const QList<QAbstract3DSeries *> &seriesList)
{
    for (auto series: seriesList) {
        SeriesRenderCache *cache = m_renderCacheList.value(series, 0);
        if (cache)
            cache->setDataDirty(true);
    }
}

void Abstract3DRenderer::fixMeshFileName(QString &fileName, QAbstract3DSeries::Mesh mesh)
{
    // Default implementation does nothing.
    Q_UNUSED(fileName);
    Q_UNUSED(mesh);
}

void Abstract3DRenderer::updateSeries(const QList<QAbstract3DSeries *> &seriesList)
{
    for (auto cache: m_renderCacheList)
        cache->setValid(false);

    m_visibleSeriesCount = 0;
    int seriesCount = seriesList.size();
    for (int i = 0; i < seriesCount; i++) {
        QAbstract3DSeries *series = seriesList.at(i);
        SeriesRenderCache *cache = m_renderCacheList.value(series);
        bool newSeries = false;
        if (!cache) {
            cache = createNewCache(series);
            m_renderCacheList[series] = cache;
            newSeries = true;
        }
        cache->setValid(true);
        cache->populate(newSeries);
        if (cache->isVisible())
            m_visibleSeriesCount++;
    }

    // Remove non-valid objects from the cache list
    for (auto cache: m_renderCacheList) {
        if (!cache->isValid())
            cleanCache(cache);
    }
}

void Abstract3DRenderer::updateCustomData(const QList<QCustom3DItem *> &customItems)
{
    if (customItems.isEmpty() && m_customRenderCache.isEmpty())
        return;

    for (auto item: m_customRenderCache)
        item->setValid(false);

    int itemCount = customItems.size();
    // Check custom item list for items that are not yet in render item cache
    for (int i = 0; i < itemCount; i++) {
        QCustom3DItem *item = customItems.at(i);
        CustomRenderItem *renderItem = m_customRenderCache.value(item);
        if (!renderItem)
            renderItem = addCustomItem(item);
        if (renderItem) {
            renderItem->setValid(true);
            renderItem->setIndex(i); // always update index, as it must match the custom item index
        }
    }

    // Check render item cache and remove items that are not in customItems list anymore
    for (auto renderItem: m_customRenderCache) {
        if (!renderItem->isValid()) {
            m_customRenderCache.remove(renderItem->itemPointer());
            GLuint texture = renderItem->texture();
            m_textureHelper->deleteTexture(&texture);
            delete renderItem;
        }
    }

    m_customItemDrawOrder.clear();
    m_customItemDrawOrder = QList<QCustom3DItem *>(customItems);
}

void Abstract3DRenderer::updateCustomItems()
{
    // Check all items
    for (auto item: m_customRenderCache)
        updateCustomItem(item);
}

SeriesRenderCache *Abstract3DRenderer::createNewCache(QAbstract3DSeries *series)
{
    return new SeriesRenderCache(series, this);
}

void Abstract3DRenderer::cleanCache(SeriesRenderCache *cache)
{
    m_renderCacheList.remove(cache->series());
    cache->cleanup(m_textureHelper);
    delete cache;
}

AxisRenderCache &Abstract3DRenderer::axisCacheForOrientation(
        QAbstract3DAxis::AxisOrientation orientation)
{
    switch (orientation) {
    case QAbstract3DAxis::AxisOrientationX:
        return m_axisCacheX;
    case QAbstract3DAxis::AxisOrientationY:
        return m_axisCacheY;
    case QAbstract3DAxis::AxisOrientationZ:
        return m_axisCacheZ;
    default:
        qFatal("Abstract3DRenderer::axisCacheForOrientation");
        return m_axisCacheX;
    }
}

void Abstract3DRenderer::lowerShadowQuality()
{
    QAbstract3DGraph::ShadowQuality newQuality = QAbstract3DGraph::ShadowQualityNone;

    switch (m_cachedShadowQuality) {
    case QAbstract3DGraph::ShadowQualityHigh:
        qWarning("Creating high quality shadows failed. Changing to medium quality.");
        newQuality = QAbstract3DGraph::ShadowQualityMedium;
        break;
    case QAbstract3DGraph::ShadowQualityMedium:
        qWarning("Creating medium quality shadows failed. Changing to low quality.");
        newQuality = QAbstract3DGraph::ShadowQualityLow;
        break;
    case QAbstract3DGraph::ShadowQualityLow:
        qWarning("Creating low quality shadows failed. Switching shadows off.");
        newQuality = QAbstract3DGraph::ShadowQualityNone;
        break;
    case QAbstract3DGraph::ShadowQualitySoftHigh:
        qWarning("Creating soft high quality shadows failed. Changing to soft medium quality.");
        newQuality = QAbstract3DGraph::ShadowQualitySoftMedium;
        break;
    case QAbstract3DGraph::ShadowQualitySoftMedium:
        qWarning("Creating soft medium quality shadows failed. Changing to soft low quality.");
        newQuality = QAbstract3DGraph::ShadowQualitySoftLow;
        break;
    case QAbstract3DGraph::ShadowQualitySoftLow:
        qWarning("Creating soft low quality shadows failed. Switching shadows off.");
        newQuality = QAbstract3DGraph::ShadowQualityNone;
        break;
    default:
        // You'll never get here
        break;
    }

    emit requestShadowQuality(newQuality);
    updateShadowQuality(newQuality);
}

void Abstract3DRenderer::drawAxisTitleY(const QVector3D &sideLabelRotation,
                                        const QVector3D &backLabelRotation,
                                        const QVector3D &sideLabelTrans,
                                        const QVector3D &backLabelTrans,
                                        const QQuaternion &totalSideRotation,
                                        const QQuaternion &totalBackRotation,
                                        AbstractRenderItem &dummyItem,
                                        const Q3DCamera *activeCamera,
                                        float labelsMaxWidth,
                                        const QMatrix4x4 &viewMatrix,
                                        const QMatrix4x4 &projectionMatrix,
                                        ShaderHelper *shader)
{
    float scaleFactor = m_drawer->scaledFontSize() / m_axisCacheY.titleItem().size().height();
    float titleOffset = 2.0f * (labelMargin + (labelsMaxWidth * scaleFactor));
    float yRotation;
    QVector3D titleTrans;
    QQuaternion totalRotation;
    if (m_xFlipped == m_zFlipped) {
        yRotation = backLabelRotation.y();
        titleTrans = backLabelTrans;
        totalRotation = totalBackRotation;
    } else {
        yRotation = sideLabelRotation.y();
        titleTrans = sideLabelTrans;
        totalRotation = totalSideRotation;
    }

    QQuaternion offsetRotator = QQuaternion::fromAxisAndAngle(0.0f, 1.0f, 0.0f, yRotation);
    QVector3D titleOffsetVector =
            offsetRotator.rotatedVector(QVector3D(-titleOffset, 0.0f, 0.0f));

    QQuaternion titleRotation;
    if (m_axisCacheY.isTitleFixed()) {
        titleRotation = QQuaternion::fromAxisAndAngle(0.0f, 1.0f, 0.0f, yRotation)
                * m_zRightAngleRotation;
    } else {
        titleRotation = totalRotation * m_zRightAngleRotation;
    }
    dummyItem.setTranslation(titleTrans + titleOffsetVector);

    m_drawer->drawLabel(dummyItem, m_axisCacheY.titleItem(), viewMatrix,
                        projectionMatrix, zeroVector, titleRotation, 0,
                        m_cachedSelectionMode, shader, m_labelObj, activeCamera,
                        true, true, Drawer::LabelMid, Qt::AlignBottom);
}

void Abstract3DRenderer::drawAxisTitleX(const QVector3D &labelRotation,
                                        const QVector3D &labelTrans,
                                        const QQuaternion &totalRotation,
                                        AbstractRenderItem &dummyItem,
                                        const Q3DCamera *activeCamera,
                                        float labelsMaxWidth,
                                        const QMatrix4x4 &viewMatrix,
                                        const QMatrix4x4 &projectionMatrix,
                                        ShaderHelper *shader,
                                        bool radial)
{
    float scaleFactor = m_drawer->scaledFontSize() / m_axisCacheX.titleItem().size().height();
    float titleOffset;
    if (radial)
        titleOffset = -2.0f * (labelMargin + m_drawer->scaledFontSize());
    else
        titleOffset = 2.0f * (labelMargin + (labelsMaxWidth * scaleFactor));
    float zRotation = 0.0f;
    float yRotation = 0.0f;
    float xRotation = -90.0f + labelRotation.z();
    float offsetRotation = labelRotation.z();
    float extraRotation = -90.0f;
    Qt::AlignmentFlag alignment = Qt::AlignTop;
    if (m_yFlippedForGrid) {
        alignment = Qt::AlignBottom;
        zRotation = 180.0f;
        if (m_zFlipped) {
            titleOffset = -titleOffset;
            if (m_xFlipped) {
                offsetRotation = -offsetRotation;
                extraRotation = -extraRotation;
            } else {
                xRotation = -90.0f - labelRotation.z();
            }
        } else {
            yRotation = 180.0f;
            if (m_xFlipped) {
                offsetRotation = -offsetRotation;
                xRotation = -90.0f - labelRotation.z();
            } else {
                extraRotation = -extraRotation;
            }
        }
    } else {
        if (m_zFlipped) {
            titleOffset = -titleOffset;
            if (m_xFlipped) {
                yRotation = 180.0f;
                offsetRotation = -offsetRotation;
            } else {
                yRotation = 180.0f;
                xRotation = -90.0f - labelRotation.z();
                extraRotation = -extraRotation;
            }
            if (m_yFlipped) {
                alignment = Qt::AlignBottom;
                extraRotation = -extraRotation;
                if (m_xFlipped)
                    xRotation = 90.0f + labelRotation.z();
                else
                    xRotation = 90.0f - labelRotation.z();
            }
        } else {
            if (m_xFlipped) {
                offsetRotation = -offsetRotation;
                xRotation = -90.0f - labelRotation.z();
                extraRotation = -extraRotation;
            }
            if (m_yFlipped) {
                xRotation = 90.0f + labelRotation.z();
                alignment = Qt::AlignBottom;
                extraRotation = -extraRotation;
                if (m_xFlipped)
                    xRotation = 90.0f - labelRotation.z();
            }
        }
    }

    if (radial) {
        if (m_zFlipped) {
            titleOffset = -titleOffset;
        } else {
            if (m_yFlippedForGrid)
                alignment = Qt::AlignTop;
            else
                alignment = Qt::AlignBottom;
        }
    }

    if (offsetRotation == 180.0f || offsetRotation == -180.0f)
        offsetRotation = 0.0f;
    QQuaternion offsetRotator = QQuaternion::fromAxisAndAngle(1.0f, 0.0f, 0.0f, offsetRotation);
    QVector3D titleOffsetVector =
            offsetRotator.rotatedVector(QVector3D(0.0f, 0.0f, titleOffset));

    QQuaternion titleRotation;
    if (m_axisCacheX.isTitleFixed()) {
        titleRotation = QQuaternion::fromAxisAndAngle(0.0f, 0.0f, 1.0f, zRotation)
                * QQuaternion::fromAxisAndAngle(0.0f, 1.0f, 0.0f, yRotation)
                * QQuaternion::fromAxisAndAngle(1.0f, 0.0f, 0.0f, xRotation);
    } else {
        titleRotation = totalRotation
                * QQuaternion::fromAxisAndAngle(0.0f, 0.0f, 1.0f, extraRotation);
    }
    dummyItem.setTranslation(labelTrans + titleOffsetVector);

    m_drawer->drawLabel(dummyItem, m_axisCacheX.titleItem(), viewMatrix,
                        projectionMatrix, zeroVector, titleRotation, 0,
                        m_cachedSelectionMode, shader, m_labelObj, activeCamera,
                        true, true, Drawer::LabelMid, alignment);
}

void Abstract3DRenderer::drawAxisTitleZ(const QVector3D &labelRotation,
                                        const QVector3D &labelTrans,
                                        const QQuaternion &totalRotation,
                                        AbstractRenderItem &dummyItem,
                                        const Q3DCamera *activeCamera,
                                        float labelsMaxWidth,
                                        const QMatrix4x4 &viewMatrix,
                                        const QMatrix4x4 &projectionMatrix,
                                        ShaderHelper *shader)
{
    float scaleFactor = m_drawer->scaledFontSize() / m_axisCacheZ.titleItem().size().height();
    float titleOffset = 2.0f * (labelMargin + (labelsMaxWidth * scaleFactor));
    float zRotation = labelRotation.z();
    float yRotation = -90.0f;
    float xRotation = -90.0f;
    float extraRotation = 90.0f;
    Qt::AlignmentFlag alignment = Qt::AlignTop;
    if (m_yFlippedForGrid) {
        alignment = Qt::AlignBottom;
        xRotation = -xRotation;
        if (m_zFlipped) {
            if (m_xFlipped) {
                titleOffset = -titleOffset;
                zRotation = -zRotation;
                extraRotation = -extraRotation;
            } else {
                zRotation = -zRotation;
                yRotation = -yRotation;
            }
        } else {
            if (m_xFlipped) {
                titleOffset = -titleOffset;
            } else {
                extraRotation = -extraRotation;
                yRotation = -yRotation;
            }
        }
    } else {
        if (m_zFlipped) {
            zRotation = -zRotation;
            if (m_xFlipped) {
                titleOffset = -titleOffset;
            } else {
                extraRotation = -extraRotation;
                yRotation = -yRotation;
            }
        } else {
            if (m_xFlipped) {
                titleOffset = -titleOffset;
                extraRotation = -extraRotation;
            } else {
                yRotation = -yRotation;
            }
        }
        if (m_yFlipped) {
            xRotation = -xRotation;
            alignment = Qt::AlignBottom;
            extraRotation = -extraRotation;
        }
    }

    float offsetRotation = zRotation;
    if (offsetRotation == 180.0f || offsetRotation == -180.0f)
        offsetRotation = 0.0f;
    QQuaternion offsetRotator = QQuaternion::fromAxisAndAngle(0.0f, 0.0f, 1.0f, offsetRotation);
    QVector3D titleOffsetVector =
            offsetRotator.rotatedVector(QVector3D(titleOffset, 0.0f, 0.0f));

    QQuaternion titleRotation;
    if (m_axisCacheZ.isTitleFixed()) {
        titleRotation = QQuaternion::fromAxisAndAngle(0.0f, 0.0f, 1.0f, zRotation)
                * QQuaternion::fromAxisAndAngle(0.0f, 1.0f, 0.0f, yRotation)
                * QQuaternion::fromAxisAndAngle(1.0f, 0.0f, 0.0f, xRotation);
    } else {
        titleRotation = totalRotation
                * QQuaternion::fromAxisAndAngle(0.0f, 0.0f, 1.0f, extraRotation);
    }
    dummyItem.setTranslation(labelTrans + titleOffsetVector);

    m_drawer->drawLabel(dummyItem, m_axisCacheZ.titleItem(), viewMatrix,
                        projectionMatrix, zeroVector, titleRotation, 0,
                        m_cachedSelectionMode, shader, m_labelObj, activeCamera,
                        true, true, Drawer::LabelMid, alignment);
}

void Abstract3DRenderer::loadGridLineMesh()
{
    ObjectHelper::resetObjectHelper(this, m_gridLineObj,
                                    QStringLiteral(":/defaultMeshes/plane"));
}

void Abstract3DRenderer::loadLabelMesh()
{
    ObjectHelper::resetObjectHelper(this, m_labelObj,
                                    QStringLiteral(":/defaultMeshes/plane"));
}

void Abstract3DRenderer::loadPositionMapperMesh()
{
    ObjectHelper::resetObjectHelper(this, m_positionMapperObj,
                                    QStringLiteral(":/defaultMeshes/barFull"));
}

void Abstract3DRenderer::generateBaseColorTexture(const QColor &color, GLuint *texture)
{
    m_textureHelper->deleteTexture(texture);
    *texture = m_textureHelper->createUniformTexture(color);
}

void Abstract3DRenderer::fixGradientAndGenerateTexture(QLinearGradient *gradient,
                                                       GLuint *gradientTexture)
{
    // Readjust start/stop to match gradient texture size
    gradient->setStart(qreal(gradientTextureWidth), qreal(gradientTextureHeight));
    gradient->setFinalStop(0.0, 0.0);

    m_textureHelper->deleteTexture(gradientTexture);

    *gradientTexture = m_textureHelper->createGradientTexture(*gradient);
}

LabelItem &Abstract3DRenderer::selectionLabelItem()
{
    if (!m_selectionLabelItem)
        m_selectionLabelItem = new LabelItem;
    return *m_selectionLabelItem;
}

void Abstract3DRenderer::setSelectionLabel(const QString &label)
{
    if (m_selectionLabelItem)
        m_selectionLabelItem->clear();
    m_selectionLabel = label;
}

QString &Abstract3DRenderer::selectionLabel()
{
    return m_selectionLabel;
}

QVector4D Abstract3DRenderer::indexToSelectionColor(GLint index)
{
    GLubyte idxRed = index & 0xff;
    GLubyte idxGreen = (index & 0xff00) >> 8;
    GLubyte idxBlue = (index & 0xff0000) >> 16;

    return QVector4D(idxRed, idxGreen, idxBlue, 0);
}

CustomRenderItem *Abstract3DRenderer::addCustomItem(QCustom3DItem *item)
{
    CustomRenderItem *newItem = new CustomRenderItem();
    newItem->setRenderer(this);
    newItem->setItemPointer(item); // Store pointer for render item updates
    if (!newItem->setMesh(item->meshFile())) {
        delete newItem;
        return nullptr;
    }
    newItem->setOrigPosition(item->position());
    newItem->setOrigScaling(item->scaling());
    newItem->setScalingAbsolute(item->isScalingAbsolute());
    newItem->setPositionAbsolute(item->isPositionAbsolute());
    QImage textureImage = item->d_ptr->textureImage();
    bool facingCamera = false;
    GLuint texture = 0;
    if (item->d_ptr->m_isLabelItem) {
        QCustom3DLabel *labelItem = static_cast<QCustom3DLabel *>(item);
        newItem->setLabelItem(true);
        float pointSize = labelItem->font().pointSizeF();
        // Check do we have custom visuals or need to use theme
        if (!labelItem->dptr()->m_customVisuals) {
            // Recreate texture using theme
            labelItem->dptr()->createTextureImage(m_cachedTheme->labelBackgroundColor(),
                                                  m_cachedTheme->labelTextColor(),
                                                  m_cachedTheme->isLabelBackgroundEnabled(),
                                                  m_cachedTheme->isLabelBorderEnabled());
            pointSize = m_cachedTheme->font().pointSizeF();
            textureImage = item->d_ptr->textureImage();
        }
        // Calculate scaling based on text (texture size), font size and asked scaling
        float scaledFontSize = (0.05f + pointSize / 500.0f) / float(textureImage.height());
        QVector3D scaling = newItem->origScaling();
        scaling.setX(scaling.x() * textureImage.width() * scaledFontSize);
        scaling.setY(scaling.y() * textureImage.height() * scaledFontSize);
        newItem->setOrigScaling(scaling);
        // Check if facing camera
        facingCamera = labelItem->isFacingCamera();
    } else if (item->d_ptr->m_isVolumeItem && !m_isOpenGLES) {
        QCustom3DVolume *volumeItem = static_cast<QCustom3DVolume *>(item);
        newItem->setTextureWidth(volumeItem->textureWidth());
        newItem->setTextureHeight(volumeItem->textureHeight());
        newItem->setTextureDepth(volumeItem->textureDepth());
        if (volumeItem->textureFormat() == QImage::Format_Indexed8)
            newItem->setColorTable(volumeItem->colorTable());
        newItem->setTextureFormat(volumeItem->textureFormat());
        newItem->setVolume(true);
        newItem->setBlendNeeded(true);
        texture = m_textureHelper->create3DTexture(volumeItem->textureData(),
                                                   volumeItem->textureWidth(),
                                                   volumeItem->textureHeight(),
                                                   volumeItem->textureDepth(),
                                                   volumeItem->textureFormat());
        newItem->setSliceIndexX(volumeItem->sliceIndexX());
        newItem->setSliceIndexY(volumeItem->sliceIndexY());
        newItem->setSliceIndexZ(volumeItem->sliceIndexZ());
        newItem->setAlphaMultiplier(volumeItem->alphaMultiplier());
        newItem->setPreserveOpacity(volumeItem->preserveOpacity());
        newItem->setUseHighDefShader(volumeItem->useHighDefShader());

        newItem->setDrawSlices(volumeItem->drawSlices());
        newItem->setDrawSliceFrames(volumeItem->drawSliceFrames());
        newItem->setSliceFrameColor(volumeItem->sliceFrameColor());
        newItem->setSliceFrameWidths(volumeItem->sliceFrameWidths());
        newItem->setSliceFrameGaps(volumeItem->sliceFrameGaps());
        newItem->setSliceFrameThicknesses(volumeItem->sliceFrameThicknesses());
    }
    recalculateCustomItemScalingAndPos(newItem);
    newItem->setRotation(item->rotation());

    // In OpenGL ES we simply draw volumes as regular custom item placeholders.
    if (!item->d_ptr->m_isVolumeItem || m_isOpenGLES)
    {
        newItem->setBlendNeeded(textureImage.hasAlphaChannel());
        texture = m_textureHelper->create2DTexture(textureImage, true, true, true);
    }
    newItem->setTexture(texture);
    item->d_ptr->clearTextureImage();
    newItem->setVisible(item->isVisible());
    newItem->setShadowCasting(item->isShadowCasting());
    newItem->setFacingCamera(facingCamera);
    m_customRenderCache.insert(item, newItem);
    return newItem;
}

void Abstract3DRenderer::recalculateCustomItemScalingAndPos(CustomRenderItem *item)
{
    if (!m_polarGraph && !item->isLabel() && !item->isScalingAbsolute()
            && !item->isPositionAbsolute()) {
        QVector3D scale = item->origScaling() / 2.0f;
        QVector3D pos = item->origPosition();
        QVector3D minBounds(pos.x() - scale.x(),
                            pos.y() - scale.y(),
                            pos.z() + scale.z());
        QVector3D maxBounds(pos.x() + scale.x(),
                            pos.y() + scale.y(),
                            pos.z() - scale.z());
        QVector3D minCorner = convertPositionToTranslation(minBounds, false);
        QVector3D maxCorner = convertPositionToTranslation(maxBounds, false);
        scale = QVector3D(qAbs(maxCorner.x() - minCorner.x()),
                          qAbs(maxCorner.y() - minCorner.y()),
                          qAbs(maxCorner.z() - minCorner.z())) / 2.0f;
        if (item->isVolume()) {
            // Only volume items need to scale and reposition according to bounds
            QVector3D minBoundsNormal = minCorner;
            QVector3D maxBoundsNormal = maxCorner;
            // getVisibleItemBounds returns bounds normalized for fragment shader [-1,1]
            // Y and Z are also flipped.
            getVisibleItemBounds(minBoundsNormal, maxBoundsNormal);
            item->setMinBounds(minBoundsNormal);
            item->setMaxBounds(maxBoundsNormal);
            // For scaling calculations, we want [0,1] normalized values
            minBoundsNormal = item->minBoundsNormal();
            maxBoundsNormal = item->maxBoundsNormal();

            // Rescale and reposition the item so that it doesn't go over the edges
            QVector3D adjScaling =
                    QVector3D(scale.x() * (maxBoundsNormal.x() - minBoundsNormal.x()),
                              scale.y() * (maxBoundsNormal.y() - minBoundsNormal.y()),
                              scale.z() * (maxBoundsNormal.z() - minBoundsNormal.z()));

            item->setScaling(adjScaling);

            QVector3D adjPos = item->origPosition();
            QVector3D dataExtents = QVector3D(maxBounds.x() - minBounds.x(),
                                              maxBounds.y() - minBounds.y(),
                                              maxBounds.z() - minBounds.z()) / 2.0f;
            adjPos.setX(adjPos.x() + (dataExtents.x() * minBoundsNormal.x())
                        - (dataExtents.x() * (1.0f - maxBoundsNormal.x())));
            adjPos.setY(adjPos.y() + (dataExtents.y() * minBoundsNormal.y())
                        - (dataExtents.y() * (1.0f - maxBoundsNormal.y())));
            adjPos.setZ(adjPos.z() + (dataExtents.z() * minBoundsNormal.z())
                        - (dataExtents.z() * (1.0f - maxBoundsNormal.z())));
            item->setPosition(adjPos);
        } else {
            // Only scale for non-volume items, and do not readjust position
            item->setScaling(scale);
            item->setPosition(item->origPosition());
        }
    } else {
        item->setScaling(item->origScaling());
        item->setPosition(item->origPosition());
        if (item->isVolume()) {
            // Y and Z need to be flipped as shader flips those axes
            item->setMinBounds(QVector3D(-1.0f, 1.0f, 1.0f));
            item->setMaxBounds(QVector3D(1.0f, -1.0f, -1.0f));
        }
    }
    QVector3D translation = convertPositionToTranslation(item->position(),
                                                         item->isPositionAbsolute());
    item->setTranslation(translation);
}

void Abstract3DRenderer::updateCustomItem(CustomRenderItem *renderItem)
{
    QCustom3DItem *item = renderItem->itemPointer();
    if (item->d_ptr->m_dirtyBits.meshDirty) {
        renderItem->setMesh(item->meshFile());
        item->d_ptr->m_dirtyBits.meshDirty = false;
    }
    if (item->d_ptr->m_dirtyBits.positionDirty) {
        renderItem->setOrigPosition(item->position());
        renderItem->setPositionAbsolute(item->isPositionAbsolute());
        if (!item->d_ptr->m_dirtyBits.scalingDirty)
            recalculateCustomItemScalingAndPos(renderItem);
        item->d_ptr->m_dirtyBits.positionDirty = false;
    }
    if (item->d_ptr->m_dirtyBits.scalingDirty) {
        QVector3D scaling = item->scaling();
        renderItem->setOrigScaling(scaling);
        renderItem->setScalingAbsolute(item->isScalingAbsolute());
        // In case we have label item, we need to recreate texture for scaling adjustment
        if (item->d_ptr->m_isLabelItem) {
            QCustom3DLabel *labelItem = static_cast<QCustom3DLabel *>(item);
            float pointSize = labelItem->font().pointSizeF();
            // Check do we have custom visuals or need to use theme
            if (labelItem->dptr()->m_customVisuals) {
                // Recreate texture
                labelItem->dptr()->createTextureImage();
            } else {
                // Recreate texture using theme
                labelItem->dptr()->createTextureImage(m_cachedTheme->labelBackgroundColor(),
                                                      m_cachedTheme->labelTextColor(),
                                                      m_cachedTheme->isLabelBackgroundEnabled(),
                                                      m_cachedTheme->isLabelBorderEnabled());
                pointSize = m_cachedTheme->font().pointSizeF();
            }
            QImage textureImage = item->d_ptr->textureImage();
            // Calculate scaling based on text (texture size), font size and asked scaling
            float scaledFontSize = (0.05f + pointSize / 500.0f) / float(textureImage.height());
            scaling.setX(scaling.x() * textureImage.width() * scaledFontSize);
            scaling.setY(scaling.y() * textureImage.height() * scaledFontSize);
            item->d_ptr->clearTextureImage();
            renderItem->setOrigScaling(scaling);
        }
        recalculateCustomItemScalingAndPos(renderItem);
        item->d_ptr->m_dirtyBits.scalingDirty = false;
    }
    if (item->d_ptr->m_dirtyBits.rotationDirty) {
        renderItem->setRotation(item->rotation());
        item->d_ptr->m_dirtyBits.rotationDirty = false;
    }
    if (item->d_ptr->m_dirtyBits.textureDirty) {
        QImage textureImage = item->d_ptr->textureImage();
        if (item->d_ptr->m_isLabelItem) {
            QCustom3DLabel *labelItem = static_cast<QCustom3DLabel *>(item);
            // Check do we have custom visuals or need to use theme
            if (!labelItem->dptr()->m_customVisuals) {
                // Recreate texture using theme
                labelItem->dptr()->createTextureImage(m_cachedTheme->labelBackgroundColor(),
                                                      m_cachedTheme->labelTextColor(),
                                                      m_cachedTheme->isLabelBackgroundEnabled(),
                                                      m_cachedTheme->isLabelBorderEnabled());
                textureImage = item->d_ptr->textureImage();
            }
        } else if (!item->d_ptr->m_isVolumeItem || m_isOpenGLES) {
            renderItem->setBlendNeeded(textureImage.hasAlphaChannel());
            GLuint oldTexture = renderItem->texture();
            m_textureHelper->deleteTexture(&oldTexture);
            GLuint texture = m_textureHelper->create2DTexture(textureImage, true, true, true);
            renderItem->setTexture(texture);
        }
        item->d_ptr->clearTextureImage();
        item->d_ptr->m_dirtyBits.textureDirty = false;
    }
    if (item->d_ptr->m_dirtyBits.visibleDirty) {
        renderItem->setVisible(item->isVisible());
        item->d_ptr->m_dirtyBits.visibleDirty = false;
    }
    if (item->d_ptr->m_dirtyBits.shadowCastingDirty) {
        renderItem->setShadowCasting(item->isShadowCasting());
        item->d_ptr->m_dirtyBits.shadowCastingDirty = false;
    }
    if (item->d_ptr->m_isLabelItem) {
        QCustom3DLabel *labelItem = static_cast<QCustom3DLabel *>(item);
        if (labelItem->dptr()->m_facingCameraDirty) {
            renderItem->setFacingCamera(labelItem->isFacingCamera());
            labelItem->dptr()->m_facingCameraDirty = false;
        }
    } else if (item->d_ptr->m_isVolumeItem && !m_isOpenGLES) {
        QCustom3DVolume *volumeItem = static_cast<QCustom3DVolume *>(item);
        if (volumeItem->dptr()->m_dirtyBitsVolume.colorTableDirty) {
            renderItem->setColorTable(volumeItem->colorTable());
            volumeItem->dptr()->m_dirtyBitsVolume.colorTableDirty = false;
        }
        if (volumeItem->dptr()->m_dirtyBitsVolume.textureDimensionsDirty
                || volumeItem->dptr()->m_dirtyBitsVolume.textureDataDirty
                || volumeItem->dptr()->m_dirtyBitsVolume.textureFormatDirty) {
            GLuint oldTexture = renderItem->texture();
            m_textureHelper->deleteTexture(&oldTexture);
            GLuint texture = m_textureHelper->create3DTexture(volumeItem->textureData(),
                                                              volumeItem->textureWidth(),
                                                              volumeItem->textureHeight(),
                                                              volumeItem->textureDepth(),
                                                              volumeItem->textureFormat());
            renderItem->setTexture(texture);
            renderItem->setTextureWidth(volumeItem->textureWidth());
            renderItem->setTextureHeight(volumeItem->textureHeight());
            renderItem->setTextureDepth(volumeItem->textureDepth());
            renderItem->setTextureFormat(volumeItem->textureFormat());
            volumeItem->dptr()->m_dirtyBitsVolume.textureDimensionsDirty = false;
            volumeItem->dptr()->m_dirtyBitsVolume.textureDataDirty = false;
            volumeItem->dptr()->m_dirtyBitsVolume.textureFormatDirty = false;
        }
        if (volumeItem->dptr()->m_dirtyBitsVolume.slicesDirty) {
            renderItem->setDrawSlices(volumeItem->drawSlices());
            renderItem->setDrawSliceFrames(volumeItem->drawSliceFrames());
            renderItem->setSliceFrameColor(volumeItem->sliceFrameColor());
            renderItem->setSliceFrameWidths(volumeItem->sliceFrameWidths());
            renderItem->setSliceFrameGaps(volumeItem->sliceFrameGaps());
            renderItem->setSliceFrameThicknesses(volumeItem->sliceFrameThicknesses());
            renderItem->setSliceIndexX(volumeItem->sliceIndexX());
            renderItem->setSliceIndexY(volumeItem->sliceIndexY());
            renderItem->setSliceIndexZ(volumeItem->sliceIndexZ());
            volumeItem->dptr()->m_dirtyBitsVolume.slicesDirty = false;
        }
        if (volumeItem->dptr()->m_dirtyBitsVolume.alphaDirty) {
            renderItem->setAlphaMultiplier(volumeItem->alphaMultiplier());
            renderItem->setPreserveOpacity(volumeItem->preserveOpacity());
            volumeItem->dptr()->m_dirtyBitsVolume.alphaDirty = false;
        }
        if (volumeItem->dptr()->m_dirtyBitsVolume.shaderDirty) {
            renderItem->setUseHighDefShader(volumeItem->useHighDefShader());
            volumeItem->dptr()->m_dirtyBitsVolume.shaderDirty = false;
        }
    }
}

void Abstract3DRenderer::updateCustomItemPositions()
{
    for (auto renderItem: m_customRenderCache)
        recalculateCustomItemScalingAndPos(renderItem);
}

void Abstract3DRenderer::drawCustomItems(RenderingState state,
                                         ShaderHelper *regularShader,
                                         const QMatrix4x4 &viewMatrix,
                                         const QMatrix4x4 &projectionViewMatrix,
                                         const QMatrix4x4 &depthProjectionViewMatrix,
                                         GLuint depthTexture,
                                         GLfloat shadowQuality,
                                         GLfloat reflection)
{
    if (m_customRenderCache.isEmpty())
        return;

    ShaderHelper *shader = regularShader;
    shader->bind();

    if (RenderingNormal == state) {
        shader->setUniformValue(shader->lightP(), m_cachedScene->activeLight()->position());
        shader->setUniformValue(shader->ambientS(), m_cachedTheme->ambientLightStrength());
        shader->setUniformValue(shader->lightColor(),
                                Utils::vectorFromColor(m_cachedTheme->lightColor()));
        shader->setUniformValue(shader->view(), viewMatrix);
    }

    // Draw custom items - first regular and then volumes
    bool volumeDetected = false;
    int loopCount = 0;
    while (loopCount < 2) {
        for (QCustom3DItem *customItem : std::as_const(m_customItemDrawOrder)) {
            CustomRenderItem *item = m_customRenderCache.value(customItem);
            // Check that the render item is visible, and skip drawing if not
            // Also check if reflected item is on the "wrong" side, and skip drawing if it is
            if (!item->isVisible() || ((m_reflectionEnabled && reflection < 0.0f)
                    && (m_yFlipped == (item->translation().y() >= 0.0)))) {
                continue;
            }
            if (loopCount == 0) {
                if (item->isVolume()) {
                    volumeDetected = true;
                    continue;
                }
            } else {
                if (!item->isVolume())
                    continue;
            }

            // If the render item is in data coordinates and not within axis ranges, skip it
            if (!item->isPositionAbsolute()
                    && (item->position().x() < m_axisCacheX.min()
                        || item->position().x() > m_axisCacheX.max()
                        || item->position().z() < m_axisCacheZ.min()
                        || item->position().z() > m_axisCacheZ.max()
                        || item->position().y() < m_axisCacheY.min()
                        || item->position().y() > m_axisCacheY.max())) {
                continue;
            }

            QMatrix4x4 modelMatrix;
            QMatrix4x4 itModelMatrix;
            QMatrix4x4 MVPMatrix;

            QQuaternion rotation = item->rotation();
            // Check if the (label) item should be facing camera, and adjust rotation accordingly
            if (item->isFacingCamera()) {
                float camRotationX = m_cachedScene->activeCamera()->xRotation();
                float camRotationY = m_cachedScene->activeCamera()->yRotation();
                rotation = QQuaternion::fromAxisAndAngle(0.0f, 1.0f, 0.0f, -camRotationX)
                        * QQuaternion::fromAxisAndAngle(1.0f, 0.0f, 0.0f, -camRotationY);
            }

            if (m_reflectionEnabled) {
                if (reflection < 0.0f) {
                    if (item->itemPointer()->d_ptr->m_isLabelItem)
                        continue;
                    else
                        glCullFace(GL_FRONT);
                } else {
                    glCullFace(GL_BACK);
                }
                QVector3D trans = item->translation();
                trans.setY(reflection * trans.y());
                modelMatrix.translate(trans);
                if (reflection < 0.0f) {
                    QQuaternion mirror = QQuaternion(rotation.scalar(),
                                                     -rotation.x(), rotation.y(), -rotation.z());
                    modelMatrix.rotate(mirror);
                    itModelMatrix.rotate(mirror);
                } else {
                    modelMatrix.rotate(rotation);
                    itModelMatrix.rotate(rotation);
                }
                QVector3D scale = item->scaling();
                scale.setY(reflection * scale.y());
                modelMatrix.scale(scale);
            } else {
                modelMatrix.translate(item->translation());
                modelMatrix.rotate(rotation);
                modelMatrix.scale(item->scaling());
                itModelMatrix.rotate(rotation);
            }
            if (!item->isFacingCamera())
                itModelMatrix.scale(item->scaling());
            MVPMatrix = projectionViewMatrix * modelMatrix;

            if (RenderingNormal == state) {
                // Normal render
                ShaderHelper *prevShader = shader;
                if (item->isVolume() && !m_isOpenGLES) {
                    if (item->drawSlices() &&
                            (item->sliceIndexX() >= 0
                             || item->sliceIndexY() >= 0
                             || item->sliceIndexZ() >= 0)) {
                        shader = m_volumeTextureSliceShader;
                    } else if (item->useHighDefShader()) {
                        shader = m_volumeTextureShader;
                    } else {
                        shader = m_volumeTextureLowDefShader;
                    }
                } else if (item->isLabel()) {
                    shader = m_labelShader;
                } else {
                    shader = regularShader;
                }
                if (shader != prevShader)
                    shader->bind();
                shader->setUniformValue(shader->model(), modelMatrix);
                shader->setUniformValue(shader->MVP(), MVPMatrix);
                shader->setUniformValue(shader->nModel(), itModelMatrix.inverted().transposed());

                if (item->isBlendNeeded()) {
                    glEnable(GL_BLEND);
                    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
                    if (!item->isVolume() && !m_isOpenGLES)
                        glDisable(GL_CULL_FACE);
                } else {
                    glDisable(GL_BLEND);
                    glEnable(GL_CULL_FACE);
                }

                if (!m_isOpenGLES && m_cachedShadowQuality > QAbstract3DGraph::ShadowQualityNone
                        && !item->isVolume()) {
                    // Set shadow shader bindings
                    shader->setUniformValue(shader->shadowQ(), shadowQuality);
                    shader->setUniformValue(shader->depth(), depthProjectionViewMatrix * modelMatrix);
                    shader->setUniformValue(shader->lightS(), m_cachedTheme->lightStrength() / 10.0f);
                    m_drawer->drawObject(shader, item->mesh(), item->texture(), depthTexture);
                } else {
                    // Set shadowless shader bindings
                    if (item->isVolume() && !m_isOpenGLES) {
                        QVector3D cameraPos = m_cachedScene->activeCamera()->position();
                        cameraPos = MVPMatrix.inverted().map(cameraPos);
                        // Adjust camera position according to min/max bounds
                        cameraPos = -(cameraPos
                                      + ((oneVector - cameraPos) * item->minBoundsNormal())
                                      - ((oneVector + cameraPos) * (oneVector - item->maxBoundsNormal())));
                        shader->setUniformValue(shader->cameraPositionRelativeToModel(), cameraPos);
                        GLint color8Bit = (item->textureFormat() == QImage::Format_Indexed8) ? 1 : 0;
                        if (color8Bit) {
                            shader->setUniformValueArray(shader->colorIndex(),
                                                         item->colorTable().constData(), 256);
                        }
                        shader->setUniformValue(shader->color8Bit(), color8Bit);
                        shader->setUniformValue(shader->alphaMultiplier(), item->alphaMultiplier());
                        shader->setUniformValue(shader->preserveOpacity(),
                                                item->preserveOpacity() ? 1 : 0);

                        shader->setUniformValue(shader->minBounds(), item->minBounds());
                        shader->setUniformValue(shader->maxBounds(), item->maxBounds());

                        if (shader == m_volumeTextureSliceShader) {
                            shader->setUniformValue(shader->volumeSliceIndices(),
                                                    item->sliceFractions());
                        } else {
                            // Precalculate texture dimensions so we can optimize
                            // ray stepping to hit every texture layer.
                            QVector3D textureDimensions(1.0f / float(item->textureWidth()),
                                                        1.0f / float(item->textureHeight()),
                                                        1.0f / float(item->textureDepth()));

                            // Worst case scenario sample count
                            int sampleCount;
                            if (shader == m_volumeTextureLowDefShader) {
                                sampleCount = qMax(item->textureWidth(),
                                                   qMax(item->textureDepth(), item->textureHeight()));
                                // Further improve speed with big textures by simply dropping every
                                // other sample:
                                if (sampleCount > 256)
                                    sampleCount /= 2;
                            } else {
                                sampleCount = item->textureWidth() + item->textureHeight()
                                        + item->textureDepth();
                            }
                            shader->setUniformValue(shader->textureDimensions(), textureDimensions);
                            shader->setUniformValue(shader->sampleCount(), sampleCount);
                        }
                        if (item->drawSliceFrames()) {
                            // Set up the slice frame shader
                            glDisable(GL_CULL_FACE);
                            m_volumeSliceFrameShader->bind();
                            m_volumeSliceFrameShader->setUniformValue(
                                        m_volumeSliceFrameShader->color(), item->sliceFrameColor());

                            // Draw individual slice frames.
                            if (item->sliceIndexX() >= 0)
                                drawVolumeSliceFrame(item, Qt::XAxis, projectionViewMatrix);
                            if (item->sliceIndexY() >= 0)
                                drawVolumeSliceFrame(item, Qt::YAxis, projectionViewMatrix);
                            if (item->sliceIndexZ() >= 0)
                                drawVolumeSliceFrame(item, Qt::ZAxis, projectionViewMatrix);

                            glEnable(GL_CULL_FACE);
                            shader->bind();
                        }
                        m_drawer->drawObject(shader, item->mesh(), 0, 0, item->texture());
                    } else {
                        shader->setUniformValue(shader->lightS(), m_cachedTheme->lightStrength());
                        m_drawer->drawObject(shader, item->mesh(), item->texture());
                    }
                }
            } else if (RenderingSelection == state && !volumeDetected) {
                // Selection render
                shader->setUniformValue(shader->MVP(), MVPMatrix);
                QVector4D itemColor = indexToSelectionColor(item->index());
                itemColor.setW(customItemAlpha);
                itemColor /= 255.0f;
                shader->setUniformValue(shader->color(), itemColor);
                m_drawer->drawObject(shader, item->mesh());
            } else if (item->isShadowCasting()) {
                // Depth render
                shader->setUniformValue(shader->MVP(), depthProjectionViewMatrix * modelMatrix);
                m_drawer->drawObject(shader, item->mesh());
            }
        }
        loopCount++;
        if (!volumeDetected)
            loopCount++; // Skip second run if no volumes detected
    }

    if (RenderingNormal == state) {
        glDisable(GL_BLEND);
        glEnable(GL_CULL_FACE);
    }
}

void Abstract3DRenderer::drawVolumeSliceFrame(const CustomRenderItem *item, Qt::Axis axis,
                                              const QMatrix4x4 &projectionViewMatrix)
{
    QVector2D frameWidth;
    QVector3D frameScaling;
    QVector3D translation = item->translation();
    QQuaternion rotation = item->rotation();
    float fracTrans;
    bool needRotate = !rotation.isIdentity();
    QMatrix4x4 rotationMatrix;
    if (needRotate)
        rotationMatrix.rotate(rotation);

    if (axis == Qt::XAxis) {
        fracTrans = item->sliceFractions().x();
        float range = item->maxBoundsNormal().x() - item->minBoundsNormal().x();
        float minMult = item->minBoundsNormal().x() / range;
        float maxMult = (1.0f - item->maxBoundsNormal().x()) / range;
        fracTrans = fracTrans - ((1.0f - fracTrans) * minMult) + ((1.0f + fracTrans) * maxMult);
        if (needRotate)
            translation += rotationMatrix.map(QVector3D(fracTrans * item->scaling().x(), 0.0f, 0.0f));
        else
            translation.setX(translation.x() + fracTrans * item->scaling().x());
        frameScaling = QVector3D(item->scaling().z()
                                + (item->scaling().z() * item->sliceFrameGaps().z())
                                + (item->scaling().z() * item->sliceFrameWidths().z()),
                                item->scaling().y()
                                + (item->scaling().y() * item->sliceFrameGaps().y())
                                + (item->scaling().y() * item->sliceFrameWidths().y()),
                                item->scaling().x() * item->sliceFrameThicknesses().x());
        frameWidth = QVector2D(item->scaling().z() * item->sliceFrameWidths().z(),
                               item->scaling().y() * item->sliceFrameWidths().y());
        rotation *= m_yRightAngleRotation;
    } else if (axis == Qt::YAxis) {
        fracTrans = item->sliceFractions().y();
        float range = item->maxBoundsNormal().y() - item->minBoundsNormal().y();
        // Y axis is logically flipped, so we need to swam min and max bounds
        float minMult = (1.0f - item->maxBoundsNormal().y()) / range;
        float maxMult = item->minBoundsNormal().y() / range;
        fracTrans = fracTrans - ((1.0f - fracTrans) * minMult) + ((1.0f + fracTrans) * maxMult);
        if (needRotate)
            translation -= rotationMatrix.map(QVector3D(0.0f, fracTrans * item->scaling().y(), 0.0f));
        else
            translation.setY(translation.y() - fracTrans * item->scaling().y());
        frameScaling = QVector3D(item->scaling().x()
                                + (item->scaling().x() * item->sliceFrameGaps().x())
                                + (item->scaling().x() * item->sliceFrameWidths().x()),
                                item->scaling().z()
                                + (item->scaling().z() * item->sliceFrameGaps().z())
                                + (item->scaling().z() * item->sliceFrameWidths().z()),
                                item->scaling().y() * item->sliceFrameThicknesses().y());
        frameWidth = QVector2D(item->scaling().x() * item->sliceFrameWidths().x(),
                               item->scaling().z() * item->sliceFrameWidths().z());
        rotation *= m_xRightAngleRotation;
    } else { // Z axis
        fracTrans = item->sliceFractions().z();
        float range = item->maxBoundsNormal().z() - item->minBoundsNormal().z();
        // Z axis is logically flipped, so we need to swam min and max bounds
        float minMult = (1.0f - item->maxBoundsNormal().z()) / range;
        float maxMult = item->minBoundsNormal().z() / range;
        fracTrans = fracTrans - ((1.0f - fracTrans) * minMult) + ((1.0f + fracTrans) * maxMult);
        if (needRotate)
            translation -= rotationMatrix.map(QVector3D(0.0f, 0.0f, fracTrans * item->scaling().z()));
        else
            translation.setZ(translation.z() - fracTrans * item->scaling().z());
        frameScaling = QVector3D(item->scaling().x()
                                + (item->scaling().x() * item->sliceFrameGaps().x())
                                + (item->scaling().x() * item->sliceFrameWidths().x()),
                                item->scaling().y()
                                + (item->scaling().y() * item->sliceFrameGaps().y())
                                + (item->scaling().y() * item->sliceFrameWidths().y()),
                                item->scaling().z() * item->sliceFrameThicknesses().z());
        frameWidth = QVector2D(item->scaling().x() * item->sliceFrameWidths().x(),
                               item->scaling().y() * item->sliceFrameWidths().y());
    }

    // If the slice is outside the shown area, don't show the frame
    if (fracTrans < -1.0 || fracTrans > 1.0)
        return;

    // Shader needs the width of clear space in the middle.
    frameWidth.setX(1.0f - (frameWidth.x() / frameScaling.x()));
    frameWidth.setY(1.0f - (frameWidth.y() / frameScaling.y()));

    QMatrix4x4 modelMatrix;
    QMatrix4x4 mvpMatrix;

    modelMatrix.translate(translation);
    modelMatrix.rotate(rotation);
    modelMatrix.scale(frameScaling);
    mvpMatrix = projectionViewMatrix * modelMatrix;
    m_volumeSliceFrameShader->setUniformValue(m_volumeSliceFrameShader->MVP(), mvpMatrix);
    m_volumeSliceFrameShader->setUniformValue(m_volumeSliceFrameShader->sliceFrameWidth(),
                                              frameWidth);

    m_drawer->drawObject(m_volumeSliceFrameShader, item->mesh());

}

void Abstract3DRenderer::queriedGraphPosition(const QMatrix4x4 &projectionViewMatrix,
                                              const QVector3D &scaling,
                                              GLuint defaultFboHandle)
{
    m_cursorPositionShader->bind();

    // Set up mapper framebuffer
    glBindFramebuffer(GL_FRAMEBUFFER, m_cursorPositionFrameBuffer);
    glViewport(0, 0,
               m_primarySubViewport.width(),
               m_primarySubViewport.height());
    glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    glDisable(GL_DITHER); // Dither may affect colors if enabled
    glEnable(GL_CULL_FACE);
    glCullFace(GL_FRONT);

    // Draw a cube scaled to the graph dimensions
    QMatrix4x4 modelMatrix;
    QMatrix4x4 MVPMatrix;

    modelMatrix.scale(scaling);

    MVPMatrix = projectionViewMatrix * modelMatrix;
    m_cursorPositionShader->setUniformValue(m_cursorPositionShader->MVP(), MVPMatrix);
    m_drawer->drawObject(m_cursorPositionShader, m_positionMapperObj);

    QVector4D dataColor = Utils::getSelection(m_graphPositionQuery,
                                              m_primarySubViewport.height());
    if (dataColor.w() > 0.0f) {
        // If position is outside the graph, set the position well outside the graph boundaries
        dataColor = QVector4D(-10000.0f, -10000.0f, -10000.0f, 0.0f);
    } else {
        // Normalize to range [0.0, 1.0]
        dataColor /= 255.0f;
    }

    // Restore state
    glEnable(GL_DITHER);
    glCullFace(GL_BACK);

    // Note: Zeroing the frame buffer before resetting it is a workaround for flickering that occurs
    // during zoom in some environments.
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

    glBindFramebuffer(GL_FRAMEBUFFER, defaultFboHandle);
    glViewport(m_primarySubViewport.x(),
               m_primarySubViewport.y(),
               m_primarySubViewport.width(),
               m_primarySubViewport.height());

    QVector3D normalizedValues = dataColor.toVector3D() * 2.0f;
    normalizedValues -= oneVector;
    m_queriedGraphPosition = QVector3D(normalizedValues.x(),
                                       normalizedValues.y(),
                                       normalizedValues.z());
    m_graphPositionQueryResolved = true;
    m_graphPositionQueryPending = false;
}

void Abstract3DRenderer::calculatePolarXZ(const QVector3D &dataPos, float &x, float &z) const
{
    // x is angular, z is radial
    qreal angle = m_axisCacheX.formatter()->positionAt(dataPos.x()) * doublePi;
    qreal radius = m_axisCacheZ.formatter()->positionAt(dataPos.z());

    // Convert angle & radius to X and Z coords
    x = float(radius * qSin(angle)) * m_polarRadius;
    z = -float(radius * qCos(angle)) * m_polarRadius;
}

void Abstract3DRenderer::drawRadialGrid(ShaderHelper *shader, float yFloorLinePos,
                                        const QMatrix4x4 &projectionViewMatrix,
                                        const QMatrix4x4 &depthMatrix)
{
    static QList<QQuaternion> lineRotations;
    if (!lineRotations.size()) {
        lineRotations.resize(polarGridRoundness);
        for (int j = 0; j < polarGridRoundness; j++) {
            lineRotations[j] = QQuaternion::fromAxisAndAngle(0.0f, 1.0f, 0.0f,
                                                             polarGridAngleDegrees * float(j));
        }
    }
    int gridLineCount = m_axisCacheZ.gridLineCount();
    const QList<float> &gridPositions = m_axisCacheZ.formatter()->gridPositions();
    const QList<float> &subGridPositions = m_axisCacheZ.formatter()->subGridPositions();
    int mainSize = gridPositions.size();
    QVector3D translateVector(0.0f, yFloorLinePos, 0.0f);
    QQuaternion finalRotation = m_xRightAngleRotationNeg;
    if (m_yFlippedForGrid)
        finalRotation *= m_xFlipRotation;

    for (int i = 0; i < gridLineCount; i++) {
        float gridPosition = (i >= mainSize)
                ? subGridPositions.at(i - mainSize) : gridPositions.at(i);
        float radiusFraction = m_polarRadius * gridPosition;
        QVector3D gridLineScaler(radiusFraction * float(qSin(polarGridHalfAngle)),
                                 gridLineWidth, gridLineWidth);
        translateVector.setZ(gridPosition * m_polarRadius);
        for (int j = 0; j < polarGridRoundness; j++) {
            QMatrix4x4 modelMatrix;
            QMatrix4x4 itModelMatrix;
            modelMatrix.rotate(lineRotations.at(j));
            itModelMatrix.rotate(lineRotations.at(j));
            modelMatrix.translate(translateVector);
            modelMatrix.scale(gridLineScaler);
            itModelMatrix.scale(gridLineScaler);
            modelMatrix.rotate(finalRotation);
            itModelMatrix.rotate(finalRotation);
            QMatrix4x4 MVPMatrix = projectionViewMatrix * modelMatrix;

            shader->setUniformValue(shader->model(), modelMatrix);
            shader->setUniformValue(shader->nModel(), itModelMatrix.inverted().transposed());
            shader->setUniformValue(shader->MVP(), MVPMatrix);
            if (!m_isOpenGLES) {
                if (m_cachedShadowQuality > QAbstract3DGraph::ShadowQualityNone) {
                    // Set shadow shader bindings
                    QMatrix4x4 depthMVPMatrix = depthMatrix * modelMatrix;
                    shader->setUniformValue(shader->depth(), depthMVPMatrix);
                    // Draw the object
                    m_drawer->drawObject(shader, m_gridLineObj, 0, m_depthTexture);
                } else {
                    // Draw the object
                    m_drawer->drawObject(shader, m_gridLineObj);
                }
            } else {
                m_drawer->drawLine(shader);
            }
        }
    }
}

void Abstract3DRenderer::drawAngularGrid(ShaderHelper *shader, float yFloorLinePos,
                                         const QMatrix4x4 &projectionViewMatrix,
                                         const QMatrix4x4 &depthMatrix)
{
    float halfRatio((m_polarRadius + (labelMargin / 2.0f)) / 2.0f);
    QVector3D gridLineScaler(gridLineWidth, gridLineWidth, halfRatio);
    int gridLineCount = m_axisCacheX.gridLineCount();
    const QList<float> &gridPositions = m_axisCacheX.formatter()->gridPositions();
    const QList<float> &subGridPositions = m_axisCacheX.formatter()->subGridPositions();
    int mainSize = gridPositions.size();
    QVector3D translateVector(0.0f, yFloorLinePos, -halfRatio);
    QQuaternion finalRotation;
    if (m_isOpenGLES)
        finalRotation = m_yRightAngleRotationNeg;
    else
        finalRotation = m_xRightAngleRotationNeg;
    if (m_yFlippedForGrid)
        finalRotation *= m_xFlipRotation;
    for (int i = 0; i < gridLineCount; i++) {
        QMatrix4x4 modelMatrix;
        QMatrix4x4 itModelMatrix;
        float gridPosition = (i >= mainSize)
                ? subGridPositions.at(i - mainSize) : gridPositions.at(i);
        QQuaternion lineRotation = QQuaternion::fromAxisAndAngle(upVector, gridPosition * 360.0f);
        modelMatrix.rotate(lineRotation);
        itModelMatrix.rotate(lineRotation);
        modelMatrix.translate(translateVector);
        modelMatrix.scale(gridLineScaler);
        itModelMatrix.scale(gridLineScaler);
        modelMatrix.rotate(finalRotation);
        itModelMatrix.rotate(finalRotation);
        QMatrix4x4 MVPMatrix = projectionViewMatrix * modelMatrix;

        shader->setUniformValue(shader->model(), modelMatrix);
        shader->setUniformValue(shader->nModel(), itModelMatrix.inverted().transposed());
        shader->setUniformValue(shader->MVP(), MVPMatrix);
        if (m_isOpenGLES) {
            m_drawer->drawLine(shader);
        } else {
            if (m_cachedShadowQuality > QAbstract3DGraph::ShadowQualityNone) {
                // Set shadow shader bindings
                QMatrix4x4 depthMVPMatrix = depthMatrix * modelMatrix;
                shader->setUniformValue(shader->depth(), depthMVPMatrix);
                // Draw the object
                m_drawer->drawObject(shader, m_gridLineObj, 0, m_depthTexture);
            } else {
                // Draw the object
                m_drawer->drawObject(shader, m_gridLineObj);
            }
        }
    }
}

float Abstract3DRenderer::calculatePolarBackgroundMargin()
{
    // Check each extents of each angular label
    // Calculate angular position
    QList<float> &labelPositions = m_axisCacheX.formatter()->labelPositions();
    float actualLabelHeight = m_drawer->scaledFontSize() * 2.0f; // All labels are same height
    float maxNeededMargin = 0.0f;

    // Axis title needs to be accounted for
    if (m_axisCacheX.isTitleVisible())
        maxNeededMargin = 2.0f * actualLabelHeight + 3.0f * labelMargin;

    for (int label = 0; label < labelPositions.size(); label++) {
        QSize labelSize = m_axisCacheX.labelItems().at(label)->size();
        float actualLabelWidth = actualLabelHeight / labelSize.height() * labelSize.width();
        float labelPosition = labelPositions.at(label);
        qreal angle = labelPosition * M_PI * 2.0;
        float x = qAbs((m_polarRadius + labelMargin) * float(qSin(angle)))
                + actualLabelWidth - m_polarRadius + labelMargin;
        float z = qAbs(-(m_polarRadius + labelMargin) * float(qCos(angle)))
                + actualLabelHeight - m_polarRadius + labelMargin;
        float neededMargin = qMax(x, z);
        maxNeededMargin = qMax(maxNeededMargin, neededMargin);
    }

    return maxNeededMargin;
}

void Abstract3DRenderer::updateCameraViewport()
{
    QVector3D adjustedTarget = m_cachedScene->activeCamera()->target();
    fixCameraTarget(adjustedTarget);
    if (m_oldCameraTarget != adjustedTarget) {
        QVector3D cameraBase = cameraDistanceVector + adjustedTarget;

        m_cachedScene->activeCamera()->d_ptr->setBaseOrientation(cameraBase,
                                                                 adjustedTarget,
                                                                 upVector);
        m_oldCameraTarget = adjustedTarget;
    }
    m_cachedScene->activeCamera()->d_ptr->updateViewMatrix(m_autoScaleAdjustment);
    // Set light position (i.e rotate light with activeCamera, a bit above it).
    // Check if we want to use automatic light positioning even without shadows
    if (m_cachedScene->activeLight()->isAutoPosition()
            || m_cachedShadowQuality > QAbstract3DGraph::ShadowQualityNone) {
        m_cachedScene->d_ptr->setLightPositionRelativeToCamera(defaultLightPos);
    }
}

QT_END_NAMESPACE
