// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

#include "testgeometry.h"

#include <qvector4d.h>
#include <qvector3d.h>
#include <qvector2d.h>
#include <qmath.h>
#include <qcolor.h>

struct Vertex {
    QVector3D position;
    QVector3D normal;
    QVector3D tangent;
    QVector3D binormal;
    QVector2D uv0;
    QVector2D uv1;
    QVector4D color;

    float pad[4];
};

static const int s_vertexSize = sizeof(Vertex);
Q_STATIC_ASSERT(s_vertexSize == 96);

template <typename T>
void byteArrayPtr(T *&ptr, QByteArray &positions)
{
    ptr = reinterpret_cast<T *>(positions.data());
}
template <typename T>
void byteArrayPtr(const T *&ptr, const QByteArray &positions)
{
    ptr = reinterpret_cast<const T *>(positions.constData());
}

struct Face {
    quint32 v0, v1, v2;
    Face() {}
    Face(quint32 a, quint32 b, quint32 c)
        : v0(a), v1(b), v2(c) {}
    void ensureHandedness(const QByteArray &pos, QVector3D n)
    {
        const QVector3D *p = nullptr;
        byteArrayPtr(p, pos);
        QVector3D a = p[v1] - p[v0];
        QVector3D b = p[v2] - p[v1];
        QVector3D c = QVector3D::crossProduct(a, b);
        if (QVector3D::dotProduct(c, n) < 0.0f)
            qSwap(v1, v2);
    }
};

static void packVertices(const QByteArray &pos, const QByteArray &norm,
                         const QByteArray &uv0, const QByteArray &uv1,
                         const QByteArray &tan, const QByteArray &bin,
                         const QByteArray &col, QByteArray &dest)
{
    int count = int(pos.size() / sizeof(QVector3D));
    QByteArray vertices;
    vertices.resize(count * s_vertexSize);
    vertices.fill(0);

    const QVector3D *p = reinterpret_cast<const QVector3D *>(pos.constData());
    const QVector3D *n = norm.size() ? reinterpret_cast<const QVector3D *>(norm.constData()) : nullptr;
    const QVector2D *u0 = uv0.size() ? reinterpret_cast<const QVector2D *>(uv0.constData()) : nullptr;
    const QVector2D *u1 = uv1.size() ? reinterpret_cast<const QVector2D *>(uv1.constData()) : nullptr;
    const QVector3D *t = tan.size() ? reinterpret_cast<const QVector3D *>(tan.constData()) : nullptr;
    const QVector3D *bn = bin.size() ? reinterpret_cast<const QVector3D *>(bin.constData()) : nullptr;
    const QVector4D *c = col.size() ? reinterpret_cast<const QVector4D *>(col.constData()) : nullptr;

    Vertex *vert = reinterpret_cast<Vertex *>(vertices.data());
    for (int i = 0; i < count; i++) {
        Vertex &v = vert[i];
        v.position = p[i];
        if (n)
            v.normal = n[i];
        if (u0)
            v.uv0 = u0[i];
        if (u1)
            v.uv1 = u1[i];
        if (t)
            v.tangent = t[i];
        if (bn)
            v.binormal = bn[i];
        if (c)
            v.color = c[i];
    }
    dest = vertices;
}

static QByteArray s_normals = QByteArray();

QByteArray &generateNormals(const QByteArray &positions, const QByteArray &indices)
{
    if (!s_normals.size()) {
        int count = int(indices.size() / sizeof(Face));
        s_normals.resize(positions.size());
        s_normals.fill(0);
        const Face *faces = reinterpret_cast<const Face *>(indices.constData());
        QVector3D *n = reinterpret_cast<QVector3D *>(s_normals.data());
        const QVector3D *p = reinterpret_cast<const QVector3D *>(positions.constData());
        for (int i = 0; i < count; i++) {
            const Face &f = faces[i];
            QVector3D u = p[f.v1] - p[f.v0];
            QVector3D v = p[f.v2] - p[f.v0];
            QVector3D normal = u.crossProduct(u, v).normalized();
            if (normal.y() < 0.0f)
                normal *= -1.0f;
            n[f.v0] += normal;
            n[f.v1] += normal;
            n[f.v2] += normal;
        }
        count = int(positions.size() / sizeof(QVector3D));
        for (int i = 0; i < count; i++)
            n[i] = n[i].normalized();
    }
    return s_normals;
}

static QByteArray s_tangents = QByteArray();

QByteArray &generateTangents(const QByteArray &positions, const QByteArray &normals, const QByteArray &indices)
{
    if (!s_tangents.size()) {
        int count = int(indices.size() / sizeof(Face));
        s_tangents.resize(positions.size());
        s_tangents.fill(0);
        const Face *faces = reinterpret_cast<const Face *>(indices.constData());
        const QVector3D *n = reinterpret_cast<const QVector3D *>(normals.constData());
        QVector3D *t = reinterpret_cast<QVector3D *>(s_tangents.data());
        const QVector3D *p = reinterpret_cast<const QVector3D *>(positions.constData());
        for (int i = 0; i < count; i++) {
            const Face &f = faces[i];
            QVector3D u = p[f.v1] - p[f.v0];
            t[f.v0] += QVector3D::crossProduct(n[f.v0], u).normalized();
            t[f.v1] += QVector3D::crossProduct(n[f.v1], u).normalized();
            t[f.v2] += QVector3D::crossProduct(n[f.v2], u).normalized();
        }
        count = int(positions.size() / sizeof(QVector3D));
        for (int i = 0; i < count; i++)
            t[i] = t[i].normalized();
    }
    return s_tangents;
}

static QByteArray s_binormals = QByteArray();

QByteArray &generateBinormals(const QByteArray &normals, const QByteArray &tangents)
{
    if (!s_binormals.size()) {
        int count = int(normals.size() / sizeof(QVector3D));
        s_binormals.resize(normals.size());
        s_binormals.fill(0);

        const QVector3D *n = reinterpret_cast<const QVector3D *>(normals.constData());
        const QVector3D *t = reinterpret_cast<const QVector3D *>(tangents.constData());
        QVector3D *bn = reinterpret_cast<QVector3D *>(s_binormals.data());

        for (int i = 0; i < count; i++)
            bn[i] = QVector3D::crossProduct(n[i], t[i]).normalized();
    }
    return s_binormals;
}

static QByteArray s_texcoords0 = QByteArray();

QByteArray &generateTexcoords0(const QByteArray &positions, const QByteArray &normals,
                     QVector3D &minPos, QVector3D &maxPos, float tstep)
{
    if (!s_texcoords0.size()) {
        int count = int(positions.size() / sizeof(QVector3D));
        s_texcoords0.resize(count * sizeof(QVector2D));
        s_texcoords0.fill(0);
        QVector2D *t = reinterpret_cast<QVector2D *>(s_texcoords0.data());
        const QVector3D *n = normals.size() ? reinterpret_cast<const QVector3D *>(normals.constData()) : nullptr;
        const QVector3D *p = reinterpret_cast<const QVector3D *>(positions.constData());

        float xScale = 1.f / (maxPos.x() - minPos.x());
        float zScale = 1.f / (maxPos.z() - minPos.z());

        for (int i = 0; i < count; ++i) {
            float u = p[i].x() * xScale;
            float v = p[i].z() * zScale;
            if (n) {
                const QVector3D &normal = n[i];
                u -= tstep * normal.x() * xScale;
                v -= tstep * normal.z() * zScale;
            }
            t[i] = QVector2D(u, v);
        }
    }
    return s_texcoords0;
}

static QByteArray s_texcoords1 = QByteArray();

QByteArray &generateTexcoords1(const QByteArray &positions)
{
    if (!s_texcoords1.size()) {
        int count = int(positions.size() / sizeof(QVector3D));
        s_texcoords1.resize(count * sizeof(QVector2D));
        s_texcoords1.fill(0);
        QVector2D *t = reinterpret_cast<QVector2D *>(s_texcoords1.data());
        const QVector3D *p = reinterpret_cast<const QVector3D *>(positions.constData());

        for (int i = 0; i < count; ++i) {
            float u = 1.0 + p->y();
            float v = 1.0 - p->y();
            t[i] = QVector2D(u, v);
        }
    }
    return s_texcoords1;
}

static QByteArray s_colors = QByteArray();

QByteArray &generateColors(const QByteArray &positions)
{
    if (!s_colors.size()) {
        int count = int(positions.size() / sizeof(QVector3D));
        s_colors.resize(count * sizeof(QVector4D));
        s_colors.fill(0);
        QVector4D colorF = QVector4D(1.f, 0.5f, 0.5f, 1.0f);
        QVector4D colorL = QVector4D(0.125f, 0.125f, 0.125f, 1.0f);
        QVector4D *c = reinterpret_cast<QVector4D *>(s_colors.data());
        const QVector3D *p = reinterpret_cast<const QVector3D *>(positions.constData());
        float div = 5.0f;
        float w = 1.0f;
        for (int i = 0; i < count; i++) {
            float y = p[i].y();
            float t = qFloor(y / div);
            y = y - t * div;
            if (qAbs(y) < w)
                c[i] = colorL;
            else
                c[i] = colorF;
        }
    }
    return s_colors;
}

template <typename T>
quint32 insertData(const T &c, QByteArray &data)
{
    int count = int(data.size() / sizeof(T));
    QByteArray d;
    d.resize(sizeof(T));
    memcpy(d.data(), &c, sizeof(T));
    data.append(d);
    return count;
}

quint32 findOrInsertVertex(const QVector3D v, QByteArray &vertices, int &lsty, int step)
{
    const QVector3D *ptr = reinterpret_cast<const QVector3D *>(vertices.constData());
    int count = int(vertices.size() / sizeof(QVector3D));
    for (int i = 0; i < count; ++i) {
        if (qFuzzyCompare(v, ptr[(i + lsty) % count])) {
            lsty = (i + lsty) % count;
            return lsty;
        }
    }
    lsty = qMax(count - step, 0);
    return insertData(v, vertices);
}

float camp(int x, int z, float fx, float fz, float amp)
{
    float y = cos(qDegreesToRadians(z * fz)) * amp;
    y += sin(qDegreesToRadians(x * fx)) * amp;
    y *= 0.5f;
    return y;
}

static QByteArray s_positions = QByteArray();
static QByteArray s_indices = QByteArray();

static QByteArray generatePositions(int count, QByteArray &outIndices, bool flat, QVector3D &minPos, QVector3D &maxPos, float size)
{
    float xfrequency = 3*360 / (100 / (size * count) * count);
    float zfrequency = 7*360 / (100 / (size * count) * count);
    float aplitude = 10.f;
    float aplitude2 = 5.f;
    float xfrequency2 = 360 / (100 / (size * count) * count);
    float zfrequency2 = 360 / (100 / (size * count) * count);

    minPos = QVector3D(0, -aplitude2 - aplitude, 0);
    maxPos = QVector3D(count * size, aplitude2 + aplitude, count * size);
    if (!s_positions.size()) {
        int lsdy = 0;
        for (int z = 0; z < count; z++) {
            for (int x = 0; x < count; x++) {
                float y0 = camp(x, z, xfrequency, zfrequency, aplitude);
                float y1 = camp(x, z+1, xfrequency, zfrequency, aplitude);
                float y2 = camp(x+1, z+1, xfrequency, zfrequency, aplitude);
                float y3 = camp(x+1, z, xfrequency, zfrequency, aplitude);

                y0 += camp(x, z, xfrequency2, zfrequency2, aplitude2);
                y1 += camp(x, z+1, xfrequency2, zfrequency2, aplitude2);
                y2 += camp(x+1, z+1, xfrequency2, zfrequency2, aplitude2);
                y3 += camp(x+1, z, xfrequency2, zfrequency2, aplitude2);

                float x0 = x * size;
                float x1 = (x+1) * size;
                float z0 = z * size;
                float z1 = (z+1) * size;
                QVector3D v0(x0, y0, z0);
                QVector3D v1(x0, y1, z1);
                QVector3D v2(x1, y2, z1);
                QVector3D v3(x1, y3, z0);
                quint32 t0;
                quint32 t1;
                quint32 t2;
                quint32 t3;
                if (!flat) {
                    t0 = findOrInsertVertex(v0, s_positions, lsdy, count);
                    t1 = findOrInsertVertex(v1, s_positions, lsdy, count);
                    t2 = findOrInsertVertex(v2, s_positions, lsdy, count);
                    t3 = findOrInsertVertex(v3, s_positions, lsdy, count);
                } else {
                    t0 = insertData(v0, s_positions);
                    t1 = insertData(v1, s_positions);
                    t2 = insertData(v2, s_positions);
                    t3 = insertData(v3, s_positions);
                }
                QVector3D n = QVector3D::crossProduct(v1 - v0, v2 - v1).normalized();
                if (n.y() < 0.0f)
                    n *= -1.0f;
                Face f0(t0, t1, t2);
                Face f1(t0, t2, t3);
                f0.ensureHandedness(s_positions, n);
                f1.ensureHandedness(s_positions, n);
                insertData(f0, s_indices);
                insertData(f1, s_indices);
            }
        }
    }
    outIndices = s_indices;
    return s_positions;
}

TestGeometry::TestGeometry(QQuick3DObject *parent)
    : QQuick3DGeometry(parent)
{

}

bool TestGeometry::position() const
{
    return m_position;
}

bool TestGeometry::normal() const
{
    return m_normal;
}

bool TestGeometry::texcoord0() const
{
    return m_texcoord0;
}

bool TestGeometry::texcoord1() const
{
    return m_texcoord1;
}

bool TestGeometry::tangent() const
{
    return m_tangent;
}

bool TestGeometry::binormal() const
{
    return m_binormal;
}

bool TestGeometry::color() const
{
    return m_color;
}

void TestGeometry::setPosition(bool enable)
{
    if (enable == m_position)
        return;
    m_position = enable;
    emit positionChanged();
    m_dirty = true;
}

void TestGeometry::setNormal(bool enable)
{
    if (enable == m_normal)
        return;
    m_normal = enable;
    emit normalChanged();
    m_dirty = true;
}

void TestGeometry::setTexcoord0(bool enable)
{
    if (enable == m_texcoord0)
        return;
    m_texcoord0 = enable;
    emit texcoord0Changed();
    m_dirty = true;
}

void TestGeometry::setTexcoord1(bool enable)
{
    if (enable == m_texcoord1)
        return;
    m_texcoord1 = enable;
    emit texcoord1Changed();
    m_dirty = true;
}

void TestGeometry::setTangent(bool enable)
{
    if (enable == m_tangent)
        return;
    m_tangent = enable;
    emit tangentChanged();
    m_dirty = true;
}

void TestGeometry::setBinormal(bool enable)
{
    if (enable == m_binormal)
        return;
    m_binormal = enable;
    emit binormalChanged();
    m_dirty = true;
}

void TestGeometry::setColor(bool enable)
{
    if (enable == m_color)
        return;
    m_color = enable;
    emit colorChanged();
    m_dirty = true;
}

QSSGRenderGraphObject *TestGeometry::updateSpatialNode(QSSGRenderGraphObject *node)
{
    if (m_dirty) {
        QVector3D min, max;
        int count = 120;
        QByteArray positionData = generatePositions(count, m_indexBuffer, false, min, max, 100.0f / float(count));
        QByteArray normalData;
        QByteArray uv0Data;
        QByteArray uv1Data;
        QByteArray colorData;
        QByteArray tangentData;
        QByteArray binormalData;
        clear();
        addAttribute(QQuick3DGeometry::Attribute::PositionSemantic, 0,
                     QQuick3DGeometry::Attribute::ComponentType::F32Type);

        addAttribute(QQuick3DGeometry::Attribute::IndexSemantic, 0,
                     QQuick3DGeometry::Attribute::ComponentType::U32Type);
        if (normal()) {
            normalData = generateNormals(positionData, m_indexBuffer);
            addAttribute(QQuick3DGeometry::Attribute::NormalSemantic, offsetof(Vertex, normal),
                         QQuick3DGeometry::Attribute::ComponentType::F32Type);
        }
        if (texcoord0()) {
            uv0Data = generateTexcoords0(positionData, normalData, min, max, 0.1f);
            addAttribute(QQuick3DGeometry::Attribute::TexCoordSemantic, offsetof(Vertex, uv0),
                         QQuick3DGeometry::Attribute::ComponentType::F32Type);
        }
        if (texcoord1()) {
            uv1Data = generateTexcoords1(positionData);
            addAttribute(QQuick3DGeometry::Attribute::TexCoord1Semantic, offsetof(Vertex, uv1),
                         QQuick3DGeometry::Attribute::ComponentType::F32Type);
        }
        if (tangent()) {
            tangentData = generateTangents(positionData, normalData, m_indexBuffer);
            addAttribute(QQuick3DGeometry::Attribute::TangentSemantic, offsetof(Vertex, tangent),
                         QQuick3DGeometry::Attribute::ComponentType::F32Type);
        }
        if (binormal()) {
            binormalData = generateBinormals(normalData, tangentData);
            addAttribute(QQuick3DGeometry::Attribute::BinormalSemantic, offsetof(Vertex, binormal),
                         QQuick3DGeometry::Attribute::ComponentType::F32Type);
        }
        if (color()) {
            colorData = generateColors(positionData);
            addAttribute(QQuick3DGeometry::Attribute::ColorSemantic, offsetof(Vertex, color),
                         QQuick3DGeometry::Attribute::ComponentType::F32Type);
        }
        packVertices(positionData, normalData, uv0Data, uv1Data, tangentData, binormalData, colorData, m_vertexBuffer);
        setStride(s_vertexSize);
        setVertexData(m_vertexBuffer);
        setIndexData(m_indexBuffer);
        setPrimitiveType(QQuick3DGeometry::PrimitiveType::Triangles);
        setBounds(min, max);
        m_dirty = false;
    }
    node = QQuick3DGeometry::updateSpatialNode(node);
    return node;
}

