/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Quick 3D.
**
** $QT_BEGIN_LICENSE:GPL$
** 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 or (at your option) any later version
** approved by the KDE Free Qt Foundation. The licenses are as published by
** the Free Software Foundation and appearing in the file LICENSE.GPL3
** 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 "uipimporter.h"
#include "uipparser.h"
#include "uiaparser.h"
#include "uippresentation.h"
#include "datamodelparser.h"
#include "keyframegroupgenerator.h"
#include "uniqueidmapper.h"
#include <QtQuick3DAssetImport/private/qssgqmlutilities_p.h>

#include <QBuffer>
#include <QJsonDocument>
#include <QJsonObject>

QT_BEGIN_NAMESPACE

#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
// QTextStream functions are moved to a namespace in Qt6
using Qt::endl;
#endif

UipImporter::UipImporter()
{
    QFile optionFile(":/uipimporter/options.json");
    optionFile.open(QIODevice::ReadOnly);
    QByteArray options = optionFile.readAll();
    optionFile.close();
    auto optionsDocument = QJsonDocument::fromJson(options);
    m_options = optionsDocument.object().toVariantMap();
}

const QString UipImporter::name() const
{
    return QStringLiteral("uip");
}

const QStringList UipImporter::inputExtensions() const
{
    QStringList extensions;
    extensions.append(QStringLiteral("uia"));
    extensions.append(QStringLiteral("uip"));
    return extensions;
}

const QString UipImporter::outputExtension() const
{
    return QStringLiteral(".qml");
}

const QString UipImporter::type() const
{
    return QStringLiteral("Scene");
}

const QString UipImporter::typeDescription() const
{
    return QObject::tr("Qt 3D Studio Presentation");
}

const QVariantMap UipImporter::importOptions() const
{
    return m_options;
}

namespace  {
bool copyRecursively(const QString &sourceFolder, const QString &destFolder)
{
    bool success = false;
    QDir sourceDir(sourceFolder);

    if(!sourceDir.exists())
        return false;

    QDir destDir(destFolder);
    if(!destDir.exists())
        destDir.mkdir(destFolder);

    auto files = sourceDir.entryList(QDir::Files);
    for (const auto &file : files) {
        QString srcName = sourceFolder + QDir::separator() + file;
        QString destName = destFolder + QDir::separator() + file;
        success = QFile::copy(srcName, destName);
        if(!success)
            return false;
    }

    files = sourceDir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot);
    for (const auto &file : files) {
        QString srcName = sourceFolder + QDir::separator() + file;
        QString destName = destFolder + QDir::separator() + file;
        success = copyRecursively(srcName, destName);
        if(!success)
            return false;
    }

    return true;
}
}

const QString UipImporter::import(const QString &sourceFile, const QDir &savePath,
                                  const QVariantMap &options, QStringList *generatedFiles)
{
    // Reset UniqueIdMapper cache so different imports do not affect each other's ids
    UniqueIdMapper::instance()->reset();

    m_sourceFile = sourceFile;
    m_exportPath = savePath;
    m_options = options;

    processOptions(options);

    // Verify sourceFile and savePath
    QFileInfo source(sourceFile);
    if (!source.exists())
        return QStringLiteral("Source File: %s does not exist").arg(sourceFile);
    if (!savePath.exists())
        return QStringLiteral("Export Directory Invalid");

    QString uiaComponentName;
    QSize uiaComponentSize;

    // If sourceFile is a UIA file
    if (sourceFile.endsWith(QStringLiteral(".uia"), Qt::CaseInsensitive)) {
        auto uia = m_uiaParser.parse(sourceFile);
        uiaComponentName = uia.initialPresentationId;
        for (auto presentation : uia.presentations) {
            if (presentation.type == UiaParser::Uia::Presentation::Qml) {
                m_hasQMLSubPresentations = true;
                QFileInfo qmlFile(source.absolutePath() + QDir::separator() + presentation.source);
                if (!m_qmlDirs.contains(qmlFile.dir()))
                    m_qmlDirs.append(qmlFile.dir());
                generateQmlComponent(presentation.id, qmlFile.baseName());
            }
        }

        for (auto presentation : uia.presentations) {
            if (presentation.type == UiaParser::Uia::Presentation::Uip) {
                // UIP
                auto uip = m_uipParser.parse(source.absolutePath() + QDir::separator()
                                             + presentation.source, presentation.id);
                processUipPresentation(uip, savePath.absolutePath() + QDir::separator());
                if (presentation.id == uiaComponentName)
                    uiaComponentSize = QSize(uip->presentationWidth(), uip->presentationHeight());
            }
        }
        if (m_hasQMLSubPresentations) {
            // If there is any QML in the project at all, we have to copy the entire
            // qml folder over
            for (QDir dir : m_qmlDirs)
                copyRecursively(dir.absolutePath(), m_exportPath.absolutePath() + QDir::separator() + QStringLiteral("qml"));
        }

    } else if (sourceFile.endsWith(QStringLiteral(".uip"), Qt::CaseInsensitive)) {
        auto uip = m_uipParser.parse(sourceFile, QString());
        processUipPresentation(uip, savePath.absolutePath() + QDir::separator());
    }

    QString errorString;

    // Copy any resource files to export directory
    for (auto file : m_resourcesList) {
        QFileInfo sourceFile(source.absolutePath() + QDir::separator() + file);
        if (!sourceFile.exists()) {
            // Try again after stripping the parent directory
            sourceFile = QFileInfo(source.absolutePath() + QDir::separator() + QSSGQmlUtilities::stripParentDirectory(file));
            if (!sourceFile.exists()) {
                errorString += QStringLiteral("Resource file does not exist: ") + sourceFile.absoluteFilePath() + QChar('\n');
                continue;
            }
        }
        QFileInfo destFile(savePath.absoluteFilePath(QSSGQmlUtilities::stripParentDirectory(file)));
        QDir destDir(destFile.absolutePath());
        destDir.mkpath(".");

        if (QFile::copy(sourceFile.absoluteFilePath(), destFile.absoluteFilePath()))
            m_generatedFiles += destFile.absoluteFilePath();
    }

    // Generate UIA Component if we converted a uia
    if (m_createProjectWrapper && !uiaComponentName.isEmpty())
        generateApplicationComponent(QSSGQmlUtilities::qmlComponentName(uiaComponentName), uiaComponentSize);

    if (generatedFiles)
        generatedFiles = &m_generatedFiles;

    return errorString;
}

void UipImporter::processNode(GraphObject *object, QTextStream &output, int tabLevel, bool isInRootLevel, bool processSiblings)
{
    GraphObject *obj = object;
    while (obj) {
        if (obj->type() == GraphObject::Scene) {
            // Ignore Scene for now
            processNode(obj->firstChild(), output, tabLevel, isInRootLevel);
        } else if ( obj->type() == GraphObject::DefaultMaterial &&
                    obj->qmlId() == QStringLiteral("__Container")) {
            // UIP version > 5 which tries to be clever with reference materials
            // Instead of parsing these items as normal, instead we iterate the
            // materials container and generate new Components for each one and output them
            // to the "materials" folder
            GraphObject *materialObject = obj->firstChild();
            while(materialObject) {
                generateMaterialComponent(materialObject);
                materialObject = materialObject->nextSibling();
            }
        } else {
            // Output QML
            output << endl;
            obj->writeQmlHeader(output, tabLevel);
            obj->writeQmlProperties(output, tabLevel + 1, isInRootLevel);

            if (obj->type() != GraphObject::Component && obj->type() != GraphObject::Layer)
                processNode(obj->firstChild(), output, tabLevel + 1, isInRootLevel);

            if (obj->type() == GraphObject::Layer) {
//                // effects array
//                // get all children that are effects, and add their id's to effects: array
//                QString effects;
//                GraphObject *effectObject = obj->firstChild();
//                while (effectObject) {
//                    if (effectObject->type() == GraphObject::Effect)
//                        effects += effectObject->qmlId() + QStringLiteral(", ");
//                    effectObject = effectObject->nextSibling();
//                }
//                if (!effects.isEmpty()) {
//                    // remove final ", "
//                    effects.chop(2);
//                    output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("effects: [") << effects << QStringLiteral("]") << endl;
//                }


                // Check if layer has a sub-presentation set, in which case we can skip children processing and timeline generation
                auto layer = static_cast<LayerNode*>(obj);
                if (layer->m_sourcePath.isEmpty()) {
                    // Process children nodes
                    processNode(obj->firstChild(), output, tabLevel + 1, isInRootLevel);
//                    // Generate Animation Timeline
//                    generateAnimationTimeLine(obj, m_presentation->masterSlide(), output, tabLevel + 1);
//                    // Generate States from Slides
//                    generateStatesFromSlides(obj, m_presentation->masterSlide(), output, tabLevel + 1);
                }


            } else if (obj->type() == GraphObject::Model) {
                // materials array
                // get all children that are materials, and add their id's to materials: array
                QString materials;
                GraphObject *materialObject = obj->firstChild();
                while (materialObject) {
                    if (materialObject->type() == GraphObject::DefaultMaterial ||
                        materialObject->type() == GraphObject::CustomMaterial ||
                        materialObject->type() == GraphObject::ReferencedMaterial)
                        materials += materialObject->qmlId() + QStringLiteral(", ");
                    materialObject = materialObject->nextSibling();
                }
                if (!materials.isEmpty()) {
                    // remove final ", "
                    materials.chop(2);
                    output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("materials: [") << materials << QStringLiteral("]") << endl;
                }
            } else if (obj->type() == GraphObject::ReferencedMaterial) {
                m_referencedMaterials.append(static_cast<ReferencedMaterial *>(obj));
            } else if (obj->type() == GraphObject::Alias) {
                m_aliasNodes.append(static_cast<AliasNode*>(obj));
            } else if (obj->type() == GraphObject::Component) {
                m_componentNodes.append(static_cast<ComponentNode*>(obj));
            }

            checkForResourceFiles(obj);

            obj->writeQmlFooter(output, tabLevel);
        }
        if (processSiblings)
            obj = obj->nextSibling();
        else
            break;
    }
}

void UipImporter::checkForResourceFiles(GraphObject *object)
{
    if (!object)
        return;
    if (object->type() == GraphObject::Image) {
        Image *image = static_cast<Image*>(object);
        if (image->m_subPresentation.isEmpty() && !m_resourcesList.contains(image->m_sourcePath))
            m_resourcesList.append(image->m_sourcePath);
    } else if (object->type() == GraphObject::Model) {
        ModelNode *model = static_cast<ModelNode*>(object);
        QString meshLocation = model->m_mesh_unresolved;
        // Remove trailing # directive
        int hashLocation = meshLocation.indexOf("#");
        // if mesh source starts with #, it's a primitive, so skip
        if (hashLocation == 1)
            return;
        if (hashLocation != -1) {
            meshLocation.chop(meshLocation.length() - hashLocation);
        }
        if (!m_resourcesList.contains(meshLocation))
            m_resourcesList.append(meshLocation);
    } else if (object->type() == GraphObject::Effect) {
        // ### maybe remove #
        //EffectInstance *effect = static_cast<EffectInstance*>(object);
        //if (!m_resourcesList.contains(effect->m_effect_unresolved))
        //    m_resourcesList.append(effect->m_effect_unresolved);
    } else if (object->type() == GraphObject::CustomMaterial) {
        // ### maybe remove #
        // CustomMaterialInstance *material = static_cast<CustomMaterialInstance*>(object);
        //if (!m_resourcesList.contains(material->m_material_unresolved))
        //    m_resourcesList.append(material->m_material_unresolved);
    }
}

void UipImporter::generateMaterialComponent(GraphObject *object)
{
    QDir materialPath = m_exportPath.absolutePath() + QDir::separator() + QStringLiteral("materials");

    QString id = object->qmlId();
    if (id.startsWith("materials_"))
        id = id.remove(QStringLiteral("materials_"));
    // Default matterial has two //'s
    if (id.startsWith("_"))
        id.remove(0, 1);

    QString materialComponent = QSSGQmlUtilities::qmlComponentName(id);
    QString targetFile = materialPath.absolutePath() + QDir::separator() + materialComponent + QStringLiteral(".qml");
    QFile materialComponentFile(targetFile);

    if (m_generatedFiles.contains(targetFile)) {
        // if we already generated this material
        return;
    }

    if (!materialComponentFile.open(QIODevice::WriteOnly)) {
        qWarning() << "Could not write to file : " << materialComponentFile;
        return;
    }

    QTextStream output(&materialComponentFile);
    output << "import QtQuick3D 1.12" << endl;
    if (object->type() == GraphObject::ReferencedMaterial)
        output << "import \"./\"" << endl;
    processNode(object, output, 0, false, false);

    materialComponentFile.close();
    m_generatedFiles += targetFile;
}

void UipImporter::generateAliasComponent(GraphObject *reference)
{
    // create materials folder
    QDir aliasPath = m_exportPath.absolutePath() + QDir::separator() + QStringLiteral("aliases");

    QString aliasComponentName = QSSGQmlUtilities::qmlComponentName(reference->qmlId());
    QString targetFile = aliasPath.absolutePath() + QDir::separator() + aliasComponentName + QStringLiteral(".qml");
    QFile aliasComponentFile(targetFile);

    if (m_generatedFiles.contains(targetFile))
        return;

    if (!aliasComponentFile.open(QIODevice::WriteOnly)) {
        qWarning() << "Could not write to file: " << aliasComponentFile;
        return;
    }

    QTextStream output(&aliasComponentFile);
    output << "import QtQuick3D 1.12" << endl;
    processNode(reference, output, 0, false, false);

    aliasComponentFile.close();
    m_generatedFiles += targetFile;
}

namespace {

#if 0
QSet<GraphObject*> getSubtreeItems(GraphObject *node)
{
    QSet<GraphObject *> items;
    if (!node)
        return items;

    std::function<void(GraphObject *, QSet<GraphObject *> &)> treeWalker;
    treeWalker = [&treeWalker](GraphObject *obj, QSet<GraphObject *> &items) {
        while (obj) {
            items.insert(obj);
            treeWalker(obj->firstChild(), items);
            obj = obj->nextSibling();
        }
    };

    treeWalker(node->firstChild(), items);

    return items;
}
#endif

void generateTimelineAnimation(Slide *slide, int startFrame, int endFrame, int duration,
                               bool isRunning, QTextStream &output, int tabLevel,
                               const QString &componentName)
{
    QString looping = QStringLiteral("1");
    QString pingPong = QStringLiteral("false");
    if (slide->m_playMode == Slide::Looping) {
        looping = QStringLiteral("-1");
    } else if (slide->m_playMode == Slide::PingPong) {
        looping = QStringLiteral("-1");
        pingPong = QStringLiteral("true");
    }

    output << QSSGQmlUtilities::insertTabs(tabLevel) << QStringLiteral("TimelineAnimation {")
           << endl;
    output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("id: ")
           << QSSGQmlUtilities::sanitizeQmlId(slide->m_name + QStringLiteral("TimelineAnimation"))
           << endl;
    output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("duration: ")
           << duration << endl;
    output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("from: ")
           << startFrame << endl;
    output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("to: ")
           << endFrame << endl;
    output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("running: " )
           << (isRunning ? QStringLiteral("true") : QStringLiteral("false")) << endl;
    output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("loops: ")
           << looping << endl;
    output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("pingPong: ")
           << pingPong << endl;
    if (slide->m_playMode == Slide::PlayThroughTo) {
        // when the animation is done playing, change to the state defined by PlayThrough
        // onFinished: item1.state = "firsthalf"
        QString slideName;
        if (slide->m_playThrough == Slide::Next) {
            if (slide->nextSibling())
                slideName = slide->nextSibling()->m_name;
        } else if (slide->m_playThrough == Slide::Previous) {
            if (slide->nextSibling())
                slideName = slide->previousSibling()->m_name;
        } else {
            // value
            if (slide->m_playThroughValue.type() == QVariant::String) {
                slideName = slide->m_playThroughValue.toString();
            } else {
                int slideIndex = slide->m_playThroughValue.toInt();
                Slide *newSlide = static_cast<Slide*>(slide->parent()->childAtIndex(slideIndex));
                slideName = newSlide->m_name;
            }
        }
        if (!slideName.isEmpty()) {
            output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("onFinished: ")
                   << componentName << QStringLiteral(".state = \"") << slideName
                   << QStringLiteral("\"") << endl;
        }
    }
    output << QSSGQmlUtilities::insertTabs(tabLevel) << QStringLiteral("}");
}

QVector<AnimationTrack> combineAnimationTracks(const QVector<AnimationTrack> &master, const QVector<AnimationTrack> &slide) {
    // We can't have animations that target the same object and property,
    // so slides overwrite master animations
    QVector<AnimationTrack> animations;
    for (auto masterAnimation : master) {
        bool skip = false;
        for (auto slideAnimation : slide) {
            if (masterAnimation.m_target == slideAnimation.m_target &&
                    masterAnimation.m_property == slideAnimation.m_property) {
                skip = true;
                break;
            }
        }
        if (!skip)
            animations.append(masterAnimation);
    }
    animations.append(slide);
    return animations;
}

void calculateStartAndEndFrames(Slide *currentSlide, UipPresentation *presentation,
                                ComponentNode *component, float fps, int &startFrame,
                                int &endFrame, int &duration) {

    GraphObject *object = nullptr;
    if (presentation) {
        //presentation
        presentation->applySlidePropertyChanges(currentSlide);
        object = presentation->scene()->firstChild();
    } else {
        //component
        const auto &changeList = currentSlide->propertyChanges();
        for (auto it = changeList.cbegin(), ite = changeList.cend(); it != ite; ++it) {
            it.key()->applyPropertyChanges(*it.value());
        }
        object = component->firstChild();
    }

    float startTime = std::numeric_limits<float>::max();
    float endTime = std::numeric_limits<float>::min();
    while (object) {
        startTime = qMin(object->startTime(), startTime);
        endTime = qMax(object->endTime(), endTime);
        object = object->nextSibling();
    }

    startFrame = qRound(startTime * fps);
    endFrame = qRound(endTime * fps);
    duration = qRound((endTime - startTime) * 1000.f);
}

}

void UipImporter::generateAnimationTimeLine(QTextStream &output, int tabLevel, UipPresentation *presentation, ComponentNode *component)
{
    // Either presentation or component should be valid
    Slide *masterSlide;
    QString componentName;
    if (presentation) {
        masterSlide = presentation->masterSlide();
        componentName = presentation->name();
    } else if (component) {
        masterSlide = component->m_masterSlide;
        componentName = component->qmlId();
    } else {
        Q_UNREACHABLE();
        qDebug("something went wrong");
        return;
    }

    // Generate a 1 Timeline and 1 TimelineAnimation for each slide
    // Each combines the master slide and current slide

    // For each slide, create a TimelineAnimation
    auto slide = static_cast<Slide*>(masterSlide->firstChild());
    while (slide) {
        int startFrame = 0;
        int endFrame = 0;
        int duration = 0;
        calculateStartAndEndFrames(slide, presentation, component, m_fps,
                                   startFrame, endFrame, duration);
        output << endl;
        output << QSSGQmlUtilities::insertTabs(tabLevel) << QStringLiteral("Timeline {") << endl;
        output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("id: ")
               << QSSGQmlUtilities::sanitizeQmlId(slide->m_name + QStringLiteral("Timeline"))
               << endl;
        output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("startFrame: ")
               << startFrame << endl;
        output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("endFrame: ")
               << endFrame << endl;
        output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("currentFrame: ")
               << startFrame << endl;
        output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("enabled: false")
               << endl;
        output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("animations: [")
               << endl;
        generateTimelineAnimation(slide, startFrame, endFrame, duration, false, output,
                                  tabLevel + 2, componentName);
        output << endl;
        output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("]") << endl;

        // Keyframe groups for master + current slide
        // Get a list off all animations for the master and first slide
        const auto animations = combineAnimationTracks(masterSlide->animations(),
                                                       slide->animations());
        // Create a list of KeyframeGroups
        KeyframeGroupGenerator generator(m_fps);

        for (auto animation : animations)
            generator.addAnimation(animation);

        generator.generateKeyframeGroups(output, tabLevel + 1);

        output << QSSGQmlUtilities::insertTabs(tabLevel) << QStringLiteral("}") << endl;
        slide = static_cast<Slide*>(slide->nextSibling());
    }
}

void UipImporter::generateStatesFromSlides(Slide *masterSlide, QTextStream &output, int tabLevel)
{
    output << endl;
    output << QSSGQmlUtilities::insertTabs(tabLevel) << QStringLiteral("states: [") << endl;

    auto slide = static_cast<Slide*>(masterSlide->firstChild());
    bool isFirst = true;
    QString firstStateName;
    while (slide) {
        if (isFirst) {
            isFirst = false;
            firstStateName = slide->m_name;
        } else {
            output << QStringLiteral(",") << endl;
        }

        output << QSSGQmlUtilities::insertTabs(tabLevel+1) << QStringLiteral("State {") << endl;
        output << QSSGQmlUtilities::insertTabs(tabLevel+2) << QStringLiteral("name: \"") << slide->m_name << QStringLiteral("\"") << endl;

        // Add property changes here
        // First enable the Timeline for this state
        output << QSSGQmlUtilities::insertTabs(tabLevel+2) << QStringLiteral("PropertyChanges {") << endl;
        output << QSSGQmlUtilities::insertTabs(tabLevel+3) << QStringLiteral("target: ") << QSSGQmlUtilities::sanitizeQmlId(slide->m_name + QStringLiteral("Timeline")) << endl;
        output << QSSGQmlUtilities::insertTabs(tabLevel+3) << QStringLiteral("enabled: true") << endl;
        output << QSSGQmlUtilities::insertTabs(tabLevel+3) << QStringLiteral("currentFrame: 0") << endl;
        output << QSSGQmlUtilities::insertTabs(tabLevel+2) << QStringLiteral("}") << endl;

        output << QSSGQmlUtilities::insertTabs(tabLevel+2) << QStringLiteral("PropertyChanges {") << endl;
        output << QSSGQmlUtilities::insertTabs(tabLevel+3) << QStringLiteral("target: ") << QSSGQmlUtilities::sanitizeQmlId(slide->m_name + QStringLiteral("TimelineAnimation")) << endl;
        output << QSSGQmlUtilities::insertTabs(tabLevel+3) << QStringLiteral("running: true") << endl;
        output << QSSGQmlUtilities::insertTabs(tabLevel+2) << QStringLiteral("}") << endl;

        // Now all other properties changed by the slide
        auto changeList = slide->propertyChanges();
        for (auto it = changeList.cbegin(), ite = changeList.cend(); it != ite; ++it) {
            // First see if there are any property changes we actually care about
            QBuffer propertyChangesBuffer;
            propertyChangesBuffer.open(QBuffer::WriteOnly);
            QTextStream propertyChangesOutput(&propertyChangesBuffer);
            // output each of the properties changed
            it.key()->writeQmlProperties(*it.value(), propertyChangesOutput, tabLevel + 3);
            propertyChangesBuffer.close();

            if (!propertyChangesBuffer.data().isEmpty()) {

                output << QSSGQmlUtilities::insertTabs(tabLevel+2) << QStringLiteral("PropertyChanges {") << endl;
                output << QSSGQmlUtilities::insertTabs(tabLevel+3) << QStringLiteral("target: ") << it.key()->qmlId() << endl;

                output << propertyChangesBuffer.data();

                output << QSSGQmlUtilities::insertTabs(tabLevel+2) << QStringLiteral("}") << endl;
            }
        }

        output << QSSGQmlUtilities::insertTabs(tabLevel+1) << QStringLiteral("}");

        slide = static_cast<Slide*>(slide->nextSibling());
    }

    output << endl;

    output << QSSGQmlUtilities::insertTabs(tabLevel) << QStringLiteral("]") << endl;

    // Set the initial state (works correctly even when empty)
    output << QSSGQmlUtilities::insertTabs(tabLevel) << QStringLiteral("state: \"") << firstStateName << QStringLiteral("\"") << endl;
 }

void UipImporter::generateComponent(GraphObject *component)
{
    QDir componentPath = m_exportPath.absolutePath() + QDir::separator() + QStringLiteral("components");

    QString componentName = QSSGQmlUtilities::qmlComponentName(component->qmlId());
    QString targetFileName = componentPath.absolutePath() + QDir::separator() + componentName + QStringLiteral(".qml");
    QFile componentFile(targetFileName);
    if (!componentFile.open(QIODevice::WriteOnly)) {
        qWarning() << "Could not write to file: " << componentFile;
        return;
    }

    QTextStream output(&componentFile);
    writeHeader(output);

    output << QStringLiteral("Node {") << endl;
    component->writeQmlProperties(output, 1);

    processNode(component->firstChild(), output, 1, false);

    // Generate Animation Timeline
    auto componentNode = static_cast<ComponentNode*>(component);
    generateAnimationTimeLine(output, 1, nullptr, componentNode);

    // Generate States from Slides
    generateStatesFromSlides(componentNode->m_masterSlide, output, 1);

    // Footer
    component->writeQmlFooter(output, 0);

    componentFile.close();
    m_generatedFiles += targetFileName;
}

void UipImporter::writeHeader(QTextStream &output, bool isRootLevel)
{
    output << "import QtQuick3D 1.12" << endl;
    output << "import QtQuick 2.12" << endl;
    output << "import QtQuick.Timeline 1.0" << endl;

    QString relativePath = isRootLevel ? "./" : "../";

    if (m_referencedMaterials.count() > 0)
        output << "import \"" << relativePath << "materials\"" << endl;

    if (m_aliasNodes.count() > 0)
        output << "import \"" << relativePath << "aliases\"" << endl;

    if (m_componentNodes.count() > 0 || m_qmlDirs.count() > 0)
        output << "import \"" << relativePath << "components\"" << endl;

    output << endl;
}

void UipImporter::generateApplicationComponent(const QString &initialPresentationComponent, const QSize &size)
{
    // Create File
    QString targetFileName = m_exportPath.absolutePath() + QDir::separator() + initialPresentationComponent + QStringLiteral("Window.qml");
    QFile applicationComponentFile(targetFileName);
    if (!applicationComponentFile.open(QIODevice::WriteOnly)) {
        qDebug() << "couldn't open " << targetFileName << " for writing";
        return;
    }

    QTextStream output(&applicationComponentFile);

    // Header
    output << "import QtQuick 2.12" << endl;
    output << "import QtQuick.Window 2.12" << endl;
    output << endl;

    // Window
    output << "Window {" << endl;
    output << QSSGQmlUtilities::insertTabs(1) << "width: " << size.width() << endl;
    output << QSSGQmlUtilities::insertTabs(1) << "height: " << size.height() << endl;
    output << QSSGQmlUtilities::insertTabs(1) << "title: " << "\"" << initialPresentationComponent << "\"" << endl;
    output << QSSGQmlUtilities::insertTabs(1) << "visible: true" << endl;
    output << endl;

    // Component
    output << QSSGQmlUtilities::insertTabs(1) << initialPresentationComponent << " {" << endl;
    output << QSSGQmlUtilities::insertTabs(2) << "anchors.fill: parent" << endl;
    output << QSSGQmlUtilities::insertTabs(1) << "}" << endl;

    output << "}" << endl;

    applicationComponentFile.close();
    m_generatedFiles += targetFileName;
}

void UipImporter::generateQmlComponent(const QString componentName, const QString componentSource)
{
    QDir componentPath = m_exportPath.absolutePath() + QDir::separator() + QStringLiteral("components");
    componentPath.mkdir(".");

    // Basically creates an alias component to a QML one
    QString qmlComponentName = QSSGQmlUtilities::qmlComponentName(componentName);
    QString targetFileName = componentPath.absolutePath() + QDir::separator() + qmlComponentName + QStringLiteral(".qml");
    QFile componentFile(targetFileName);
    if (!componentFile.open(QIODevice::WriteOnly)) {
        qWarning() << "Could not write to file: " << componentFile;
        return;
    }

    QTextStream output(&componentFile);

    output << "import QtQuick 2.12" << endl;
    output << "import \"../qml\"" << endl << endl;

    output << componentSource << QStringLiteral(" { }");

    componentFile.close();
    m_generatedFiles.append(targetFileName);
}

void UipImporter::processOptions(const QVariantMap &options)
{
    // Setup import settings based given options
    // You can either pass the whole options object, or just the "options" object
    // so get the right scope.
    QJsonObject optionsObject = QJsonObject::fromVariantMap(options);
    if (optionsObject.contains(QStringLiteral("options")))
        optionsObject = optionsObject.value(QStringLiteral("options")).toObject();

    if (optionsObject.isEmpty())
        return;

    // parse the options list for values
    m_createProjectWrapper = checkBooleanOption(QStringLiteral("createProjectWrapper"),
                                                optionsObject);
    m_createIndividualLayers = checkBooleanOption(QStringLiteral("createIndividualLayers"),
                                                  optionsObject);
    m_fps = float(getRealOption(QStringLiteral("framesPerSecond"), optionsObject));
}

bool UipImporter::checkBooleanOption(const QString &optionName, const QJsonObject &options)
{
    if (!options.contains(optionName))
        return false;

    QJsonObject option = options.value(optionName).toObject();
    return option.value(QStringLiteral("value")).toBool();
}

double UipImporter::getRealOption(const QString &optionName, const QJsonObject &options)
{
    if (!options.contains(optionName))
        return false;

    QJsonObject option = options.value(optionName).toObject();
    return option.value(QStringLiteral("value")).toDouble();
}

QString UipImporter::processUipPresentation(UipPresentation *presentation, const QString &ouputFilePath)
{
    m_referencedMaterials.clear();
    m_aliasNodes.clear();
    m_componentNodes.clear();
    m_presentation = presentation;

    // Apply the properties of the first slide before running generator
//    Slide *firstSlide = static_cast<Slide*>(presentation->masterSlide()->firstChild());
//    if (firstSlide)
//        presentation->applySlidePropertyChanges(firstSlide);
    QString errorString;

    // create one component per layer
    GraphObject *layer = presentation->scene()->lastChild();
    QHash<QString, QBuffer *> layerComponentsMap;
    while (layer) {
        if (layer->type() == GraphObject::Layer) {
            // Create qml component from .uip presentation
            QString targetFile = ouputFilePath + QSSGQmlUtilities::qmlComponentName(presentation->name()) + QSSGQmlUtilities::qmlComponentName(layer->qmlId());
            QBuffer *qmlBuffer = new QBuffer();
            qmlBuffer->open(QIODevice::WriteOnly);
            QTextStream output(qmlBuffer);
            processNode(layer, output, 0, true, false);
            qmlBuffer->close();
            layerComponentsMap.insert(targetFile, qmlBuffer);

        } else if ( layer->type() == GraphObject::DefaultMaterial &&
                    layer->qmlId() == QStringLiteral("__Container")) {
            // UIP version > 5 which tries to be clever with reference materials
            // Instead of parsing these items as normal, instead we iterate the
            // materials container and generate new Components for each one and output them
            // to the "materials" folder
            m_exportPath.mkdir("materials");

            GraphObject *object = layer->firstChild();
            while (object) {
                generateMaterialComponent(object);
                object = object->nextSibling();
            }
        }

        layer = layer->previousSibling();
    }

   // create aliases folder
    if (m_referencedMaterials.count() > 0) {
        m_exportPath.mkdir("materials");
    }

    if (m_aliasNodes.count() > 0) {
        m_exportPath.mkdir("aliases");
    }

    if(m_componentNodes.count() > 0) {
        m_exportPath.mkdir("components");
    }

    // Generate Alias, Components, and ReferenceMaterials (2nd pass)
    // Use iterators because generateComponent can contain additional nested components
    QVector<ComponentNode *>::iterator componentIterator;
    for (componentIterator = m_componentNodes.begin(); componentIterator != m_componentNodes.end(); ++componentIterator)
        generateComponent(*componentIterator);

    for (auto material : m_referencedMaterials) {
        QString id = material->m_referencedMaterial_unresolved;
        if (id.startsWith("#"))
            id.remove(0, 1);
        auto obj = presentation->object(UniqueIdMapper::instance()->queryId(id.toUtf8()));
        if (!obj) {
            qWarning("Couldn't find object with id: %s", qPrintable(id));
            continue;
        }
        generateMaterialComponent(obj);
    }

    for (auto alias : m_aliasNodes) {
        QString id = alias->m_referencedNode_unresolved;
        if (id.startsWith("#"))
            id.remove(0, 1);
        generateAliasComponent(presentation->object(UniqueIdMapper::instance()->queryId(id.toUtf8())));
    }

    // Generate actual files from the buffers we created
    if (m_createIndividualLayers) {
        // Create a file for each component buffer
        for (auto targetName : layerComponentsMap.keys()) {
            QString targetFileName = targetName + QStringLiteral(".qml");
            QFile targetFile(targetFileName);
            if (!targetFile.open(QIODevice::WriteOnly)) {
                errorString += QString("Could not write to file: ") + targetFileName;
            } else {
                QTextStream output(&targetFile);
                writeHeader(output, true);
                QBuffer *componentBuffer = layerComponentsMap.value(targetName);
                componentBuffer->open(QIODevice::ReadOnly);
                output << componentBuffer->readAll();
                componentBuffer->close();
                targetFile.close();
                m_generatedFiles += targetFileName;
            }
        }
    }

    QString outputFileName = ouputFilePath
            + QSSGQmlUtilities::qmlComponentName(presentation->name()) + QStringLiteral(".qml");
    QFile outputFile(outputFileName);
    if (!outputFile.open(QIODevice::WriteOnly)) {
        errorString += QString(QStringLiteral("Could not write to file: ") + outputFileName);
    } else {
        QTextStream output(&outputFile);
        // Write header
        writeHeader(output, true);

        // Window header
        if (m_presentation->scene()->m_useClearColor)
            output << QStringLiteral("Rectangle {") << endl;
        else
            output << QStringLiteral("Item {") << endl;
        output << QSSGQmlUtilities::insertTabs(1) << QStringLiteral("id: ")
               << QSSGQmlUtilities::sanitizeQmlId(m_presentation->name()) << endl;
        output << QSSGQmlUtilities::insertTabs(1) << QStringLiteral("width: ")
               << m_presentation->presentationWidth()<< endl;
        output << QSSGQmlUtilities::insertTabs(1) << QStringLiteral("height: ")
               << m_presentation->presentationHeight() << endl;
        if (m_presentation->scene()->m_useClearColor) {
            output << QSSGQmlUtilities::insertTabs(1) << QStringLiteral("color: ")
                   << QSSGQmlUtilities::colorToQml(m_presentation->scene()->m_clearColor) << endl;
        }

        // For each component buffer paste in each line with tablevel +1
        if (m_createIndividualLayers) {
            const auto layerFiles = layerComponentsMap.keys();
            output << endl;
            for (const auto &layerFile : layerFiles) {
                output << QSSGQmlUtilities::insertTabs(1)
                       << QFileInfo(layerFile).baseName() << QStringLiteral(" {}") << endl << endl;
            }
        } else {
            for (auto buffer : layerComponentsMap) {
                buffer->open(QIODevice::ReadOnly);
                buffer->seek(0);
                while (!buffer->atEnd()) {
                    QByteArray line = buffer->readLine();
                    output << QSSGQmlUtilities::insertTabs(1) << line;
                }
                buffer->close();
            }
        }
        // Do States and AnimationTimelines here (same for all layers of the presentation)
        // Generate Animation Timeline
        generateAnimationTimeLine(output, 1, m_presentation, nullptr);
        // Generate States from Slides
        generateStatesFromSlides(m_presentation->masterSlide(), output, 1);

        // Window footer
        output << QStringLiteral("}") << endl;
        outputFile.close();
        m_generatedFiles += outputFileName;
    }

    // Cleanup
    for (auto buffer : layerComponentsMap.values())
        delete buffer;

    return errorString;
}

QT_END_NAMESPACE
