/****************************************************************************
**
** Copyright (C) 2017 The Qt Company Ltd.
** Contact: http://www.qt.io/licensing/
**
** This file is part of Qt 3D Studio.
**
** $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$
**
****************************************************************************/

#ifndef Q3DSENGINE_P_H
#define Q3DSENGINE_P_H

//
//  W A R N I N G
//  -------------
//
// This file is not part of the Qt API.  It exists purely as an
// implementation detail.  This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//

#include <QObject>
#include <QThread>
#include <QElapsedTimer>
#include <QSurfaceFormat>
#include <Qt3DCore/QAspectEngine>
#include "q3dsuipdocument_p.h"
#include "q3dsuiadocument_p.h"
#include "q3dsdatainput.h"
#include "q3dsuiaparser_p.h"
#include "q3dsscenemanager_p.h"
#include "private/q3dsbehaviorobject_p.h"
#include "q3dssourceinfo_p.h"

QT_BEGIN_NAMESPACE

class QKeyEvent;
class QMouseEvent;
class QWheelEvent;
class QTouchEvent;
class QTabletEvent;
class QQmlEngine;
class QQmlComponent;
class Q3DSInlineQmlSubPresentation;
class Q3DSViewportSettings;

namespace Qt3DRender {
class QRenderCapture;
class QRenderCaptureReply;
namespace Quick {
class QScene2D;
}
}

struct Q3DSV_PRIVATE_EXPORT Q3DSBehaviorHandle
{
    Q3DSBehaviorInstance *behaviorInstance = nullptr;
    QQmlComponent *component = nullptr;
    Q3DSBehaviorObject *object = nullptr;

    bool initialized = false;
    bool active = false;

    void updateProperties() const;
};

struct Q3DSV_PRIVATE_EXPORT Q3DSRenderLoopStats
{
    bool usingAsyncRenderAspect = false;
    bool usingSGRenderThread = false;
    float quickDeltaMs = 0;
};

struct ParentChildPairs
{
    QVector<QPair<QObject *, QObject *>> m_parentChildPairs;

    void addPair(QObject *parent, QObject *child)
    {
        m_parentChildPairs.push_back(QPair<QObject *, QObject *>(parent, child));
    }
};

class Q3DSV_PRIVATE_EXPORT Q3DSEngine : public QObject
{
    Q_OBJECT
public:
    Q3DSEngine();
    ~Q3DSEngine();

    enum Flag {
        Force4xMSAA = 0x01,
        EnableProfiling = 0x02,
        WithoutRenderAspect = 0x04,
        PreLoadSubPresentations = 0x08,
        AwakenTheDragon = 0x10
    };
    Q_DECLARE_FLAGS(Flags, Flag)

    static void clearLog();

    Flags flags() const { return m_flags; }
    void setFlags(Flags flags);
    void setFlag(Flag flag, bool enabled);

    void setSharedSubPresentationQmlEngine(QQmlEngine *qmlEngine);
    void setSharedBehaviorQmlEngine(QQmlEngine *qmlEngine);

    static void setQmlProfileUiInstance(QObject *imguiItem) { m_imguiItem = imguiItem; }
    static QObject *qmlProfileUiInstance() { return m_imguiItem; }

    using InlineSubPresList = QVector<Q3DSInlineQmlSubPresentation *>;
    // Load presentation from a uip/uia file.
    bool setSource(const Q3DSSourceInfo &sourceInfo,
                   QString *error = nullptr,
                   const InlineSubPresList &inlineQmlSubPresentations = InlineSubPresList());
    Q3DSSourceInfo source() const;

    // Load presentation from a uip document object.
    bool setDocument(const Q3DSUipDocument &uipDocument, QString *error = nullptr);

    // Load presentation from a uia document object.
    bool setDocument(const Q3DSUiaDocument &uiaDocument, QString *error = nullptr);

    // Provide a pre-constructed presentation. Entry point for programatically
    // constructed scenes.
    bool setPresentation(Q3DSUipPresentation *presentations);

    // Start animating all uip presentations.
    void startAnimations();

    qint64 behaviorLoadTimeMsecs() const;
    qint64 totalLoadTimeMsecs() const;

    int presentationCount() const;
    QString uipFileName(int index = 0) const;
    Q3DSUipDocument *uipDocument(int index = 0) const;
    Q3DSUipPresentation *presentation(int index = 0) const;
    Q3DSUipPresentation *presentationByName(const QString &name) const;
    Q3DSSceneManager *sceneManager(int index = 0) const;

    Qt3DCore::QAspectEngine *aspectEngine() const;
    Qt3DCore::QEntity *rootEntity() const;

    Q3DSViewportSettings *viewportSettings() const;
    void setViewportSettings(Q3DSViewportSettings *viewportSettings);

    void setOnDemandRendering(bool enabled);
    void setMainLayerComposition(bool enabled);
    void setDelayedLoading(bool enable);

    QSize implicitSize() const;

    void setSurface(QObject *surface);
    QObject *surface() const;

    bool start();

    void resize(const QSize &size, qreal dpr = qreal(1.0), bool forceSynchronous = false);

    void setDataInputValue(const QString &name, const QVariant &value);
    void setDataInputMin(const QString &name, float min);
    void setDataInputMax(const QString &name, float max);
    QVector<Q3DSDataInput *> dataInputs();
    QVector<Q3DSDataInput *> dataInputs(const QVariant &metadataKey);
    bool isValidDataInput(const Q3DSDataInput *di) const;
    QVariant dataInputMetaData(const Q3DSDataInput *di, const QVariant &key) const;
    QList<QVariant> dataInputMetaDataKeys(const Q3DSDataInput *di) const;
    void setDataInputMetaData(const QString &diName, const QVariant &key, const QVariant &metaData);
    void removeDatainputMetadata(const QString &name, const QVariant &key);
    float dataInputMin(const QString &name) const;
    float dataInputMax(const QString &name) const;
    void fireEvent(Q3DSGraphObject *target, Q3DSUipPresentation *presentation, const QString &event);
    void goToTime(Q3DSGraphObject *context, Q3DSUipPresentation *presentation, float milliseconds);
    void goToSlideByName(Q3DSGraphObject *context, Q3DSUipPresentation *presentation, const QString &name);
    void goToSlideByIndex(Q3DSGraphObject *context, Q3DSUipPresentation *presentation, int index);
    void goToSlideByDirection(Q3DSGraphObject *context, Q3DSUipPresentation *presentation, bool next, bool wrap);
    void preloadSlide(Q3DSSlide *slide, Q3DSUipPresentation *presentation);
    void unloadSlide(Q3DSSlide *slide, Q3DSUipPresentation *presentation);

    void handleKeyPressEvent(QKeyEvent *e);
    void handleKeyReleaseEvent(QKeyEvent *e);
    void handleMousePressEvent(QMouseEvent *e, Q3DSLayerNode *targetLayer = nullptr);
    void handleMouseMoveEvent(QMouseEvent *e, Q3DSLayerNode *targetLayer = nullptr);
    void handleMouseReleaseEvent(QMouseEvent *e, Q3DSLayerNode *targetLayer = nullptr);
    void handleMouseDoubleClickEvent(QMouseEvent *e, Q3DSLayerNode *targetLayer = nullptr);
#if QT_CONFIG(wheelevent)
    void handleWheelEvent(QWheelEvent *e, Q3DSLayerNode *targetLayer = nullptr);
#endif
    void handleTouchEvent(QTouchEvent *e, Q3DSLayerNode *targetLayer = nullptr);
#if QT_CONFIG(tabletevent)
    void handleTabletEvent(QTabletEvent *e, Q3DSLayerNode *targetLayer = nullptr);
#endif

    void setAutoToggleProfileUi(bool enabled) { m_autoToggleProfileUi = enabled; }
    void setProfileUiVisible(bool visible, bool openLogAndConsole = false);
    bool isStereoscopic() const;
    bool isProfileUiVisible() const;
    bool isAnimatorsReady() const;
    void configureProfileUi(float scale);
    float profileUiScaleFactor() const { return m_profileUiScale; }

    typedef QHash<Q3DSBehaviorInstance *, Q3DSBehaviorHandle> BehaviorMap;

    typedef std::function<void(Q3DSBehaviorInstance *, const QString &)> BehaviorLoadedCallback;
    void loadBehaviorInstance(Q3DSBehaviorInstance *behaviorInstance,
                              Q3DSUipPresentation *pres,
                              BehaviorLoadedCallback callback = nullptr);
    void unloadBehaviorInstance(Q3DSBehaviorInstance *behaviorInstance);
    const BehaviorMap &behaviorHandles() const { return m_behaviorHandles; }

    // These two functions are the only place where the elementPath concept is
    // present in the engine (so that mappings can be made for the 3DS1-style
    // APIs). Proper objects must be used everywhere else.
    Q3DSGraphObject *findObjectByHashIdOrNameOrPath(Q3DSGraphObject *thisObject,
                                                    Q3DSUipPresentation *defaultPresentation,
                                                    const QString &idOrNameOrPath,
                                                    Q3DSUipPresentation **actualPresentation = nullptr);
    QString makePath(Q3DSGraphObject *obj) const;

    Qt3DCore::QNodeId layerTextureNodeId(Q3DSLayerNode *layer3DS) const;

    void reportQuickRenderLoopStats(float deltaMs, bool isThreaded);
    Q3DSRenderLoopStats renderLoopStats();

    Q3DSSubPresentation loadSubUipPresentation(const QString &subPresId);

    template <typename FinalizeFunc, typename... Params>
    Q3DSSubPresentation loadSubUipPresentation(const QString &subPresId, FinalizeFunc finalize, Params... params)
    {
        Q3DSSubPresentation sp;
        SceneLoaderAsync *loader = nullptr;
        UipPresentation *uip = nullptr;

        for (int i = 0; i < m_uipPresentations.count(); ++i) {
            if (m_uipPresentations[i].subPres.id == subPresId) {
                uip = &m_uipPresentations[i];
                break;
            }
        }
        if (!uip)
            return sp;

        loader = new SceneLoaderAsync;
        loader->function = [loader, finalize, params...]() {
            loader->reparentChildren();
            finalize(&loader->sp, params...);
        };

        loader->pres = uip;
        beginAsyncLoad(sp, loader);
        return sp;
    }

    void finishAsyncLoad(bool wait = true);
    void loadSlideResources(Q3DSSlide *slide, Q3DSUipPresentation *presentation);
    void unloadSlideResources(Q3DSSlide *slide, Q3DSUipPresentation *presentation);
    void createAspectEngine();

public Q_SLOTS:
    void requestGrab();

Q_SIGNALS:
    void presentationLoaded();
    void nextFrameStarting();
    void grabReady(const QImage &image);
    void customSignalEmitted(Q3DSGraphObject *obj, const QString &name);
    void slideEntered(Q3DSGraphObject *context, int index, const QString &name);
    void slideExited(Q3DSGraphObject *context, int index, const QString &name);
    void layerResized(Q3DSLayerNode *layer3DS);
    void layerTextureNodeChanged(Q3DSLayerNode *layer3DS);
    void stereoModeChanged();

private:
    Q_DISABLE_COPY(Q3DSEngine)

    struct Presentation {
        Q3DSSubPresentation subPres;
    };

    struct UipPresentation : Presentation {
        Q3DSUipDocument *uipDocument = nullptr;
        Q3DSSceneManager::Scene q3dscene;
        Q3DSSceneManager *sceneManager = nullptr;
        Q3DSUipPresentation *presentation = nullptr;
    };

    struct QmlPresentation : Presentation {
        // we have either a QmlDocument with the source or filename, or an
        // inline subpresentation object with a QQuickItem* in it
        Q3DSQmlDocument *qmlDocument = nullptr;
        Q3DSInlineQmlSubPresentation *inlineQmlSubPres = nullptr;
        Qt3DRender::Quick::QScene2D *scene2d = nullptr;
    };

    struct SceneLoaderAsync : public ParentChildPairs
    {
        QThread *targetThread;
        QFuture<void> future;
        UipPresentation *pres;
        std::function<void()> function;
        Q3DSSubPresentation sp;
        bool done;

        SceneLoaderAsync()
            : targetThread(nullptr), pres(nullptr), done(false)
        {}

        SceneLoaderAsync(const SceneLoaderAsync &loader)
            : ParentChildPairs(loader), targetThread(loader.targetThread),
              future(loader.future), pres(loader.pres), function(loader.function),
              sp(loader.sp), done(loader.done)
        {}

        void reparentChildren();
    };

    void beginAsyncLoad(Q3DSSubPresentation &sp, SceneLoaderAsync *loader);
    void finishAsyncLoadForSubpresentation(const QString &name);
    void reparentScene(SceneLoaderAsync *loader);

    struct SlideResourceCounter
    {
        QHash<QUrl, int> counters;
        QSet<QUrl> createSet;
        QSet<QUrl> deleteSet;

        QVector<Q3DSSlide *> loadedSlides;

        void increment(const QSet<QUrl> &set)
        {
            for (auto &r : set) {
                if (counters.value(r, 0) == 0)
                    createSet.insert(r);
                counters[r]++;
            }
        }
        void decrement(const QSet<QUrl> &set)
        {
            for (auto &r : set) {
                if (counters.contains(r)) {
                    int count = qMax(counters[r] - 1, 0);
                    if (count == 0)
                        deleteSet.insert(r);
                    counters[r] = count;
                }
            }
        }
        void begin()
        {
            createSet.clear();
            deleteSet.clear();
        }
        void reset()
        {
            loadedSlides.clear();
            counters.clear();
            begin();
        }
        void handleLoadSlide(Q3DSSlide *slide, Q3DSUipPresentation *presentation)
        {
            if (loadedSlides.contains(slide))
                return;
            slide->generateResourceSet(presentation);
            loadedSlides.push_back(slide);
            begin();
            increment(slide->resourceSet());
            print();
        }
        void handleUnloadSlide(Q3DSSlide *slide, Q3DSUipPresentation *presentation)
        {
            if (!loadedSlides.contains(slide))
                return;
            slide->generateResourceSet(presentation);
            loadedSlides.removeOne(slide);
            begin();
            decrement(slide->resourceSet());
            print();
        }
        void print();
    };

    bool loadPresentations();
    void finalizePresentations();
    bool loadUipPresentation(UipPresentation *pres);
    bool buildUipPresentationScene(UipPresentation *pres);
    bool loadSubUipPresentation(UipPresentation *pres);
    bool buildSubUipPresentationScene(UipPresentation *pres, SceneLoaderAsync *loader = nullptr);
    bool loadSubQmlPresentation(QmlPresentation *pres);

    bool parseUipDocument(UipPresentation *pres);
    bool parseUiaDocument(Q3DSUiaParser::Uia &uiaDoc, const QString &sourcePrefix);

    void destroy();
    void prepareForReload();

    void loadBehaviors();
    void destroyBehaviorHandle(const Q3DSBehaviorHandle &h);
    void behaviorFrameUpdate(float dt);
    QRect calculateViewport(const QSize &surfaceSize, const QSize &presentationSize) const;
    void registerQmlTypes();
    void initializeDataInputs();

    QObject *m_surface = nullptr;
    QSize m_implicitSize;
    QSize m_size;
    qreal m_dpr = 1;
    Flags m_flags;
    Q3DSSourceInfo m_sourceInfo; // uip or uia file + variant info
    QVector<UipPresentation> m_uipPresentations;
    QVector<QmlPresentation> m_qmlPresentations;
    QVector<Q3DSInlineQmlSubPresentation *> m_inlineQmlPresentations;
    Q3DSDataInputEntry::Map m_dataInputEntries;
    Q3DSDataInputEntry::MetadataMap m_dataInputMetaData;
    // In addition to datainput bookkeeping data, let engine instantiate
    // and manage actual datainput objects created. This removes the need
    // to create datainputs on-the-fly when Q3DSPresentation API client
    // requests a list of datainputs
    QVector<Q3DSDataInput *> m_dataInputs;

    QQmlEngine *m_qmlSubPresentationEngine = nullptr;
    bool m_ownsQmlSubPresentationEngine = false;
    QScopedPointer<Qt3DCore::QAspectEngine> m_aspectEngine;

    qint64 m_loadTime = 0;
    qint64 m_behaviorLoadTime = 0;

    QElapsedTimer m_profilerActivateTimer;
    QElapsedTimer m_sourceLoadTimer;

    Qt3DRender::QRenderCapture *m_capture = nullptr;
    QHash<Qt3DRender::QRenderCaptureReply*, QMetaObject::Connection> m_captureConnections;

    QObject m_profileUiEventSource;
    float m_profileUiScale = 1.0f;
    bool m_autoToggleProfileUi = true;

    QQmlEngine *m_behaviorQmlEngine = nullptr;
    bool m_ownsBehaviorQmlEngine = false;
    BehaviorMap m_behaviorHandles;

    bool m_onDemandRendering = false;
    bool m_mainLayerComposition = true;
    Q3DSViewportSettings *m_viewportSettings = nullptr;
    static QObject *m_imguiItem;

    Q3DSRenderLoopStats m_renderLoopStats;
    QMutex m_renderLoopStatsMutex;
    float m_quickDeltaSum = 0;
    int m_quickDeltaCount = 0;

    QVector<SceneLoaderAsync *> m_asyncSceneLoaders;
    SlideResourceCounter m_resourceReferenceCounter;
    QMutex *m_slideResourceMutex = nullptr;
    QMutex *m_asyncLoadMutex= nullptr;
};

Q_DECLARE_OPERATORS_FOR_FLAGS(Q3DSEngine::Flags)

QT_END_NAMESPACE

#endif // Q3DSENGINE_P_H
