// 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 "loadskeletonjob_p.h"
#include <Qt3DCore/qjoint.h>
#include <Qt3DCore/qabstractskeleton.h>
#include <Qt3DCore/qskeletonloader.h>
#include <Qt3DCore/private/qabstractskeleton_p.h>
#include <Qt3DCore/private/qabstractnodefactory_p.h>
#include <Qt3DCore/private/qaspectmanager_p.h>
#include <Qt3DCore/private/qskeletonloader_p.h>
#include <Qt3DCore/private/qurlhelper_p.h>
#include <Qt3DRender/private/managers_p.h>
#include <Qt3DRender/private/nodemanagers_p.h>
#include <Qt3DRender/private/job_common_p.h>
#include <Qt3DRender/private/gltfskeletonloader_p.h>
#include <Qt3DRender/private/renderlogging_p.h>
#include <QtCore/QCoreApplication>
#include <QtCore/QFileInfo>

QT_BEGIN_NAMESPACE

namespace Qt3DRender {
namespace Render {

class LoadSkeletonJobPrivate : public Qt3DCore::QAspectJobPrivate
{
public:
    LoadSkeletonJobPrivate() : m_backendSkeleton(nullptr), m_loadedRootJoint(nullptr) { }
    ~LoadSkeletonJobPrivate() override { }

    void postFrame(Qt3DCore::QAspectManager *manager) override;

    Skeleton *m_backendSkeleton;
    Qt3DCore::QJoint* m_loadedRootJoint;
};

LoadSkeletonJob::LoadSkeletonJob(const HSkeleton &handle)
    : QAspectJob(*new LoadSkeletonJobPrivate)
    , m_handle(handle)
    , m_nodeManagers(nullptr)
{
    SET_JOB_RUN_STAT_TYPE(this, JobTypes::LoadSkeleton, 0)
}

void LoadSkeletonJob::run()
{
    Q_D(LoadSkeletonJob);
    d->m_backendSkeleton = nullptr;

    Skeleton *skeleton = m_nodeManagers->skeletonManager()->data(m_handle);
    if (skeleton != nullptr) {
        d->m_backendSkeleton = skeleton;
        loadSkeleton(skeleton);
    }
}

void LoadSkeletonJob::loadSkeleton(Skeleton *skeleton)
{
    qCDebug(Jobs) << Q_FUNC_INFO << skeleton->source();
    skeleton->clearData();

    // Load the data
    switch (skeleton->dataType()) {
    case Skeleton::File:
        loadSkeletonFromUrl(skeleton);
        break;

    case Skeleton::Data:
        loadSkeletonFromData(skeleton);
        break;

    default:
        Q_UNREACHABLE();
    }

    // If using a loader inform the frontend of the status change.
    // Don't bother if asked to create frontend joints though. When
    // the backend gets notified of those joints we'll update the
    // status at that point.
    if (skeleton->dataType() == Skeleton::File && !skeleton->createJoints()) {
        if (skeleton->jointCount() == 0)
            skeleton->setStatus(Qt3DCore::QSkeletonLoader::Error);
        else
            skeleton->setStatus(Qt3DCore::QSkeletonLoader::Ready);
    }

    qCDebug(Jobs) << "Loaded skeleton data:" << *skeleton;
}

void LoadSkeletonJob::loadSkeletonFromUrl(Skeleton *skeleton)
{
    Q_D(LoadSkeletonJob);

    using namespace Qt3DCore;

    // TODO: Handle remote files
    QString filePath = Qt3DCore::QUrlHelper::urlToLocalFileOrQrc(skeleton->source());
    QFileInfo info(filePath);
    if (!info.exists()) {
        qWarning() << "Could not open skeleton file:" << filePath;
        skeleton->setStatus(Qt3DCore::QSkeletonLoader::Error);
        return;
    }

    QFile file(filePath);
    if (!file.open(QIODevice::ReadOnly)) {
        qWarning() << "Could not open skeleton file:" << filePath;
        skeleton->setStatus(QSkeletonLoader::Error);
        return;
    }

    // TODO: Make plugin based for more file type support. For now gltf or native
    const QString ext = info.suffix();
    SkeletonData skeletonData;
    if (ext == QLatin1String("gltf")) {
        GLTFSkeletonLoader loader;
        loader.load(&file);
        skeletonData = loader.createSkeleton(skeleton->name());

        // If the user has requested it, create the frontend nodes for the joints
        // and send them to the (soon to be owning) QSkeletonLoader.
        if (skeleton->createJoints()) {
            QJoint *rootJoint = createFrontendJoints(skeletonData);
            if (!rootJoint) {
                qWarning() << "Failed to create frontend joints";
                skeleton->setStatus(QSkeletonLoader::Error);
                return;
            }

            // Move the QJoint tree to the main thread and notify the
            // corresponding QSkeletonLoader
            const auto appThread = QCoreApplication::instance()->thread();
            rootJoint->moveToThread(appThread);

            d->m_loadedRootJoint = rootJoint;

            // Clear the skeleton data. It will be recreated from the
            // frontend joints. A little bit inefficient but ensures
            // that joints created this way and via QSkeleton go through
            // the same code path.
            skeletonData = SkeletonData();
        }
    } else if (ext == QLatin1String("json")) {
        // TODO: Support native skeleton type
    } else {
        qWarning() << "Unknown skeleton file type:" << ext;
        skeleton->setStatus(QSkeletonLoader::Error);
        return;
    }

    skeleton->setSkeletonData(skeletonData);
}

void LoadSkeletonJob::loadSkeletonFromData(Skeleton *skeleton)
{
    // Recurse down through the joint hierarchy and process it into
    // the vector of joints used within SkeletonData. The recursion
    // ensures that a parent always appears before its children in
    // the vector of JointInfo objects.
    //
    // In addition, we set up a mapping from the joint ids to the
    // index of the corresponding JointInfo object in the vector.
    // This will allow us to easily update entries in the vector of
    // JointInfos when a Joint node marks itself as dirty.
    const int rootParentIndex = -1;
    auto skeletonData = skeleton->skeletonData();
    processJointHierarchy(skeleton->rootJointId(), rootParentIndex, skeletonData);
    skeleton->setSkeletonData(skeletonData);
}

Qt3DCore::QJoint *LoadSkeletonJob::createFrontendJoints(const SkeletonData &skeletonData) const
{
    if (skeletonData.joints.isEmpty())
        return nullptr;

    // Create frontend joints from the joint info objects
    QList<Qt3DCore::QJoint *> frontendJoints;
    const qsizetype jointCount = skeletonData.joints.size();
    frontendJoints.reserve(jointCount);
    for (qsizetype i = 0; i < jointCount; ++i) {
        const QMatrix4x4 &inverseBindMatrix = skeletonData.joints[i].inverseBindPose;
        const QString &jointName = skeletonData.jointNames[i];
        const Qt3DCore::Sqt &localPose = skeletonData.localPoses[i];
        frontendJoints.push_back(createFrontendJoint(jointName, localPose, inverseBindMatrix));
    }

    // Now go through and resolve the parent for each joint
    for (qsizetype i = 0; i < frontendJoints.size(); ++i) {
        const auto parentIndex = skeletonData.joints[i].parentIndex;
        if (parentIndex == -1)
            continue;

        // It's not enough to just set up the QObject parent-child relationship.
        // We need to explicitly add the child to the parent's list of joints so
        // that information is then propagated to the backend.
        frontendJoints[parentIndex]->addChildJoint(frontendJoints[i]);
    }

    return frontendJoints[0];
}

Qt3DCore::QJoint *LoadSkeletonJob::createFrontendJoint(const QString &jointName,
                                                       const Qt3DCore::Sqt &localPose,
                                                       const QMatrix4x4 &inverseBindMatrix) const
{
    auto joint = Qt3DCore::QAbstractNodeFactory::createNode<Qt3DCore::QJoint>("QJoint");
    joint->setTranslation(localPose.translation);
    joint->setRotation(localPose.rotation);
    joint->setScale(localPose.scale);
    joint->setInverseBindMatrix(inverseBindMatrix);
    joint->setName(jointName);
    return joint;
}

void LoadSkeletonJob::processJointHierarchy(Qt3DCore::QNodeId jointId,
                                            int parentJointIndex,
                                            SkeletonData &skeletonData)
{
    // Lookup the joint, create a JointInfo, and add an entry to the index map
    Joint *joint = m_nodeManagers->jointManager()->lookupResource(jointId);
    Q_ASSERT(joint);
    joint->setOwningSkeleton(m_handle);
    const JointInfo jointInfo(joint, parentJointIndex);
    skeletonData.joints.push_back(jointInfo);
    skeletonData.localPoses.push_back(joint->localPose());
    skeletonData.jointNames.push_back(joint->name());

    const qsizetype jointIndex = skeletonData.joints.size() - 1;
    const HJoint jointHandle = m_nodeManagers->jointManager()->lookupHandle(jointId);
    skeletonData.jointIndices.insert(jointHandle, jointIndex);

    // Recurse to the children
    const auto childIds = joint->childJointIds();
    for (const auto &childJointId : childIds)
        processJointHierarchy(childJointId, jointIndex, skeletonData);
}

void LoadSkeletonJobPrivate::postFrame(Qt3DCore::QAspectManager *manager)
{
    if (!m_backendSkeleton)
        return;

    using namespace Qt3DCore;
    QAbstractSkeleton *node = qobject_cast<QAbstractSkeleton *>(manager->lookupNode(m_backendSkeleton->peerId()));
    if (!node)
        return;

    QAbstractSkeletonPrivate *dnode = QAbstractSkeletonPrivate::get(node);
    dnode->m_jointCount = m_backendSkeleton->jointCount();
    dnode->m_jointNames = m_backendSkeleton->jointNames();
    dnode->m_localPoses = m_backendSkeleton->localPoses();
    dnode->update();

    QSkeletonLoader *loaderNode = qobject_cast<QSkeletonLoader *>(node);
    if (loaderNode) {
        QSkeletonLoaderPrivate *dloaderNode = static_cast<QSkeletonLoaderPrivate *>(QSkeletonLoaderPrivate::get(loaderNode));
        dloaderNode->setStatus(m_backendSkeleton->status());

        if (m_loadedRootJoint) {
            dloaderNode->setRootJoint(m_loadedRootJoint);
            m_loadedRootJoint = nullptr;
        }
    }
}

} // namespace Render
} // namespace Qt3DRender

QT_END_NAMESPACE
