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

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

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

QT_BEGIN_NAMESPACE

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

    SphereGeometry is a geometry type that represents a sphere. The sphere's size is
    defined by its radius. The topology of the sphere is defined by the number of
    rings and segments.
*/

/*!
    \qmlproperty real SphereGeometry::radius
    The radius of the sphere. The default value is 100.0.
*/

/*!
    \qmlproperty int SphereGeometry::rings
    The number of rings in the sphere. The default value is 16.
*/

/*!
    \qmlproperty int SphereGeometry::segments
    The number of segments in the sphere. The default value is 32.
*/

/*!
    \qmlproperty bool SphereGeometry::asynchronous

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

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

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

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

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

SphereGeometry::~SphereGeometry()
{

}

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

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

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

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

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

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

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

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

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

void SphereGeometry::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 validity of the geometry parameters
    if (m_radius < 0 || m_rings < 1 || m_segments < 3) {
        clear();
        update();
        return;
    }

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

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

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

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

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

    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();
}

SphereGeometry::GeometryData SphereGeometry::generateSphereGeometry(float radius, int rings, int segments)
{
    GeometryData geometryData;

    // Pre-compute the size of the vertex and index data
    const int numVertices = (rings + 1) * (segments + 1);
    const int numIndices = rings * segments * 6;
    const int vertexStride = sizeof(float) * (3 + 2 + 3); // 3 for position, 2 for uv, 3 for normal
    const int indexStride = sizeof(uint16_t);

    // Resize the QByteArrays to fit all the vertex and index data
    geometryData.vertexData.resize(numVertices * vertexStride);
    geometryData.indexData.resize(numIndices * indexStride);


    // Bounds initialization using std::numeric_limits<float>::max() and lowest()
    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());

    // Temporary pointers for direct writing into the QByteArray
    float* vertexPtr = reinterpret_cast<float*>(geometryData.vertexData.data());
    uint16_t* indexPtr = reinterpret_cast<uint16_t*>(geometryData.indexData.data());

    // Loop through rings and segments to generate the vertex data
    for (int i = 0; i <= rings; ++i) {
        float phi = M_PI * i / rings;  // from 0 to PI
        float y = radius * std::cos(phi);
        float ringRadius = radius * std::sin(phi);

        for (int j = 0; j <= segments; ++j) {
            float theta = 2 * M_PI * j / segments;  // from 0 to 2PI
            float x = ringRadius * std::cos(theta);
            float z = ringRadius * std::sin(theta);

            // Position (vec3)
            *vertexPtr++ = x;
            *vertexPtr++ = y;
            *vertexPtr++ = z;

            // UV coordinates (vec2)
            *vertexPtr++ = 1.0f - static_cast<float>(j) / segments;
            *vertexPtr++ = 1.0f - static_cast<float>(i) / rings;

            // Normalized normal vector (vec3)
            QVector3D normal(x, y, z);
            normal.normalize();
            *vertexPtr++ = normal.x();
            *vertexPtr++ = normal.y();
            *vertexPtr++ = normal.z();

            // Update bounds
            boundsMin.setX(std::min(boundsMin.x(), x));
            boundsMin.setY(std::min(boundsMin.y(), y));
            boundsMin.setZ(std::min(boundsMin.z(), z));
            boundsMax.setX(std::max(boundsMax.x(), x));
            boundsMax.setY(std::max(boundsMax.y(), y));
            boundsMax.setZ(std::max(boundsMax.z(), z));
        }
    }

    // Loop through rings and segments to generate the index data
    for (int i = 0; i < rings; ++i) {
        for (int j = 0; j < segments; ++j) {
            uint16_t a = static_cast<uint16_t>(i * (segments + 1) + j);
            uint16_t b = static_cast<uint16_t>(a + segments + 1);
            uint16_t c = static_cast<uint16_t>(b + 1);
            uint16_t d = static_cast<uint16_t>(a + 1);

            // First triangle (a, d, b)
            *indexPtr++ = a;
            *indexPtr++ = d;
            *indexPtr++ = b;

            // Second triangle (b, d, c)
            *indexPtr++ = b;
            *indexPtr++ = d;
            *indexPtr++ = c;
        }
    }

    geometryData.boundsMin = boundsMin;
    geometryData.boundsMax = boundsMax;

    // Return the geometry data
    return geometryData;
}

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

QT_END_NAMESPACE
