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

#include "torusgeometry_p.h"
#include <limits>

#if QT_CONFIG(concurrent)
#include <QtConcurrentRun>
#endif

QT_BEGIN_NAMESPACE

/*!
    \qmltype TorusGeometry
    \inqmlmodule QtQuick3D.Helpers
    \inherits Geometry
    \since 6.9
    \brief Provides geometry for a torus.

    TorusGeometry is a geometry type that represents a torus. The torus is defined by the number of
    rings and segments, and the radius of the torus.

*/

/*!
    \qmlproperty int TorusGeometry::rings
    Specifies the number of rings in the torus. The default value is 50.
*/

/*!
    \qmlproperty int TorusGeometry::segments
    Specifies the number of segments in the torus. The default value is 50.
*/

/*!
    \qmlproperty real TorusGeometry::radius
    Specifies the radius of the torus. The default value is 100.
*/

/*!
    \qmlproperty real TorusGeometry::tubeRadius
    Specifies the radius of the tube of the torus. The default value is 10.
*/

/*!
    \qmlproperty bool TorusGeometry::asynchronous

    This property holds whether the geometry generation should be asynchronous.
*/

/*!
    \qmlproperty bool TorusGeometry::status
    \readonly

    This property holds the status of the geometry generation when asynchronous is true.

    \value TorusGeometry.Null The geometry generation has not started
    \value TorusGeometry.Ready The geometry generation is complete.
    \value TorusGeometry.Loading The geometry generation is in progress.
    \value TorusGeometry.Error The geometry generation failed.
*/

TorusGeometry::TorusGeometry(QQuick3DObject *parent)
    : QQuick3DGeometry(parent)
{
#if QT_CONFIG(concurrent)
    connect(&m_geometryDataWatcher, &QFutureWatcher<GeometryData>::finished, this, &TorusGeometry::requestFinished);
#endif
    scheduleGeometryUpdate();
}

TorusGeometry::~TorusGeometry()
{

}

int TorusGeometry::rings() const
{
    return m_rings;
}

void TorusGeometry::setRings(int newRings)
{
    if (m_rings == newRings)
        return;
    m_rings = newRings;
    emit ringsChanged();
    scheduleGeometryUpdate();
}

int TorusGeometry::segments() const
{
    return m_segments;
}

void TorusGeometry::setSegments(int newSegments)
{
    if (m_segments == newSegments)
        return;
    m_segments = newSegments;
    emit segmentsChanged();
    scheduleGeometryUpdate();
}

float TorusGeometry::radius() const
{
    return m_radius;
}

void TorusGeometry::setRadius(float newRadius)
{
    if (qFuzzyCompare(m_radius, newRadius))
        return;
    m_radius = newRadius;
    emit radiusChanged();
    scheduleGeometryUpdate();
}

float TorusGeometry::tubeRadius() const
{
    return m_tubeRadius;
}

void TorusGeometry::setTubeRadius(float newTubeRadius)
{
    if (qFuzzyCompare(m_tubeRadius, newTubeRadius))
        return;
    m_tubeRadius = newTubeRadius;
    emit tubeRadiusChanged();
    scheduleGeometryUpdate();
}

bool TorusGeometry::asynchronous() const
{
    return m_asynchronous;
}

void TorusGeometry::setAsynchronous(bool newAsynchronous)
{
    if (m_asynchronous == newAsynchronous)
        return;
    m_asynchronous = newAsynchronous;
    emit asynchronousChanged();
}

TorusGeometry::Status TorusGeometry::status() const
{
    return m_status;
}

void TorusGeometry::doUpdateGeometry()
{
    // reset the flag since we are processing the update
    m_geometryUpdateRequested = false;

#if QT_CONFIG(concurrent)
    if (m_geometryDataFuture.isRunning()) {
        m_pendingAsyncUpdate = true;
        return;
    }
#endif

    // Check for validity of the parameters before generation
    if (m_rings <= 0 || m_segments <= 0 || m_radius <= 0 || m_tubeRadius <= 0) {
        clear();
        update();
        return;
    }

#if QT_CONFIG(concurrent)

    if (m_asynchronous) {
        m_geometryDataFuture = QtConcurrent::run(generateTorusGeometryAsync,
                                                 m_rings,
                                                 m_segments,
                                                 m_radius,
                                                 m_tubeRadius);
        m_geometryDataWatcher.setFuture(m_geometryDataFuture);
        m_status = Status::Loading;
        Q_EMIT statusChanged();
    } else {
#else
    {

#endif // QT_CONFIG(concurrent)
        updateGeometry(generateTorusGeometry(m_rings, m_segments, m_radius, m_tubeRadius));
    }
}

void TorusGeometry::requestFinished()
{
#if QT_CONFIG(concurrent)
    const auto output = m_geometryDataFuture.takeResult();
    updateGeometry(output);
#endif
}

void TorusGeometry::scheduleGeometryUpdate()
{
    if (!m_geometryUpdateRequested) {
        QMetaObject::invokeMethod(this, "doUpdateGeometry", Qt::QueuedConnection);
        m_geometryUpdateRequested = true;
    }
}

void TorusGeometry::updateGeometry(const GeometryData &geometryData)
{
    setStride(sizeof(float) * 8); // 3 for position, 3 for normal, 2 for uv0
    setPrimitiveType(QQuick3DGeometry::PrimitiveType::Triangles);
    addAttribute(QQuick3DGeometry::Attribute::PositionSemantic,
                 0,
                 QQuick3DGeometry::Attribute::F32Type);
    addAttribute(QQuick3DGeometry::Attribute::NormalSemantic,
                 3 * sizeof(float),
                 QQuick3DGeometry::Attribute::F32Type);
    addAttribute(QQuick3DGeometry::Attribute::TexCoord0Semantic,
                 6 * sizeof(float),
                 QQuick3DGeometry::Attribute::F32Type);
    addAttribute(QQuick3DGeometry::Attribute::IndexSemantic,
                 0,
                 QQuick3DGeometry::Attribute::U32Type);

    setBounds(geometryData.boundsMin, geometryData.boundsMax);
    setVertexData(geometryData.vertexData);
    setIndexData(geometryData.indexData);

    // If the geometry update was requested while the geometry was being generated asynchronously,
    // we need to schedule another geometry update now that the geometry is ready.
    if (m_pendingAsyncUpdate) {
        m_pendingAsyncUpdate = false;
        scheduleGeometryUpdate();
    } else {
        m_status = Status::Ready;
        Q_EMIT statusChanged();
    }
    update();
    emit geometryChanged();
}

const size_t FLOAT_SIZE = sizeof(float);
const size_t VEC3_SIZE = sizeof(QVector3D);
const size_t VEC2_SIZE = sizeof(QVector2D);
const size_t UINT_SIZE = sizeof(quint32);

TorusGeometry::GeometryData TorusGeometry::generateTorusGeometry(int rings, int segments, float radius, float tubeRadius)
{
    GeometryData geomData;

    // Bounds initialization
    QVector3D boundsMin(std::numeric_limits<float>::max(),
                        std::numeric_limits<float>::max(),
                        std::numeric_limits<float>::max());

    QVector3D boundsMax(std::numeric_limits<float>::lowest(),
                        std::numeric_limits<float>::lowest(),
                        std::numeric_limits<float>::lowest());

    // Pre-calculate the number of vertices and indices
    int numVerts = (rings + 1) * (segments + 1);
    int numIndices = rings * segments * 6; // Two triangles per quad

    // Reserve space in the QByteArrays (vertex data: position (3 floats), normal (3 floats), UV (2 floats))
    int vertexDataSize = numVerts * (VEC3_SIZE + VEC3_SIZE + VEC2_SIZE); // Position, Normal, UV
    geomData.vertexData.resize(vertexDataSize);

    // Indices are stored as unsigned 32-bit integers
    int indexDataSize = numIndices * UINT_SIZE;
    geomData.indexData.resize(indexDataSize);

    // Get raw pointers to the QByteArray data
    char* vertexPtr = geomData.vertexData.data();
    char* indexPtr = geomData.indexData.data();

    int vertexOffset = 0;
    int indexOffset = 0;

    // Iterate over the rings and segments to compute vertices, normals, and UVs
    for (int i = 0; i <= rings; ++i) {
        for (int j = 0; j <= segments; ++j) {
            float u = (i / (float)rings) * M_PI * 2;
            float v = (j / (float)segments) * M_PI * 2;

            float centerX = radius * qCos(u);
            float centerZ = radius * qSin(u);

            float posX = centerX + tubeRadius * qCos(v) * qCos(u);
            float posY = tubeRadius * qSin(v);
            float posZ = centerZ + tubeRadius * qCos(v) * qSin(u);

            // Update bounds
            boundsMin.setX(qMin(boundsMin.x(), posX));
            boundsMin.setY(qMin(boundsMin.y(), posY));
            boundsMin.setZ(qMin(boundsMin.z(), posZ));

            boundsMax.setX(qMax(boundsMax.x(), posX));
            boundsMax.setY(qMax(boundsMax.y(), posY));
            boundsMax.setZ(qMax(boundsMax.z(), posZ));

            // Position data (3 floats)
            memcpy(vertexPtr + vertexOffset, &posX, FLOAT_SIZE);
            memcpy(vertexPtr + vertexOffset + FLOAT_SIZE, &posY, FLOAT_SIZE);
            memcpy(vertexPtr + vertexOffset + 2 * FLOAT_SIZE, &posZ, FLOAT_SIZE);
            vertexOffset += 3 * FLOAT_SIZE;

            // Calculate normal
            QVector3D normal = QVector3D(posX - centerX, posY, posZ - centerZ).normalized();

            // Normal data (3 floats)
            memcpy(vertexPtr + vertexOffset, &normal, VEC3_SIZE);
            vertexOffset += VEC3_SIZE;

            // UV data (2 floats)
            float uvX = 1.0f - (i / (float)rings);
            float uvY = j / (float)segments;
            memcpy(vertexPtr + vertexOffset, &uvX, FLOAT_SIZE);
            memcpy(vertexPtr + vertexOffset + FLOAT_SIZE, &uvY, FLOAT_SIZE);
            vertexOffset += 2 * FLOAT_SIZE;
        }
    }

    // Generate indices
    for (int i = 0; i < rings; ++i) {
        for (int j = 0; j < segments; ++j) {
            int a = (segments + 1) * i + j;
            int b = (segments + 1) * (i + 1) + j;
            int c = (segments + 1) * (i + 1) + j + 1;
            int d = (segments + 1) * i + j + 1;

            // First triangle (a, d, b)
            memcpy(indexPtr + indexOffset, &a, UINT_SIZE);
            memcpy(indexPtr + indexOffset + UINT_SIZE, &d, UINT_SIZE);
            memcpy(indexPtr + indexOffset + 2 * UINT_SIZE, &b, UINT_SIZE);
            indexOffset += 3 * UINT_SIZE;

            // Second triangle (b, d, c)
            memcpy(indexPtr + indexOffset, &b, UINT_SIZE);
            memcpy(indexPtr + indexOffset + UINT_SIZE, &d, UINT_SIZE);
            memcpy(indexPtr + indexOffset + 2 * UINT_SIZE, &c, UINT_SIZE);
            indexOffset += 3 * UINT_SIZE;
        }
    }

    // Set the computed bounds
    geomData.boundsMin = boundsMin;
    geomData.boundsMax = boundsMax;

    return geomData;
}

#if QT_CONFIG(concurrent)
void TorusGeometry::generateTorusGeometryAsync(QPromise<TorusGeometry::GeometryData> &promise, int rings, int segments, float radius, float tubeRadius)
{
    auto output = generateTorusGeometry(rings, segments, radius, tubeRadius);
    promise.addResult(output);
}
#endif

QT_END_NAMESPACE
