/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the Qt3D module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include <assimp/Importer.hpp>
#include <assimp/IOStream.hpp>
#include <assimp/IOSystem.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>

#include <qiodevice.h>
#include <qfile.h>
#include <qfileinfo.h>
#include <qdir.h>
#include <qhash.h>
#include <qdebug.h>
#include <qcoreapplication.h>
#include <qcommandlineparser.h>
#include <qjsondocument.h>
#include <qjsonobject.h>
#include <qjsonarray.h>
#include <qmath.h>

#define GLT_UNSIGNED_SHORT 0x1403
#define GLT_UNSIGNED_INT 0x1405
#define GLT_FLOAT 0x1406

#define GLT_FLOAT_VEC2 0x8B50
#define GLT_FLOAT_VEC3 0x8B51
#define GLT_FLOAT_VEC4 0x8B52
#define GLT_FLOAT_MAT3 0x8B5B
#define GLT_FLOAT_MAT4 0x8B5C
#define GLT_SAMPLER_2D 0x8B5E

#define GLT_ARRAY_BUFFER 0x8892
#define GLT_ELEMENT_ARRAY_BUFFER 0x8893

#define GLT_DEPTH_TEST 0x0B71
#define GLT_CULL_FACE 0x0B44
#define GLT_BLEND 0x0BE2

class AssimpIOStream : public Assimp::IOStream
{
public:
    AssimpIOStream(QIODevice *device);
    ~AssimpIOStream();

    size_t Read(void *pvBuffer, size_t pSize, size_t pCount) override;
    size_t Write(const void *pvBuffer, size_t pSize, size_t pCount) override;
    aiReturn Seek(size_t pOffset, aiOrigin pOrigin) override;
    size_t Tell() const override;
    size_t FileSize() const override;
    void Flush() override;

private:
    QIODevice *m_device;
};

class AssimpIOSystem : public Assimp::IOSystem
{
public:
    bool Exists(const char *pFile) const override;
    char getOsSeparator() const override;
    Assimp::IOStream *Open(const char *pFile, const char *pMode) override;
    void Close(Assimp::IOStream *pFile) override;
};

AssimpIOStream::AssimpIOStream(QIODevice *device) :
    m_device(device)
{
    Q_ASSERT(m_device);
}

AssimpIOStream::~AssimpIOStream()
{
    delete m_device;
}

size_t AssimpIOStream::Read(void *pvBuffer, size_t pSize, size_t pCount)
{
    qint64 readBytes = m_device->read((char *)pvBuffer, pSize * pCount);
    if (readBytes < 0)
        qWarning() << Q_FUNC_INFO << " read failed";
    return readBytes;
}

size_t AssimpIOStream::Write(const void *pvBuffer, size_t pSize, size_t pCount)
{
    qint64 writtenBytes = m_device->write((char *)pvBuffer, pSize * pCount);
    if (writtenBytes < 0)
        qWarning() << Q_FUNC_INFO << " write failed";
    return writtenBytes;
}

aiReturn AssimpIOStream::Seek(size_t pOffset, aiOrigin pOrigin)
{
    qint64 seekPos = pOffset;

    if (pOrigin == aiOrigin_CUR)
        seekPos += m_device->pos();
    else if (pOrigin == aiOrigin_END)
        seekPos += m_device->size();

    if (!m_device->seek(seekPos)) {
        qWarning() << Q_FUNC_INFO << " seek failed";
        return aiReturn_FAILURE;
    }
    return aiReturn_SUCCESS;
}

size_t AssimpIOStream::Tell() const
{
    return m_device->pos();
}

size_t AssimpIOStream::FileSize() const
{
    return m_device->size();
}

void AssimpIOStream::Flush()
{
    // we don't write via assimp
}

static QIODevice::OpenMode openModeFromText(const char *name) noexcept
{
    static const struct OpenModeMapping {
        char name[2];
        int mode;
    } openModeMapping[] = {
        { { 'r',   0 },  QIODevice::ReadOnly  },
        { { 'r', '+' },  QIODevice::ReadWrite },
        { { 'w',   0 },  QIODevice::WriteOnly | QIODevice::Truncate },
        { { 'w', '+' },  QIODevice::ReadWrite | QIODevice::Truncate },
        { { 'a',   0 },  QIODevice::WriteOnly | QIODevice::Append },
        { { 'a', '+' },  QIODevice::ReadWrite | QIODevice::Append },
        { { 'w', 'b' },  QIODevice::WriteOnly },
        { { 'w', 't' },  QIODevice::WriteOnly | QIODevice::Text },
        { { 'r', 'b' },  QIODevice::ReadOnly  },
        { { 'r', 't' },  QIODevice::ReadOnly  | QIODevice::Text },
    };

    for (auto e : openModeMapping) {
        if (qstrncmp(e.name, name, sizeof(OpenModeMapping::name)) == 0)
            return static_cast<QIODevice::OpenMode>(e.mode);
    }
    return QIODevice::NotOpen;
}

bool AssimpIOSystem::Exists(const char *pFile) const
{
    return QFileInfo::exists(QString::fromUtf8(pFile));
}

char AssimpIOSystem::getOsSeparator() const
{
    return QDir::separator().toLatin1();
}

Assimp::IOStream *AssimpIOSystem::Open(const char *pFile, const char *pMode)
{
    const QString fileName(QString::fromUtf8(pFile));
    const QLatin1String cleanedMode = QLatin1String{pMode}.trimmed();

    if (const QIODevice::OpenMode openMode = openModeFromText(cleanedMode.data())) {
        QScopedPointer<QFile> file(new QFile(fileName));
        if (file->open(openMode))
            return new AssimpIOStream(file.take());
    }

    return nullptr;
}

void AssimpIOSystem::Close(Assimp::IOStream *pFile)
{
    delete pFile;
}

static inline QString ai2qt(const aiString &str)
{
    return QString::fromUtf8(str.data, int(str.length));
}

static inline QVector<float> ai2qt(const aiMatrix4x4 &matrix)
{
    return QVector<float>() << matrix.a1 << matrix.b1 << matrix.c1 << matrix.d1
                            << matrix.a2 << matrix.b2 << matrix.c2 << matrix.d2
                            << matrix.a3 << matrix.b3 << matrix.c3 << matrix.d3
                            << matrix.a4 << matrix.b4 << matrix.c4 << matrix.d4;
}

struct Options {
    QString outDir;
#ifndef QT_BOOTSTRAPPED
    bool genBin;
#endif
    bool compact;
    bool compress;
    bool genTangents;
    bool interleave;
    float scale;
    bool genCore;
    enum TextureCompression {
        NoTextureCompression,
        ETC1
    };
    TextureCompression texComp;
    bool commonMat;
    bool shaders;
    bool showLog;
} opts;

class Importer
{
public:
    Importer();
    virtual ~Importer();

    virtual bool load(const QString &filename) = 0;

    struct BufferInfo {
        QString name;
        QByteArray data;
    };
    QVector<BufferInfo> buffers() const;

    struct MeshInfo {
        struct BufferView {
            BufferView() : bufIndex(0), offset(0), length(0), componentType(0), target(0) { }
            QString name;
            uint bufIndex;
            uint offset;
            uint length;
            uint componentType;
            uint target;
        };
        QVector<BufferView> views;
        struct Accessor {
            Accessor() : offset(0), stride(0), count(0), componentType(0) { }
            QString name;
            QString usage;
            QString bufferView;
            uint offset;
            uint stride;
            uint count;
            uint componentType;
            QString type;
            QVector<float> minVal;
            QVector<float> maxVal;
        };
        QVector<Accessor> accessors;
        QString name; // generated
        QString originalName; // may be empty
        uint materialIndex;
    };

    QVector<MeshInfo::BufferView> bufferViews() const;
    QVector<MeshInfo::Accessor> accessors() const;
    uint meshCount() const;
    MeshInfo meshInfo(uint meshIndex) const;

    struct MaterialInfo {
        QString name;
        QString originalName;
        QHash<QByteArray, QVector<float> > m_colors;
        QHash<QByteArray, float> m_values;
        QHash<QByteArray, QString> m_textures;
    };
    uint materialCount() const;
    MaterialInfo materialInfo(uint materialIndex) const;

    QSet<QString> externalTextures() const;

    struct CameraInfo {
        QString name; // suffixed
        float aspectRatio;
        float yfov;
        float zfar;
        float znear;
    };
    QHash<QString, CameraInfo> cameraInfo() const;

    struct EmbeddedTextureInfo {
        EmbeddedTextureInfo() { }
        QString name;
#ifdef HAS_QIMAGE
        EmbeddedTextureInfo(const QString &name, const QImage &image) : name(name), image(image) { }
        QImage image;
#endif
    };
    QHash<QString, EmbeddedTextureInfo> embeddedTextures() const;

    struct Node {
        QString name;
        QString uniqueName; // generated
        QVector<float> transformation;
        QVector<Node *> children;
        QVector<uint> meshes;
    };
    const Node *rootNode() const;

    struct KeyFrame {
        KeyFrame() : t(0), transValid(false), rotValid(false), scaleValid(false) { }
        float t;
        bool transValid;
        QVector<float> trans;
        bool rotValid;
        QVector<float> rot;
        bool scaleValid;
        QVector<float> scale;
    };
    struct AnimationInfo {
        AnimationInfo() : hasTranslation(false), hasRotation(false), hasScale(false) { }
        QString name;
        QString targetNode;
        bool hasTranslation;
        bool hasRotation;
        bool hasScale;
        QVector<KeyFrame> keyFrames;
    };
    QVector<AnimationInfo> animations() const;

    bool allMeshesForMaterialHaveTangents(uint materialIndex) const;

    const Node *findNode(const Node *root, const QString &originalName) const;

protected:
    void delNode(Importer::Node *n);

    QByteArray m_buffer;
    QHash<uint, MeshInfo> m_meshInfo;
    QHash<uint, MaterialInfo> m_materialInfo;
    QHash<QString, EmbeddedTextureInfo> m_embeddedTextures;
    QSet<QString> m_externalTextures;
    QHash<QString, CameraInfo> m_cameraInfo;
    Node *m_rootNode;
    QVector<AnimationInfo> m_animations;
};
QT_BEGIN_NAMESPACE
Q_DECLARE_TYPEINFO(Importer::BufferInfo,           Q_MOVABLE_TYPE);
Q_DECLARE_TYPEINFO(Importer::MeshInfo::BufferView, Q_MOVABLE_TYPE);
Q_DECLARE_TYPEINFO(Importer::MeshInfo::Accessor,   Q_MOVABLE_TYPE);
Q_DECLARE_TYPEINFO(Importer::MaterialInfo,         Q_MOVABLE_TYPE);
Q_DECLARE_TYPEINFO(Importer::CameraInfo,           Q_MOVABLE_TYPE);
Q_DECLARE_TYPEINFO(Importer::EmbeddedTextureInfo,  Q_MOVABLE_TYPE);
Q_DECLARE_TYPEINFO(Importer::Node,                 Q_COMPLEX_TYPE); // uses address as identity
Q_DECLARE_TYPEINFO(Importer::KeyFrame,             Q_MOVABLE_TYPE);
Q_DECLARE_TYPEINFO(Importer::AnimationInfo,        Q_MOVABLE_TYPE);
QT_END_NAMESPACE

Importer::Importer()
    : m_rootNode(nullptr)
{
}

void Importer::delNode(Importer::Node *n)
{
    if (!n)
        return;
    for (Importer::Node *c : qAsConst(n->children))
        delNode(c);
    delete n;
}

Importer::~Importer()
{
    delNode(m_rootNode);
}

QVector<Importer::BufferInfo> Importer::buffers() const
{
    BufferInfo b;
    b.name = QStringLiteral("buf");
    b.data = m_buffer;
    return QVector<BufferInfo>() << b;
}

const Importer::Node *Importer::rootNode() const
{
    return m_rootNode;
}

bool Importer::allMeshesForMaterialHaveTangents(uint materialIndex) const
{
    for (const MeshInfo &mi : m_meshInfo) {
        if (mi.materialIndex == materialIndex) {
            bool hasTangents = false;
            for (const MeshInfo::Accessor &acc : mi.accessors) {
                if (acc.usage == QStringLiteral("TANGENT")) {
                    hasTangents = true;
                    break;
                }
            }
            if (!hasTangents)
                return false;
        }
    }
    return true;
}

QVector<Importer::MeshInfo::BufferView> Importer::bufferViews() const
{
    QVector<Importer::MeshInfo::BufferView> bv;
    for (const MeshInfo &mi : m_meshInfo) {
        for (const MeshInfo::BufferView &v : mi.views)
            bv << v;
    }
    return bv;
}

QVector<Importer::MeshInfo::Accessor> Importer::accessors() const
{
    QVector<Importer::MeshInfo::Accessor> acc;
    for (const MeshInfo &mi : m_meshInfo) {
        for (const MeshInfo::Accessor &a : mi.accessors)
            acc << a;
    }
    return acc;
}

uint Importer::meshCount() const
{
    return m_meshInfo.count();
}

Importer::MeshInfo Importer::meshInfo(uint meshIndex) const
{
    return m_meshInfo[meshIndex];
}

uint Importer::materialCount() const
{
    return m_materialInfo.count();
}

Importer::MaterialInfo Importer::materialInfo(uint materialIndex) const
{
    return m_materialInfo[materialIndex];
}

QHash<QString, Importer::CameraInfo> Importer::cameraInfo() const
{
    return m_cameraInfo;
}

QSet<QString> Importer::externalTextures() const
{
    return m_externalTextures;
}

QHash<QString, Importer::EmbeddedTextureInfo> Importer::embeddedTextures() const
{
    return m_embeddedTextures;
}

QVector<Importer::AnimationInfo> Importer::animations() const
{
    return m_animations;
}

const Importer::Node *Importer::findNode(const Node *root, const QString &originalName) const
{
    for (const Node *c : root->children) {
        if (c->name == originalName)
            return c;
        const Node *cn = findNode(c, originalName);
        if (cn)
            return cn;
    }
    return nullptr;
}

class AssimpImporter : public Importer
{
public:
    AssimpImporter();

    bool load(const QString &filename) override;

private:
    const aiScene *scene() const;
    void printNodes(const aiNode *node, int level = 1);
    void buildBuffer();
    void parseEmbeddedTextures();
    void parseMaterials();
    void parseCameras();
    void parseNode(Importer::Node *dst, const aiNode *src);
    void parseScene();
    void parseAnimations();
    void addKeyFrame(QVector<KeyFrame> &keyFrames, float t, aiVector3D *vt, aiQuaternion *vr, aiVector3D *vs);

    QScopedPointer<Assimp::Importer> m_importer;
};

AssimpImporter::AssimpImporter() :
    m_importer(new Assimp::Importer)
{
    m_importer->SetIOHandler(new AssimpIOSystem);
    m_importer->SetPropertyInteger(AI_CONFIG_PP_SBP_REMOVE, aiPrimitiveType_LINE | aiPrimitiveType_POINT);
}

bool AssimpImporter::load(const QString &filename)
{
    uint flags = aiProcess_Triangulate | aiProcess_SortByPType
            | aiProcess_JoinIdenticalVertices
            | aiProcess_GenSmoothNormals
            | aiProcess_GenUVCoords
            | aiProcess_FlipUVs
            | aiProcess_FindDegenerates;

    if (opts.genTangents)
        flags |= aiProcess_CalcTangentSpace;

    const aiScene *scene = m_importer->ReadFile(filename.toUtf8().constData(), flags);
    if (!scene)
        return false;

    if (opts.showLog) {
        qDebug().noquote() << filename
                           << scene->mNumMeshes << "meshes,"
                           << scene->mNumMaterials << "materials,"
                           << scene->mNumTextures << "embedded textures,"
                           << scene->mNumCameras << "cameras,"
                           << scene->mNumLights << "lights,"
                           << scene->mNumAnimations << "animations";
        qDebug() << "Scene:";
        printNodes(scene->mRootNode);
    }

    buildBuffer();
    parseEmbeddedTextures();
    parseMaterials();
    parseCameras();
    parseScene();
    parseAnimations();

    return true;
}

void AssimpImporter::printNodes(const aiNode *node, int level)
{
    qDebug().noquote() << QString().fill('-', level * 4) << ai2qt(node->mName) << node->mNumMeshes << "mesh refs";
    for (uint i = 0; i < node->mNumChildren; ++i)
        printNodes(node->mChildren[i], level + 1);
}

template<class T> void copyIndexBuf(T *dst, const aiMesh *src)
{
    for (uint j = 0; j < src->mNumFaces; ++j) {
        const aiFace *f = &src->mFaces[j];
        if (f->mNumIndices != 3)
            qFatal("Face %d is not a triangle (index count %d instead of 3)", j, f->mNumIndices);
        *dst++ = f->mIndices[0];
        *dst++ = f->mIndices[1];
        *dst++ = f->mIndices[2];
    }
}

static QString newBufferViewName()
{
    static int cnt = 0;
    return QString(QStringLiteral("bufferView_%1")).arg(++cnt);
}

static QString newAccessorName()
{
    static int cnt = 0;
    return QString(QStringLiteral("accessor_%1")).arg(++cnt);
}

static QString newMeshName()
{
    static int cnt = 0;
    return QString(QStringLiteral("mesh_%1")).arg(++cnt);
}

static QString newMaterialName()
{
    static int cnt = 0;
    return QString(QStringLiteral("material_%1")).arg(++cnt);
}

static QString newTechniqueName()
{
    static int cnt = 0;
    return QString(QStringLiteral("technique_%1")).arg(++cnt);
}

static QString newTextureName()
{
    static int cnt = 0;
    return QString(QStringLiteral("texture_%1")).arg(++cnt);
}

static QString newImageName()
{
    static int cnt = 0;
    return QString(QStringLiteral("image_%1")).arg(++cnt);
}

static QString newShaderName()
{
    static int cnt = 0;
    return QString(QStringLiteral("shader_%1")).arg(++cnt);
}

static QString newProgramName()
{
    static int cnt = 0;
    return QString(QStringLiteral("program_%1")).arg(++cnt);
}

static QString newNodeName()
{
    static int cnt = 0;
    return QString(QStringLiteral("node_%1")).arg(++cnt);
}

static QString newAnimationName()
{
    static int cnt = 0;
    return QString(QStringLiteral("animation_%1")).arg(++cnt);
}

template<class T> void calcBB(QVector<float> &minVal, QVector<float> &maxVal, T *data, int vertexCount, int compCount)
{
    minVal.resize(compCount);
    maxVal.resize(compCount);
    for (int i = 0; i < vertexCount; ++i) {
        for (int j = 0; j < compCount; ++j) {
            if (i == 0) {
                minVal[j] = maxVal[j] = data[i][j];
            } else {
                if (data[i][j] < minVal[j])
                    minVal[j] = data[i][j];
                if (data[i][j] > maxVal[j])
                    maxVal[j] = data[i][j];
            }
        }
    }
}

// One buffer per importer (scene).
// Two buffer views (array, index) + three or more accessors per mesh.

void AssimpImporter::buildBuffer()
{
    m_buffer.clear();
    m_meshInfo.clear();

    if (opts.showLog)
        qDebug() << "Meshes:";

    const aiScene *sc = scene();
    for (uint i = 0; i < sc->mNumMeshes; ++i) {
        aiMesh *m = sc->mMeshes[i];
        MeshInfo meshInfo;
        meshInfo.originalName = ai2qt(m->mName);
        meshInfo.name = newMeshName();
        meshInfo.materialIndex = m->mMaterialIndex;

        aiVector3D *vertices = m->mVertices;
        aiVector3D *normals = m->mNormals;
        aiVector3D *textureCoords = m->mTextureCoords[0];
        aiColor4D *colors = m->mColors[0];
        aiVector3D *tangents = m->mTangents;

        if (opts.scale != 1) {
            for (uint j = 0; j < m->mNumVertices; ++j) {
                vertices[j].x *= opts.scale;
                vertices[j].y *= opts.scale;
                vertices[j].z *= opts.scale;
            }
        }

        // Vertex (3), Normal (3), Coord? (2), Color? (4), Tangent? (3)
        uint stride = 3 + 3 + (textureCoords ? 2 : 0) + (colors ? 4 : 0) + (tangents ? 3 : 0);
        QByteArray vertexBuf;
        vertexBuf.resize(stride * m->mNumVertices * sizeof(float));
        float *p = reinterpret_cast<float *>(vertexBuf.data());

        if (opts.interleave) {
            for (uint j = 0; j < m->mNumVertices; ++j) {
                // Vertex
                *p++ = vertices[j].x;
                *p++ = vertices[j].y;
                *p++ = vertices[j].z;

                // Normal
                *p++ = normals[j].x;
                *p++ = normals[j].y;
                *p++ = normals[j].z;

                // Coord
                if (textureCoords) {
                    *p++ = textureCoords[j].x;
                    *p++ = textureCoords[j].y;
                }

                // Color
                if (colors) {
                    *p++ = colors[j].r;
                    *p++ = colors[j].g;
                    *p++ = colors[j].b;
                    *p++ = colors[j].a;
                }

                // Tangent
                if (tangents) {
                    *p++ = tangents[j].x;
                    *p++ = tangents[j].y;
                    *p++ = tangents[j].z;
                }
            }
        } else {
            // Vertex
            for (uint j = 0; j < m->mNumVertices; ++j) {
                *p++ = vertices[j].x;
                *p++ = vertices[j].y;
                *p++ = vertices[j].z;
            }

            // Normal
            for (uint j = 0; j < m->mNumVertices; ++j) {
                *p++ = normals[j].x;
                *p++ = normals[j].y;
                *p++ = normals[j].z;
            }

            // Coord
            if (textureCoords) {
                for (uint j = 0; j < m->mNumVertices; ++j) {
                    *p++ = textureCoords[j].x;
                    *p++ = textureCoords[j].y;
                }
            }

            // Color
            if (colors) {
                for (uint j = 0; j < m->mNumVertices; ++j) {
                    *p++ = colors[j].r;
                    *p++ = colors[j].g;
                    *p++ = colors[j].b;
                    *p++ = colors[j].a;
                }
            }

            // Tangent
            if (tangents) {
                for (uint j = 0; j < m->mNumVertices; ++j) {
                    *p++ = tangents[j].x;
                    *p++ = tangents[j].y;
                    *p++ = tangents[j].z;
                }
            }
        }

        MeshInfo::BufferView vertexBufView;
        vertexBufView.name = newBufferViewName();
        vertexBufView.length = vertexBuf.size();
        vertexBufView.offset = m_buffer.size();
        vertexBufView.componentType = GLT_FLOAT;
        vertexBufView.target = GLT_ARRAY_BUFFER;
        meshInfo.views.append(vertexBufView);

        QByteArray indexBuf;
        uint indexCount = m->mNumFaces * 3;
        if (indexCount >= USHRT_MAX) {
            indexBuf.resize(indexCount * sizeof(quint32));
            quint32 *p = reinterpret_cast<quint32 *>(indexBuf.data());
            copyIndexBuf(p, m);
        } else {
            indexBuf.resize(indexCount * sizeof(quint16));
            quint16 *p = reinterpret_cast<quint16 *>(indexBuf.data());
            copyIndexBuf(p, m);
        }

        MeshInfo::BufferView indexBufView;
        indexBufView.name = newBufferViewName();
        indexBufView.length = indexBuf.size();
        indexBufView.offset = vertexBufView.offset + vertexBufView.length;
        indexBufView.componentType = indexCount >= USHRT_MAX ? GLT_UNSIGNED_INT : GLT_UNSIGNED_SHORT;
        indexBufView.target = GLT_ELEMENT_ARRAY_BUFFER;
        meshInfo.views.append(indexBufView);

        MeshInfo::Accessor acc;
        uint startOffset = 0;
        // Vertex
        acc.name = newAccessorName();
        acc.usage = QStringLiteral("POSITION");
        acc.bufferView = vertexBufView.name;
        acc.offset = 0;
        acc.stride = opts.interleave ? stride * sizeof(float) : 3 * sizeof(float);
        acc.count = m->mNumVertices;
        acc.componentType = vertexBufView.componentType;
        acc.type = QStringLiteral("VEC3");
        calcBB(acc.minVal, acc.maxVal, vertices, m->mNumVertices, 3);
        meshInfo.accessors.append(acc);
        startOffset += opts.interleave ? 3 : 3 * m->mNumVertices;
        // Normal
        acc.name = newAccessorName();
        acc.usage = QStringLiteral("NORMAL");
        acc.offset = startOffset * sizeof(float);
        if (!opts.interleave)
            acc.stride = 3 * sizeof(float);
        calcBB(acc.minVal, acc.maxVal, normals, m->mNumVertices, 3);
        meshInfo.accessors.append(acc);
        startOffset += opts.interleave ? 3 : 3 * m->mNumVertices;
        // Coord
        if (textureCoords) {
            acc.name = newAccessorName();
            acc.usage = QStringLiteral("TEXCOORD_0");
            acc.offset = startOffset * sizeof(float);
            if (!opts.interleave)
                acc.stride = 2 * sizeof(float);
            acc.type = QStringLiteral("VEC2");
            calcBB(acc.minVal, acc.maxVal, textureCoords, m->mNumVertices, 2);
            meshInfo.accessors.append(acc);
            startOffset += opts.interleave ? 2 : 2 * m->mNumVertices;
        }
        // Color
        if (colors) {
            acc.name = newAccessorName();
            acc.usage = QStringLiteral("COLOR");
            acc.offset = startOffset * sizeof(float);
            if (!opts.interleave)
                acc.stride = 4 * sizeof(float);
            acc.type = QStringLiteral("VEC4");
            calcBB(acc.minVal, acc.maxVal, colors, m->mNumVertices, 4);
            meshInfo.accessors.append(acc);
            startOffset += opts.interleave ? 4 : 4 * m->mNumVertices;
        }
        // Tangent
        if (tangents) {
            acc.name = newAccessorName();
            acc.usage = QStringLiteral("TANGENT");
            acc.offset = startOffset * sizeof(float);
            if (!opts.interleave)
                acc.stride = 3 * sizeof(float);
            acc.type = QStringLiteral("VEC3");
            calcBB(acc.minVal, acc.maxVal, tangents, m->mNumVertices, 3);
            meshInfo.accessors.append(acc);
            startOffset += opts.interleave ? 3 : 3 * m->mNumVertices;
        }

        // Index
        acc.name = newAccessorName();
        acc.usage = QStringLiteral("INDEX");
        acc.bufferView = indexBufView.name;
        acc.offset = 0;
        acc.stride = 0;
        acc.count = indexCount;
        acc.componentType = indexBufView.componentType;
        acc.type = QStringLiteral("SCALAR");
        acc.minVal = acc.maxVal = QVector<float>();
        meshInfo.accessors.append(acc);

        if (opts.showLog) {
            qDebug().noquote() << "#" << i << "(" << meshInfo.name << "/" << meshInfo.originalName << ")"
                               << m->mNumVertices << "vertices,"
                               << m->mNumFaces << "faces," << stride << "bytes per vertex,"
                               << vertexBuf.size() << "vertex bytes," << indexBuf.size() << "index bytes";
            if (opts.scale != 1)
                qDebug() << "  scaled by" << opts.scale;
            if (!opts.interleave)
                qDebug() << "  non-interleaved layout";
            QStringList sl;
            for (const MeshInfo::BufferView &bv : qAsConst(meshInfo.views)) sl << bv.name;
            qDebug() << "  buffer views:" << sl;
            sl.clear();
            for (const MeshInfo::Accessor &acc : qAsConst(meshInfo.accessors)) sl << acc.name;
            qDebug() << "  accessors:" << sl;
            qDebug() << "  material: #" << meshInfo.materialIndex;
        }

        m_buffer.append(vertexBuf);
        m_buffer.append(indexBuf);

        m_meshInfo.insert(i, meshInfo);
    }

    if (opts.showLog)
        qDebug().noquote() << "Total buffer size" << m_buffer.size();
}

void AssimpImporter::parseEmbeddedTextures()
{
#ifdef HAS_QIMAGE
    m_embeddedTextures.clear();

    const aiScene *sc = scene();
    if (opts.showLog && sc->mNumTextures)
        qDebug() << "Embedded textures:";

    for (uint i = 0; i < sc->mNumTextures; ++i) {
        aiTexture *t = sc->mTextures[i];
        QImage img;
        if (t->mHeight == 0) {
            img = QImage::fromData(reinterpret_cast<uchar *>(t->pcData), t->mWidth);
        } else {
            uint sz = t->mWidth * t->mHeight;
            QByteArray data;
            data.resize(sz * 4);
            uchar *p = reinterpret_cast<uchar *>(data.data());
            for (uint j = 0; j < sz; ++j) {
                *p++ = t->pcData[j].r;
                *p++ = t->pcData[j].g;
                *p++ = t->pcData[j].b;
                *p++ = t->pcData[j].a;
            }
            img = QImage(reinterpret_cast<const uchar *>(data.constData()), t->mWidth, t->mHeight, QImage::Format_RGBA8888);
            img.detach();
        }
        QString name;
        static int imgCnt = 0;
        name = QString(QStringLiteral("texture_%1.png")).arg(++imgCnt);
        QString embeddedTextureRef = QStringLiteral("*") + QString::number(i); // see AI_MAKE_EMBEDDED_TEXNAME
        m_embeddedTextures.insert(embeddedTextureRef, EmbeddedTextureInfo(name, img));
        if (opts.showLog)
            qDebug().noquote() << "#" << i << name << img;
    }
#else
    if (scene()->mNumTextures)
        qWarning() << "WARNING: No image support, ignoring" << scene()->mNumTextures << "embedded textures";
#endif
}

void AssimpImporter::parseMaterials()
{
    m_materialInfo.clear();
    m_externalTextures.clear();

    if (opts.showLog)
        qDebug() << "Materials:";

    const aiScene *sc = scene();
    for (uint i = 0; i < sc->mNumMaterials; ++i) {
        const aiMaterial *mat = sc->mMaterials[i];
        MaterialInfo matInfo;
        matInfo.name = newMaterialName();

        aiString s;
        if (mat->Get(AI_MATKEY_NAME, s) == aiReturn_SUCCESS)
            matInfo.originalName = ai2qt(s);

        aiColor4D color;
        if (mat->Get(AI_MATKEY_COLOR_DIFFUSE, color) == aiReturn_SUCCESS)
            matInfo.m_colors.insert("diffuse", QVector<float>() << color.r << color.g << color.b << color.a);
        if (mat->Get(AI_MATKEY_COLOR_SPECULAR, color) == aiReturn_SUCCESS)
            matInfo.m_colors.insert("specular", QVector<float>() << color.r << color.g << color.b);
        if (mat->Get(AI_MATKEY_COLOR_AMBIENT, color) == aiReturn_SUCCESS)
            matInfo.m_colors.insert("ambient", QVector<float>() << color.r << color.g << color.b);

        float f;
        if (mat->Get(AI_MATKEY_SHININESS, f) == aiReturn_SUCCESS)
            matInfo.m_values.insert("shininess", f);

        if (mat->GetTexture(aiTextureType_DIFFUSE, 0, &s) == aiReturn_SUCCESS)
            matInfo.m_textures.insert("diffuse", ai2qt(s));
        if (mat->GetTexture(aiTextureType_SPECULAR, 0, &s) == aiReturn_SUCCESS)
            matInfo.m_textures.insert("specular", ai2qt(s));
        if (mat->GetTexture(aiTextureType_NORMALS, 0, &s) == aiReturn_SUCCESS)
            matInfo.m_textures.insert("normal", ai2qt(s));

        QHash<QByteArray, QString>::iterator texIt = matInfo.m_textures.begin();
        while (texIt != matInfo.m_textures.end()) {
            // Map embedded texture references to real files.
            if (texIt->startsWith('*'))
                *texIt = m_embeddedTextures[*texIt].name;
            else
                m_externalTextures.insert(*texIt);
            ++texIt;
        }

        m_materialInfo.insert(i, matInfo);

        if (opts.showLog) {
            qDebug().noquote() << "#" << i << "(" << matInfo.name << "/" << matInfo.originalName << ")";
            qDebug() << "  colors:" << matInfo.m_colors;
            qDebug() << "  values:" << matInfo.m_values;
            qDebug() << "  textures:" << matInfo.m_textures;
        }
    }
}

void AssimpImporter::parseCameras()
{
    m_cameraInfo.clear();

    if (opts.showLog)
        qDebug() << "Cameras:";

    const aiScene *sc = scene();
    for (uint i = 0; i < sc->mNumCameras; ++i) {
        const aiCamera *cam = sc->mCameras[i];
        QString name = ai2qt(cam->mName);
        CameraInfo c;

        c.name = name + QStringLiteral("_cam");
        c.aspectRatio = qFuzzyIsNull(cam->mAspect) ? 1.5f : cam->mAspect;
        c.yfov = cam->mHorizontalFOV;
        if (c.yfov < (M_PI / 10.0)) // this can't be right (probably orthographic source camera)
            c.yfov = float(M_PI / 4.0);
        c.znear = cam->mClipPlaneNear;
        c.zfar = cam->mClipPlaneFar;

        // Collada / glTF cameras point in -Z by default, the rest is in the
        // node matrix, no separate look-at params given here.

        m_cameraInfo.insert(name, c);

        if (opts.showLog)
            qDebug().noquote() << "#" << i << "(" << name << ")" << c.aspectRatio << c.yfov << c.znear << c.zfar;
    }
}

void AssimpImporter::parseNode(Importer::Node *dst, const aiNode *src)
{
    dst->name = ai2qt(src->mName);
    dst->uniqueName = newNodeName();
    for (uint j = 0; j < src->mNumChildren; ++j) {
        Node *c = new Node;
        parseNode(c, src->mChildren[j]);
        dst->children << c;
    }
    dst->transformation = ai2qt(src->mTransformation);
    for (uint j = 0; j < src->mNumMeshes; ++j)
        dst->meshes << src->mMeshes[j];
}

void AssimpImporter::parseScene()
{
    delNode(m_rootNode);
    const aiScene *sc = scene();
    m_rootNode = new Node;
    parseNode(m_rootNode, sc->mRootNode);
}

void AssimpImporter::addKeyFrame(QVector<KeyFrame> &keyFrames, float t, aiVector3D *vt, aiQuaternion *vr, aiVector3D *vs)
{
    KeyFrame kf;
    int idx = -1;
    for (int i = 0; i < keyFrames.count(); ++i) {
        if (qFuzzyCompare(keyFrames[i].t, t)) {
            kf = keyFrames[i];
            idx = i;
            break;
        }
    }

    kf.t = t;
    if (vt) {
        kf.transValid = true;
        kf.trans = QVector<float>() << vt->x << vt->y << vt->z;
    }
    if (vr) {
        kf.rotValid = true;
        kf.rot = QVector<float>() << vr->w << vr->x << vr->y << vr->z;
    }
    if (vs) {
        kf.scaleValid = true;
        kf.scale = QVector<float>() << vs->x << vs->y << vs->z;
    }

    if (idx >= 0)
        keyFrames[idx] = kf;
    else
        keyFrames.append(kf);
}

void AssimpImporter::parseAnimations()
{
    const aiScene *sc = scene();
    if (opts.showLog && sc->mNumAnimations)
        qDebug() << "Animations:";

    for (uint i = 0; i < sc->mNumAnimations; ++i) {
        const aiAnimation *anim = sc->mAnimations[i];

        // Only care about node animations.
        for (uint j = 0; j < anim->mNumChannels; ++j) {
            const aiNodeAnim *a = anim->mChannels[j];
            AnimationInfo animInfo;
            QVector<KeyFrame> keyFrames;

            if (opts.showLog)
                qDebug().noquote() << ai2qt(anim->mName) << "->" << ai2qt(a->mNodeName);

            // Target values in the keyframes are local absolute (relative to parent, like node.matrix).
            for (uint kf = 0; kf < a->mNumPositionKeys; ++kf) {
                float t = float(a->mPositionKeys[kf].mTime);
                aiVector3D v = a->mPositionKeys[kf].mValue;
                animInfo.hasTranslation = true;
                addKeyFrame(keyFrames, t, &v, nullptr, nullptr);
            }
            for (uint kf = 0; kf < a->mNumRotationKeys; ++kf) {
                float t = float(a->mRotationKeys[kf].mTime);
                aiQuaternion v = a->mRotationKeys[kf].mValue;
                animInfo.hasRotation = true;
                addKeyFrame(keyFrames, t, nullptr, &v, nullptr);
            }
            for (uint kf = 0; kf < a->mNumScalingKeys; ++kf) {
                float t = float(a->mScalingKeys[kf].mTime);
                aiVector3D v = a->mScalingKeys[kf].mValue;
                animInfo.hasScale = true;
                addKeyFrame(keyFrames, t, nullptr, nullptr, &v);
            }

            // Here we should ideally get rid of non-animated properties (that
            // just set the t-r-s value from node.matrix in every frame) but
            // let's leave that as a future exercise.

            if (!keyFrames.isEmpty()) {
                animInfo.name = ai2qt(anim->mName);
                QString nodeName = ai2qt(a->mNodeName); // have to map to our generated, unique node names
                const Node *targetNode = findNode(m_rootNode, nodeName);
                if (targetNode)
                    animInfo.targetNode = targetNode->uniqueName;
                else
                    qWarning().noquote() << "ERROR: Cannot find target node" << nodeName << "for animation" << animInfo.name;
                animInfo.keyFrames = keyFrames;
                m_animations << animInfo;

                if (opts.showLog) {
                    for (const KeyFrame &kf : qAsConst(keyFrames)) {
                        QString msg;
                        QTextStream s(&msg);
                        s << "  @ " << kf.t;
                        if (kf.transValid)
                            s << " T=(" << kf.trans[0] << ", " << kf.trans[1] << ", " << kf.trans[2] << ")";
                        if (kf.rotValid)
                            s << " R=(w=" << kf.rot[0] << ", " << kf.rot[1] << ", " << kf.rot[2] << ", " << kf.rot[3] << ")";
                        if (kf.scaleValid)
                            s << " S=(" << kf.scale[0] << ", " << kf.scale[1] << ", " << kf.scale[2] << ")";
                        qDebug().noquote() << msg;
                    }
                }
            }
        }
    }
}

const aiScene *AssimpImporter::scene() const
{
    return m_importer->GetScene();
}

class Exporter
{
public:
    Exporter(Importer *importer) : m_importer(importer) { }
    virtual ~Exporter() { }

    virtual void save(const QString &inputFilename) = 0;

protected:
    bool nodeIsUseful(const Importer::Node *n) const;
    void copyExternalTextures(const QString &inputFilename);
    void exportEmbeddedTextures();
    void compressTextures();

    Importer *m_importer;
    QSet<QString> m_files;
    QHash<QString, QString> m_compressedTextures;
};

bool Exporter::nodeIsUseful(const Importer::Node *n) const
{
    if (!n->meshes.isEmpty() || m_importer->cameraInfo().contains(n->name))
        return true;

    for (const Importer::Node *c : n->children) {
        if (nodeIsUseful(c))
            return true;
    }

    return false;
}

void Exporter::copyExternalTextures(const QString &inputFilename)
{
    const auto textureFilenames = m_importer->externalTextures();
    for (const QString &textureFilename : textureFilenames) {
        const QString dst = opts.outDir + textureFilename;
        m_files.insert(QFileInfo(dst).fileName());
        // External textures need copying only when output dir was specified.
        if (!opts.outDir.isEmpty()) {
            const QString src = QFileInfo(inputFilename).path() + QStringLiteral("/") + textureFilename;
            if (QFileInfo(src).absolutePath() != QFileInfo(dst).absolutePath()) {
                if (opts.showLog)
                    qDebug().noquote() << "Copying" << src << "to" << dst;
                QFile(src).copy(dst);
            }
        }
    }
}

void Exporter::exportEmbeddedTextures()
{
#ifdef HAS_QIMAGE
    const auto embeddedTextures = m_importer->embeddedTextures();
    for (const Importer::EmbeddedTextureInfo &embTex : embeddedTextures) {
        QString fn = opts.outDir + embTex.name;
        m_files.insert(QFileInfo(fn).fileName());
        if (opts.showLog)
            qDebug().noquote() << "Writing" << fn;
        embTex.image.save(fn);
    }
#endif
}

void Exporter::compressTextures()
{
    if (opts.texComp != Options::ETC1)
        return;

    const auto textureFilenames = m_importer->externalTextures();
    const auto embeddedTextures = m_importer->embeddedTextures();
    QStringList imageList;
    imageList.reserve(textureFilenames.size() + embeddedTextures.size());
    for (const QString &textureFilename : textureFilenames)
        imageList << opts.outDir + textureFilename;
    for (const Importer::EmbeddedTextureInfo &embTex : embeddedTextures)
        imageList << opts.outDir + embTex.name;

    for (const QString &filename : qAsConst(imageList)) {
        if (QFileInfo(filename).suffix().toLower() != QStringLiteral("png"))
            continue;
        QByteArray cmd = QByteArrayLiteral("etc1tool ");
        cmd += filename.toUtf8();
        qDebug().noquote() << "Invoking" << cmd;
        // No QProcess in bootstrap
        if (system(cmd.constData()) == -1) {
            qWarning() << "ERROR: Failed to launch etc1tool";
        } else {
            QString src = QFileInfo(filename).fileName();
            QString dst = QFileInfo(src).baseName() + QStringLiteral(".pkm");
            m_compressedTextures.insert(src, dst);
            m_files.remove(src);
            m_files.insert(dst);
        }
    }
}

class GltfExporter : public Exporter
{
public:
    GltfExporter(Importer *importer);
    void save(const QString &inputFilename) override;

private:
    struct ProgramInfo {
        struct Param {
            Param() : type(0) { }
            Param(QString name, QString nameInShader, QString semantic, uint type)
                : name(name), nameInShader(nameInShader), semantic(semantic), type(type) { }
            QString name;
            QString nameInShader;
            QString semantic;
            uint type;
        };
        QString commonTechniqueName;
        QString vertShader;
        QString fragShader;
        QVector<Param> attributes;
        QVector<Param> uniforms;
    };
    friend class QTypeInfo<ProgramInfo>;
    friend class QTypeInfo<ProgramInfo::Param>;

    void writeShader(const QString &src, const QString &dst, const QVector<QPair<QByteArray, QByteArray> > &substTab);
    QString exportNode(const Importer::Node *n, QJsonObject &nodes);
    void exportMaterials(QJsonObject &materials, QHash<QString, QString> *textureNameMap);
    void exportParameter(QJsonObject &dst, const QVector<ProgramInfo::Param> &params);
    void exportTechniques(QJsonObject &obj, const QString &basename);
    void exportAnimations(QJsonObject &obj, QVector<Importer::BufferInfo> &bufList,
                          QVector<Importer::MeshInfo::BufferView> &bvList,
                          QVector<Importer::MeshInfo::Accessor> &accList);
    void initShaderInfo();
    ProgramInfo *chooseProgram(uint materialIndex);

    QJsonObject m_obj;
    QJsonDocument m_doc;
    QVector<ProgramInfo> m_progs;

    struct TechniqueInfo {
        TechniqueInfo() : opaque(true), prog(nullptr) { }
        TechniqueInfo(const QString &name, bool opaque, ProgramInfo *prog)
            : name(name)
            , opaque(opaque)
            , prog(prog)
        {
            coreName = name + QStringLiteral("_core");
            gl2Name = name + QStringLiteral("_gl2");
        }
        QString name;
        QString coreName;
        QString gl2Name;
        bool opaque;
        ProgramInfo *prog;
    };
    friend class QTypeInfo<TechniqueInfo>;
    QVector<TechniqueInfo> m_techniques;
    QSet<ProgramInfo *> m_usedPrograms;

    QVector<QPair<QByteArray, QByteArray> > m_subst_es2;
    QVector<QPair<QByteArray, QByteArray> > m_subst_core;

    QHash<QString, bool> m_imageHasAlpha;
};
QT_BEGIN_NAMESPACE
Q_DECLARE_TYPEINFO(GltfExporter::ProgramInfo,        Q_MOVABLE_TYPE);
Q_DECLARE_TYPEINFO(GltfExporter::ProgramInfo::Param, Q_MOVABLE_TYPE);
Q_DECLARE_TYPEINFO(GltfExporter::TechniqueInfo,      Q_MOVABLE_TYPE);
QT_END_NAMESPACE

GltfExporter::GltfExporter(Importer *importer)
    : Exporter(importer)
{
    initShaderInfo();
}

struct Shader {
    const char *name;
    const char *text;
} shaders[] = {
    {
        "color.vert",
"$VERSION\n"
"$ATTRIBUTE vec3 vertexPosition;\n"
"$ATTRIBUTE vec3 vertexNormal;\n"
"$VVARYING vec3 vPosition;\n"
"$VVARYING vec3 vNormal;\n"
"uniform mat4 projection;\n"
"uniform mat4 modelView;\n"
"uniform mat3 modelViewNormal;\n"
"void main()\n"
"{\n"
"    vNormal = normalize( modelViewNormal * vertexNormal );\n"
"    vPosition = vec3( modelView * vec4( vertexPosition, 1.0 ) );\n"
"    gl_Position = projection * modelView * vec4( vertexPosition, 1.0 );\n"
"}\n"
    },
    {
        "color.frag",
"$VERSION\n"
"uniform $HIGHP vec4 lightPosition;\n"
"uniform $HIGHP vec3 lightIntensity;\n"
"uniform $HIGHP vec3 ka;\n"
"uniform $HIGHP vec4 kd;\n"
"uniform $HIGHP vec3 ks;\n"
"uniform $HIGHP float shininess;\n"
"$FVARYING $HIGHP vec3 vPosition;\n"
"$FVARYING $HIGHP vec3 vNormal;\n"
"$DECL_FRAGCOLOR\n"
"$HIGHP vec3 adsModel( const $HIGHP vec3 pos, const $HIGHP vec3 n )\n"
"{\n"
"    $HIGHP vec3 s = normalize( vec3( lightPosition ) - pos );\n"
"    $HIGHP vec3 v = normalize( -pos );\n"
"    $HIGHP vec3 r = reflect( -s, n );\n"
"    $HIGHP float diffuse = max( dot( s, n ), 0.0 );\n"
"    $HIGHP float specular = 0.0;\n"
"    if ( dot( s, n ) > 0.0 )\n"
"        specular = pow( max( dot( r, v ), 0.0 ), shininess );\n"
"    return lightIntensity * ( ka + kd.rgb * diffuse + ks * specular );\n"
"}\n"
"void main()\n"
"{\n"
"    $FRAGCOLOR = vec4( adsModel( vPosition, normalize( vNormal ) ) * kd.a, kd.a );\n"
"}\n"
    },
    {
        "diffusemap.vert",
"$VERSION\n"
"$ATTRIBUTE vec3 vertexPosition;\n"
"$ATTRIBUTE vec3 vertexNormal;\n"
"$ATTRIBUTE vec2 vertexTexCoord;\n"
"$VVARYING vec3 vPosition;\n"
"$VVARYING vec3 vNormal;\n"
"$VVARYING vec2 vTexCoord;\n"
"uniform mat4 projection;\n"
"uniform mat4 modelView;\n"
"uniform mat3 modelViewNormal;\n"
"void main()\n"
"{\n"
"    vTexCoord = vertexTexCoord;\n"
"    vNormal = normalize( modelViewNormal * vertexNormal );\n"
"    vPosition = vec3( modelView * vec4( vertexPosition, 1.0 ) );\n"
"    gl_Position = projection * modelView * vec4( vertexPosition, 1.0 );\n"
"}\n"
    },
    {
        "diffusemap.frag",
"$VERSION\n"
"uniform $HIGHP vec4 lightPosition;\n"
"uniform $HIGHP vec3 lightIntensity;\n"
"uniform $HIGHP vec3 ka;\n"
"uniform $HIGHP vec3 ks;\n"
"uniform $HIGHP float shininess;\n"
"uniform sampler2D diffuseTexture;\n"
"$FVARYING $HIGHP vec3 vPosition;\n"
"$FVARYING $HIGHP vec3 vNormal;\n"
"$FVARYING $HIGHP vec2 vTexCoord;\n"
"$DECL_FRAGCOLOR\n"
"$HIGHP vec4 adsModel( const $HIGHP vec3 pos, const $HIGHP vec3 n )\n"
"{\n"
"    $HIGHP vec3 s = normalize( vec3( lightPosition ) - pos );\n"
"    $HIGHP vec3 v = normalize( -pos );\n"
"    $HIGHP vec3 r = reflect( -s, n );\n"
"    $HIGHP float diffuse = max( dot( s, n ), 0.0 );\n"
"    $HIGHP float specular = 0.0;\n"
"    if ( dot( s, n ) > 0.0 )\n"
"        specular = pow( max( dot( r, v ), 0.0 ), shininess );\n"
"    $HIGHP vec4 kd = $TEXTURE2D( diffuseTexture, vTexCoord );\n"
"    return vec4( lightIntensity * ( ka + kd.rgb * diffuse + ks * specular ) * kd.a, kd.a );\n"
"}\n"
"void main()\n"
"{\n"
"    $FRAGCOLOR = adsModel( vPosition, normalize( vNormal ) );\n"
"}\n"
    },
    {
        "diffusespecularmap.frag",
"$VERSION\n"
"uniform $HIGHP vec4 lightPosition;\n"
"uniform $HIGHP vec3 lightIntensity;\n"
"uniform $HIGHP vec3 ka;\n"
"uniform $HIGHP float shininess;\n"
"uniform sampler2D diffuseTexture;\n"
"uniform sampler2D specularTexture;\n"
"$FVARYING $HIGHP vec3 vPosition;\n"
"$FVARYING $HIGHP vec3 vNormal;\n"
"$FVARYING $HIGHP vec2 vTexCoord;\n"
"$DECL_FRAGCOLOR\n"
"$HIGHP vec4 adsModel( const in $HIGHP vec3 pos, const in $HIGHP vec3 n )\n"
"{\n"
"    $HIGHP vec3 s = normalize( vec3( lightPosition ) - pos );\n"
"    $HIGHP vec3 v = normalize( -pos );\n"
"    $HIGHP vec3 r = reflect( -s, n );\n"
"    $HIGHP float diffuse = max( dot( s, n ), 0.0 );\n"
"    $HIGHP float specular = 0.0;\n"
"    if ( dot( s, n ) > 0.0 )\n"
"        specular = ( shininess / ( 8.0 * 3.14 ) ) * pow( max( dot( r, v ), 0.0 ), shininess );\n"
"    $HIGHP vec4 kd = $TEXTURE2D( diffuseTexture, vTexCoord );\n"
"    $HIGHP vec3 ks = $TEXTURE2D( specularTexture, vTexCoord );\n"
"    return vec4( lightIntensity * ( ka + kd.rgb * diffuse + ks * specular ) * kd.a, kd.a );\n"
"}\n"
"void main()\n"
"{\n"
"    $FRAGCOLOR = vec4( adsModel( vPosition, normalize( vNormal ) ), 1.0 );\n"
"}\n"
    },
    {
        "normaldiffusemap.vert",
"$VERSION\n"
"$ATTRIBUTE vec3 vertexPosition;\n"
"$ATTRIBUTE vec3 vertexNormal;\n"
"$ATTRIBUTE vec2 vertexTexCoord;\n"
"$ATTRIBUTE vec4 vertexTangent;\n"
"$VVARYING vec3 lightDir;\n"
"$VVARYING vec3 viewDir;\n"
"$VVARYING vec2 texCoord;\n"
"uniform mat4 projection;\n"
"uniform mat4 modelView;\n"
"uniform mat3 modelViewNormal;\n"
"uniform vec4 lightPosition;\n"
"void main()\n"
"{\n"
"    texCoord = vertexTexCoord;\n"
"    vec3 normal = normalize( modelViewNormal * vertexNormal );\n"
"    vec3 tangent = normalize( modelViewNormal * vertexTangent.xyz );\n"
"    vec3 position = vec3( modelView * vec4( vertexPosition, 1.0 ) );\n"
"    vec3 binormal = normalize( cross( normal, tangent ) );\n"
"    mat3 tangentMatrix = mat3 (\n"
"        tangent.x, binormal.x, normal.x,\n"
"        tangent.y, binormal.y, normal.y,\n"
"        tangent.z, binormal.z, normal.z );\n"
"    vec3 s = vec3( lightPosition ) - position;\n"
"    lightDir = normalize( tangentMatrix * s );\n"
"    vec3 v = -position;\n"
"    viewDir = normalize( tangentMatrix * v );\n"
"    gl_Position = projection * modelView * vec4( vertexPosition, 1.0 );\n"
"}\n"
    },
    {
        "normaldiffusemap.frag",
"$VERSION\n"
"uniform $HIGHP vec3 lightIntensity;\n"
"uniform $HIGHP vec3 ka;\n"
"uniform $HIGHP vec3 ks;\n"
"uniform $HIGHP float shininess;\n"
"uniform sampler2D diffuseTexture;\n"
"uniform sampler2D normalTexture;\n"
"$FVARYING $HIGHP vec3 lightDir;\n"
"$FVARYING $HIGHP vec3 viewDir;\n"
"$FVARYING $HIGHP vec2 texCoord;\n"
"$DECL_FRAGCOLOR\n"
"$HIGHP vec3 adsModel( const $HIGHP vec3 norm, const $HIGHP vec3 diffuseReflect)\n"
"{\n"
"    $HIGHP vec3 r = reflect( -lightDir, norm );\n"
"    $HIGHP vec3 ambient = lightIntensity * ka;\n"
"    $HIGHP float sDotN = max( dot( lightDir, norm ), 0.0 );\n"
"    $HIGHP vec3 diffuse = lightIntensity * diffuseReflect * sDotN;\n"
"    $HIGHP vec3 ambientAndDiff = ambient + diffuse;\n"
"    $HIGHP vec3 spec = vec3( 0.0 );\n"
"    if ( sDotN > 0.0 )\n"
"        spec = lightIntensity * ks * pow( max( dot( r, viewDir ), 0.0 ), shininess );\n"
"    return ambientAndDiff + spec;\n"
"}\n"
"void main()\n"
"{\n"
"    $HIGHP vec4 kd = $TEXTURE2D( diffuseTexture, texCoord );\n"
"    $HIGHP vec4 normal = 2.0 * $TEXTURE2D( normalTexture, texCoord ) - vec4( 1.0 );\n"
"    $FRAGCOLOR = vec4( adsModel( normalize( normal.xyz ), kd.rgb) * kd.a, kd.a );\n"
"}\n"
    },
    {
        "normaldiffusespecularmap.frag",
"$VERSION\n"
"uniform $HIGHP vec3 lightIntensity;\n"
"uniform $HIGHP vec3 ka;\n"
"uniform $HIGHP float shininess;\n"
"uniform sampler2D diffuseTexture;\n"
"uniform sampler2D specularTexture;\n"
"uniform sampler2D normalTexture;\n"
"$FVARYING $HIGHP vec3 lightDir;\n"
"$FVARYING $HIGHP vec3 viewDir;\n"
"$FVARYING $HIGHP vec2 texCoord;\n"
"$DECL_FRAGCOLOR\n"
"$HIGHP vec3 adsModel( const $HIGHP vec3 norm, const $HIGHP vec3 diffuseReflect, const $HIGHP vec3 specular )\n"
"{\n"
"    $HIGHP vec3 r = reflect( -lightDir, norm );\n"
"    $HIGHP vec3 ambient = lightIntensity * ka;\n"
"    $HIGHP float sDotN = max( dot( lightDir, norm ), 0.0 );\n"
"    $HIGHP vec3 diffuse = lightIntensity * diffuseReflect * sDotN;\n"
"    $HIGHP vec3 ambientAndDiff = ambient + diffuse;\n"
"    $HIGHP vec3 spec = vec3( 0.0 );\n"
"    if ( sDotN > 0.0 )\n"
"        spec = lightIntensity * ( shininess / ( 8.0 * 3.14 ) ) * pow( max( dot( r, viewDir ), 0.0 ), shininess );\n"
"    return (ambientAndDiff + spec * specular.rgb);\n"
"}\n"
"void main()\n"
"{\n"
"    $HIGHP vec4 kd = $TEXTURE2D( diffuseTexture, texCoord );\n"
"    $HIGHP vec3 ks = $TEXTURE2D( specularTexture, texCoord );\n"
"    $HIGHP vec4 normal = 2.0 * $TEXTURE2D( normalTexture, texCoord ) - vec4( 1.0 );\n"
"    $FRAGCOLOR = vec4( adsModel( normalize( normal.xyz ), kd.rgb, ks ) * kd.a, kd.a );\n"
"}\n"
    }
};

void GltfExporter::initShaderInfo()
{
    ProgramInfo p;

    p = ProgramInfo();
    p.commonTechniqueName = "PHONG"; // diffuse RGBA, specular RGBA
    p.vertShader = "color.vert";
    p.fragShader = "color.frag";
    p.attributes << ProgramInfo::Param("position", "vertexPosition", "POSITION", GLT_FLOAT_VEC3);
    p.attributes << ProgramInfo::Param("normal", "vertexNormal", "NORMAL", GLT_FLOAT_VEC3);
    p.uniforms << ProgramInfo::Param("projection", "projection", "PROJECTION", GLT_FLOAT_MAT4);
    p.uniforms << ProgramInfo::Param("modelView", "modelView", "MODELVIEW", GLT_FLOAT_MAT4);
    p.uniforms << ProgramInfo::Param("normalMatrix", "modelViewNormal", "MODELVIEWINVERSETRANSPOSE", GLT_FLOAT_MAT3);
    p.uniforms << ProgramInfo::Param("lightPosition", "lightPosition", QString(), GLT_FLOAT_VEC4);
    p.uniforms << ProgramInfo::Param("lightIntensity", "lightIntensity", QString(), GLT_FLOAT_VEC3);
    p.uniforms << ProgramInfo::Param("ambient", "ka", QString(), GLT_FLOAT_VEC3);
    p.uniforms << ProgramInfo::Param("diffuse", "kd", QString(), GLT_FLOAT_VEC4);
    p.uniforms << ProgramInfo::Param("specular", "ks", QString(), GLT_FLOAT_VEC3);
    p.uniforms << ProgramInfo::Param("shininess", "shininess", QString(), GLT_FLOAT);
    m_progs << p;

    p = ProgramInfo();
    p.commonTechniqueName = "PHONG"; //  diffuse texture, specular RGBA
    p.vertShader = "diffusemap.vert";
    p.fragShader = "diffusemap.frag";
    p.attributes << ProgramInfo::Param("position", "vertexPosition", "POSITION", GLT_FLOAT_VEC3);
    p.attributes << ProgramInfo::Param("normal", "vertexNormal", "NORMAL", GLT_FLOAT_VEC3);
    p.attributes << ProgramInfo::Param("texcoord0", "vertexTexCoord", "TEXCOORD_0", GLT_FLOAT_VEC2);
    p.uniforms << ProgramInfo::Param("projection", "projection", "PROJECTION", GLT_FLOAT_MAT4);
    p.uniforms << ProgramInfo::Param("modelView", "modelView", "MODELVIEW", GLT_FLOAT_MAT4);
    p.uniforms << ProgramInfo::Param("normalMatrix", "modelViewNormal", "MODELVIEWINVERSETRANSPOSE", GLT_FLOAT_MAT3);
    p.uniforms << ProgramInfo::Param("lightPosition", "lightPosition", QString(), GLT_FLOAT_VEC4);
    p.uniforms << ProgramInfo::Param("lightIntensity", "lightIntensity", QString(), GLT_FLOAT_VEC3);
    p.uniforms << ProgramInfo::Param("ambient", "ka", QString(), GLT_FLOAT_VEC3);
    p.uniforms << ProgramInfo::Param("specular", "ks", QString(), GLT_FLOAT_VEC3);
    p.uniforms << ProgramInfo::Param("shininess", "shininess", QString(), GLT_FLOAT);
    p.uniforms << ProgramInfo::Param("diffuse", "diffuseTexture", QString(), GLT_SAMPLER_2D);
    m_progs << p;

    p = ProgramInfo();
    p.commonTechniqueName = "PHONG"; // diffuse texture, specular texture
    p.vertShader = "diffusemap.vert";
    p.fragShader = "diffusespecularmap.frag";
    p.attributes << ProgramInfo::Param("position", "vertexPosition", "POSITION", GLT_FLOAT_VEC3);
    p.attributes << ProgramInfo::Param("normal", "vertexNormal", "NORMAL", GLT_FLOAT_VEC3);
    p.attributes << ProgramInfo::Param("texcoord0", "vertexTexCoord", "TEXCOORD_0", GLT_FLOAT_VEC2);
    p.uniforms << ProgramInfo::Param("projection", "projection", "PROJECTION", GLT_FLOAT_MAT4);
    p.uniforms << ProgramInfo::Param("modelView", "modelView", "MODELVIEW", GLT_FLOAT_MAT4);
    p.uniforms << ProgramInfo::Param("normalMatrix", "modelViewNormal", "MODELVIEWINVERSETRANSPOSE", GLT_FLOAT_MAT3);
    p.uniforms << ProgramInfo::Param("lightPosition", "lightPosition", QString(), GLT_FLOAT_VEC4);
    p.uniforms << ProgramInfo::Param("lightIntensity", "lightIntensity", QString(), GLT_FLOAT_VEC3);
    p.uniforms << ProgramInfo::Param("ambient", "ka", QString(), GLT_FLOAT_VEC3);
    p.uniforms << ProgramInfo::Param("shininess", "shininess", QString(), GLT_FLOAT);
    p.uniforms << ProgramInfo::Param("diffuse", "diffuseTexture", QString(), GLT_SAMPLER_2D);
    p.uniforms << ProgramInfo::Param("specular", "specularTexture", QString(), GLT_SAMPLER_2D);
    m_progs << p;

    p = ProgramInfo();
    p.commonTechniqueName = "PHONG"; // diffuse texture, specular RGBA, normalmap texture
    p.vertShader = "normaldiffusemap.vert";
    p.fragShader = "normaldiffusemap.frag";
    p.attributes << ProgramInfo::Param("position", "vertexPosition", "POSITION", GLT_FLOAT_VEC3);
    p.attributes << ProgramInfo::Param("normal", "vertexNormal", "NORMAL", GLT_FLOAT_VEC3);
    p.attributes << ProgramInfo::Param("texcoord0", "vertexTexCoord", "TEXCOORD_0", GLT_FLOAT_VEC2);
    p.attributes << ProgramInfo::Param("tangent", "vertexTangent", "TANGENT", GLT_FLOAT_VEC3);
    p.uniforms << ProgramInfo::Param("projection", "projection", "PROJECTION", GLT_FLOAT_MAT4);
    p.uniforms << ProgramInfo::Param("modelView", "modelView", "MODELVIEW", GLT_FLOAT_MAT4);
    p.uniforms << ProgramInfo::Param("normalMatrix", "modelViewNormal", "MODELVIEWINVERSETRANSPOSE", GLT_FLOAT_MAT3);
    p.uniforms << ProgramInfo::Param("lightPosition", "lightPosition", QString(), GLT_FLOAT_VEC4);
    p.uniforms << ProgramInfo::Param("lightIntensity", "lightIntensity", QString(), GLT_FLOAT_VEC3);
    p.uniforms << ProgramInfo::Param("ambient", "ka", QString(), GLT_FLOAT_VEC3);
    p.uniforms << ProgramInfo::Param("specular", "ks", QString(), GLT_FLOAT_VEC3);
    p.uniforms << ProgramInfo::Param("shininess", "shininess", QString(), GLT_FLOAT);
    p.uniforms << ProgramInfo::Param("diffuse", "diffuseTexture", QString(), GLT_SAMPLER_2D);
    p.uniforms << ProgramInfo::Param("normalmap", "normalTexture", QString(), GLT_SAMPLER_2D);
    m_progs << p;

    p = ProgramInfo();
    p.commonTechniqueName = "PHONG"; // diffuse texture, specular texture, normalmap texture
    p.vertShader = "normaldiffusemap.vert";
    p.fragShader = "normaldiffusespecularmap.frag";
    p.attributes << ProgramInfo::Param("position", "vertexPosition", "POSITION", GLT_FLOAT_VEC3);
    p.attributes << ProgramInfo::Param("normal", "vertexNormal", "NORMAL", GLT_FLOAT_VEC3);
    p.attributes << ProgramInfo::Param("texcoord0", "vertexTexCoord", "TEXCOORD_0", GLT_FLOAT_VEC2);
    p.attributes << ProgramInfo::Param("tangent", "vertexTangent", "TANGENT", GLT_FLOAT_VEC3);
    p.uniforms << ProgramInfo::Param("projection", "projection", "PROJECTION", GLT_FLOAT_MAT4);
    p.uniforms << ProgramInfo::Param("modelView", "modelView", "MODELVIEW", GLT_FLOAT_MAT4);
    p.uniforms << ProgramInfo::Param("normalMatrix", "modelViewNormal", "MODELVIEWINVERSETRANSPOSE", GLT_FLOAT_MAT3);
    p.uniforms << ProgramInfo::Param("lightPosition", "lightPosition", QString(), GLT_FLOAT_VEC4);
    p.uniforms << ProgramInfo::Param("lightIntensity", "lightIntensity", QString(), GLT_FLOAT_VEC3);
    p.uniforms << ProgramInfo::Param("ambient", "ka", QString(), GLT_FLOAT_VEC3);
    p.uniforms << ProgramInfo::Param("shininess", "shininess", QString(), GLT_FLOAT);
    p.uniforms << ProgramInfo::Param("diffuse", "diffuseTexture", QString(), GLT_SAMPLER_2D);
    p.uniforms << ProgramInfo::Param("specular", "specularTexture", QString(), GLT_SAMPLER_2D);
    p.uniforms << ProgramInfo::Param("normalmap", "normalTexture", QString(), GLT_SAMPLER_2D);
    m_progs << p;

    m_subst_es2 << qMakePair(QByteArrayLiteral("$VERSION"), QByteArray());
    m_subst_es2 << qMakePair(QByteArrayLiteral("$ATTRIBUTE"), QByteArrayLiteral("attribute"));
    m_subst_es2 << qMakePair(QByteArrayLiteral("$VVARYING"), QByteArrayLiteral("varying"));
    m_subst_es2 << qMakePair(QByteArrayLiteral("$FVARYING"), QByteArrayLiteral("varying"));
    m_subst_es2 << qMakePair(QByteArrayLiteral("$TEXTURE2D"), QByteArrayLiteral("texture2D"));
    m_subst_es2 << qMakePair(QByteArrayLiteral("$DECL_FRAGCOLOR"), QByteArray());
    m_subst_es2 << qMakePair(QByteArrayLiteral("$FRAGCOLOR"), QByteArrayLiteral("gl_FragColor"));
    m_subst_es2 << qMakePair(QByteArrayLiteral("$HIGHP"), QByteArrayLiteral("highp"));

    m_subst_core << qMakePair(QByteArrayLiteral("$VERSION"), QByteArrayLiteral("#version 150 core"));
    m_subst_core << qMakePair(QByteArrayLiteral("$ATTRIBUTE"), QByteArrayLiteral("in"));
    m_subst_core << qMakePair(QByteArrayLiteral("$VVARYING"), QByteArrayLiteral("out"));
    m_subst_core << qMakePair(QByteArrayLiteral("$FVARYING"), QByteArrayLiteral("in"));
    m_subst_core << qMakePair(QByteArrayLiteral("$TEXTURE2D"), QByteArrayLiteral("texture"));
    m_subst_core << qMakePair(QByteArrayLiteral("$DECL_FRAGCOLOR"), QByteArrayLiteral("out vec4 fragColor;"));
    m_subst_core << qMakePair(QByteArrayLiteral("$FRAGCOLOR"), QByteArrayLiteral("fragColor"));
    m_subst_core << qMakePair(QByteArrayLiteral("$HIGHP "), QByteArray());
}

GltfExporter::ProgramInfo *GltfExporter::chooseProgram(uint materialIndex)
{
    Importer::MaterialInfo matInfo = m_importer->materialInfo(materialIndex);
    const bool hasNormalTexture = matInfo.m_textures.contains("normal");
    const bool hasSpecularTexture = matInfo.m_textures.contains("specular");
    const bool hasDiffuseTexture = matInfo.m_textures.contains("diffuse");

    if (hasNormalTexture && !m_importer->allMeshesForMaterialHaveTangents(materialIndex))
        qWarning() << "WARNING: Tangent vectors not exported while the material requires it. (hint: try -t)";

    if (hasNormalTexture && hasSpecularTexture && hasDiffuseTexture) {
        if (opts.showLog)
            qDebug() << "Using program taking diffuse, specular, normal textures";
        return &m_progs[4];
    }

    if (hasNormalTexture && hasDiffuseTexture) {
        if (opts.showLog)
            qDebug() << "Using program taking diffuse, normal textures";
        return &m_progs[3];
    }

    if (hasSpecularTexture && hasDiffuseTexture) {
        if (opts.showLog)
            qDebug() << "Using program taking diffuse, specular textures";
        return &m_progs[2];
    }

    if (hasDiffuseTexture) {
        if (opts.showLog)
            qDebug() << "Using program taking diffuse texture";
        return &m_progs[1];
    }

    if (opts.showLog)
        qDebug() << "Using program without textures";
    return &m_progs[0];
}

QString GltfExporter::exportNode(const Importer::Node *n, QJsonObject &nodes)
{
    QJsonObject node;
    node["name"] = n->name;
    QJsonArray children;
    for (const Importer::Node *c : n->children) {
        if (nodeIsUseful(c))
            children << exportNode(c, nodes);
    }
    node["children"] = children;
    QJsonArray matrix;
    const float *mtxp = n->transformation.constData();
    for (int j = 0; j < 16; ++j)
        matrix.append(*mtxp++);
    node["matrix"] = matrix;
    QJsonArray meshList;
    for (int j = 0; j < n->meshes.count(); ++j)
        meshList.append(m_importer->meshInfo(n->meshes[j]).name);
    if (!meshList.isEmpty()) {
        node["meshes"] = meshList;
    } else {
        QHash<QString, Importer::CameraInfo> cam = m_importer->cameraInfo();
        if (cam.contains(n->name))
            node["camera"] = cam[n->name].name;
    }

    nodes[n->uniqueName] = node;
    return n->uniqueName;
}

static inline QJsonArray col2jsvec(const QVector<float> &color, bool alpha = false)
{
    QJsonArray arr;
    arr << color[0] << color[1] << color[2];
    if (alpha)
        arr << color[3];
    return arr;
}

static inline QJsonArray vec2jsvec(const QVector<float> &v)
{
    QJsonArray arr;
    for (int i = 0; i < v.count(); ++i)
        arr << v[i];
    return arr;
}

static inline void promoteColorsToRGBA(QJsonObject *obj)
{
    QJsonObject::iterator it = obj->begin(), itEnd = obj->end();
    while (it != itEnd) {
        QJsonArray arr = it.value().toArray();
        if (arr.count() == 3) {
            const QString key = it.key();
            if (key == QStringLiteral("ambient")
                    || key == QStringLiteral("diffuse")
                    || key == QStringLiteral("specular")) {
                arr.append(1);
                *it = arr;
            }
        }
        ++it;
    }
}

void GltfExporter::exportMaterials(QJsonObject &materials, QHash<QString, QString> *textureNameMap)
{
    for (uint i = 0; i < m_importer->materialCount(); ++i) {
        Importer::MaterialInfo matInfo = m_importer->materialInfo(i);
        QJsonObject material;
        material["name"] = matInfo.originalName;

        bool opaque = true;
        QJsonObject vals;
        for (QHash<QByteArray, QString>::const_iterator it = matInfo.m_textures.constBegin(); it != matInfo.m_textures.constEnd(); ++it) {
            if (!textureNameMap->contains(it.value()))
                textureNameMap->insert(it.value(), newTextureName());
            QByteArray key = it.key();
            if (key == QByteArrayLiteral("normal")) // avoid clashing with the vertex normals
                key = QByteArrayLiteral("normalmap");
            // alpha is supported for diffuse textures, but have to check the image data to decide if blending is needed
            if (key == QByteArrayLiteral("diffuse")) {
                QString imgFn = opts.outDir + it.value();
                if (m_imageHasAlpha.contains(imgFn)) {
                    if (m_imageHasAlpha[imgFn])
                        opaque = false;
                } else {
#ifdef HAS_QIMAGE
                    QImage img(imgFn);
                    if (!img.isNull()) {
                        if (img.hasAlphaChannel()) {
                            for (int y = 0; opaque && y < img.height(); ++y)
                                for (int x = 0; opaque && x < img.width(); ++x)
                                    if (qAlpha(img.pixel(x, y)) < 255)
                                        opaque = false;
                        }
                        m_imageHasAlpha[imgFn] = !opaque;
                    } else {
                        qWarning() << "WARNING: Cannot determine presence of alpha for" << imgFn;
                    }
#else
                    qWarning() << "WARNING: No image support, assuming all textures are opaque";
#endif
                }
            }
            vals[key] = textureNameMap->value(it.value());
        }
        for (QHash<QByteArray, float>::const_iterator it = matInfo.m_values.constBegin();
             it != matInfo.m_values.constEnd(); ++it) {
            if (vals.contains(it.key()))
                continue;
            vals[it.key()] = it.value();
        }
        for (QHash<QByteArray, QVector<float> >::const_iterator it = matInfo.m_colors.constBegin();
             it != matInfo.m_colors.constEnd(); ++it) {
            if (vals.contains(it.key()))
                continue;
            // alpha is supported for the diffuse color. < 1 will enable blending.
            const bool alpha = it.key() == QByteArrayLiteral("diffuse");
            if (alpha && it.value()[3] < 1.0f)
                opaque = false;
            vals[it.key()] = col2jsvec(it.value(), alpha);
        }
        if (opts.shaders)
            material["values"] = vals;

        ProgramInfo *prog = chooseProgram(i);
        TechniqueInfo techniqueInfo;
        bool needsNewTechnique = true;
        for (int j = 0; j < m_techniques.count(); ++j) {
            if (m_techniques[j].prog == prog) {
                techniqueInfo = m_techniques[j];
                needsNewTechnique = opaque != techniqueInfo.opaque;
            }
            if (!needsNewTechnique)
                break;
        }
        if (needsNewTechnique) {
            QString techniqueName = newTechniqueName();
            techniqueInfo = TechniqueInfo(techniqueName, opaque, prog);
            m_techniques.append(techniqueInfo);
            m_usedPrograms.insert(prog);
        }

        if (opts.shaders) {
            if (opts.showLog)
                qDebug().noquote() << "Material #" << i << "->" << techniqueInfo.name;

            material["technique"] = techniqueInfo.name;
            if (opts.genCore) {
                material["techniqueCore"] = techniqueInfo.coreName;
                material["techniqueGL2"] = techniqueInfo.gl2Name;
            }
        }

        if (opts.commonMat) {
            // The built-in shaders we output are of little use in practice.
            // Ideally we want Qt3D's own standard materials in order to have our
            // models participate in lighting for example. To achieve this, output
            // a KHR_materials_common block which Qt3D's loader will recognize and
            // prefer over the shader-based techniques.
            if (!prog->commonTechniqueName.isEmpty()) {
                QJsonObject commonMat;
                commonMat["technique"] = prog->commonTechniqueName;
                // Set the values as-is. "normalmap" is our own extension, not in the spec.
                // However, RGB colors have to be promoted to RGBA since the spec uses
                // vec4, and all types are pre-defined for common material values.
                promoteColorsToRGBA(&vals);
                commonMat["values"] = vals;
                if (!opaque)
                    commonMat["transparent"] = true;
                QJsonObject extensions;
                extensions["KHR_materials_common"] = commonMat;
                material["extensions"] = extensions;
            }
        }

        materials[matInfo.name] = material;
    }
}

void GltfExporter::writeShader(const QString &src, const QString &dst, const QVector<QPair<QByteArray, QByteArray> > &substTab)
{
    for (const Shader shader : shaders) {
        QByteArray name = src.toUtf8();
        if (!qstrcmp(shader.name, name.constData())) {
            QString outfn = opts.outDir + dst;
            QFile outf(outfn);
            if (outf.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
                m_files.insert(QFileInfo(outf.fileName()).fileName());
                if (opts.showLog)
                    qDebug() << "Writing" << outfn;
                const auto lines = QString::fromUtf8(shader.text).split('\n');
                for (QString line : lines) {
                    for (const auto &subst : substTab)
                        line.replace(subst.first, subst.second);
                    line += QStringLiteral("\n");
                    outf.write(line.toUtf8());
                }
            }
            return;
        }
    }
    qWarning() << "ERROR: No shader found for" << src;
}

void GltfExporter::exportParameter(QJsonObject &dst, const QVector<ProgramInfo::Param> &params)
{
    for (const ProgramInfo::Param &param : params) {
        QJsonObject parameter;
        parameter["type"] = int(param.type);
        if (!param.semantic.isEmpty())
            parameter["semantic"] = param.semantic;
        if (param.name == QStringLiteral("lightIntensity"))
            parameter["value"] = QJsonArray() << 1 << 1 << 1;
        if (param.name == QStringLiteral("lightPosition"))
            parameter["value"] = QJsonArray() << 0 << 0 << 0 << 1;
        dst[param.name] = parameter;
    }
}

namespace {
struct ProgramNames
{
    QString name;
    QString coreName;
};
}

void GltfExporter::exportTechniques(QJsonObject &obj, const QString &basename)
{
    if (!opts.shaders)
        return;

    QJsonObject shaders;
    QHash<QString, QString> shaderMap;
    for (ProgramInfo *prog : qAsConst(m_usedPrograms)) {
        QString newName;
        if (!shaderMap.contains(prog->vertShader)) {
            QJsonObject vertexShader;
            vertexShader["type"] = 35633;
            if (newName.isEmpty())
                newName = newShaderName();
            QString key = basename + QStringLiteral("_") + newName + QStringLiteral("_v");
            QString fn = QString(QStringLiteral("%1.vert")).arg(key);
            vertexShader["uri"] = fn;
            writeShader(prog->vertShader, fn, m_subst_es2);
            if (opts.genCore) {
                QJsonObject coreVertexShader;
                QString coreKey = QString(QStringLiteral("%1_core").arg(key));
                fn = QString(QStringLiteral("%1.vert")).arg(coreKey);
                coreVertexShader["type"] = 35633;
                coreVertexShader["uri"] = fn;
                writeShader(prog->vertShader, fn, m_subst_core);
                shaders[coreKey] = coreVertexShader;
                shaderMap.insert(QString(prog->vertShader + QStringLiteral("_core")), coreKey);
            }
            shaders[key] = vertexShader;
            shaderMap.insert(prog->vertShader, key);
        }
        if (!shaderMap.contains(prog->fragShader)) {
            QJsonObject fragmentShader;
            fragmentShader["type"] = 35632;
            if (newName.isEmpty())
                newName = newShaderName();
            QString key = basename + QStringLiteral("_") + newName + QStringLiteral("_f");
            QString fn = QString(QStringLiteral("%1.frag")).arg(key);
            fragmentShader["uri"] = fn;
            writeShader(prog->fragShader, fn, m_subst_es2);
            if (opts.genCore) {
                QJsonObject coreFragmentShader;
                QString coreKey = QString(QStringLiteral("%1_core").arg(key));
                fn = QString(QStringLiteral("%1.frag")).arg(coreKey);
                coreFragmentShader["type"] = 35632;
                coreFragmentShader["uri"] = fn;
                writeShader(prog->fragShader, fn, m_subst_core);
                shaders[coreKey] = coreFragmentShader;
                shaderMap.insert(QString(prog->fragShader + QStringLiteral("_core")), coreKey);
            }
            shaders[key] = fragmentShader;
            shaderMap.insert(prog->fragShader, key);
        }
    }
    obj["shaders"] = shaders;

    QJsonObject programs;
    QHash<const ProgramInfo *, ProgramNames> programMap;
    for (const ProgramInfo *prog : qAsConst(m_usedPrograms)) {
        QJsonObject program;
        program["vertexShader"] = shaderMap[prog->vertShader];
        program["fragmentShader"] = shaderMap[prog->fragShader];
        QJsonArray attrs;
        for (const ProgramInfo::Param &param : prog->attributes) {
            attrs << param.nameInShader;
        }
        program["attributes"] = attrs;
        QString programName = newProgramName();
        programMap[prog].name = programName;
        programs[programMap[prog].name] = program;
        if (opts.genCore) {
            program["vertexShader"] = shaderMap[QString(prog->vertShader + QLatin1String("_core"))];
            program["fragmentShader"] = shaderMap[QString(prog->fragShader + QLatin1String("_core"))];
            QJsonArray attrs;
            for (const ProgramInfo::Param &param : prog->attributes) {
                attrs << param.nameInShader;
            }
            program["attributes"] = attrs;
            programMap[prog].coreName = programName + QLatin1String("_core");
            programs[programMap[prog].coreName] = program;
        }
    }
    obj["programs"] = programs;

    QJsonObject techniques;
    for (const TechniqueInfo &techniqueInfo : qAsConst(m_techniques)) {
        QJsonObject technique;
        QJsonObject parameters;
        const ProgramInfo *prog = techniqueInfo.prog;
        exportParameter(parameters, prog->attributes);
        exportParameter(parameters, prog->uniforms);
        technique["parameters"] = parameters;
        technique["program"] = programMap[prog].name;
        QJsonObject progAttrs;
        for (const ProgramInfo::Param &param : prog->attributes) {
            progAttrs[param.nameInShader] = param.name;
        }
        technique["attributes"] = progAttrs;
        QJsonObject progUniforms;
        for (const ProgramInfo::Param &param : prog->uniforms) {
            progUniforms[param.nameInShader] = param.name;
        }
        technique["uniforms"] = progUniforms;
        QJsonObject states;
        QJsonArray enabledStates;
        enabledStates << GLT_DEPTH_TEST << GLT_CULL_FACE;
        if (!techniqueInfo.opaque) {
            enabledStates << GLT_BLEND;
            QJsonObject funcs;
            // GL_ONE, GL_ONE_MINUS_SRC_ALPHA
            funcs["blendFuncSeparate"] = QJsonArray() << 1 << 771 << 1 << 771;
            states["functions"] = funcs;
        }
        states["enable"] = enabledStates;
        technique["states"] = states;
        techniques[techniqueInfo.name] = technique;

        if (opts.genCore) {
            //GL2 (same as ES2)
            techniques[techniqueInfo.gl2Name] = technique;

            //Core
            technique["program"] = programMap[prog].coreName;
            techniques[techniqueInfo.coreName] = technique;
        }
    }
    obj["techniques"] = techniques;
}

void GltfExporter::exportAnimations(QJsonObject &obj,
                                    QVector<Importer::BufferInfo> &bufList,
                                    QVector<Importer::MeshInfo::BufferView> &bvList,
                                    QVector<Importer::MeshInfo::Accessor> &accList)
{
    const auto animationInfos = m_importer->animations();
    if (animationInfos.empty()) {
        obj["animations"] = QJsonObject();
        return;
    }

    QString bvName = newBufferViewName();
    QByteArray extraData;

    int sz = 0;
    for (const Importer::AnimationInfo &ai : animationInfos)
        sz += ai.keyFrames.count() * (1 + 3 + 4 + 3) * sizeof(float);
    extraData.resize(sz);

    float *base = reinterpret_cast<float *>(extraData.data());
    float *p = base;

    QJsonObject animations;
    for (const Importer::AnimationInfo &ai : animationInfos) {
        QJsonObject animation;
        animation["name"] = ai.name;
        animation["count"] = ai.keyFrames.count();
        QJsonObject samplers;
        QJsonArray channels;

        if (ai.hasTranslation) {
            QJsonObject sampler;
            sampler["input"] = QStringLiteral("TIME");
            sampler["interpolation"] = QStringLiteral("LINEAR");
            sampler["output"] = QStringLiteral("translation");
            samplers["sampler_translation"] = sampler;
            QJsonObject channel;
            channel["sampler"] = QStringLiteral("sampler_translation");
            QJsonObject target;
            target["id"] = ai.targetNode;
            target["path"] = QStringLiteral("translation");
            channel["target"] = target;
            channels << channel;
        }
        if (ai.hasRotation) {
            QJsonObject sampler;
            sampler["input"] = QStringLiteral("TIME");
            sampler["interpolation"] = QStringLiteral("LINEAR");
            sampler["output"] = QStringLiteral("rotation");
            samplers["sampler_rotation"] = sampler;
            QJsonObject channel;
            channel["sampler"] = QStringLiteral("sampler_rotation");
            QJsonObject target;
            target["id"] = ai.targetNode;
            target["path"] = QStringLiteral("rotation");
            channel["target"] = target;
            channels << channel;
        }
        if (ai.hasScale) {
            QJsonObject sampler;
            sampler["input"] = QStringLiteral("TIME");
            sampler["interpolation"] = QStringLiteral("LINEAR");
            sampler["output"] = QStringLiteral("scale");
            samplers["sampler_scale"] = sampler;
            QJsonObject channel;
            channel["sampler"] = QStringLiteral("sampler_scale");
            QJsonObject target;
            target["id"] = ai.targetNode;
            target["path"] = QStringLiteral("scale");
            channel["target"] = target;
            channels << channel;
        }

        animation["samplers"] = samplers;
        animation["channels"] = channels;
        QJsonObject parameters;

        // Multiple animations sharing the same data should ideally use the
        // same accessors. This we unfortunately cannot do due to assimp's/our
        // own data structures so everything will get its own accessor and data
        // for now.

        Importer::MeshInfo::Accessor acc;
        acc.name = newAccessorName();
        acc.bufferView = bvName;
        acc.count = ai.keyFrames.count();
        acc.componentType = GLT_FLOAT;
        acc.type = QStringLiteral("SCALAR");
        acc.offset = uint((p - base) * sizeof(float));
        for (const Importer::KeyFrame &kf : ai.keyFrames)
            *p++ = kf.t;
        parameters["TIME"] = acc.name;
        accList << acc;

        if (ai.hasTranslation) {
            acc.name = newAccessorName();
            acc.componentType = GLT_FLOAT;
            acc.type = QStringLiteral("VEC3");
            acc.offset = uint((p - base) * sizeof(float));
            QVector<float> lastV;
            for (const Importer::KeyFrame &kf : ai.keyFrames) {
                const QVector<float> *v = kf.transValid ? &kf.trans : &lastV;
                *p++ = v->at(0);
                *p++ = v->at(1);
                *p++ = v->at(2);
                if (kf.transValid)
                    lastV = *v;
            }
            parameters["translation"] = acc.name;
            accList << acc;
        }
        if (ai.hasRotation) {
            acc.name = newAccessorName();
            acc.componentType = GLT_FLOAT;
            acc.type = QStringLiteral("VEC4");
            acc.offset = uint((p - base) * sizeof(float));
            QVector<float> lastV;
            for (const Importer::KeyFrame &kf : ai.keyFrames) {
                const QVector<float> *v = kf.rotValid ? &kf.rot : &lastV;
                *p++ = v->at(1); // x
                *p++ = v->at(2); // y
                *p++ = v->at(3); // z
                *p++ = v->at(0); // w
                if (kf.rotValid)
                    lastV = *v;
            }
            parameters["rotation"] = acc.name;
            accList << acc;
        }
        if (ai.hasScale) {
            acc.name = newAccessorName();
            acc.componentType = GLT_FLOAT;
            acc.type = QStringLiteral("VEC3");
            acc.offset = uint((p - base) * sizeof(float));
            QVector<float> lastV;
            for (const Importer::KeyFrame &kf : ai.keyFrames) {
                const QVector<float> *v = kf.scaleValid ? &kf.scale : &lastV;
                *p++ = v->at(0);
                *p++ = v->at(1);
                *p++ = v->at(2);
                if (kf.scaleValid)
                    lastV = *v;
            }
            parameters["scale"] = acc.name;
            accList << acc;
        }
        animation["parameters"] = parameters;

        animations[newAnimationName()] = animation;
    }
    obj["animations"] = animations;

    // Now all the key frame data is in extraData. Append it to the first buffer
    // and create a single buffer view for it.
    if (!extraData.isEmpty()) {
        if (bufList.isEmpty()) {
            Importer::BufferInfo b;
            b.name = QStringLiteral("buf");
            bufList << b;
        }
        Importer::BufferInfo &buf(bufList[0]);
        Importer::MeshInfo::BufferView bv;
        bv.name = bvName;
        bv.offset = buf.data.size();
        bv.length = uint((p - base) * sizeof(float));
        bv.componentType = GLT_FLOAT;
        bvList << bv;
        extraData.resize(bv.length);
        buf.data += extraData;
        if (opts.showLog)
            qDebug().noquote() << "Animation data in buffer uses" << extraData.size() << "bytes";
    }
}

void GltfExporter::save(const QString &inputFilename)
{
    if (opts.showLog)
        qDebug() << "Exporting";

    m_files.clear();
    m_techniques.clear();
    m_usedPrograms.clear();

    QFile f;
    QString basename = QFileInfo(inputFilename).baseName();
    QString bufNameTempl = basename + QStringLiteral("_%1.bin");

    copyExternalTextures(inputFilename);
    exportEmbeddedTextures();
    compressTextures();

    m_obj = QJsonObject();

    QVector<Importer::BufferInfo> bufList = m_importer->buffers();
    QVector<Importer::MeshInfo::BufferView> bvList = m_importer->bufferViews();
    QVector<Importer::MeshInfo::Accessor> accList = m_importer->accessors();

    // Animations add data to the buffer so process them first.
    exportAnimations(m_obj, bufList, bvList, accList);

    QJsonObject asset;
    asset["generator"] = QString(QStringLiteral("qgltf %1")).arg(QCoreApplication::applicationVersion());
    asset["version"] = QStringLiteral("1.0");
    asset["premultipliedAlpha"] = true;
    m_obj["asset"] = asset;

    for (int i = 0; i < bufList.count(); ++i) {
        QString bufName = bufNameTempl.arg(i + 1);
        f.setFileName(opts.outDir + bufName);
        if (opts.showLog)
            qDebug().noquote() << (opts.compress ? "Writing (compressed)" : "Writing") << (opts.outDir + bufName);
        if (f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
            m_files.insert(QFileInfo(f.fileName()).fileName());
            QByteArray data = bufList[i].data;
            if (opts.compress)
                data = qCompress(data);
            f.write(data);
            f.close();
        }
    }

    QJsonObject buffers;
    for (int i = 0; i < bufList.count(); ++i) {
        QJsonObject buffer;
        buffer["byteLength"] = bufList[i].data.size();
        buffer["type"] = QStringLiteral("arraybuffer");
        buffer["uri"] = bufNameTempl.arg(i + 1);
        if (opts.compress)
            buffer["compression"] = QStringLiteral("Qt");
        buffers[bufList[i].name] = buffer;
    }
    m_obj["buffers"] = buffers;

    QJsonObject bufferViews;
    for (const Importer::MeshInfo::BufferView &bv : qAsConst(bvList)) {
        QJsonObject bufferView;
        bufferView["buffer"] = bufList[bv.bufIndex].name;
        bufferView["byteLength"] = int(bv.length);
        bufferView["byteOffset"] = int(bv.offset);
        if (bv.target)
            bufferView["target"] = int(bv.target);
        bufferViews[bv.name] = bufferView;
    }
    m_obj["bufferViews"] = bufferViews;

    QJsonObject accessors;
    for (const Importer::MeshInfo::Accessor &acc : qAsConst(accList)) {
        QJsonObject accessor;
        accessor["bufferView"] = acc.bufferView;
        accessor["byteOffset"] = int(acc.offset);
        accessor["byteStride"] = int(acc.stride);
        accessor["count"] = int(acc.count);
        accessor["componentType"] = int(acc.componentType);
        accessor["type"] = acc.type;
        if (!acc.minVal.isEmpty() && !acc.maxVal.isEmpty()) {
            accessor["min"] = vec2jsvec(acc.minVal);
            accessor["max"] = vec2jsvec(acc.maxVal);
        }
        accessors[acc.name] = accessor;
    }
    m_obj["accessors"] = accessors;

    QJsonObject meshes;
    for (uint i = 0; i < m_importer->meshCount(); ++i) {
        const Importer::MeshInfo meshInfo = m_importer->meshInfo(i);
        QJsonObject mesh;
        mesh["name"] = meshInfo.originalName;
        QJsonArray prims;
        QJsonObject prim;
        prim["mode"] = 4; // triangles
        QJsonObject attrs;
        for (const Importer::MeshInfo::Accessor &acc : meshInfo.accessors) {
            if (acc.usage != QStringLiteral("INDEX"))
                attrs[acc.usage] = acc.name;
            else
                prim["indices"] = acc.name;
        }
        prim["attributes"] = attrs;
        prim["material"] = m_importer->materialInfo(meshInfo.materialIndex).name;
        prims.append(prim);
        mesh["primitives"] = prims;
        meshes[meshInfo.name] = mesh;
    }
    m_obj["meshes"] = meshes;

    QJsonObject cameras;
    const auto cameraInfos = m_importer->cameraInfo();
    for (const Importer::CameraInfo &camInfo : cameraInfos) {
        QJsonObject camera;
        QJsonObject persp;
        persp["aspect_ratio"] = camInfo.aspectRatio;
        persp["yfov"] = camInfo.yfov;
        persp["znear"] = camInfo.znear;
        persp["zfar"] = camInfo.zfar;
        camera["perspective"] = persp;
        camera["type"] = QStringLiteral("perspective");
        cameras[camInfo.name] = camera;
    }
    m_obj["cameras"] = cameras;

    QJsonArray sceneNodes;
    QJsonObject nodes;
    for (const Importer::Node *n : qAsConst(m_importer->rootNode()->children)) {
        if (nodeIsUseful(n))
            sceneNodes << exportNode(n, nodes);
    }
    m_obj["nodes"] = nodes;

    QJsonObject scenes;
    QJsonObject defaultScene;
    defaultScene["nodes"] = sceneNodes;
    scenes["defaultScene"] = defaultScene;
    m_obj["scenes"] = scenes;
    m_obj["scene"] = QStringLiteral("defaultScene");

    QJsonObject materials;
    QHash<QString, QString> textureNameMap;
    exportMaterials(materials, &textureNameMap);
    m_obj["materials"] = materials;

    QJsonObject textures;
    QHash<QString, QString> imageMap; // uri -> key
    for (QHash<QString, QString>::const_iterator it = textureNameMap.constBegin(); it != textureNameMap.constEnd(); ++it) {
        QJsonObject texture;
        if (!imageMap.contains(it.key()))
            imageMap[it.key()] = newImageName();
        texture["source"] = imageMap[it.key()];
        texture["format"] = 0x1908; // RGBA
        const bool compressed = m_compressedTextures.contains(it.key());
        texture["internalFormat"] = !compressed ? 0x1908 : 0x8D64; // RGBA / ETC1
        texture["sampler"] = !compressed ? QStringLiteral("sampler_mip_rep") : QStringLiteral("sampler_nonmip_rep");
        texture["target"] = 3553; // TEXTURE_2D
        texture["type"] = 5121; // UNSIGNED_BYTE
        textures[it.value()] = texture;
    }
    m_obj["textures"] = textures;

    QJsonObject images;
    for (QHash<QString, QString>::const_iterator it = imageMap.constBegin(); it != imageMap.constEnd(); ++it) {
        QJsonObject image;
        image["uri"] = m_compressedTextures.contains(it.key()) ? m_compressedTextures[it.key()] : it.key();
        images[it.value()] = image;
    }
    m_obj["images"] = images;

    QJsonObject samplers;
    QJsonObject sampler;
    sampler["magFilter"] = 9729; // LINEAR
    sampler["minFilter"] = 9987; // LINEAR_MIPMAP_LINEAR
    sampler["wrapS"] = 10497; // REPEAT
    sampler["wrapT"] = 10497;
    samplers["sampler_mip_rep"] = sampler;
    // Compressed textures may not support mipmapping with GLES.
    if (!m_compressedTextures.isEmpty()) {
        sampler["minFilter"] = 9729; // LINEAR
        samplers["sampler_nonmip_rep"] = sampler;
    }
    m_obj["samplers"] = samplers;

    exportTechniques(m_obj, basename);

    m_doc.setObject(m_obj);

    QString gltfName = opts.outDir + basename + QStringLiteral(".qgltf");
    f.setFileName(gltfName);

#ifndef QT_BOOTSTRAPPED
    if (opts.showLog)
        qDebug().noquote() << (opts.genBin ? "Writing (binary JSON)" : "Writing") << gltfName;

    const QIODevice::OpenMode openMode = opts.genBin
            ? (QIODevice::WriteOnly | QIODevice::Truncate)
            : (QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text);
QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED
    const QByteArray json = opts.genBin
            ? m_doc.toBinaryData()
            : m_doc.toJson(opts.compact ? QJsonDocument::Compact : QJsonDocument::Indented);
QT_WARNING_POP
#else
    if (opts.showLog)
        qDebug().noquote() << "Writing" << gltfName;

    const QIODevice::OpenMode openMode
            = QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text;
    const QByteArray json
            = m_doc.toJson(opts.compact ? QJsonDocument::Compact : QJsonDocument::Indented);
#endif

    if (f.open(openMode)) {
        m_files.insert(QFileInfo(f.fileName()).fileName());
        f.write(json);
        f.close();
    }

    QString qrcName = opts.outDir + basename + QStringLiteral(".qrc");
    f.setFileName(qrcName);
    if (opts.showLog)
        qDebug().noquote() << "Writing" << qrcName;
    if (f.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
        QByteArray pre = "<RCC><qresource prefix=\"/models\">\n";
        QByteArray post = "</qresource></RCC>\n";
        f.write(pre);
        for (const QString &file : qAsConst(m_files)) {
            QString line = QString(QStringLiteral("  <file>%1</file>\n")).arg(file);
            f.write(line.toUtf8());
        }
        f.write(post);
        f.close();
    }

    if (opts.showLog)
        qDebug() << "Done\n";
}

static const char *description =
        "qgltf uses Assimp to import a variety of 3D model formats "
        "and export it into fast-to-load, optimized glTF "
        "assets embedded into Qt resource files.\n\n"
        "Note: this tool should typically not be invoked directly. Instead, "
        "let qmake manage it based on QT3D_MODELS in the .pro file.\n\n"
        "For standard Qt 3D usage the recommended options are -b -S.";

int main(int argc, char **argv)
{
    QCoreApplication app(argc, argv);
    app.setApplicationVersion(QStringLiteral("0.2"));
    app.setApplicationName(QStringLiteral("Qt glTF converter"));

    QCommandLineParser cmdLine;
    cmdLine.addHelpOption();
    cmdLine.addVersionOption();
    cmdLine.setApplicationDescription(QString::fromUtf8(description));
    QCommandLineOption outDirOpt(QStringLiteral("d"), QStringLiteral("Place all output data into <dir>"), QStringLiteral("dir"));
    cmdLine.addOption(outDirOpt);
#ifndef QT_BOOTSTRAPPED
    QCommandLineOption binOpt(QStringLiteral("b"), QStringLiteral("Store binary JSON data in the .qgltf file"));
    cmdLine.addOption(binOpt);
#endif
    QCommandLineOption compactOpt(QStringLiteral("m"), QStringLiteral("Store compact JSON in the .qgltf file"));
    cmdLine.addOption(compactOpt);
    QCommandLineOption compOpt(QStringLiteral("c"), QStringLiteral("qCompress() vertex/index data in the .bin file"));
    cmdLine.addOption(compOpt);
    QCommandLineOption tangentOpt(QStringLiteral("t"), QStringLiteral("Generate tangent vectors"));
    cmdLine.addOption(tangentOpt);
    QCommandLineOption nonInterleavedOpt(QStringLiteral("n"), QStringLiteral("Use non-interleaved buffer layout"));
    cmdLine.addOption(nonInterleavedOpt);
    QCommandLineOption scaleOpt(QStringLiteral("e"), QStringLiteral("Scale vertices by the float scale factor <factor>"), QStringLiteral("factor"));
    cmdLine.addOption(scaleOpt);
    QCommandLineOption coreOpt(QStringLiteral("g"), QStringLiteral("Generate OpenGL 3.2+ core profile shaders too"));
    cmdLine.addOption(coreOpt);
    QCommandLineOption etc1Opt(QStringLiteral("1"), QStringLiteral("Generate ETC1 compressed textures by invoking etc1tool (PNG only)"));
    cmdLine.addOption(etc1Opt);
    QCommandLineOption noCommonMatOpt(QStringLiteral("T"), QStringLiteral("Do not generate KHR_materials_common block"));
    cmdLine.addOption(noCommonMatOpt);
    QCommandLineOption noShadersOpt(QStringLiteral("S"), QStringLiteral("Do not generate shaders/programs/techniques"));
    cmdLine.addOption(noShadersOpt);
    QCommandLineOption silentOpt(QStringLiteral("s"), QStringLiteral("Silence debug output"));
    cmdLine.addOption(silentOpt);
    cmdLine.process(app);
    opts.outDir = cmdLine.value(outDirOpt);
#ifndef QT_BOOTSTRAPPED
    opts.genBin = cmdLine.isSet(binOpt);
#endif
    opts.compact = cmdLine.isSet(compactOpt);
    opts.compress = cmdLine.isSet(compOpt);
    opts.genTangents = cmdLine.isSet(tangentOpt);
    opts.interleave = !cmdLine.isSet(nonInterleavedOpt);
    opts.scale = 1;
    if (cmdLine.isSet(scaleOpt)) {
        bool ok = false;
        float v;
        v = cmdLine.value(scaleOpt).toFloat(&ok);
        if (ok)
            opts.scale = v;
    }
    opts.genCore = cmdLine.isSet(coreOpt);
    opts.texComp = cmdLine.isSet(etc1Opt) ? Options::ETC1 : Options::NoTextureCompression;
    opts.commonMat = !cmdLine.isSet(noCommonMatOpt);
    opts.shaders = !cmdLine.isSet(noShadersOpt);
    opts.showLog = !cmdLine.isSet(silentOpt);
    if (!opts.outDir.isEmpty()) {
        if (!opts.outDir.endsWith('/'))
            opts.outDir.append('/');
        QDir().mkpath(opts.outDir);
    }

    const auto fileNames = cmdLine.positionalArguments();
    if (fileNames.isEmpty())
        cmdLine.showHelp();

    AssimpImporter importer;
    GltfExporter exporter(&importer);
    for (const QString &fn : fileNames) {
        if (!importer.load(fn)) {
            qWarning() << "Failed to import" << fn;
            continue;
        }
        exporter.save(fn);
    }

    return 0;
}
