// 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 "gltfimporter_p.h"

#include <Qt3DCore/private/qloadgltf_p.h>

#include <Qt3DAnimation/private/animationlogging_p.h>
#include <Qt3DAnimation/private/fcurve_p.h>
#include <Qt3DAnimation/private/keyframe_p.h>

#include <qopengl.h>
#include <QtGui/qquaternion.h>
#include <QtGui/qvector2d.h>
#include <QtGui/qvector3d.h>
#include <QtGui/qvector4d.h>
#include <QtCore/qdir.h>
#include <QtCore/qfile.h>
#include <QtCore/qfileinfo.h>
#include <QtCore/qiodevice.h>
#include <QtCore/qversionnumber.h>

QT_BEGIN_NAMESPACE

namespace Qt3DAnimation {
namespace Animation {

namespace {

QString gltfTargetPropertyToChannelName(const QString &propertyName)
{
    if (propertyName == QLatin1String("rotation"))
        return QLatin1String("Rotation");
    else if (propertyName == QLatin1String("translation"))
        return QLatin1String("Location");
    else if (propertyName == QLatin1String("scale"))
        return QLatin1String("Scale");

    qWarning() << "Unknown target property name";
    return QString();
}

QKeyFrame::InterpolationType gltfToQKeyFrameInterpolation(GLTFImporter::Sampler::InterpolationMode mode)
{
    switch (mode) {
    case GLTFImporter::Sampler::Linear:
        return QKeyFrame::LinearInterpolation;
    case GLTFImporter::Sampler::Step:
        return QKeyFrame::ConstantInterpolation;
    case GLTFImporter::Sampler::CubicSpline:
        return QKeyFrame::BezierInterpolation;
    case GLTFImporter::Sampler::CatmullRomSpline:
        // TODO: Implement this interpolation type
        qWarning() << "Unhandled interpolation type";
        return QKeyFrame::LinearInterpolation;
    }

    return QKeyFrame::LinearInterpolation;
}

void jsonArrayToSqt(const QJsonArray &jsonArray, Qt3DCore::Sqt &sqt)
{
    Q_ASSERT(jsonArray.size() == 16);
    QMatrix4x4 m;
    float *data = m.data();
    int i = 0;
    for (const auto element : jsonArray)
        *(data + i++) = static_cast<float>(element.toDouble());

    decomposeQMatrix4x4(m, sqt);
}

void jsonArrayToVector3D(const QJsonArray &jsonArray, QVector3D &v)
{
    Q_ASSERT(jsonArray.size() == 3);
    v.setX(static_cast<float>(jsonArray.at(0).toDouble()));
    v.setY(static_cast<float>(jsonArray.at(1).toDouble()));
    v.setZ(static_cast<float>(jsonArray.at(2).toDouble()));
}

void jsonArrayToQuaternion(const QJsonArray &jsonArray, QQuaternion &q)
{
    Q_ASSERT(jsonArray.size() == 4);
    q.setX(static_cast<float>(jsonArray.at(0).toDouble()));
    q.setY(static_cast<float>(jsonArray.at(1).toDouble()));
    q.setZ(static_cast<float>(jsonArray.at(2).toDouble()));
    q.setScalar(static_cast<float>(jsonArray.at(3).toDouble()));
}

}

#define KEY_ACCESSORS               QLatin1String("accessors")
#define KEY_ANIMATIONS              QLatin1String("animations")
#define KEY_ASSET                   QLatin1String("asset")
#define KEY_BUFFER                  QLatin1String("buffer")
#define KEY_BUFFERS                 QLatin1String("buffers")
#define KEY_BUFFER_VIEW             QLatin1String("bufferView")
#define KEY_BUFFER_VIEWS            QLatin1String("bufferViews")
#define KEY_BYTE_LENGTH             QLatin1String("byteLength")
#define KEY_BYTE_OFFSET             QLatin1String("byteOffset")
#define KEY_BYTE_STRIDE             QLatin1String("byteStride")
#define KEY_CAMERA                  QLatin1String("camera")
#define KEY_CHANNELS                QLatin1String("channels")
#define KEY_CHILDREN                QLatin1String("children")
#define KEY_COMPONENT_TYPE          QLatin1String("componentType")
#define KEY_COUNT                   QLatin1String("count")
#define KEY_JOINTS                  QLatin1String("joints")
#define KEY_INPUT                   QLatin1String("input")
#define KEY_INTERPOLATION           QLatin1String("interpolation")
#define KEY_INVERSE_BIND_MATRICES   QLatin1String("inverseBindMatrices")
#define KEY_MATRIX                  QLatin1String("matrix")
#define KEY_MESH                    QLatin1String("mesh")
#define KEY_NAME                    QLatin1String("name")
#define KEY_NODE                    QLatin1String("node")
#define KEY_NODES                   QLatin1String("nodes")
#define KEY_OUTPUT                  QLatin1String("output")
#define KEY_PATH                    QLatin1String("path")
#define KEY_ROTATION                QLatin1String("rotation")
#define KEY_SAMPLER                 QLatin1String("sampler")
#define KEY_SAMPLERS                QLatin1String("samplers")
#define KEY_SCALE                   QLatin1String("scale")
#define KEY_SKIN                    QLatin1String("skin")
#define KEY_SKINS                   QLatin1String("skins")
#define KEY_TARGET                  QLatin1String("target")
#define KEY_TRANSLATION             QLatin1String("translation")
#define KEY_TYPE                    QLatin1String("type")
#define KEY_URI                     QLatin1String("uri")
#define KEY_VERSION                 QLatin1String("version")

GLTFImporter::BufferData::BufferData()
    : byteLength(0)
    , data()
{
}

GLTFImporter::BufferData::BufferData(const QJsonObject &json)
    : byteLength(json.value(KEY_BYTE_LENGTH).toInt())
    , path(json.value(KEY_URI).toString())
    , data()
{
}

GLTFImporter::BufferView::BufferView()
    : byteOffset(0)
    , byteLength(0)
    , bufferIndex(-1)
    , target(0)
{
}

GLTFImporter::BufferView::BufferView(const QJsonObject &json)
    : byteOffset(json.value(KEY_BYTE_OFFSET).toInt())
    , byteLength(json.value(KEY_BYTE_LENGTH).toInt())
    , bufferIndex(json.value(KEY_BUFFER).toInt())
    , target(0)
{
    const auto targetValue = json.value(KEY_TARGET);
    if (!targetValue.isUndefined())
        target = targetValue.toInt();
}

GLTFImporter::AccessorData::AccessorData()
    : type(Qt3DCore::QAttribute::Float)
    , dataSize(0)
    , count(0)
    , byteOffset(0)
    , byteStride(0)
{
}

GLTFImporter::AccessorData::AccessorData(const QJsonObject &json)
    : bufferViewIndex(json.value(KEY_BUFFER_VIEW).toInt(-1))
    , type(accessorTypeFromJSON(json.value(KEY_COMPONENT_TYPE).toInt()))
    , dataSize(accessorDataSizeFromJson(json.value(KEY_TYPE).toString()))
    , count(json.value(KEY_COUNT).toInt())
    , byteOffset(0)
    , byteStride(0)
{
    const auto byteOffsetValue = json.value(KEY_BYTE_OFFSET);
    if (!byteOffsetValue.isUndefined())
        byteOffset = byteOffsetValue.toInt();
    const auto byteStrideValue = json.value(KEY_BYTE_STRIDE);
    if (!byteStrideValue.isUndefined())
        byteStride = byteStrideValue.toInt();
}

GLTFImporter::Skin::Skin()
    : inverseBindAccessorIndex(-1)
    , jointNodeIndices()
{
}

GLTFImporter::Skin::Skin(const QJsonObject &json)
    : name(json.value(KEY_NAME).toString())
    , inverseBindAccessorIndex(json.value(KEY_INVERSE_BIND_MATRICES).toInt())
{
    QJsonArray jointNodes = json.value(KEY_JOINTS).toArray();
    jointNodeIndices.reserve(jointNodes.size());
    for (const auto jointNodeValue : jointNodes)
        jointNodeIndices.push_back(jointNodeValue.toInt());
}

GLTFImporter::Channel::Channel()
    : samplerIndex(-1)
    , targetNodeIndex(-1)
    , targetProperty()
{
}

GLTFImporter::Channel::Channel(const QJsonObject &json)
    : samplerIndex(json.value(KEY_SAMPLER).toInt())
    , targetNodeIndex(-1)
    , targetProperty()
{
    const auto targetJson = json.value(KEY_TARGET).toObject();
    targetNodeIndex = targetJson.value(KEY_NODE).toInt();
    targetProperty = targetJson.value(KEY_PATH).toString();
}

GLTFImporter::Sampler::Sampler()
    : inputAccessorIndex(-1)
    , outputAccessorIndex(-1)
    , interpolationMode(Linear)
{
}

GLTFImporter::Sampler::Sampler(const QJsonObject &json)
    : inputAccessorIndex(json.value(KEY_INPUT).toInt())
    , outputAccessorIndex(json.value(KEY_OUTPUT).toInt())
    , interpolationMode(Linear)
{
    const auto interpolation = json.value(KEY_INTERPOLATION).toString();
    if (interpolation == QLatin1String("LINEAR"))
        interpolationMode = Linear;
    else if (interpolation == QLatin1String("STEP"))
        interpolationMode = Step;
    else if (interpolation == QLatin1String("CATMULLROMSPLINE"))
        interpolationMode = CatmullRomSpline;
    else if (interpolation == QLatin1String("CUBICSPLINE"))
        interpolationMode = CubicSpline;
}

QString GLTFImporter::Sampler::interpolationModeString() const
{
    switch (interpolationMode) {
    case Linear: return QLatin1String("LINEAR");
    case Step: return QLatin1String("STEP");
    case CatmullRomSpline: return QLatin1String("CATMULLROMSPLINE");
    case CubicSpline: return QLatin1String("CUBICSPLINE");
    }

    return QLatin1String("Unknown");
}

GLTFImporter::Animation::Animation()
    : name()
    , channels()
    , samplers()
{
}

GLTFImporter::Animation::Animation(const QJsonObject &json)
    : name(json.value(KEY_NAME).toString())
{
    QJsonArray channelsArray = json.value(KEY_CHANNELS).toArray();
    channels.reserve(channelsArray.size());
    for (const auto channelValue : channelsArray) {
        Channel channel(channelValue.toObject());
        channels.push_back(channel);
    }

    QJsonArray samplersArray = json.value(KEY_SAMPLERS).toArray();
    samplers.reserve(samplersArray.size());
    for (const auto samplerValue : samplersArray) {
        Sampler sampler(samplerValue.toObject());
        samplers.push_back(sampler);
    }
}

GLTFImporter::Node::Node()
    : localTransform()
    , childNodeIndices()
    , name()
    , parentNodeIndex(-1)
    , cameraIndex(-1)
    , meshIndex(-1)
    , skinIndex(-1)
{
}

GLTFImporter::Node::Node(const QJsonObject &json)
    : localTransform()
    , childNodeIndices()
    , name(json.value(KEY_NAME).toString())
    , parentNodeIndex(-1)
    , cameraIndex(-1)
    , meshIndex(-1)
    , skinIndex(-1)
{
    // Child nodes - we setup the parent links in a later pass
    QJsonArray childNodes = json.value(KEY_CHILDREN).toArray();
    childNodeIndices.reserve(childNodes.size());
    for (const auto childNodeValue : childNodes)
        childNodeIndices.push_back(childNodeValue.toInt());

    // Local transform - matrix or scale, rotation, translation
    const auto matrixValue = json.value(KEY_MATRIX);
    if (!matrixValue.isUndefined()) {
        jsonArrayToSqt(matrixValue.toArray(), localTransform);
    } else {
        const auto scaleValue = json.value(KEY_SCALE);
        const auto rotationValue = json.value(KEY_ROTATION);
        const auto translationValue = json.value(KEY_TRANSLATION);

        if (!scaleValue.isUndefined())
            jsonArrayToVector3D(scaleValue.toArray(), localTransform.scale);

        if (!rotationValue.isUndefined())
            jsonArrayToQuaternion(json.value(KEY_ROTATION).toArray(), localTransform.rotation);

        if (!translationValue.isUndefined())
            jsonArrayToVector3D(json.value(KEY_TRANSLATION).toArray(), localTransform.translation);
    }

    // Referenced objects
    const auto cameraValue = json.value(KEY_CAMERA);
    if (!cameraValue.isUndefined())
        cameraIndex = cameraValue.toInt();

    const auto meshValue = json.value(KEY_MESH);
    if (!meshValue.isUndefined())
        meshIndex = meshValue.toInt();

    const auto skinValue = json.value(KEY_SKIN);
    if (!skinValue.isUndefined())
        skinIndex = skinValue.toInt();
}

Qt3DCore::QAttribute::VertexBaseType GLTFImporter::accessorTypeFromJSON(int componentType)
{
    if (componentType == GL_BYTE)
        return Qt3DCore::QAttribute::Byte;
    else if (componentType == GL_UNSIGNED_BYTE)
        return Qt3DCore::QAttribute::UnsignedByte;
    else if (componentType == GL_SHORT)
        return Qt3DCore::QAttribute::Short;
    else if (componentType == GL_UNSIGNED_SHORT)
        return Qt3DCore::QAttribute::UnsignedShort;
    else if (componentType == GL_UNSIGNED_INT)
        return Qt3DCore::QAttribute::UnsignedInt;
    else if (componentType == GL_FLOAT)
        return Qt3DCore::QAttribute::Float;

    // There shouldn't be an invalid case here
    qWarning("unsupported accessor type %d", componentType);
    return Qt3DCore::QAttribute::Float;
}

uint GLTFImporter::accessorTypeSize(Qt3DCore::QAttribute::VertexBaseType componentType)
{
    switch (componentType) {
    case Qt3DCore::QAttribute::Byte:
    case Qt3DCore::QAttribute::UnsignedByte:
         return 1;

    case Qt3DCore::QAttribute::Short:
    case Qt3DCore::QAttribute::UnsignedShort:
        return 2;

    case Qt3DCore::QAttribute::Int:
    case Qt3DCore::QAttribute::Float:
        return 4;

    default:
        qWarning("Unhandled accessor data type %d", componentType);
        return 0;
    }
}

uint GLTFImporter::accessorDataSizeFromJson(const QString &type)
{
    QString typeName = type.toUpper();
    if (typeName == QLatin1String("SCALAR"))
        return 1;
    if (typeName == QLatin1String("VEC2"))
        return 2;
    if (typeName == QLatin1String("VEC3"))
        return 3;
    if (typeName == QLatin1String("VEC4"))
        return 4;
    if (typeName == QLatin1String("MAT2"))
        return 4;
    if (typeName == QLatin1String("MAT3"))
        return 9;
    if (typeName == QLatin1String("MAT4"))
        return 16;

    return 0;
}

GLTFImporter::GLTFImporter()
{
}

bool GLTFImporter::load(QIODevice *ioDev)
{
    if (Q_UNLIKELY(!setJSON(qLoadGLTF(ioDev->readAll())))) {
        qWarning("not a JSON document");
        return false;
    }

    auto file = qobject_cast<QFile*>(ioDev);
    if (file) {
        QFileInfo finfo(file->fileName());
        setBasePath(finfo.dir().absolutePath());
    }

    return parse();
}

QHash<qsizetype, qsizetype> GLTFImporter::createNodeIndexToJointIndexMap(const Skin &skin) const
{
    const qsizetype jointCount = skin.jointNodeIndices.size();
    QHash<qsizetype, qsizetype> nodeIndexToJointIndexMap;
    nodeIndexToJointIndexMap.reserve(jointCount);
    for (qsizetype i = 0; i < jointCount; ++i)
        nodeIndexToJointIndexMap.insert(skin.jointNodeIndices[i], i);
    return nodeIndexToJointIndexMap;
}

GLTFImporter::AnimationNameAndChannels GLTFImporter::createAnimationData(qsizetype animationIndex, const QString &animationName) const
{
    AnimationNameAndChannels nameAndChannels;
    if (m_animations.isEmpty()) {
        qCWarning(Jobs) << "File does not contain any animation data";
        return nameAndChannels;
    }

    if (m_animations.size() == 1) {
        animationIndex = 0;
    } else if (animationIndex < 0 && !animationName.isEmpty()) {
        for (qsizetype i = 0; i < m_animations.size(); ++i) {
            if (m_animations[i].name == animationName) {
                animationIndex = i;
                break;
            }
        }
    }

    if (animationIndex < 0 || animationIndex >= m_animations.size()) {
        qCWarning(Jobs) << "Invalid animation index. Skipping.";
        return nameAndChannels;
    }
    const Animation &animation = m_animations[animationIndex];
    nameAndChannels.name = animation.name;

    // Create node index to joint index lookup tables for each skin
    QList<QHash<qsizetype, qsizetype>> nodeIndexToJointIndexMaps;
    nodeIndexToJointIndexMaps.reserve(m_skins.size());
    for (const auto &skin : m_skins)
        nodeIndexToJointIndexMaps.push_back(createNodeIndexToJointIndexMap(skin));

    int channelIndex = 0;
    for (const auto &channel : animation.channels) {
        Qt3DAnimation::Animation::Channel outputChannel;
        outputChannel.name = gltfTargetPropertyToChannelName(channel.targetProperty);

        // Find the node index to joint index map that contains the target node and
        // look up the joint index from it. If no such map is found, the target joint
        // is not part of a skeleton and so we can just set the jointIndex to -1.
        qsizetype jointIndex = -1;
        for (const auto &map : nodeIndexToJointIndexMaps) {
            const auto result = map.find(channel.targetNodeIndex);
            if (result != map.cend()) {
                jointIndex = result.value();
                break;
            }
        }
        outputChannel.jointIndex = jointIndex;

        const auto &sampler = animation.samplers[channel.samplerIndex];
        const auto interpolationType = gltfToQKeyFrameInterpolation(sampler.interpolationMode);

        if (sampler.inputAccessorIndex == -1 || sampler.outputAccessorIndex == -1) {
            qWarning() << "Skipping channel due to invalid accessor indices in the sampler" << Qt::endl;
            continue;
        }

        const auto &inputAccessor = m_accessors[sampler.inputAccessorIndex];
        const auto &outputAccessor = m_accessors[sampler.outputAccessorIndex];

        if (inputAccessor.type != Qt3DCore::QAttribute::Float) {
            qWarning() << "Input accessor has wrong data type. Skipping channel.";
            continue;
        }

        if (outputAccessor.type != Qt3DCore::QAttribute::Float) {
            qWarning() << "Output accessor has wrong data type. Skipping channel.";
            continue;
        }

        if (inputAccessor.count != outputAccessor.count) {
            qWarning() << "Warning!!! Input accessor has" << inputAccessor.count
                       << "entries and output accessor has" << outputAccessor.count
                       << "entries. They should match. Please check your data.";
            continue;
        }

        // TODO: Allow Qt 3D animation data to share timestamps between multiple
        // channel components. I.e. allow key frame values of composite types.
        // Doesn't give as much freedom but more efficient at runtime.

        // Get the key frame times first as these are common to all components of the
        // key frame values.
        const int keyFrameCount = inputAccessor.count;
        QVector<float> keyframeTimes(keyFrameCount);
        for (int i = 0; i < keyFrameCount; ++i) {
            const auto rawTimestamp = accessorData(sampler.inputAccessorIndex, i);
            keyframeTimes[i] = *reinterpret_cast<const float*>(rawTimestamp.data);
        }

        // Create a ChannelComponent for each component of the output sampler and
        // populate it with data.
        switch (outputAccessor.dataSize) {
        // TODO: Handle other types as needed
        case 3: {
            // vec3
            const int componentCount = 3;

            // Construct the channel component names and add component to the channel
            const QStringList suffixes
                    = (QStringList() << QLatin1String("X") << QLatin1String("Y") << QLatin1String("Z"));
            outputChannel.channelComponents.resize(componentCount);
            for (int componentIndex = 0; componentIndex < componentCount; ++componentIndex) {
                outputChannel.channelComponents[componentIndex].name
                        = QString(QLatin1String("%1 %2")).arg(outputChannel.name,
                                                              suffixes[componentIndex]);
            }

            // Populate the fcurves in the channel components
            for (int i = 0; i < keyFrameCount; ++i) {
                const auto rawKeyframeValue = accessorData(sampler.outputAccessorIndex, i);
                QVector3D v;
                memcpy(&v, rawKeyframeValue.data, rawKeyframeValue.byteLength);

                for (int componentIndex = 0; componentIndex < componentCount; ++componentIndex) {
                    Keyframe keyFrame;
                    keyFrame.interpolation = interpolationType;
                    keyFrame.value = v[componentIndex];
                    outputChannel.channelComponents[componentIndex].fcurve.appendKeyframe(keyframeTimes[i], keyFrame);
                }
            }

            break;
        } // case 3

        case 4: {
            // vec4 or quaternion
            const int componentCount = 4;

            // Construct the channel component names and add component to the channel
            const QStringList rotationSuffixes = (QStringList()
                << QLatin1String("X") << QLatin1String("Y") << QLatin1String("Z") << QLatin1String("W"));
            const QStringList standardSuffixes = (QStringList()
                << QLatin1String("X") << QLatin1String("Y") << QLatin1String("Z"));
            const QStringList suffixes = (channel.targetProperty == QLatin1String("rotation"))
                   ? rotationSuffixes : standardSuffixes;
            outputChannel.channelComponents.resize(componentCount);
            for (int componentIndex = 0; componentIndex < componentCount; ++componentIndex) {
                outputChannel.channelComponents[componentIndex].name
                        = QString(QLatin1String("%1 %2")).arg(outputChannel.name,
                                                              suffixes[componentIndex]);
            }

            // Populate the fcurves in the channel components
            for (int i = 0; i < keyFrameCount; ++i) {
                const auto rawKeyframeValue = accessorData(sampler.outputAccessorIndex, i);
                QVector4D v;
                memcpy(&v, rawKeyframeValue.data, rawKeyframeValue.byteLength);

                for (int componentIndex = 0; componentIndex < componentCount; ++componentIndex) {
                    Keyframe keyFrame;
                    keyFrame.interpolation = interpolationType;
                    keyFrame.value = v[componentIndex];
                    outputChannel.channelComponents[componentIndex].fcurve.appendKeyframe(keyframeTimes[i], keyFrame);
                }
            }

            break;
        } // case 4
        }

        nameAndChannels.channels.push_back(outputChannel);
        ++channelIndex;
    }

    return nameAndChannels;
}

GLTFImporter::RawData GLTFImporter::accessorData(int accessorIndex, int index) const
{
    const AccessorData &accessor = m_accessors[accessorIndex];
    const BufferView &bufferView = m_bufferViews[accessor.bufferViewIndex];
    const BufferData &bufferData = m_bufferDatas[bufferView.bufferIndex];
    const QByteArray &ba = bufferData.data;
    const char *rawData = ba.constData() + bufferView.byteOffset + accessor.byteOffset;

    const uint typeSize = accessorTypeSize(accessor.type);
    const qsizetype stride = (accessor.byteStride == 0)
            ? accessor.dataSize * typeSize
            : accessor.byteStride;

    const char* data = rawData + index * stride;
    if (data - rawData > ba.size()) {
        qWarning("Attempting to access data beyond end of buffer");
        return RawData{ nullptr, 0 };
    }

    const quint64 byteLength = accessor.dataSize * typeSize;
    RawData rd{ data, byteLength };

    return rd;
}

void GLTFImporter::setBasePath(const QString &path)
{
    m_basePath = path;
}

bool GLTFImporter::setJSON(const QJsonDocument &json)
{
    if (!json.isObject())
        return false;
    m_json = json;
    cleanup();
    return true;
}

bool GLTFImporter::parse()
{
    // Find the glTF version
    const QJsonObject asset = m_json.object().value(KEY_ASSET).toObject();
    const QString versionString = asset.value(KEY_VERSION).toString();
    const auto version = QVersionNumber::fromString(versionString);
    switch (version.majorVersion()) {
    case 2:
        return parseGLTF2();

    default:
        qWarning() << "Unsupported version of glTF" << versionString;
        return false;
    }
}

bool GLTFImporter::parseGLTF2()
{
    bool success = true;
    const QJsonArray buffers = m_json.object().value(KEY_BUFFERS).toArray();
    for (const auto bufferValue : buffers)
        success &= processJSONBuffer(bufferValue.toObject());

    const QJsonArray bufferViews = m_json.object().value(KEY_BUFFER_VIEWS).toArray();
    for (const auto bufferViewValue : bufferViews)
        success &= processJSONBufferView(bufferViewValue.toObject());

    const QJsonArray accessors = m_json.object().value(KEY_ACCESSORS).toArray();
    for (const auto accessorValue : accessors)
        success &= processJSONAccessor(accessorValue.toObject());

    const QJsonArray skins = m_json.object().value(KEY_SKINS).toArray();
    for (const auto skinValue : skins)
        success &= processJSONSkin(skinValue.toObject());

    const QJsonArray animations = m_json.object().value(KEY_ANIMATIONS).toArray();
    for (const auto animationValue : animations)
        success &= processJSONAnimation(animationValue.toObject());

    const QJsonArray nodes = m_json.object().value(KEY_NODES).toArray();
    for (const auto nodeValue : nodes)
        success &= processJSONNode(nodeValue.toObject());
    setupNodeParentLinks();

    // TODO: Make a complete GLTF 2 parser by extending to other top level elements:
    // scenes, animations, meshes etc.

    return success;
}

void GLTFImporter::cleanup()
{
    m_accessors.clear();
    m_bufferViews.clear();
    m_bufferDatas.clear();
}

bool GLTFImporter::processJSONBuffer(const QJsonObject &json)
{
    // Store buffer details and load data into memory
    BufferData buffer(json);
    buffer.data = resolveLocalData(buffer.path);
    if (buffer.data.isEmpty())
        return false;

    m_bufferDatas.push_back(buffer);
    return true;
}

bool GLTFImporter::processJSONBufferView(const QJsonObject &json)
{
    BufferView bufferView(json);

    // Perform sanity checks
    const auto bufferIndex = bufferView.bufferIndex;
    if (Q_UNLIKELY(bufferIndex) >= m_bufferDatas.size()) {
        qWarning("Unknown buffer %d when processing buffer view", bufferIndex);
        return false;
    }

    const auto &bufferData = m_bufferDatas[bufferIndex];
    if (bufferView.byteOffset > bufferData.byteLength) {
        qWarning("Bufferview has offset greater than buffer %d length", bufferIndex);
        return false;
    }

    if (Q_UNLIKELY(bufferView.byteOffset + bufferView.byteLength > bufferData.byteLength)) {
        qWarning("BufferView extends beyond end of buffer %d", bufferIndex);
        return false;
    }

    m_bufferViews.push_back(bufferView);
    return true;
}

bool GLTFImporter::processJSONAccessor(const QJsonObject &json)
{
    AccessorData accessor(json);

    // TODO: Perform sanity checks

    m_accessors.push_back(accessor);
    return true;
}

bool GLTFImporter::processJSONSkin(const QJsonObject &json)
{
    Skin skin(json);

    // TODO: Perform sanity checks

    m_skins.push_back(skin);
    return true;
}

bool GLTFImporter::processJSONAnimation(const QJsonObject &json)
{
    const Animation animation(json);

    for (const auto &channel : animation.channels) {
        if (channel.samplerIndex == -1)
            qWarning() << "Invalid sampler index in animation"
                       << animation.name << "for channel targeting node"
                       << channel.targetNodeIndex << " and property"
                       << channel.targetProperty;
    }

    for (const auto &sampler : animation.samplers) {
        if (sampler.inputAccessorIndex == -1) {
            qWarning() << "Sampler for animaton" << animation.name
                       << "references has an invalid input accessor index";
        }

        if (sampler.outputAccessorIndex == -1) {
            qWarning() << "Sampler for animaton" << animation.name
                       << "references has an invalid output accessor index";
        }
    }

    m_animations.push_back(animation);
    return true;
}

bool GLTFImporter::processJSONNode(const QJsonObject &json)
{
    Node node(json);

    // TODO: Perform sanity checks

    m_nodes.push_back(node);
    return true;
}

void GLTFImporter::setupNodeParentLinks()
{
    const qsizetype nodeCount = m_nodes.size();
    for (qsizetype i = 0; i < nodeCount; ++i) {
        const Node &node = m_nodes[i];
        const QList<qsizetype> &childNodeIndices = node.childNodeIndices;
        for (const auto childNodeIndex : childNodeIndices) {
            Q_ASSERT(childNodeIndex < m_nodes.size());
            Node &childNode = m_nodes[childNodeIndex];
            Q_ASSERT(childNode.parentNodeIndex == -1);
            childNode.parentNodeIndex = i;
        }
    }
}

QByteArray GLTFImporter::resolveLocalData(const QString &path) const
{
    QDir d(m_basePath);
    Q_ASSERT(d.exists());

    QString absPath = d.absoluteFilePath(path);
    QFile f(absPath);
    f.open(QIODevice::ReadOnly);
    return f.readAll();
}

} // namespace Animation
} // namespace Qt3DAnimation

QT_END_NAMESPACE
