/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtQuick module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "qquickshapenvprrenderer_p.h"
#include <QOpenGLExtraFunctions>
#include <QOpenGLFramebufferObject>
#include <QOpenGLShaderProgram>
#include <QOpenGLBuffer>
#include <qmath.h>
#include <private/qpainterpath_p.h>
#include <private/qquickpath_p_p.h>

QT_BEGIN_NAMESPACE

void QQuickShapeNvprRenderer::beginSync(int totalCount)
{
    if (m_sp.count() != totalCount) {
        m_sp.resize(totalCount);
        m_accDirty |= DirtyList;
    }
}

void QQuickShapeNvprRenderer::setPath(int index, const QQuickPath *path)
{
    ShapePathGuiData &d(m_sp[index]);
    convertPath(path, &d);
    d.dirty |= DirtyPath;
    m_accDirty |= DirtyPath;
}

void QQuickShapeNvprRenderer::setStrokeColor(int index, const QColor &color)
{
    ShapePathGuiData &d(m_sp[index]);
    d.strokeColor = color;
    d.dirty |= DirtyStyle;
    m_accDirty |= DirtyStyle;
}

void QQuickShapeNvprRenderer::setStrokeWidth(int index, qreal w)
{
    ShapePathGuiData &d(m_sp[index]);
    d.strokeWidth = w;
    d.dirty |= DirtyStyle;
    m_accDirty |= DirtyStyle;
}

void QQuickShapeNvprRenderer::setFillColor(int index, const QColor &color)
{
    ShapePathGuiData &d(m_sp[index]);
    d.fillColor = color;
    d.dirty |= DirtyStyle;
    m_accDirty |= DirtyStyle;
}

void QQuickShapeNvprRenderer::setFillRule(int index, QQuickShapePath::FillRule fillRule)
{
    ShapePathGuiData &d(m_sp[index]);
    d.fillRule = fillRule;
    d.dirty |= DirtyFillRule;
    m_accDirty |= DirtyFillRule;
}

void QQuickShapeNvprRenderer::setJoinStyle(int index, QQuickShapePath::JoinStyle joinStyle, int miterLimit)
{
    ShapePathGuiData &d(m_sp[index]);
    d.joinStyle = joinStyle;
    d.miterLimit = miterLimit;
    d.dirty |= DirtyStyle;
    m_accDirty |= DirtyStyle;
}

void QQuickShapeNvprRenderer::setCapStyle(int index, QQuickShapePath::CapStyle capStyle)
{
    ShapePathGuiData &d(m_sp[index]);
    d.capStyle = capStyle;
    d.dirty |= DirtyStyle;
    m_accDirty |= DirtyStyle;
}

void QQuickShapeNvprRenderer::setStrokeStyle(int index, QQuickShapePath::StrokeStyle strokeStyle,
                                                qreal dashOffset, const QVector<qreal> &dashPattern)
{
    ShapePathGuiData &d(m_sp[index]);
    d.dashActive = strokeStyle == QQuickShapePath::DashLine;
    d.dashOffset = dashOffset;
    d.dashPattern = dashPattern;
    d.dirty |= DirtyDash;
    m_accDirty |= DirtyDash;
}

void QQuickShapeNvprRenderer::setFillGradient(int index, QQuickShapeGradient *gradient)
{
    ShapePathGuiData &d(m_sp[index]);
    if (gradient) {
        d.fillGradient.stops = gradient->gradientStops(); // sorted
        d.fillGradient.spread = gradient->spread();
        if (QQuickShapeLinearGradient *g  = qobject_cast<QQuickShapeLinearGradient *>(gradient)) {
            d.fillGradientActive = LinearGradient;
            d.fillGradient.a = QPointF(g->x1(), g->y1());
            d.fillGradient.b = QPointF(g->x2(), g->y2());
        } else if (QQuickShapeRadialGradient *g = qobject_cast<QQuickShapeRadialGradient *>(gradient)) {
            d.fillGradientActive = RadialGradient;
            d.fillGradient.a = QPointF(g->centerX(), g->centerY());
            d.fillGradient.b = QPointF(g->focalX(), g->focalY());
            d.fillGradient.v0 = g->centerRadius();
            d.fillGradient.v1 = g->focalRadius();
        } else if (QQuickShapeConicalGradient *g = qobject_cast<QQuickShapeConicalGradient *>(gradient)) {
            d.fillGradientActive = ConicalGradient;
            d.fillGradient.a = QPointF(g->centerX(), g->centerY());
            d.fillGradient.v0 = g->angle();
        } else {
            Q_UNREACHABLE();
        }
    } else {
        d.fillGradientActive = NoGradient;
    }
    d.dirty |= DirtyFillGradient;
    m_accDirty |= DirtyFillGradient;
}

void QQuickShapeNvprRenderer::endSync(bool)
{
}

void QQuickShapeNvprRenderer::setNode(QQuickShapeNvprRenderNode *node)
{
    if (m_node != node) {
        m_node = node;
        m_accDirty |= DirtyList;
    }
}

QDebug operator<<(QDebug debug, const QQuickShapeNvprRenderer::NvprPath &path)
{
    QDebugStateSaver saver(debug);
    debug.space().noquote();
    if (!path.str.isEmpty()) {
        debug << "Path with SVG string" << path.str;
        return debug;
    }
    debug << "Path with" << path.cmd.count() << "commands";
    int ci = 0;
    for (GLubyte cmd : path.cmd) {
        static struct { GLubyte cmd; const char *s; int coordCount; } nameTabs[] = {
        { GL_MOVE_TO_NV, "moveTo", 2 },
        { GL_LINE_TO_NV, "lineTo", 2 },
        { GL_QUADRATIC_CURVE_TO_NV, "quadTo", 4 },
        { GL_CUBIC_CURVE_TO_NV, "cubicTo", 6 },
        { GL_LARGE_CW_ARC_TO_NV, "arcTo-large-CW", 5 },
        { GL_LARGE_CCW_ARC_TO_NV, "arcTo-large-CCW", 5 },
        { GL_SMALL_CW_ARC_TO_NV, "arcTo-small-CW", 5 },
        { GL_SMALL_CCW_ARC_TO_NV, "arcTo-small-CCW", 5 },
        { GL_CLOSE_PATH_NV, "closePath", 0 } };
        for (const auto &nameTab : nameTabs) {
            if (nameTab.cmd == cmd) {
                QByteArray cs;
                for (int j = 0; j < nameTab.coordCount; ++j) {
                    cs.append(QByteArray::number(path.coord[ci++]));
                    cs.append(' ');
                }
                debug << "\n  " << nameTab.s << " " << cs;
                break;
            }
        }
    }
    return debug;
}

static inline void appendCoords(QVector<GLfloat> *v, QQuickCurve *c, QPointF *pos)
{
    QPointF p(c->hasRelativeX() ? pos->x() + c->relativeX() : c->x(),
              c->hasRelativeY() ? pos->y() + c->relativeY() : c->y());
    v->append(p.x());
    v->append(p.y());
    *pos = p;
}

static inline void appendControlCoords(QVector<GLfloat> *v, QQuickPathQuad *c, const QPointF &pos)
{
    QPointF p(c->hasRelativeControlX() ? pos.x() + c->relativeControlX() : c->controlX(),
              c->hasRelativeControlY() ? pos.y() + c->relativeControlY() : c->controlY());
    v->append(p.x());
    v->append(p.y());
}

static inline void appendControl1Coords(QVector<GLfloat> *v, QQuickPathCubic *c, const QPointF &pos)
{
    QPointF p(c->hasRelativeControl1X() ? pos.x() + c->relativeControl1X() : c->control1X(),
              c->hasRelativeControl1Y() ? pos.y() + c->relativeControl1Y() : c->control1Y());
    v->append(p.x());
    v->append(p.y());
}

static inline void appendControl2Coords(QVector<GLfloat> *v, QQuickPathCubic *c, const QPointF &pos)
{
    QPointF p(c->hasRelativeControl2X() ? pos.x() + c->relativeControl2X() : c->control2X(),
              c->hasRelativeControl2Y() ? pos.y() + c->relativeControl2Y() : c->control2Y());
    v->append(p.x());
    v->append(p.y());
}

void QQuickShapeNvprRenderer::convertPath(const QQuickPath *path, ShapePathGuiData *d)
{
    d->path = NvprPath();
    if (!path)
        return;

    const QList<QQuickPathElement *> &pp(QQuickPathPrivate::get(path)->_pathElements);
    if (pp.isEmpty())
        return;

    QPointF startPos(path->startX(), path->startY());
    QPointF pos(startPos);
    if (!qFuzzyIsNull(pos.x()) || !qFuzzyIsNull(pos.y())) {
        d->path.cmd.append(GL_MOVE_TO_NV);
        d->path.coord.append(pos.x());
        d->path.coord.append(pos.y());
    }

    for (QQuickPathElement *e : pp) {
        if (QQuickPathMove *o = qobject_cast<QQuickPathMove *>(e)) {
            d->path.cmd.append(GL_MOVE_TO_NV);
            appendCoords(&d->path.coord, o, &pos);
            startPos = pos;
        } else if (QQuickPathLine *o = qobject_cast<QQuickPathLine *>(e)) {
            d->path.cmd.append(GL_LINE_TO_NV);
            appendCoords(&d->path.coord, o, &pos);
        } else if (QQuickPathQuad *o = qobject_cast<QQuickPathQuad *>(e)) {
            d->path.cmd.append(GL_QUADRATIC_CURVE_TO_NV);
            appendControlCoords(&d->path.coord, o, pos);
            appendCoords(&d->path.coord, o, &pos);
        } else if (QQuickPathCubic *o = qobject_cast<QQuickPathCubic *>(e)) {
            d->path.cmd.append(GL_CUBIC_CURVE_TO_NV);
            appendControl1Coords(&d->path.coord, o, pos);
            appendControl2Coords(&d->path.coord, o, pos);
            appendCoords(&d->path.coord, o, &pos);
        } else if (QQuickPathArc *o = qobject_cast<QQuickPathArc *>(e)) {
            const bool sweepFlag = o->direction() == QQuickPathArc::Clockwise; // maps to CCW, not a typo
            GLenum cmd;
            if (o->useLargeArc())
                cmd = sweepFlag ? GL_LARGE_CCW_ARC_TO_NV : GL_LARGE_CW_ARC_TO_NV;
            else
                cmd = sweepFlag ? GL_SMALL_CCW_ARC_TO_NV : GL_SMALL_CW_ARC_TO_NV;
            d->path.cmd.append(cmd);
            d->path.coord.append(o->radiusX());
            d->path.coord.append(o->radiusY());
            d->path.coord.append(o->xAxisRotation());
            appendCoords(&d->path.coord, o, &pos);
        } else if (QQuickPathSvg *o = qobject_cast<QQuickPathSvg *>(e)) {
            // PathSvg cannot be combined with other elements. But take at
            // least startX and startY into account.
            if (d->path.str.isEmpty())
                d->path.str = QString(QStringLiteral("M %1 %2 ")).arg(pos.x()).arg(pos.y()).toUtf8();
            d->path.str.append(o->path().toUtf8());
        } else if (QQuickPathAngleArc *o = qobject_cast<QQuickPathAngleArc *>(e)) {
            QRectF rect(o->centerX() - o->radiusX(), o->centerY() - o->radiusY(), o->radiusX() * 2, o->radiusY() * 2);
            QPointF startPoint;
            QPointF endPoint;
            qt_find_ellipse_coords(rect, o->startAngle(), -o->sweepAngle(), &startPoint, &endPoint);

            // get to our starting position
            if (o->moveToStart())
                d->path.cmd.append(GL_MOVE_TO_NV);
            else
                d->path.cmd.append(GL_LINE_TO_NV); // ### should we check if startPoint == pos?
            d->path.coord.append(startPoint.x());
            d->path.coord.append(startPoint.y());

            const bool sweepFlag = o->sweepAngle() > 0; // maps to CCW, not a typo
            d->path.cmd.append(qAbs(o->sweepAngle()) > 180.0
                                 ? (sweepFlag ? GL_LARGE_CCW_ARC_TO_NV : GL_LARGE_CW_ARC_TO_NV)
                                 : (sweepFlag ? GL_SMALL_CCW_ARC_TO_NV : GL_SMALL_CW_ARC_TO_NV));
            d->path.coord.append(o->radiusX());
            d->path.coord.append(o->radiusY());
            d->path.coord.append(0); // xAxisRotation
            d->path.coord.append(endPoint.x());
            d->path.coord.append(endPoint.y());
            pos = endPoint;
        } else {
            qWarning() << "Shape/NVPR: unsupported Path element" << e;
        }
    }

    // For compatibility with QTriangulatingStroker. SVG and others would not
    // implicitly close the path when end_pos == start_pos (start_pos being the
    // last moveTo pos); that would still need an explicit 'z' or similar. We
    // don't have an explicit close command, so just fake a close when the
    // positions match.
    if (pos == startPos)
        d->path.cmd.append(GL_CLOSE_PATH_NV);
}

static inline QVector4D qsg_premultiply(const QColor &c, float globalOpacity)
{
    const float o = c.alphaF() * globalOpacity;
    return QVector4D(c.redF() * o, c.greenF() * o, c.blueF() * o, o);
}

void QQuickShapeNvprRenderer::updateNode()
{
    // Called on the render thread with gui blocked -> update the node with its
    // own copy of all relevant data.

    if (!m_accDirty)
        return;

    const int count = m_sp.count();
    const bool listChanged = m_accDirty & DirtyList;
    if (listChanged)
        m_node->m_sp.resize(count);

    for (int i = 0; i < count; ++i) {
        ShapePathGuiData &src(m_sp[i]);
        QQuickShapeNvprRenderNode::ShapePathRenderData &dst(m_node->m_sp[i]);

        int dirty = src.dirty;
        src.dirty = 0;
        if (listChanged)
            dirty |= DirtyPath | DirtyStyle | DirtyFillRule | DirtyDash | DirtyFillGradient;

        // updateNode() can be called several times with different dirty
        // states before render() gets invoked. So accumulate.
        dst.dirty |= dirty;

        if (dirty & DirtyPath)
            dst.source = src.path;

        if (dirty & DirtyStyle) {
            dst.strokeWidth = src.strokeWidth;
            dst.strokeColor = qsg_premultiply(src.strokeColor, 1.0f);
            dst.fillColor = qsg_premultiply(src.fillColor, 1.0f);
            switch (src.joinStyle) {
            case QQuickShapePath::MiterJoin:
                dst.joinStyle = GL_MITER_TRUNCATE_NV;
                break;
            case QQuickShapePath::BevelJoin:
                dst.joinStyle = GL_BEVEL_NV;
                break;
            case QQuickShapePath::RoundJoin:
                dst.joinStyle = GL_ROUND_NV;
                break;
            default:
                Q_UNREACHABLE();
            }
            dst.miterLimit = src.miterLimit;
            switch (src.capStyle) {
            case QQuickShapePath::FlatCap:
                dst.capStyle = GL_FLAT;
                break;
            case QQuickShapePath::SquareCap:
                dst.capStyle = GL_SQUARE_NV;
                break;
            case QQuickShapePath::RoundCap:
                dst.capStyle = GL_ROUND_NV;
                break;
            default:
                Q_UNREACHABLE();
            }
        }

        if (dirty & DirtyFillRule) {
            switch (src.fillRule) {
            case QQuickShapePath::OddEvenFill:
                dst.fillRule = GL_INVERT;
                break;
            case QQuickShapePath::WindingFill:
                dst.fillRule = GL_COUNT_UP_NV;
                break;
            default:
                Q_UNREACHABLE();
            }
        }

        if (dirty & DirtyDash) {
            // Multiply by strokeWidth because the Shape API follows QPen
            // meaning the input dash pattern and dash offset here are in width units.
            dst.dashOffset = src.dashOffset * src.strokeWidth;
            if (src.dashActive) {
                if (src.dashPattern.isEmpty()) {
                    // default values for DashLine as defined in qpen.cpp
                    dst.dashPattern.resize(2);
                    dst.dashPattern[0]  = 4 * src.strokeWidth; // dash
                    dst.dashPattern[1]  = 2 * src.strokeWidth; // space
                } else {
                    dst.dashPattern.resize(src.dashPattern.count());
                    for (int i = 0; i < src.dashPattern.count(); ++i)
                        dst.dashPattern[i] = GLfloat(src.dashPattern[i]) * src.strokeWidth;

                    // QPen expects a dash pattern of even length and so should we
                    if (src.dashPattern.count() % 2 != 0) {
                        qWarning("QQuickShapeNvprRenderNode: dash pattern not of even length");
                        dst.dashPattern << src.strokeWidth;
                    }
                }
            } else {
                dst.dashPattern.clear();
            }
        }

        if (dirty & DirtyFillGradient) {
            dst.fillGradientActive = src.fillGradientActive;
            if (src.fillGradientActive)
                dst.fillGradient = src.fillGradient;
        }
    }

    m_node->markDirty(QSGNode::DirtyMaterial);
    m_accDirty = 0;
}

bool QQuickShapeNvprRenderNode::nvprInited = false;
QQuickNvprFunctions QQuickShapeNvprRenderNode::nvpr;
QQuickNvprMaterialManager QQuickShapeNvprRenderNode::mtlmgr;

QQuickShapeNvprRenderNode::~QQuickShapeNvprRenderNode()
{
    releaseResources();
}

void QQuickShapeNvprRenderNode::releaseResources()
{
    for (ShapePathRenderData &d : m_sp) {
        if (d.path) {
            nvpr.deletePaths(d.path, 1);
            d.path = 0;
        }
        if (d.fallbackFbo) {
            delete d.fallbackFbo;
            d.fallbackFbo = nullptr;
        }
    }

    m_fallbackBlitter.destroy();
}

void QQuickNvprMaterialManager::create(QQuickNvprFunctions *nvpr)
{
    m_nvpr = nvpr;
}

void QQuickNvprMaterialManager::releaseResources()
{
    QOpenGLExtraFunctions *f = QOpenGLContext::currentContext()->extraFunctions();
    for (MaterialDesc &mtl : m_materials) {
        if (mtl.ppl) {
            f->glDeleteProgramPipelines(1, &mtl.ppl);
            mtl = MaterialDesc();
        }
    }
}

QQuickNvprMaterialManager::MaterialDesc *QQuickNvprMaterialManager::activateMaterial(Material m)
{
    QOpenGLExtraFunctions *f = QOpenGLContext::currentContext()->extraFunctions();
    MaterialDesc &mtl(m_materials[m]);

    if (!mtl.ppl) {
        if (m == MatSolid) {
            static const char *fragSrc =
                    "#version 310 es\n"
                    "precision highp float;\n"
                    "out vec4 fragColor;\n"
                    "uniform vec4 color;\n"
                    "uniform float opacity;\n"
                    "void main() {\n"
                    "  fragColor = color * opacity;\n"
                    "}\n";
            if (!m_nvpr->createFragmentOnlyPipeline(fragSrc, &mtl.ppl, &mtl.prg)) {
                qWarning("NVPR: Failed to create shader pipeline for solid fill");
                return nullptr;
            }
            Q_ASSERT(mtl.ppl && mtl.prg);
            mtl.uniLoc[0] = f->glGetProgramResourceLocation(mtl.prg, GL_UNIFORM, "color");
            Q_ASSERT(mtl.uniLoc[0] >= 0);
            mtl.uniLoc[1] = f->glGetProgramResourceLocation(mtl.prg, GL_UNIFORM, "opacity");
            Q_ASSERT(mtl.uniLoc[1] >= 0);
        } else if (m == MatLinearGradient) {
            static const char *fragSrc =
                    "#version 310 es\n"
                    "precision highp float;\n"
                    "layout(location = 0) in vec2 uv;"
                    "uniform float opacity;\n"
                    "uniform sampler2D gradTab;\n"
                    "uniform vec2 gradStart;\n"
                    "uniform vec2 gradEnd;\n"
                    "out vec4 fragColor;\n"
                    "void main() {\n"
                    "  vec2 gradVec = gradEnd - gradStart;\n"
                    "  float gradTabIndex = dot(gradVec, uv - gradStart) / (gradVec.x * gradVec.x + gradVec.y * gradVec.y);\n"
                    "  fragColor = texture(gradTab, vec2(gradTabIndex, 0.5)) * opacity;\n"
                    "}\n";
            if (!m_nvpr->createFragmentOnlyPipeline(fragSrc, &mtl.ppl, &mtl.prg)) {
                qWarning("NVPR: Failed to create shader pipeline for linear gradient");
                return nullptr;
            }
            Q_ASSERT(mtl.ppl && mtl.prg);
            mtl.uniLoc[1] = f->glGetProgramResourceLocation(mtl.prg, GL_UNIFORM, "opacity");
            Q_ASSERT(mtl.uniLoc[1] >= 0);
            mtl.uniLoc[2] = f->glGetProgramResourceLocation(mtl.prg, GL_UNIFORM, "gradStart");
            Q_ASSERT(mtl.uniLoc[2] >= 0);
            mtl.uniLoc[3] = f->glGetProgramResourceLocation(mtl.prg, GL_UNIFORM, "gradEnd");
            Q_ASSERT(mtl.uniLoc[3] >= 0);
        } else if (m == MatRadialGradient) {
            static const char *fragSrc =
                    "#version 310 es\n"
                    "precision highp float;\n"
                    "uniform sampler2D gradTab;\n"
                    "uniform float opacity;\n"
                    "uniform vec2 focalToCenter;\n"
                    "uniform float centerRadius;\n"
                    "uniform float focalRadius;\n"
                    "uniform vec2 translationPoint;\n"
                    "layout(location = 0) in vec2 uv;\n"
                    "out vec4 fragColor;\n"
                    "void main() {\n"
                    "    vec2 coord = uv - translationPoint;\n"
                    "    float rd = centerRadius - focalRadius;\n"
                    "    float b = 2.0 * (rd * focalRadius + dot(coord, focalToCenter));\n"
                    "    float fmp2_m_radius2 = -focalToCenter.x * focalToCenter.x - focalToCenter.y * focalToCenter.y + rd * rd;\n"
                    "    float inverse_2_fmp2_m_radius2 = 1.0 / (2.0 * fmp2_m_radius2);\n"
                    "    float det = b * b - 4.0 * fmp2_m_radius2 * ((focalRadius * focalRadius) - dot(coord, coord));\n"
                    "    vec4 result = vec4(0.0);\n"
                    "    if (det >= 0.0) {\n"
                    "        float detSqrt = sqrt(det);\n"
                    "        float w = max((-b - detSqrt) * inverse_2_fmp2_m_radius2, (-b + detSqrt) * inverse_2_fmp2_m_radius2);\n"
                    "        if (focalRadius + w * (centerRadius - focalRadius) >= 0.0)\n"
                    "            result = texture(gradTab, vec2(w, 0.5)) * opacity;\n"
                    "    }\n"
                    "    fragColor = result;\n"
                    "}\n";
            if (!m_nvpr->createFragmentOnlyPipeline(fragSrc, &mtl.ppl, &mtl.prg)) {
                qWarning("NVPR: Failed to create shader pipeline for radial gradient");
                return nullptr;
            }
            Q_ASSERT(mtl.ppl && mtl.prg);
            mtl.uniLoc[1] = f->glGetProgramResourceLocation(mtl.prg, GL_UNIFORM, "opacity");
            Q_ASSERT(mtl.uniLoc[1] >= 0);
            mtl.uniLoc[2] = f->glGetProgramResourceLocation(mtl.prg, GL_UNIFORM, "focalToCenter");
            Q_ASSERT(mtl.uniLoc[2] >= 0);
            mtl.uniLoc[3] = f->glGetProgramResourceLocation(mtl.prg, GL_UNIFORM, "centerRadius");
            Q_ASSERT(mtl.uniLoc[3] >= 0);
            mtl.uniLoc[4] = f->glGetProgramResourceLocation(mtl.prg, GL_UNIFORM, "focalRadius");
            Q_ASSERT(mtl.uniLoc[4] >= 0);
            mtl.uniLoc[5] = f->glGetProgramResourceLocation(mtl.prg, GL_UNIFORM, "translationPoint");
            Q_ASSERT(mtl.uniLoc[5] >= 0);
        } else if (m == MatConicalGradient) {
            static const char *fragSrc =
                    "#version 310 es\n"
                    "precision highp float;\n"
                    "#define INVERSE_2PI 0.1591549430918953358\n"
                    "uniform sampler2D gradTab;\n"
                    "uniform float opacity;\n"
                    "uniform float angle;\n"
                    "uniform vec2 translationPoint;\n"
                    "layout(location = 0) in vec2 uv;\n"
                    "out vec4 fragColor;\n"
                    "void main() {\n"
                    "    vec2 coord = uv - translationPoint;\n"
                    "    float t;\n"
                    "    if (abs(coord.y) == abs(coord.x))\n"
                    "        t = (atan(-coord.y + 0.002, coord.x) + angle) * INVERSE_2PI;\n"
                    "    else\n"
                    "        t = (atan(-coord.y, coord.x) + angle) * INVERSE_2PI;\n"
                    "    fragColor = texture(gradTab, vec2(t - floor(t), 0.5)) * opacity;\n"
                    "}\n";
            if (!m_nvpr->createFragmentOnlyPipeline(fragSrc, &mtl.ppl, &mtl.prg)) {
                qWarning("NVPR: Failed to create shader pipeline for conical gradient");
                return nullptr;
            }
            Q_ASSERT(mtl.ppl && mtl.prg);
            mtl.uniLoc[1] = f->glGetProgramResourceLocation(mtl.prg, GL_UNIFORM, "opacity");
            Q_ASSERT(mtl.uniLoc[1] >= 0);
            mtl.uniLoc[2] = f->glGetProgramResourceLocation(mtl.prg, GL_UNIFORM, "angle");
            Q_ASSERT(mtl.uniLoc[2] >= 0);
            mtl.uniLoc[3] = f->glGetProgramResourceLocation(mtl.prg, GL_UNIFORM, "translationPoint");
            Q_ASSERT(mtl.uniLoc[3] >= 0);
        } else {
            Q_UNREACHABLE();
        }
    }

    f->glBindProgramPipeline(mtl.ppl);

    return &mtl;
}

void QQuickShapeNvprRenderNode::updatePath(ShapePathRenderData *d)
{
    if (d->dirty & QQuickShapeNvprRenderer::DirtyPath) {
        if (!d->path) {
            d->path = nvpr.genPaths(1);
            Q_ASSERT(d->path != 0);
        }
        if (d->source.str.isEmpty()) {
            nvpr.pathCommands(d->path, d->source.cmd.count(), d->source.cmd.constData(),
                              d->source.coord.count(), GL_FLOAT, d->source.coord.constData());
        } else {
            nvpr.pathString(d->path, GL_PATH_FORMAT_SVG_NV, d->source.str.count(), d->source.str.constData());
        }
    }

    if (d->dirty & QQuickShapeNvprRenderer::DirtyStyle) {
        nvpr.pathParameterf(d->path, GL_PATH_STROKE_WIDTH_NV, d->strokeWidth);
        nvpr.pathParameteri(d->path, GL_PATH_JOIN_STYLE_NV, d->joinStyle);
        nvpr.pathParameteri(d->path, GL_PATH_MITER_LIMIT_NV, d->miterLimit);
        nvpr.pathParameteri(d->path, GL_PATH_END_CAPS_NV, d->capStyle);
        nvpr.pathParameteri(d->path, GL_PATH_DASH_CAPS_NV, d->capStyle);
    }

    if (d->dirty & QQuickShapeNvprRenderer::DirtyDash) {
        nvpr.pathParameterf(d->path, GL_PATH_DASH_OFFSET_NV, d->dashOffset);
        // count == 0 -> no dash
        nvpr.pathDashArray(d->path, d->dashPattern.count(), d->dashPattern.constData());
    }

    if (d->dirty)
        d->fallbackValid = false;
}

void QQuickShapeNvprRenderNode::renderStroke(ShapePathRenderData *d, int strokeStencilValue, int writeMask)
{
    QQuickNvprMaterialManager::MaterialDesc *mtl = mtlmgr.activateMaterial(QQuickNvprMaterialManager::MatSolid);
    f->glProgramUniform4f(mtl->prg, mtl->uniLoc[0],
            d->strokeColor.x(), d->strokeColor.y(), d->strokeColor.z(), d->strokeColor.w());
    f->glProgramUniform1f(mtl->prg, mtl->uniLoc[1], inheritedOpacity());

    nvpr.stencilThenCoverStrokePath(d->path, strokeStencilValue, writeMask, GL_CONVEX_HULL_NV);
}

void QQuickShapeNvprRenderNode::renderFill(ShapePathRenderData *d)
{
    QQuickNvprMaterialManager::MaterialDesc *mtl = nullptr;
    if (d->fillGradientActive) {
        QQuickShapeGradient::SpreadMode spread = d->fillGradient.spread;
        if (d->fillGradientActive == QQuickAbstractPathRenderer::LinearGradient) {
            mtl = mtlmgr.activateMaterial(QQuickNvprMaterialManager::MatLinearGradient);
            // uv = vec2(coeff[0] * x + coeff[1] * y + coeff[2], coeff[3] * x + coeff[4] * y + coeff[5])
            // where x and y are in path coordinate space, which is just what
            // we need since the gradient's start and stop are in that space too.
            GLfloat coeff[6] = { 1, 0, 0,
                                 0, 1, 0 };
            nvpr.programPathFragmentInputGen(mtl->prg, 0, GL_OBJECT_LINEAR_NV, 2, coeff);
            f->glProgramUniform2f(mtl->prg, mtl->uniLoc[2], d->fillGradient.a.x(), d->fillGradient.a.y());
            f->glProgramUniform2f(mtl->prg, mtl->uniLoc[3], d->fillGradient.b.x(), d->fillGradient.b.y());
        } else if (d->fillGradientActive == QQuickAbstractPathRenderer::RadialGradient) {
            mtl = mtlmgr.activateMaterial(QQuickNvprMaterialManager::MatRadialGradient);
            // simply drive uv (location 0) with x and y, just like for the linear gradient
            GLfloat coeff[6] = { 1, 0, 0,
                                 0, 1, 0 };
            nvpr.programPathFragmentInputGen(mtl->prg, 0, GL_OBJECT_LINEAR_NV, 2, coeff);

            const QPointF centerPoint = d->fillGradient.a;
            const QPointF focalPoint = d->fillGradient.b;
            const QPointF focalToCenter = centerPoint - focalPoint;
            const GLfloat centerRadius = d->fillGradient.v0;
            const GLfloat focalRadius = d->fillGradient.v1;

            f->glProgramUniform2f(mtl->prg, mtl->uniLoc[2], focalToCenter.x(), focalToCenter.y());
            f->glProgramUniform1f(mtl->prg, mtl->uniLoc[3], centerRadius);
            f->glProgramUniform1f(mtl->prg, mtl->uniLoc[4], focalRadius);
            f->glProgramUniform2f(mtl->prg, mtl->uniLoc[5], focalPoint.x(), focalPoint.y());
        } else if (d->fillGradientActive == QQuickAbstractPathRenderer::ConicalGradient) {
            mtl = mtlmgr.activateMaterial(QQuickNvprMaterialManager::MatConicalGradient);
            // same old
            GLfloat coeff[6] = { 1, 0, 0,
                                 0, 1, 0 };
            nvpr.programPathFragmentInputGen(mtl->prg, 0, GL_OBJECT_LINEAR_NV, 2, coeff);

            const QPointF centerPoint = d->fillGradient.a;
            const GLfloat angle = -qDegreesToRadians(d->fillGradient.v0);

            f->glProgramUniform1f(mtl->prg, mtl->uniLoc[2], angle);
            f->glProgramUniform2f(mtl->prg, mtl->uniLoc[3], centerPoint.x(), centerPoint.y());

            spread = QQuickShapeGradient::RepeatSpread;
        } else {
            Q_UNREACHABLE();
        }
        const QQuickShapeGradientCacheKey cacheKey(d->fillGradient.stops, spread);
        QSGTexture *tx = QQuickShapeGradientOpenGLCache::currentCache()->get(cacheKey);
        tx->bind();
    } else {
        mtl = mtlmgr.activateMaterial(QQuickNvprMaterialManager::MatSolid);
        f->glProgramUniform4f(mtl->prg, mtl->uniLoc[0],
                d->fillColor.x(), d->fillColor.y(), d->fillColor.z(), d->fillColor.w());
    }
    f->glProgramUniform1f(mtl->prg, mtl->uniLoc[1], inheritedOpacity());

    const int writeMask = 0xFF;
    nvpr.stencilThenCoverFillPath(d->path, d->fillRule, writeMask, GL_BOUNDING_BOX_NV);
}

void QQuickShapeNvprRenderNode::renderOffscreenFill(ShapePathRenderData *d)
{
    if (d->fallbackValid && d->fallbackFbo)
        return;

    GLfloat bb[4];
    nvpr.getPathParameterfv(d->path, GL_PATH_STROKE_BOUNDING_BOX_NV, bb);
    QSize sz = QSizeF(bb[2] - bb[0] + 1, bb[3] - bb[1] + 1).toSize();
    d->fallbackSize = QSize(qMax(32, sz.width()), qMax(32, sz.height()));
    d->fallbackTopLeft = QPointF(bb[0], bb[1]);

    if (d->fallbackFbo && d->fallbackFbo->size() != d->fallbackSize) {
        delete d->fallbackFbo;
        d->fallbackFbo = nullptr;
    }
    if (!d->fallbackFbo)
        d->fallbackFbo = new QOpenGLFramebufferObject(d->fallbackSize, QOpenGLFramebufferObject::CombinedDepthStencil);
    if (!d->fallbackFbo->bind())
        return;

    GLint prevViewport[4];
    f->glGetIntegerv(GL_VIEWPORT, prevViewport);

    f->glViewport(0, 0, d->fallbackSize.width(), d->fallbackSize.height());
    f->glDisable(GL_DEPTH_TEST);
    f->glClearColor(0, 0, 0, 0);
    f->glClearStencil(0);
    f->glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
    f->glStencilFunc(GL_NOTEQUAL, 0, 0xFF);
    f->glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);

    QMatrix4x4 mv;
    mv.translate(-d->fallbackTopLeft.x(), -d->fallbackTopLeft.y());
    nvpr.matrixLoadf(GL_PATH_MODELVIEW_NV, mv.constData());
    QMatrix4x4 proj;
    proj.ortho(0, d->fallbackSize.width(), d->fallbackSize.height(), 0, 1, -1);
    nvpr.matrixLoadf(GL_PATH_PROJECTION_NV, proj.constData());

    renderFill(d);

    d->fallbackFbo->release();
    f->glEnable(GL_DEPTH_TEST);
    f->glViewport(prevViewport[0], prevViewport[1], prevViewport[2], prevViewport[3]);

    d->fallbackValid = true;
}

void QQuickShapeNvprRenderNode::setupStencilForCover(bool stencilClip, int sv)
{
    if (!stencilClip) {
        // Assume stencil buffer is cleared to 0 for each frame.
        // Within the frame dppass=GL_ZERO for glStencilOp ensures stencil is reset and so no need to clear.
        f->glStencilFunc(GL_NOTEQUAL, 0, 0xFF);
        f->glStencilOp(GL_KEEP, GL_KEEP, GL_ZERO);
    } else {
        f->glStencilFunc(GL_LESS, sv, 0xFF); // pass if (sv & 0xFF) < (stencil_value & 0xFF)
        f->glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); // dppass: replace with the original value (clip's stencil ref value)
    }
}

void QQuickShapeNvprRenderNode::render(const RenderState *state)
{
    f = QOpenGLContext::currentContext()->extraFunctions();

    if (!nvprInited) {
        if (!nvpr.create()) {
            qWarning("NVPR init failed");
            return;
        }
        mtlmgr.create(&nvpr);
        nvprInited = true;
    }

    f->glUseProgram(0);
    f->glStencilMask(~0);
    f->glEnable(GL_STENCIL_TEST);

    const bool stencilClip = state->stencilEnabled();
    // when true, the stencil buffer already has a clip path with a ref value of sv
    const int sv = state->stencilValue();
    const bool hasScissor = state->scissorEnabled();

    if (hasScissor) {
        // scissor rect is already set, just enable scissoring
        f->glEnable(GL_SCISSOR_TEST);
    }

    // Depth test against the opaque batches rendered before.
    f->glEnable(GL_DEPTH_TEST);
    f->glDepthFunc(GL_LESS);
    nvpr.pathCoverDepthFunc(GL_LESS);
    nvpr.pathStencilDepthOffset(-0.05f, -1);

    bool reloadMatrices = true;

    for (ShapePathRenderData &d : m_sp) {
        updatePath(&d);

        const bool hasFill = d.hasFill();
        const bool hasStroke = d.hasStroke();

        if (hasFill && stencilClip) {
            // Fall back to a texture when complex clipping is in use and we have
            // to fill. Reconciling glStencilFillPath's and the scenegraph's clip
            // stencil semantics has not succeeded so far...
            if (hasScissor)
                f->glDisable(GL_SCISSOR_TEST);
            renderOffscreenFill(&d);
            reloadMatrices = true;
            if (hasScissor)
                f->glEnable(GL_SCISSOR_TEST);
        }

        if (reloadMatrices) {
            reloadMatrices = false;
            nvpr.matrixLoadf(GL_PATH_MODELVIEW_NV, matrix()->constData());
            nvpr.matrixLoadf(GL_PATH_PROJECTION_NV, state->projectionMatrix()->constData());
        }

        // Fill!
        if (hasFill) {
            if (!stencilClip) {
                setupStencilForCover(false, 0);
                renderFill(&d);
            } else {
                if (!m_fallbackBlitter.isCreated())
                    m_fallbackBlitter.create();
                f->glStencilFunc(GL_EQUAL, sv, 0xFF);
                f->glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
                QMatrix4x4 mv = *matrix();
                mv.translate(d.fallbackTopLeft.x(), d.fallbackTopLeft.y());
                m_fallbackBlitter.texturedQuad(d.fallbackFbo->texture(), d.fallbackFbo->size(),
                                               *state->projectionMatrix(), mv,
                                               inheritedOpacity());
            }
        }

        // Stroke!
        if (hasStroke) {
            const int strokeStencilValue = 0x80;
            const int writeMask = 0x80;

            setupStencilForCover(stencilClip, sv);
            if (stencilClip) {
                // for the stencil step (eff. read mask == 0xFF & ~writeMask)
                nvpr.pathStencilFunc(GL_EQUAL, sv, 0xFF);
                // With stencilCLip == true the read mask for the stencil test before the stencil step is 0x7F.
                // This assumes the clip stencil value is <= 127.
                if (sv >= strokeStencilValue)
                    qWarning("Shape/NVPR: stencil clip ref value %d too large; expect rendering errors", sv);
            }

            renderStroke(&d, strokeStencilValue, writeMask);
        }

        if (stencilClip)
            nvpr.pathStencilFunc(GL_ALWAYS, 0, ~0);

        d.dirty = 0;
    }

    f->glBindProgramPipeline(0);
}

QSGRenderNode::StateFlags QQuickShapeNvprRenderNode::changedStates() const
{
    return BlendState | StencilState | DepthState | ScissorState;
}

QSGRenderNode::RenderingFlags QQuickShapeNvprRenderNode::flags() const
{
    return DepthAwareRendering; // avoid hitting the less optimal no-opaque-batch path in the renderer
}

bool QQuickShapeNvprRenderNode::isSupported()
{
    static const bool nvprDisabled = qEnvironmentVariableIntValue("QT_NO_NVPR") != 0;
    return !nvprDisabled && QQuickNvprFunctions::isSupported();
}

bool QQuickNvprBlitter::create()
{
    if (isCreated())
        destroy();

    m_program = new QOpenGLShaderProgram;
    if (QOpenGLContext::currentContext()->format().profile() == QSurfaceFormat::CoreProfile) {
        m_program->addCacheableShaderFromSourceFile(QOpenGLShader::Vertex, QStringLiteral(":/qt-project.org/shapes/shaders/blit_core.vert"));
        m_program->addCacheableShaderFromSourceFile(QOpenGLShader::Fragment, QStringLiteral(":/qt-project.org/shapes/shaders/blit_core.frag"));
    } else {
        m_program->addCacheableShaderFromSourceFile(QOpenGLShader::Vertex, QStringLiteral(":/qt-project.org/shapes/shaders/blit.vert"));
        m_program->addCacheableShaderFromSourceFile(QOpenGLShader::Fragment, QStringLiteral(":/qt-project.org/shapes/shaders/blit.frag"));
    }
    m_program->bindAttributeLocation("qt_Vertex", 0);
    m_program->bindAttributeLocation("qt_MultiTexCoord0", 1);
    if (!m_program->link())
        return false;

    m_matrixLoc = m_program->uniformLocation("qt_Matrix");
    m_opacityLoc = m_program->uniformLocation("qt_Opacity");

    m_buffer = new QOpenGLBuffer;
    if (!m_buffer->create())
        return false;
    m_buffer->bind();
    m_buffer->allocate(4 * sizeof(GLfloat) * 6);
    m_buffer->release();

    return true;
}

void QQuickNvprBlitter::destroy()
{
    if (m_program) {
        delete m_program;
        m_program = nullptr;
    }
    if (m_buffer) {
        delete m_buffer;
        m_buffer = nullptr;
    }
}

void QQuickNvprBlitter::texturedQuad(GLuint textureId, const QSize &size,
                                     const QMatrix4x4 &proj, const QMatrix4x4 &modelview,
                                     float opacity)
{
    QOpenGLExtraFunctions *f = QOpenGLContext::currentContext()->extraFunctions();

    m_program->bind();

    QMatrix4x4 m = proj * modelview;
    m_program->setUniformValue(m_matrixLoc, m);
    m_program->setUniformValue(m_opacityLoc, opacity);

    m_buffer->bind();

    if (size != m_prevSize) {
        m_prevSize = size;

        QPointF p0(size.width() - 1, size.height() - 1);
        QPointF p1(0, 0);
        QPointF p2(0, size.height() - 1);
        QPointF p3(size.width() - 1, 0);

        GLfloat vertices[6 * 4] = {
            GLfloat(p0.x()), GLfloat(p0.y()), 1, 0,
            GLfloat(p1.x()), GLfloat(p1.y()), 0, 1,
            GLfloat(p2.x()), GLfloat(p2.y()), 0, 0,

            GLfloat(p0.x()), GLfloat(p0.y()), 1, 0,
            GLfloat(p3.x()), GLfloat(p3.y()), 1, 1,
            GLfloat(p1.x()), GLfloat(p1.y()), 0, 1,
        };

        m_buffer->write(0, vertices, sizeof(vertices));
    }

    m_program->enableAttributeArray(0);
    m_program->enableAttributeArray(1);
    f->glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), nullptr);
    f->glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (const void *) (2 * sizeof(GLfloat)));

    f->glBindTexture(GL_TEXTURE_2D, textureId);

    f->glDrawArrays(GL_TRIANGLES, 0, 6);

    f->glBindTexture(GL_TEXTURE_2D, 0);
    m_buffer->release();
    m_program->release();
}

QT_END_NAMESPACE
