/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtQuick module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** 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 Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or 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.GPL2 and 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-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "qsgd3d12renderloop_p.h"
#include "qsgd3d12engine_p.h"
#include "qsgd3d12context_p.h"
#include "qsgd3d12rendercontext_p.h"
#include "qsgd3d12shadereffectnode_p.h"
#include <private/qquickwindow_p.h>
#include <private/qquickprofiler_p.h>
#include <private/qquickanimatorcontroller_p.h>
#include <QElapsedTimer>
#include <QGuiApplication>
#include <QScreen>

QT_BEGIN_NAMESPACE

// NOTE: Avoid categorized logging. It is slow.

#define DECLARE_DEBUG_VAR(variable) \
    static bool debug_ ## variable() \
    { static bool value = qgetenv("QSG_RENDERER_DEBUG").contains(QT_STRINGIFY(variable)); return value; }

DECLARE_DEBUG_VAR(loop)
DECLARE_DEBUG_VAR(time)


// This render loop operates on the gui (main) thread.
// Conceptually it matches the OpenGL 'windows' render loop.

static inline int qsgrl_animation_interval()
{
    const qreal refreshRate = QGuiApplication::primaryScreen() ? QGuiApplication::primaryScreen()->refreshRate() : 0;
    return refreshRate < 1 ? 16 : int(1000 / refreshRate);
}

QSGD3D12RenderLoop::QSGD3D12RenderLoop()
{
    if (Q_UNLIKELY(debug_loop()))
        qDebug("new d3d12 render loop");

    sg = new QSGD3D12Context;

    m_anims = sg->createAnimationDriver(this);
    connect(m_anims, &QAnimationDriver::started, this, &QSGD3D12RenderLoop::onAnimationStarted);
    connect(m_anims, &QAnimationDriver::stopped, this, &QSGD3D12RenderLoop::onAnimationStopped);
    m_anims->install();

    m_vsyncDelta = qsgrl_animation_interval();
}

QSGD3D12RenderLoop::~QSGD3D12RenderLoop()
{
    delete sg;
}

void QSGD3D12RenderLoop::show(QQuickWindow *window)
{
    if (Q_UNLIKELY(debug_loop()))
        qDebug() << "show" << window;
}

void QSGD3D12RenderLoop::hide(QQuickWindow *window)
{
    if (Q_UNLIKELY(debug_loop()))
        qDebug() << "hide" << window;
}

void QSGD3D12RenderLoop::resize(QQuickWindow *window)
{
    if (!m_windows.contains(window) || window->size().isEmpty())
        return;

    if (Q_UNLIKELY(debug_loop()))
        qDebug() << "resize" << window;

    const WindowData &data(m_windows[window]);

    if (!data.exposed)
        return;

    if (data.engine)
        data.engine->setWindowSize(window->size(), window->effectiveDevicePixelRatio());
}

void QSGD3D12RenderLoop::windowDestroyed(QQuickWindow *window)
{
    if (Q_UNLIKELY(debug_loop()))
        qDebug() << "window destroyed" << window;

    if (!m_windows.contains(window))
        return;

    QQuickWindowPrivate *wd = QQuickWindowPrivate::get(window);
    wd->fireAboutToStop();

    WindowData &data(m_windows[window]);
    QSGD3D12Engine *engine = data.engine;
    QSGD3D12RenderContext *rc = data.rc;
    m_windows.remove(window);

    // QSGNode destruction may release graphics resources in use so wait first.
    engine->waitGPU();

    // Bye bye nodes...
    wd->cleanupNodesOnShutdown();

    QSGD3D12ShaderEffectNode::cleanupMaterialTypeCache();

    rc->invalidate();

    delete rc;
    delete engine;

    delete wd->animationController;
}

void QSGD3D12RenderLoop::exposeWindow(QQuickWindow *window)
{
    WindowData data;
    data.exposed = true;
    data.engine = new QSGD3D12Engine;
    data.rc = static_cast<QSGD3D12RenderContext *>(QQuickWindowPrivate::get(window)->context);
    data.rc->setEngine(data.engine);
    m_windows[window] = data;

    const int samples = window->format().samples();
    const bool alpha = window->format().alphaBufferSize() > 0;
    const qreal dpr = window->effectiveDevicePixelRatio();

    if (Q_UNLIKELY(debug_loop()))
        qDebug() << "initializing D3D12 engine" << window << window->size() << dpr << samples << alpha;

    data.engine->attachToWindow(window->winId(), window->size(), dpr, samples, alpha);
}

void QSGD3D12RenderLoop::obscureWindow(QQuickWindow *window)
{
    m_windows[window].exposed = false;
    QQuickWindowPrivate *wd = QQuickWindowPrivate::get(window);
    wd->fireAboutToStop();
}

void QSGD3D12RenderLoop::exposureChanged(QQuickWindow *window)
{
    if (Q_UNLIKELY(debug_loop()))
        qDebug() << "exposure changed" << window << window->isExposed();

    if (window->isExposed()) {
        if (!m_windows.contains(window))
            exposeWindow(window);

        // Stop non-visual animation timer as we now have a window rendering.
        if (m_animationTimer && somethingVisible()) {
            killTimer(m_animationTimer);
            m_animationTimer = 0;
        }
        // If we have a pending timer and we get an expose, we need to stop it.
        // Otherwise we get two frames and two animation ticks in the same time interval.
        if (m_updateTimer) {
            killTimer(m_updateTimer);
            m_updateTimer = 0;
        }

        WindowData &data(m_windows[window]);
        data.exposed = true;
        data.updatePending = true;

        render();

    } else if (m_windows.contains(window)) {
        obscureWindow(window);

        // Potentially start the non-visual animation timer if nobody is rendering.
        if (m_anims->isRunning() && !somethingVisible() && !m_animationTimer)
            m_animationTimer = startTimer(m_vsyncDelta);
    }
}

QImage QSGD3D12RenderLoop::grab(QQuickWindow *window)
{
    const bool tempExpose = !m_windows.contains(window);
    if (tempExpose)
        exposeWindow(window);

    m_windows[window].grabOnly = true;

    renderWindow(window);

    QImage grabbed = m_grabContent;
    m_grabContent = QImage();

    if (tempExpose)
        obscureWindow(window);

    return grabbed;
}

bool QSGD3D12RenderLoop::somethingVisible() const
{
    for (auto it = m_windows.constBegin(), itEnd = m_windows.constEnd(); it != itEnd; ++it) {
        if (it.key()->isVisible() && it.key()->isExposed())
            return true;
    }
    return false;
}

void QSGD3D12RenderLoop::maybePostUpdateTimer()
{
    if (!m_updateTimer) {
        if (Q_UNLIKELY(debug_loop()))
            qDebug("starting update timer");
        m_updateTimer = startTimer(m_vsyncDelta / 3);
    }
}

void QSGD3D12RenderLoop::update(QQuickWindow *window)
{
    maybeUpdate(window);
}

void QSGD3D12RenderLoop::maybeUpdate(QQuickWindow *window)
{
    if (!m_windows.contains(window) || !somethingVisible())
        return;

    m_windows[window].updatePending = true;
    maybePostUpdateTimer();
}

QAnimationDriver *QSGD3D12RenderLoop::animationDriver() const
{
    return m_anims;
}

QSGContext *QSGD3D12RenderLoop::sceneGraphContext() const
{
    return sg;
}

QSGRenderContext *QSGD3D12RenderLoop::createRenderContext(QSGContext *) const
{
    // The rendercontext and engine are per-window, like with the threaded
    // loop, but unlike the non-threaded OpenGL variants.
    return sg->createRenderContext();
}

void QSGD3D12RenderLoop::releaseResources(QQuickWindow *window)
{
    if (Q_UNLIKELY(debug_loop()))
        qDebug() << "releaseResources" << window;
}

void QSGD3D12RenderLoop::postJob(QQuickWindow *window, QRunnable *job)
{
    Q_UNUSED(window);
    Q_ASSERT(job);
    Q_ASSERT(window);
    job->run();
    delete job;
}

QSurface::SurfaceType QSGD3D12RenderLoop::windowSurfaceType() const
{
    return QSurface::OpenGLSurface;
}

bool QSGD3D12RenderLoop::interleaveIncubation() const
{
    return m_anims->isRunning() && somethingVisible();
}

void QSGD3D12RenderLoop::onAnimationStarted()
{
    if (!somethingVisible()) {
        if (!m_animationTimer) {
            if (Q_UNLIKELY(debug_loop()))
                qDebug("starting non-visual animation timer");
            m_animationTimer = startTimer(m_vsyncDelta);
        }
    } else {
        maybePostUpdateTimer();
    }
}

void QSGD3D12RenderLoop::onAnimationStopped()
{
    if (m_animationTimer) {
        if (Q_UNLIKELY(debug_loop()))
            qDebug("stopping non-visual animation timer");
        killTimer(m_animationTimer);
        m_animationTimer = 0;
    }
}

bool QSGD3D12RenderLoop::event(QEvent *event)
{
    switch (event->type()) {
    case QEvent::Timer:
    {
        QTimerEvent *te = static_cast<QTimerEvent *>(event);
        if (te->timerId() == m_animationTimer) {
            if (Q_UNLIKELY(debug_loop()))
                qDebug("animation tick while no windows exposed");
            m_anims->advance();
        } else if (te->timerId() == m_updateTimer) {
            if (Q_UNLIKELY(debug_loop()))
                qDebug("update timeout - rendering");
            killTimer(m_updateTimer);
            m_updateTimer = 0;
            render();
        }
        return true;
    }
    default:
        break;
    }

    return QObject::event(event);
}

void QSGD3D12RenderLoop::render()
{
    bool rendered = false;
    for (auto it = m_windows.begin(), itEnd = m_windows.end(); it != itEnd; ++it) {
        if (it->updatePending) {
            it->updatePending = false;
            renderWindow(it.key());
            rendered = true;
        }
    }

    if (!rendered) {
        if (Q_UNLIKELY(debug_loop()))
            qDebug("render - no changes, sleep");
        QThread::msleep(m_vsyncDelta);
    }

    if (m_anims->isRunning()) {
        if (Q_UNLIKELY(debug_loop()))
            qDebug("render - advancing animations");

        m_anims->advance();

        // It is not given that animations triggered another maybeUpdate()
        // and thus another render pass, so to keep things running,
        // make sure there is another frame pending.
        maybePostUpdateTimer();

        emit timeToIncubate();
    }
}

void QSGD3D12RenderLoop::renderWindow(QQuickWindow *window)
{
    if (Q_UNLIKELY(debug_loop()))
        qDebug() << "renderWindow" << window;

    QQuickWindowPrivate *wd = QQuickWindowPrivate::get(window);
    if (!m_windows.contains(window) || !window->geometry().isValid())
        return;

    WindowData &data(m_windows[window]);
    if (!data.exposed) { // not the same as window->isExposed(), when grabbing invisible windows for instance
        if (Q_UNLIKELY(debug_loop()))
            qDebug("renderWindow - not exposed, abort");
        return;
    }

    if (!data.grabOnly)
        wd->flushFrameSynchronousEvents();

    QElapsedTimer renderTimer;
    qint64 renderTime = 0, syncTime = 0, polishTime = 0;
    const bool profileFrames = debug_time();
    if (profileFrames)
        renderTimer.start();
    Q_QUICK_SG_PROFILE_START(QQuickProfiler::SceneGraphPolishFrame);

    wd->polishItems();

    if (profileFrames)
        polishTime = renderTimer.nsecsElapsed();
    Q_QUICK_SG_PROFILE_SWITCH(QQuickProfiler::SceneGraphPolishFrame,
                              QQuickProfiler::SceneGraphRenderLoopFrame,
                              QQuickProfiler::SceneGraphPolishPolish);

    emit window->afterAnimating();

    // The native window may change in some (quite artificial) cases, e.g. due
    // to a hide - destroy - show on the QWindow.
    bool needsWindow = !data.engine->window();
    if (data.engine->window() && data.engine->window() != window->winId()) {
        if (Q_UNLIKELY(debug_loop()))
            qDebug("sync - native window handle changes for active engine");
        data.engine->waitGPU();
        wd->cleanupNodesOnShutdown();
        QSGD3D12ShaderEffectNode::cleanupMaterialTypeCache();
        data.rc->invalidate();
        data.engine->releaseResources();
        needsWindow = true;
    }
    if (needsWindow) {
        // Must only ever get here when there is no window or releaseResources() has been called.
        const int samples = window->format().samples();
        const bool alpha = window->format().alphaBufferSize() > 0;
        const qreal dpr = window->effectiveDevicePixelRatio();
        if (Q_UNLIKELY(debug_loop()))
            qDebug() << "sync - reinitializing D3D12 engine" << window << window->size() << dpr << samples << alpha;
        data.engine->attachToWindow(window->winId(), window->size(), dpr, samples, alpha);
    }

    // Recover from device loss.
    if (!data.engine->hasResources()) {
        if (Q_UNLIKELY(debug_loop()))
            qDebug("sync - device was lost, resetting scenegraph");
        wd->cleanupNodesOnShutdown();
        QSGD3D12ShaderEffectNode::cleanupMaterialTypeCache();
        data.rc->invalidate();
    }

    data.rc->initialize(nullptr);

    wd->syncSceneGraph();
    data.rc->endSync();

    if (profileFrames)
        syncTime = renderTimer.nsecsElapsed();
    Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphRenderLoopFrame,
                              QQuickProfiler::SceneGraphRenderLoopSync);

    wd->renderSceneGraph(window->size());

    if (profileFrames)
        renderTime = renderTimer.nsecsElapsed();
    Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphRenderLoopFrame,
                              QQuickProfiler::SceneGraphRenderLoopRender);

    if (!data.grabOnly) {
        // The engine is able to have multiple frames in flight. This in effect is
        // similar to BufferQueueingOpenGL. Provide an env var to force the
        // traditional blocking swap behavior, just in case.
        static bool blockOnEachFrame = qEnvironmentVariableIntValue("QT_D3D_BLOCKING_PRESENT") != 0;

        if (window->isVisible()) {
            data.engine->present();
            if (blockOnEachFrame)
                data.engine->waitGPU();
            // The concept of "frame swaps" is quite misleading by default, when
            // blockOnEachFrame is not used, but emit it for compatibility.
            wd->fireFrameSwapped();
        } else {
            if (blockOnEachFrame)
                data.engine->waitGPU();
        }
    } else {
        m_grabContent = data.engine->executeAndWaitReadbackRenderTarget();
        data.grabOnly = false;
    }

    qint64 swapTime = 0;
    if (profileFrames)
        swapTime = renderTimer.nsecsElapsed();
    Q_QUICK_SG_PROFILE_END(QQuickProfiler::SceneGraphRenderLoopFrame,
                           QQuickProfiler::SceneGraphRenderLoopSwap);

    if (Q_UNLIKELY(debug_time())) {
        static QTime lastFrameTime = QTime::currentTime();
        qDebug("Frame rendered with 'd3d12' renderloop in %dms, polish=%d, sync=%d, render=%d, swap=%d, frameDelta=%d",
               int(swapTime / 1000000),
               int(polishTime / 1000000),
               int((syncTime - polishTime) / 1000000),
               int((renderTime - syncTime) / 1000000),
               int((swapTime - renderTime) / 10000000),
               int(lastFrameTime.msecsTo(QTime::currentTime())));
        lastFrameTime = QTime::currentTime();
    }

    // Simulate device loss if requested.
    static int devLossTest = qEnvironmentVariableIntValue("QT_D3D_TEST_DEVICE_LOSS");
    if (devLossTest > 0) {
        static QElapsedTimer kt;
        static bool timerRunning = false;
        if (!timerRunning) {
            kt.start();
            timerRunning = true;
        } else if (kt.elapsed() > 5000) {
            --devLossTest;
            kt.restart();
            data.engine->simulateDeviceLoss();
        }
    }
}

int QSGD3D12RenderLoop::flags() const
{
    return SupportsGrabWithoutExpose;
}

QT_END_NAMESPACE
