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

#include "proceduralmesh_p.h"

#include <QtQuick3D/private/qquick3dobject_p.h>

#include <QtQuick/QQuickWindow>

#include <rhi/qrhi.h>

QT_BEGIN_NAMESPACE

/*!
    \qmltype ProceduralMesh
    \inqmlmodule QtQuick3D.Helpers
    \inherits Geometry
    \brief Allows creation of Geometry from QML.
    \since 6.6

    ProceduralMesh is a helper type that allows creation of Geometry instances
    from QML. The Geometry component is Abstract, and is usually created
    from C++.

    \qml
    component TorusMesh : ProceduralMesh {
        property real rings: 50
        property real segments: 50
        property real radius: 100.0
        property real tubeRadius: 10.0
        property var meshArrays: generateTorus(rings, segments, radius, tubeRadius)
        positions: meshArrays.verts
        normals: meshArrays.normals
        uv0s: meshArrays.uvs
        indexes: meshArrays.indices

        function generateTorus(rings: real, segments: real, radius: real, tubeRadius: real) {
            let verts = []
            let normals = []
            let uvs = []
            let indices = []

            for (let i = 0; i <= rings; ++i) {
                for (let j = 0; j <= segments; ++j) {
                    let u = i / rings * Math.PI * 2;
                    let v = j / segments * Math.PI * 2;

                    let centerX = radius * Math.cos(u);
                    let centerZ = radius * Math.sin(u);

                    let posX = centerX + tubeRadius * Math.cos(v) * Math.cos(u);
                    let posY = tubeRadius * Math.sin(v);
                    let posZ = centerZ + tubeRadius * Math.cos(v) * Math.sin(u);

                    verts.push(Qt.vector3d(posX, posY, posZ));

                    let normal = Qt.vector3d(posX - centerX, posY, posZ - centerZ).normalized();
                    normals.push(normal);

                    uvs.push(Qt.vector2d(i / rings, j / segments));
                }
            }

            for (let i = 0; i < rings; ++i) {
                for (let j = 0; j < segments; ++j) {
                    let a = (segments + 1) * i + j;
                    let b = (segments + 1) * (i + 1) + j;
                    let c = (segments + 1) * (i + 1) + j + 1;
                    let d = (segments + 1) * i + j + 1;

                    // Generate two triangles for each quad in the mesh
                    // Adjust order to be counter-clockwise
                    indices.push(a, d, b);
                    indices.push(b, d, c);
                }
            }
            return { verts: verts, normals: normals, uvs: uvs, indices: indices }
        }
    }
    \endqml

    The above code defines a component TorusMesh that can be used as Geometry for use
    with a Model component. When the ring, segments, radius or tubeRadius properties
    are modified the geometry will be updated.

    The ProceduralMesh component is not as flexible nor as performant as creating
    Geometry in C++, but makes up for it in convenience and simplicity. The
    properties are fixed attribute lists that when filled will automatically
    generate the necessary buffers.

*/

/*!
    \qmlproperty List<QVector3D> ProceduralMesh::positions
    The positions attribute list. If this list remains empty nothing no geometry
    will be generated.
*/

/*!
    \qmlproperty List<QVector3D> ProceduralMesh::normals
    Holds the normals attribute list.
*/

/*!
    \qmlproperty List<QVector3D> ProceduralMesh::tangents
    Holds the tangents attribute list.
*/

/*!
    \qmlproperty List<QVector3D> ProceduralMesh::binormals
    Holds the binormals attribute list.
*/

/*!
    \qmlproperty List<QVector2D> ProceduralMesh::uv0s
    This property defines a list of uv coordinates for the first uv channel (uv0)
*/

/*!
    \qmlproperty List<QVector2D> ProceduralMesh::uv1s
    This property defines a list of uv coordinates for the second uv channel (uv1)
*/

/*!
    \qmlproperty List<QVector4D> ProceduralMesh::colors
    This property defines a list of vertex color values.
*/

/*!
    \qmlproperty List<QVector4D> ProceduralMesh::joints
    This property defines a list of joint indices for skinning.
*/

/*!
    \qmlproperty List<QVector4D> ProceduralMesh::weights
    This property defines a list of joint weights for skinning.
*/

/*!
    \qmlproperty List<int> ProceduralMesh::indexes
    This property defines a list of indexes into the attribute lists. If this list remains empty
    the vertex buffer values will be used directly.
*/

/*!
    \qmlproperty enumeration ProceduralMesh::primitiveMode

    This property defines the primitive mode to use when rendering the geometry.

    \value ProceduralMesh.Points The points primitive mode is used.
    \value ProceduralMesh.LineStrip The line strip primitive mode is used.
    \value ProceduralMesh.Lines The lines primitive mode is used.
    \value ProceduralMesh.TriangleStrip The triangles strip primitive mode is
    used.
    \value ProceduralMesh.TriangleFan The triangle fan primitive mode is used.
    \value ProceduralMesh.Triangles The triangles primitive mode is used.
    \default ProceduralMesh.Triangles

    \note Not all modes are supported on all rendering backends.
*/

/*!
    \qmlproperty List<ProceduralMeshSubset> ProceduralMesh::subsets

    This property defines a list of subsets to split the geometry data into.
    Each subset can have it's own material.  The order of this array
    corresponds to the materials list of Model when using this geometry.

    This property is optional and when empty results in a single subset.

    \note Any subset that specifies values outside of the range of available
    vertex/index values will lead to that subset being ignored.
*/

/*!
    \qmltype ProceduralMeshSubset
    \inqmlmodule QtQuick3D.Helpers
    \inherits QtObject
    \brief Defines a subset of a ProceduralMesh.
    \since 6.6

    This type defines a subset of a ProceduralMesh. Each subset can have it's own
    material and can be used to split the geometry into multiple draw calls.

    \sa ProceduralMesh::subsets

*/

/*!
    \qmlproperty int ProceduralMeshSubset::offset
    This property defines the starting index for this subset. \default 0
*/

/*!
    \qmlproperty int ProceduralMeshSubset::count
    This property defines the number of indices to use for this subset. This property must be set for the subset to have content.

    \default 0
*/

/*!
    \qmlproperty Material ProceduralMeshSubset::name
    This property defines a name of the subset. This property is optional, and is only used to
    tag the subset for debugging purposes.
*/

ProceduralMesh::ProceduralMesh()
{

}

QList<QVector3D> ProceduralMesh::positions() const
{
    return m_positions;
}

void ProceduralMesh::setPositions(const QList<QVector3D> &newPositions)
{
    if (m_positions == newPositions)
        return;
    m_positions = newPositions;
    Q_EMIT positionsChanged();
    requestUpdate();
}

ProceduralMesh::PrimitiveMode ProceduralMesh::primitiveMode() const
{
    return m_primitiveMode;
}

void ProceduralMesh::setPrimitiveMode(PrimitiveMode newPrimitiveMode)
{
    if (m_primitiveMode == newPrimitiveMode)
        return;

    // Do some sanity checking
    if (newPrimitiveMode < Points || newPrimitiveMode > Triangles) {
        qWarning() << "Invalid primitive mode specified";
        return;
    }

    if (newPrimitiveMode == PrimitiveMode::TriangleFan) {
        if (!supportsTriangleFanPrimitive()) {
            qWarning() << "TriangleFan is not supported by the current backend";
            return;
        }
    }

    m_primitiveMode = newPrimitiveMode;
    Q_EMIT primitiveModeChanged();
    requestUpdate();
}

void ProceduralMesh::requestUpdate()
{
    if (!m_updateRequested) {
        QMetaObject::invokeMethod(this, "updateGeometry", Qt::QueuedConnection);
        m_updateRequested = true;
    }
}

void ProceduralMesh::updateGeometry()
{
    m_updateRequested = false;
    // reset the geometry
    clear();

    setPrimitiveType(PrimitiveType(m_primitiveMode));

    // Figure out which attributes are being used
    const auto expectedLength = m_positions.size();
    bool hasPositions = !m_positions.isEmpty();
    if (!hasPositions) {
        setStride(0);
        update();
        return; // If there are no positions, there is no point :-)
    }
    bool hasNormals = m_normals.size() >= expectedLength;
    bool hasTangents = m_tangents.size() >= expectedLength;
    bool hasBinormals = m_binormals.size() >= expectedLength;
    bool hasUV0s = m_uv0s.size() >= expectedLength;
    bool hasUV1s = m_uv1s.size() >= expectedLength;
    bool hasColors = m_colors.size() >= expectedLength;
    bool hasJoints = m_joints.size() >= expectedLength;
    bool hasWeights = m_weights.size() >= expectedLength;
    bool hasIndexes = !m_indexes.isEmpty();

    int offset = 0;
    if (hasPositions) {
        addAttribute(Attribute::Semantic::PositionSemantic, offset, Attribute::ComponentType::F32Type);
        offset += 3 * sizeof(float);
    }

    if (hasNormals) {
        addAttribute(Attribute::Semantic::NormalSemantic, offset, Attribute::ComponentType::F32Type);
        offset += 3 * sizeof(float);
    }

    if (hasTangents) {
        addAttribute(Attribute::Semantic::TangentSemantic, offset, Attribute::ComponentType::F32Type);
        offset += 3 * sizeof(float);
    }

    if (hasBinormals) {
        addAttribute(Attribute::Semantic::BinormalSemantic, offset, Attribute::ComponentType::F32Type);
        offset += 3 * sizeof(float);
    }

    if (hasUV0s) {
        addAttribute(Attribute::Semantic::TexCoord0Semantic, offset, Attribute::ComponentType::F32Type);
        offset += 2 * sizeof(float);
    }

    if (hasUV1s) {
        addAttribute(Attribute::Semantic::TexCoord1Semantic, offset, Attribute::ComponentType::F32Type);
        offset += 2 * sizeof(float);
    }

    if (hasColors) {
        addAttribute(Attribute::Semantic::ColorSemantic, offset, Attribute::ComponentType::F32Type);
        offset += 4 * sizeof(float);
    }

    if (hasJoints) {
        addAttribute(Attribute::Semantic::JointSemantic, offset, Attribute::ComponentType::F32Type);
        offset += 4 * sizeof(float);
    }

    if (hasWeights) {
        addAttribute(Attribute::Semantic::WeightSemantic, offset, Attribute::ComponentType::F32Type);
        offset += 4 * sizeof(float);
    }

    if (hasIndexes)
        addAttribute(Attribute::Semantic::IndexSemantic, 0, Attribute::ComponentType::U32Type);

    // Set up the vertex buffer
    const int stride = offset;
    const qsizetype bufferSize = expectedLength * stride;
    setStride(stride);

    QVector<float> vertexBufferData;
    vertexBufferData.reserve(bufferSize / sizeof(float));

    QVector3D minBounds;
    QVector3D maxBounds;

    for (qsizetype i = 0; i < expectedLength; ++i) {
        // start writing float values to vertexBuffer
        if (hasPositions) {
            const auto &position = m_positions[i];
            vertexBufferData.append(position.x());
            vertexBufferData.append(position.y());
            vertexBufferData.append(position.z());
            minBounds.setX(qMin(minBounds.x(), position.x()));
            maxBounds.setX(qMax(maxBounds.x(), position.x()));
            minBounds.setY(qMin(minBounds.y(), position.y()));
            maxBounds.setY(qMax(maxBounds.y(), position.y()));
            minBounds.setZ(qMin(minBounds.z(), position.z()));
            maxBounds.setZ(qMax(maxBounds.z(), position.z()));
        }
        if (hasNormals) {
            const auto &normal = m_normals[i];
            vertexBufferData.append(normal.x());
            vertexBufferData.append(normal.y());
            vertexBufferData.append(normal.z());
        }

        if (hasBinormals) {
            const auto &binormal = m_binormals[i];
            vertexBufferData.append(binormal.x());
            vertexBufferData.append(binormal.y());
            vertexBufferData.append(binormal.z());
        }

        if (hasTangents) {
            const auto &tangent = m_tangents[i];
            vertexBufferData.append(tangent.x());
            vertexBufferData.append(tangent.y());
            vertexBufferData.append(tangent.z());
        }

        if (hasUV0s) {
            const auto &uv0 = m_uv0s[i];
            vertexBufferData.append(uv0.x());
            vertexBufferData.append(uv0.y());
        }

        if (hasUV1s) {
            const auto &uv1 = m_uv1s[i];
            vertexBufferData.append(uv1.x());
            vertexBufferData.append(uv1.y());
        }

        if (hasColors) {
            const auto &color = m_colors[i];
            vertexBufferData.append(color.x());
            vertexBufferData.append(color.y());
            vertexBufferData.append(color.z());
            vertexBufferData.append(color.w());
        }

        if (hasJoints) {
            const auto &joint = m_joints[i];
            vertexBufferData.append(joint.x());
            vertexBufferData.append(joint.y());
            vertexBufferData.append(joint.z());
            vertexBufferData.append(joint.w());
        }

        if (hasWeights) {
            const auto &weight = m_weights[i];
            vertexBufferData.append(weight.x());
            vertexBufferData.append(weight.y());
            vertexBufferData.append(weight.z());
            vertexBufferData.append(weight.w());
        }
    }

    setBounds(minBounds, maxBounds);
    QByteArray vertexBuffer(reinterpret_cast<char *>(vertexBufferData.data()), bufferSize);
    setVertexData(vertexBuffer);

    // Index Buffer
    if (hasIndexes) {
        const qsizetype indexLength = m_indexes.size();
        QByteArray indexBuffer;
        indexBuffer.reserve(indexLength * sizeof(unsigned int));
        for (qsizetype i = 0; i < indexLength; ++i) {
            const auto &index = m_indexes[i];
            indexBuffer.append(reinterpret_cast<const char *>(&index), sizeof(unsigned int));
        }
        setIndexData(indexBuffer);
    }

    // Subsets
    // Subsets are optional so if none are specified the whole mesh is a single submesh
    if (!m_subsets.isEmpty()) {
        for (const auto &subset : m_subsets) {
            QVector3D subsetMinBounds;
            QVector3D subsetMaxBounds;
            // Range checking is necessary because the user could have specified subset values
            // that are out of range of the vertex/index buffer
            bool outOfRange = false;
            for (qsizetype i = subset->offset(); i < subset->offset() + subset->count(); ++i) {
                if (hasPositions) {
                    qsizetype index = i;
                    if (hasIndexes) {
                        if (i < m_indexes.size()) {
                            index = m_indexes[i];
                        } else {
                            outOfRange = true;
                            break;
                        }
                    }
                    if (index < m_positions.size()) {
                        const auto &position = m_positions[index];
                        subsetMinBounds.setX(qMin(subsetMinBounds.x(), position.x()));
                        subsetMaxBounds.setX(qMax(subsetMaxBounds.x(), position.x()));
                        subsetMinBounds.setY(qMin(subsetMinBounds.y(), position.y()));
                        subsetMaxBounds.setY(qMax(subsetMaxBounds.y(), position.y()));
                        subsetMinBounds.setZ(qMin(subsetMinBounds.z(), position.z()));
                        subsetMaxBounds.setZ(qMax(subsetMaxBounds.z(), position.z()));
                    } else {
                        outOfRange = true;
                        break;
                    }
                }
            }
            if (!outOfRange)
                addSubset(subset->offset(), subset->count(), subsetMinBounds, subsetMaxBounds, subset->name());
            else
                qWarning("Skipping invalid subset: Out of Range");
        }
    }

    update();
}

void ProceduralMesh::subsetDestroyed(QObject *subset)
{
    if (m_subsets.removeAll(subset))
        requestUpdate();
}

bool ProceduralMesh::supportsTriangleFanPrimitive() const
{
    static bool supportQueried = false;
    static bool triangleFanSupported = false;
    if (!supportQueried) {
        const auto &manager = QQuick3DObjectPrivate::get(this)->sceneManager;
        if (manager) {
            auto window = manager->window();
            if (window) {
                auto rhi = window->rhi();
                if (rhi) {
                    triangleFanSupported = rhi->isFeatureSupported(QRhi::TriangleFanTopology);
                    supportQueried = true;
                }
            }
        }
    }

    return triangleFanSupported;
}

void ProceduralMesh::qmlAppendProceduralMeshSubset(QQmlListProperty<ProceduralMeshSubset> *list, ProceduralMeshSubset *subset)
{
    if (subset == nullptr)
        return;
    ProceduralMesh *self = static_cast<ProceduralMesh *>(list->object);
    self->m_subsets.push_back(subset);

    connect(subset, &ProceduralMeshSubset::isDirty, self, &ProceduralMesh::requestUpdate);
    connect(subset, &QObject::destroyed, self, &ProceduralMesh::subsetDestroyed);

    self->requestUpdate();
}

ProceduralMeshSubset *ProceduralMesh::qmlProceduralMeshSubsetAt(QQmlListProperty<ProceduralMeshSubset> *list, qsizetype index)
{
    ProceduralMesh *self = static_cast<ProceduralMesh *>(list->object);
    return self->m_subsets.at(index);

}

qsizetype ProceduralMesh::qmlProceduralMeshSubsetCount(QQmlListProperty<ProceduralMeshSubset> *list)
{
    ProceduralMesh *self = static_cast<ProceduralMesh *>(list->object);
    return self->m_subsets.count();
}

void ProceduralMesh::qmlClearProceduralMeshSubset(QQmlListProperty<ProceduralMeshSubset> *list)
{
    ProceduralMesh *self = static_cast<ProceduralMesh *>(list->object);
    self->m_subsets.clear();
    self->requestUpdate();
}

QList<unsigned int> ProceduralMesh::indexes() const
{
    return m_indexes;
}

void ProceduralMesh::setIndexes(const QList<unsigned int> &newIndexes)
{
    if (m_indexes == newIndexes)
        return;
    m_indexes = newIndexes;
    Q_EMIT indexesChanged();
    requestUpdate();
}

QList<QVector3D> ProceduralMesh::normals() const
{
    return m_normals;
}

void ProceduralMesh::setNormals(const QList<QVector3D> &newNormals)
{
    if (m_normals == newNormals)
        return;
    m_normals = newNormals;
    Q_EMIT normalsChanged();
    requestUpdate();
}

QList<QVector3D> ProceduralMesh::tangents() const
{
    return m_tangents;
}

void ProceduralMesh::setTangents(const QList<QVector3D> &newTangents)
{
    if (m_tangents == newTangents)
        return;
    m_tangents = newTangents;
    Q_EMIT tangentsChanged();
    requestUpdate();
}

QList<QVector3D> ProceduralMesh::binormals() const
{
    return m_binormals;
}

void ProceduralMesh::setBinormals(const QList<QVector3D> &newBinormals)
{
    if (m_binormals == newBinormals)
        return;
    m_binormals = newBinormals;
    Q_EMIT binormalsChanged();
    requestUpdate();
}

QList<QVector2D> ProceduralMesh::uv0s() const
{
    return m_uv0s;
}

void ProceduralMesh::setUv0s(const QList<QVector2D> &newUv0s)
{
    if (m_uv0s == newUv0s)
        return;
    m_uv0s = newUv0s;
    Q_EMIT uv0sChanged();
    requestUpdate();
}

QList<QVector2D> ProceduralMesh::uv1s() const
{
    return m_uv1s;
}

void ProceduralMesh::setUv1s(const QList<QVector2D> &newUv1s)
{
    if (m_uv1s == newUv1s)
        return;
    m_uv1s = newUv1s;
    Q_EMIT uv1sChanged();
    requestUpdate();
}

QList<QVector4D> ProceduralMesh::colors() const
{
    return m_colors;
}

void ProceduralMesh::setColors(const QList<QVector4D> &newColors)
{
    if (m_colors == newColors)
        return;
    m_colors = newColors;
    Q_EMIT colorsChanged();
    requestUpdate();
}

QList<QVector4D> ProceduralMesh::joints() const
{
    return m_joints;
}

void ProceduralMesh::setJoints(const QList<QVector4D> &newJoints)
{
    if (m_joints == newJoints)
        return;
    m_joints = newJoints;
    Q_EMIT jointsChanged();
    requestUpdate();
}

QList<QVector4D> ProceduralMesh::weights() const
{
    return m_weights;
}

void ProceduralMesh::setWeights(const QList<QVector4D> &newWeights)
{
    if (m_weights == newWeights)
        return;
    m_weights = newWeights;
    Q_EMIT weightsChanged();
    requestUpdate();
}

QQmlListProperty<ProceduralMeshSubset> ProceduralMesh::subsets()
{
    return QQmlListProperty<ProceduralMeshSubset>(this,
                                                  nullptr,
                                                  ProceduralMesh::qmlAppendProceduralMeshSubset,
                                                  ProceduralMesh::qmlProceduralMeshSubsetCount,
                                                  ProceduralMesh::qmlProceduralMeshSubsetAt,
                                                  ProceduralMesh::qmlClearProceduralMeshSubset);
}

int ProceduralMeshSubset::offset() const
{
    return m_offset;
}

void ProceduralMeshSubset::setOffset(int newOffset)
{
    if (m_offset == newOffset)
        return;

    m_offset = newOffset;
    Q_EMIT offsetChanged();
    Q_EMIT isDirty();
}

int ProceduralMeshSubset::count() const
{
    return m_count;
}

void ProceduralMeshSubset::setCount(int newCount)
{
    if (m_count == newCount)
        return;

    m_count = newCount;
    Q_EMIT countChanged();
    Q_EMIT isDirty();
}

QString ProceduralMeshSubset::name() const
{
    return m_name;
}

void ProceduralMeshSubset::setName(const QString &newName)
{
    if (m_name == newName)
        return;

    m_name = newName;
    Q_EMIT nameChanged();
    Q_EMIT isDirty();
}

QT_END_NAMESPACE
