// Copyright (C) 2017 Klaralvdalens Datakonsult AB (KDAB).
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "fbxgeometryloader.h"

#include <QtCore/QFileDevice>

#include <Qt3DCore/QAttribute>
#include <Qt3DCore/QBuffer>
#include <Qt3DCore/QGeometry>
#include <Qt3DRender/private/renderlogging_p.h>

#include <fbxsdk.h>

QT_BEGIN_NAMESPACE

namespace Qt3DRender {

Q_LOGGING_CATEGORY(FbxGeometryLoaderLog, "Qt3D.FbxGeometryLoader", QtWarningMsg)

class FbxStreamWrapper : public FbxStream
{
    FbxManager *m_manager;
    QIODevice *m_device;
public:
    FbxStreamWrapper(FbxManager *manager)
        : m_manager(manager)
        , m_device(nullptr)
    {
    }

    EState GetState()
    {
        if (!m_device)
            return eEmpty;
        return m_device->isOpen() ? eOpen : eClosed;
    }

    bool Open(void *pStreamData)
    {
        m_device = reinterpret_cast<QIODevice *>(pStreamData);

        if (!m_device->isOpen())
            m_device->open(QIODevice::ReadOnly);

        return m_device && m_device->isOpen();
    }

    bool Close()
    {
        Q_ASSERT(m_device);

        if (Q_UNLIKELY(!m_device))
            return false;

        m_device->close();

        return !m_device->isOpen();
    }

    bool Flush()
    {
        Q_ASSERT(m_device);

        if (Q_UNLIKELY(!m_device))
            return false;

        QFileDevice *device = qobject_cast<QFileDevice*>(m_device);
        if (Q_LIKELY(device))
            return device->flush();

        return false;
    }

    int Write(const void *pData, int pSize)
    {
        Q_ASSERT(m_device);

        if (Q_UNLIKELY(!m_device))
            return -1;

        return m_device->write(reinterpret_cast<const char *>(pData), pSize);
    }

    int Read(void *pData, int pSize) const
    {
        Q_ASSERT(m_device);

        if (Q_UNLIKELY(!m_device))
            return -1;

        return m_device->read(reinterpret_cast<char *>(pData), pSize);
    }

    char *ReadString(char *pBuffer, int pMaxSize, bool pStopAtFirstWhiteSpace = false)
    {
        Q_UNUSED(pBuffer);
        Q_UNUSED(pMaxSize);
        Q_UNUSED(pStopAtFirstWhiteSpace);
        return nullptr;
    }

    int GetReaderID() const
    {
        return m_manager->GetIOPluginRegistry()->
            FindReaderIDByExtension(qPrintable(FBXGEOMETRYLOADER_EXT));
    }

    int GetWriterID() const
    {
        return m_manager->GetIOPluginRegistry()->
            FindReaderIDByExtension(qPrintable(FBXGEOMETRYLOADER_EXT));
    }

    void Seek(const FbxInt64 &pOffset, const FbxFile::ESeekPos &pSeekPos)
    {
        Q_ASSERT(m_device);

        if (Q_UNLIKELY(!m_device))
            return;

        switch (pSeekPos) {
        case FbxFile::eBegin:
            m_device->seek(pOffset);
            break;
        case FbxFile::eCurrent:
            m_device->seek(m_device->pos() + pOffset);
            break;
        case FbxFile::eEnd:
            m_device->seek(m_device->size() - pOffset);
            break;
        }
    }

    long GetPosition() const
    {
        Q_ASSERT(m_device);

        if (Q_UNLIKELY(!m_device))
            return -1;

        return m_device->pos();
    }

    void SetPosition(long pPosition)
    {
        Q_ASSERT(m_device);

        if (Q_UNLIKELY(!m_device))
            return;

        m_device->seek(pPosition);
    }

    int GetError() const
    {
        Q_ASSERT(m_device);

        if (Q_UNLIKELY(!m_device))
            return 1;

        QFileDevice *device = qobject_cast<QFileDevice *>(m_device);
        if (Q_LIKELY(device))
            return device->error() == QFileDevice::NoError ? 0 : 1;

        return 0;
    }

    void ClearError()
    {
        Q_ASSERT(m_device);

        if (Q_UNLIKELY(!m_device))
            return;

        QFileDevice *device = qobject_cast<QFileDevice *>(m_device);
        if (Q_LIKELY(device))
            device->unsetError();
    }
};

FbxGeometryLoader::FbxGeometryLoader()
    : m_manager(nullptr)
    , m_scene(nullptr)
    , m_geometry(nullptr)
{
    m_manager = FbxManager::Create();

    if (Q_LIKELY(m_manager))
        qInfo(FbxGeometryLoaderLog, "Autodesk FBX SDK version %s", m_manager->GetVersion());
    else
        qWarning(FbxGeometryLoaderLog, "Failed to create FBX Manager.");
}

FbxGeometryLoader::~FbxGeometryLoader()
{
    if (m_manager)
        m_manager->Destroy();
}

/*!
    Gives the list of attributes that can be used to render
    a 3D form.
    Returns a pointer to the geometry object.
*/
QGeometry *Qt3DRender::FbxGeometryLoader::geometry() const
{
    return m_geometry;
}

/*!
    Loads the specified \a subMesh using a device \a ioDev. Returns \c true
    on success, \c false otherwise.
*/
bool Qt3DRender::FbxGeometryLoader::load(QIODevice *ioDev, const QString &subMesh)
{
    if (m_scene)
        m_scene->Destroy();

    m_scene = FbxScene::Create(m_manager, "scene");
    if (!m_scene)
        qWarning(FbxGeometryLoaderLog, "Unable to create FBX scene!");

    QScopedPointer<FbxStreamWrapper> fbxStream(new FbxStreamWrapper(m_manager));

    FbxImporter *importer = FbxImporter::Create(m_manager, "");

    const bool hasInitialized = importer->Initialize(fbxStream.data(), ioDev);

    int fileVersion[3];

    importer->GetFileVersion(fileVersion[0], fileVersion[1], fileVersion[2]);

    if (!hasInitialized) {
        FbxString error = importer->GetStatus().GetErrorString();
        qWarning(FbxGeometryLoaderLog, "Call to FbxImporter::Initialize() failed.");
        qWarning(FbxGeometryLoaderLog, "Error returned: %s", error.Buffer());

        if (importer->GetStatus().GetCode() == FbxStatus::eInvalidFileVersion) {
            int SdkVersion[3];
            FbxManager::GetFileFormatVersion(SdkVersion[0], SdkVersion[1], SdkVersion[2]);
            qWarning(FbxGeometryLoaderLog, "FBX file format version for this FBX SDK is %d.%d.%d",
                     SdkVersion[0], SdkVersion[1], SdkVersion[2]);
            qWarning(FbxGeometryLoaderLog, "FBX file format version for the file is %d.%d.%d",
                     fileVersion[0], fileVersion[1], fileVersion[2]);
        }

        return false;
    }

    if (importer->IsFBX()) {
        qInfo(FbxGeometryLoaderLog, "FBX file format version for file is %d.%d.%d",
               fileVersion[0], fileVersion[1], fileVersion[2]);

        const int stackCount = importer->GetAnimStackCount();
        for (int i = 0; i < stackCount; ++i) {
            FbxTakeInfo *lTakeInfo = importer->GetTakeInfo(i);
            qInfo(FbxGeometryLoaderLog, "  Animation Stack %d", i);
            qInfo(FbxGeometryLoaderLog, "       Name: \"%s\"", lTakeInfo->mName.Buffer());
            qInfo(FbxGeometryLoaderLog, "       Description: \"%s\"", lTakeInfo->mDescription.Buffer());
            qInfo(FbxGeometryLoaderLog, "       Import Name: \"%s\"", lTakeInfo->mImportName.Buffer());
            qInfo(FbxGeometryLoaderLog, "       Import State: %s", lTakeInfo->mSelect ? "true" : "false");
        }

        auto settings = importer->GetIOSettings();
        settings->SetBoolProp(IMP_FBX_MATERIAL, false);
        settings->SetBoolProp(IMP_FBX_TEXTURE, false);
        settings->SetBoolProp(IMP_FBX_LINK, false);
        settings->SetBoolProp(IMP_FBX_SHAPE, false);
        settings->SetBoolProp(IMP_FBX_GOBO, false);
        settings->SetBoolProp(IMP_FBX_ANIMATION, false);
        settings->SetBoolProp(IMP_FBX_GLOBAL_SETTINGS, false);
    }

    const bool wasImported = importer->Import(m_scene);
    importer->Destroy();

    m_mesh = subMesh;

    if (wasImported)
        recurseNodes();

    return wasImported;
}

/*!
    Traverse the node hierarchy and process the children of each
    node.
*/
void FbxGeometryLoader::recurseNodes()
{
    Q_ASSERT(m_scene);

    if (!m_scene)
        return;

    FbxNode *node = m_scene->GetRootNode();
    if (node) {
        for (int i = 0; i < node->GetChildCount() && !m_geometry; ++i)
            processNode(node->GetChild(i));
    }
}
/*!
    If the parameter \a node has the attribute eMesh,
    process the Mesh. If not, process the children
    of that node.
*/
void Qt3DRender::FbxGeometryLoader::processNode(FbxNode *node)
{
    auto attr = node->GetNodeAttribute();
    if (!attr)
        return;

    switch (attr->GetAttributeType()) {
    case FbxNodeAttribute::eMesh:
        if (m_mesh.isEmpty() || m_mesh.compare(node->GetName(), Qt::CaseInsensitive) == 0) {
            qDebug(FbxGeometryLoaderLog, "Found mesh: %s", node->GetName());
            processMesh(node->GetMesh());
        }
        break;
    default:
        break;
    }

    if (m_geometry)
        return;

    for (int i = 0; i < node->GetChildCount(); ++i)
        processNode(node->GetChild(i));
}
/*!
    Process all vertices of the specified \a mesh.
*/
void Qt3DRender::FbxGeometryLoader::processMesh(FbxMesh *mesh)
{
    const int normalCount = mesh->GetElementNormalCount();
    const int polygonCount = mesh->GetPolygonCount();
    const int tangentCount = mesh->GetElementTangentCount();
    const int uvCount = mesh->GetElementUVCount();

    const bool hasNormal = (normalCount > 0);
    const bool hasTangent = (tangentCount > 0);
    const bool hasUV = (uvCount > 0);

    const unsigned int elementSize = 3 + (hasUV ? 2 : 0) + (hasNormal ? 3 : 0) + (hasTangent ? 4 : 0);
    const unsigned int elementBytes = elementSize * sizeof(double);

    int vertexCount = 0;
    for (int polygonIndex = 0; polygonIndex < polygonCount; ++polygonIndex)
        vertexCount += mesh->GetPolygonSize(polygonIndex);
    const int indexCount = (polygonCount * 3) + ((vertexCount - (polygonCount * 3)) * 3);

    QByteArray indexPayload;
    indexPayload.resize(indexCount * sizeof(quint32));
    quint32 *indexData = reinterpret_cast<quint32 *>(indexPayload.data());

    QByteArray vertexPayload;
    vertexPayload.resize(vertexCount * elementBytes);
    double *vertexData = reinterpret_cast<double *>(vertexPayload.data());

    const FbxVector4 *controlPoints = mesh->GetControlPoints();

    int vertexIndex = 0;
    for (int polygonIndex = 0; polygonIndex < polygonCount; ++polygonIndex) {
        const int polygonSize = mesh->GetPolygonSize(polygonIndex);
        for (int pVertexIndex = 0; pVertexIndex < polygonSize; ++pVertexIndex) {
            const int controlPointIndex = mesh->GetPolygonVertex(polygonIndex, pVertexIndex);
            const FbxVector4 *vertex = (controlPoints + controlPointIndex);
            *vertexData++ = (*vertex)[0];
            *vertexData++ = (*vertex)[1];
            *vertexData++ = (*vertex)[2];

            if (pVertexIndex >= 1 && pVertexIndex < (polygonSize - 1)) {
                *indexData++ = vertexIndex - pVertexIndex;
                *indexData++ = vertexIndex;
                *indexData++ = vertexIndex + 1;
            }

            if (hasUV) {
                FbxVector2 vector;
                bool unmapped;

                if (mesh->GetPolygonVertexUV(polygonIndex, pVertexIndex, NULL, vector, unmapped)) {
                    *vertexData++ = unmapped ? 0 : vector[0];
                    *vertexData++ = unmapped ? 0 : vector[1];
                } else {
                    qWarning(FbxGeometryLoaderLog,
                             "Irregularity encountered while parsing UV element.");
                    *vertexData++ = 0;
                    *vertexData++ = 0;
                }
            }

            if (hasNormal) {
                FbxVector4 vector;
                if (mesh->GetPolygonVertexNormal(polygonIndex, pVertexIndex, vector)) {
                    *vertexData++ = vector[0];
                    *vertexData++ = vector[1];
                    *vertexData++ = vector[2];
                } else {
                    qWarning(FbxGeometryLoaderLog,
                             "Irregularity encountered while parsing Normal element.");
                    *vertexData++ = 0;
                    *vertexData++ = 0;
                    *vertexData++ = 0;
                }
            }

            if (hasTangent) {
                int index = -1;

                for (int tangentIndex = 0; tangentIndex < tangentCount && index == -1; ++tangentIndex) {
                    const FbxGeometryElementTangent *tangent = mesh->GetElementTangent(tangentIndex);

                    if (tangent->GetMappingMode() == FbxGeometryElement::eByPolygonVertex) {
                        switch (tangent->GetReferenceMode()) {
                        case FbxGeometryElement::eDirect:
                            index = vertexIndex;
                            break;
                        case FbxGeometryElement::eIndexToDirect:
                            index = tangent->GetIndexArray().GetAt(vertexIndex);
                            break;
                        default: break;
                        }
                    }

                    if (index != -1) {
                        const FbxVector4 vector = tangent->GetDirectArray().GetAt(index);
                        *vertexData++ = vector[0];
                        *vertexData++ = vector[1];
                        *vertexData++ = vector[2];
                        *vertexData++ = vector[3];
                    }
                }

                if (index == -1) {
                    qWarning(FbxGeometryLoaderLog,
                             "Irregularity encountered while parsing Tangent element.");
                    *vertexData++ = 0;
                    *vertexData++ = 0;
                    *vertexData++ = 0;
                    *vertexData++ = 0;
                }
            }
            ++vertexIndex;
        }
    }

    /*
     * QGeometry Generation
     */
    m_geometry = new QGeometry();

    unsigned int offset = 0;

    QBuffer *vertexBuffer = new QBuffer(QBuffer::VertexBuffer);
    vertexBuffer->setData(vertexPayload);

    QBuffer *indexBuffer = new QBuffer(QBuffer::IndexBuffer);
    indexBuffer->setData(indexPayload);

    QAttribute *positionAttribute = new QAttribute(vertexBuffer, QAttribute::defaultPositionAttributeName(), QAttribute::Double, 3, vertexCount, offset, elementBytes);
    m_geometry->addAttribute(positionAttribute);
    offset += sizeof(double) * 3;

    if (hasUV) {
        QAttribute *attribute = new QAttribute(vertexBuffer, QAttribute::defaultTextureCoordinateAttributeName(), QAttribute::Double, 2, vertexCount, offset, elementBytes);
        m_geometry->addAttribute(attribute);
        offset += sizeof(double) * 2;
    }

    if (hasNormal) {
        QAttribute *attribute = new QAttribute(vertexBuffer, QAttribute::defaultNormalAttributeName(), QAttribute::Double, 3, vertexCount, offset, elementBytes);
        m_geometry->addAttribute(attribute);
        offset += sizeof(double) * 3;
    }

    if (hasTangent) {
        QAttribute *attribute = new QAttribute(vertexBuffer, QAttribute::defaultTangentAttributeName(),QAttribute::Double, 4, vertexCount, offset, elementBytes);
        m_geometry->addAttribute(attribute);
    }

    QAttribute *indexAttribute = new QAttribute(indexBuffer, QAttribute::UnsignedInt, 1, indexCount);
    indexAttribute->setAttributeType(QAttribute::IndexAttribute);
    m_geometry->addAttribute(indexAttribute);
}

} // namespace Qt3DRender

QT_END_NAMESPACE
