/****************************************************************************
**
** 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 "qsgwindowsrenderloop_p.h"
#include <QtCore/QCoreApplication>
#include <QtCore/QLibraryInfo>
#include <QtCore/QThread>

#include <QtGui/QScreen>
#include <QtGui/QGuiApplication>
#include <QtGui/QOffscreenSurface>

#include <QtQuick/private/qsgcontext_p.h>
#include <QtQuick/private/qquickwindow_p.h>
#include <QtQuick/private/qsgrenderer_p.h>
#include <QtQuick/private/qsgdefaultrendercontext_p.h>

#include <QtQuick/QQuickWindow>

#include <private/qquickprofiler_p.h>
#include <private/qquickanimatorcontroller_p.h>

#if QT_CONFIG(quick_shadereffect) && QT_CONFIG(opengl)
#include <private/qquickopenglshadereffectnode_p.h>
#endif

#include <qtquick_tracepoints_p.h>

QT_BEGIN_NAMESPACE

// Single-threaded render loop with a custom animation driver. Like a
// combination of basic+threaded but still working on the main thread. Only
// compatible with direct OpenGL, no RHI support here.

extern Q_GUI_EXPORT QImage qt_gl_read_framebuffer(const QSize &size, bool alpha_format, bool include_alpha);

#define RLDEBUG(x) qCDebug(QSG_LOG_RENDERLOOP, x)

static QElapsedTimer qsg_render_timer;
#define QSG_LOG_TIME_SAMPLE(sampleName) \
    qint64 sampleName = 0;                                                  \
    if (QSG_LOG_TIME_RENDERLOOP().isDebugEnabled())  \
        sampleName = qsg_render_timer.nsecsElapsed(); \

#define QSG_RENDER_TIMING_SAMPLE(frameType, sampleName, position) \
    QSG_LOG_TIME_SAMPLE(sampleName) \
    Q_QUICK_SG_PROFILE_RECORD(frameType, position);


QSGWindowsRenderLoop::QSGWindowsRenderLoop()
    : m_gl(nullptr)
    , m_sg(QSGContext::createDefaultContext())
    , m_updateTimer(0)
    , m_animationTimer(0)
{
    m_rc = static_cast<QSGDefaultRenderContext *>(m_sg->createRenderContext());

    m_vsyncDelta = 1000 / QGuiApplication::primaryScreen()->refreshRate();
    if (m_vsyncDelta <= 0)
        m_vsyncDelta = 16;

    RLDEBUG("Windows Render Loop created");

    m_animationDriver = m_sg->createAnimationDriver(m_sg);
    connect(m_animationDriver, SIGNAL(started()), this, SLOT(started()));
    connect(m_animationDriver, SIGNAL(stopped()), this, SLOT(stopped()));
    m_animationDriver->install();

    qsg_render_timer.start();
}

QSGWindowsRenderLoop::~QSGWindowsRenderLoop()
{
    delete m_rc;
    delete m_sg;
}

bool QSGWindowsRenderLoop::interleaveIncubation() const
{
    return m_animationDriver->isRunning() && anyoneShowing();
}

QSGWindowsRenderLoop::WindowData *QSGWindowsRenderLoop::windowData(QQuickWindow *window)
{
    for (int i=0; i<m_windows.size(); ++i) {
        WindowData &wd = m_windows[i];
        if (wd.window == window)
            return &wd;
    }
    return nullptr;
}

void QSGWindowsRenderLoop::maybePostUpdateTimer()
{
    if (!m_updateTimer) {
        RLDEBUG(" - posting event");
        m_updateTimer = startTimer(m_vsyncDelta / 3);
    }
}

/*
 * If no windows are showing, start ticking animations using a timer,
 * otherwise, start rendering
 */
void QSGWindowsRenderLoop::started()
{
    RLDEBUG("Animations started...");
    if (!anyoneShowing()) {
        if (m_animationTimer == 0) {
            RLDEBUG(" - starting non-visual animation timer");
            m_animationTimer = startTimer(m_vsyncDelta);
        }
    } else {
        maybePostUpdateTimer();
    }
}

void QSGWindowsRenderLoop::stopped()
{
    RLDEBUG("Animations stopped...");
    if (m_animationTimer) {
        RLDEBUG(" - stopping non-visual animation timer");
        killTimer(m_animationTimer);
        m_animationTimer = 0;
    }
}

void QSGWindowsRenderLoop::show(QQuickWindow *window)
{
    RLDEBUG("show");
    if (windowData(window) != nullptr)
        return;

    // This happens before the platform window is shown, but after
    // it is created. Creating the GL context takes a lot of time
    // (hundreds of milliseconds) and will prevent us from rendering
    // the first frame in time for the initial show on screen.
    // By preparing the GL context here, it is feasible (if the app
    // is quick enough) to have a perfect first frame.
    if (!m_gl) {
        RLDEBUG(" - creating GL context");
        m_gl = new QOpenGLContext();
        m_gl->setFormat(window->requestedFormat());
        m_gl->setScreen(window->screen());
        if (qt_gl_global_share_context())
            m_gl->setShareContext(qt_gl_global_share_context());
        bool created = m_gl->create();
        if (!created) {
            delete m_gl;
            m_gl = nullptr;
            handleContextCreationFailure(window);
            return;
        }

        QQuickWindowPrivate::get(window)->fireOpenGLContextCreated(m_gl);

        RLDEBUG(" - making current");
        bool current = m_gl->makeCurrent(window);
        RLDEBUG(" - initializing SG");
        if (current) {
            QSGDefaultRenderContext::InitParams rcParams;
            rcParams.sampleCount = qMax(1, m_gl->format().samples());
            rcParams.openGLContext = m_gl;
            rcParams.initialSurfacePixelSize = window->size() * window->effectiveDevicePixelRatio();
            rcParams.maybeSurface = window;
            m_rc->initialize(&rcParams);
        }
    }

    WindowData data;
    data.window = window;
    data.pendingUpdate = false;
    m_windows << data;

    RLDEBUG(" - done with show");
}

void QSGWindowsRenderLoop::hide(QQuickWindow *window)
{
    RLDEBUG("hide");
    // The expose event is queued while hide is sent synchronously, so
    // the value might not be updated yet. (plus that the windows plugin
    // sends exposed=true when it goes to hidden, so it is doubly broken)
    // The check is made here, after the removal from m_windows, so
    // anyoneShowing will report the right value.
    if (window->isExposed())
        handleObscurity();
    if (!m_gl)
        return;
    QQuickWindowPrivate::get(window)->fireAboutToStop();
}

void QSGWindowsRenderLoop::windowDestroyed(QQuickWindow *window)
{
    RLDEBUG("windowDestroyed");
    for (int i=0; i<m_windows.size(); ++i) {
        if (m_windows.at(i).window == window) {
            m_windows.removeAt(i);
            break;
        }
    }

    hide(window);

    QQuickWindowPrivate *d = QQuickWindowPrivate::get(window);

    bool current = false;
    QScopedPointer<QOffscreenSurface> offscreenSurface;
    if (m_gl) {
        QSurface *surface = window;
        // There may be no platform window if the window got closed.
        if (!window->handle()) {
            offscreenSurface.reset(new QOffscreenSurface);
            offscreenSurface->setFormat(m_gl->format());
            offscreenSurface->create();
            surface = offscreenSurface.data();
        }
        current = m_gl->makeCurrent(surface);
    }
    if (Q_UNLIKELY(!current))
        RLDEBUG("cleanup without an OpenGL context");

    d->cleanupNodesOnShutdown();

#if QT_CONFIG(quick_shadereffect) && QT_CONFIG(opengl)
    if (current)
        QQuickOpenGLShaderEffectMaterial::cleanupMaterialCache();
#endif

    if (m_windows.size() == 0) {
        d->context->invalidate();
        delete m_gl;
        m_gl = nullptr;
    } else if (m_gl && current) {
        m_gl->doneCurrent();
    }

    d->animationController.reset();
}

bool QSGWindowsRenderLoop::anyoneShowing() const
{
    for (const WindowData &wd : qAsConst(m_windows))
        if (wd.window->isVisible() && wd.window->isExposed() && wd.window->size().isValid())
            return true;
    return false;
}

void QSGWindowsRenderLoop::exposureChanged(QQuickWindow *window)
{

    if (windowData(window) == nullptr)
        return;

    if (window->isExposed() && window->isVisible()) {

        // Stop non-visual animation timer as we now have a window rendering
        if (m_animationTimer && anyoneShowing()) {
            RLDEBUG(" - stopping non-visual animation timer");
            killTimer(m_animationTimer);
            m_animationTimer = 0;
        }

        RLDEBUG("exposureChanged - exposed");
        WindowData *wd = windowData(window);
        wd->pendingUpdate = true;

        // 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) {
            RLDEBUG(" - killing pending update timer");
            killTimer(m_updateTimer);
            m_updateTimer = 0;
        }
        render();
    } else {
        handleObscurity();
    }
}

void QSGWindowsRenderLoop::handleObscurity()
{
    RLDEBUG("handleObscurity");
    // Potentially start the non-visual animation timer if nobody is rendering
    if (m_animationDriver->isRunning() && !anyoneShowing() && !m_animationTimer) {
        RLDEBUG(" - starting non-visual animation timer");
        m_animationTimer = startTimer(m_vsyncDelta);
    }
}

QImage QSGWindowsRenderLoop::grab(QQuickWindow *window)
{
    RLDEBUG("grab");
    if (!m_gl)
        return QImage();

    m_gl->makeCurrent(window);

    QQuickWindowPrivate *d = QQuickWindowPrivate::get(window);
    d->polishItems();
    d->syncSceneGraph();
    d->renderSceneGraph(window->size());

    bool alpha = window->format().alphaBufferSize() > 0 && window->color().alpha() != 255;
    QImage image = qt_gl_read_framebuffer(window->size() * window->effectiveDevicePixelRatio(), alpha, alpha);
    image.setDevicePixelRatio(window->effectiveDevicePixelRatio());
    return image;
}

void QSGWindowsRenderLoop::update(QQuickWindow *window)
{
    RLDEBUG("update");
    maybeUpdate(window);
}

void QSGWindowsRenderLoop::maybeUpdate(QQuickWindow *window)
{
    RLDEBUG("maybeUpdate");

    WindowData *wd = windowData(window);
    if (!wd || !anyoneShowing())
        return;

    wd->pendingUpdate = true;
    maybePostUpdateTimer();
}

QSGRenderContext *QSGWindowsRenderLoop::createRenderContext(QSGContext *) const
{
    return m_rc;
}

bool QSGWindowsRenderLoop::event(QEvent *event)
{
    switch (event->type()) {
    case QEvent::Timer: {
        QTimerEvent *te = static_cast<QTimerEvent *>(event);
        if (te->timerId() == m_animationTimer) {
            RLDEBUG("event : animation tick while nothing is showing");
            m_animationDriver->advance();
        } else if (te->timerId() == m_updateTimer) {
            RLDEBUG("event : update");
            killTimer(m_updateTimer);
            m_updateTimer = 0;
            render();
        }
        return true; }
    default:
        break;
    }

    return QObject::event(event);
}

/*
 * Go through all windows we control and render them in turn.
 * Then tick animations if active.
 */
void QSGWindowsRenderLoop::render()
{
    RLDEBUG("render");
    Q_TRACE(QSG_render_entry);
    bool rendered = false;
    for (const WindowData &wd : qAsConst(m_windows)) {
        if (wd.pendingUpdate) {
            const_cast<WindowData &>(wd).pendingUpdate = false;
            renderWindow(wd.window);
            rendered = true;
        }
    }

    if (!rendered) {
        RLDEBUG("no changes, sleep");
        QThread::msleep(m_vsyncDelta);
    }

    Q_TRACE(QSG_render_exit);

    if (m_animationDriver->isRunning()) {
        RLDEBUG("advancing animations");
        QSG_LOG_TIME_SAMPLE(time_start);
        Q_QUICK_SG_PROFILE_START(QQuickProfiler::SceneGraphWindowsAnimations);
        Q_TRACE(QSG_animations_entry);
        m_animationDriver->advance();
        RLDEBUG("animations advanced");

        qCDebug(QSG_LOG_TIME_RENDERLOOP,
                "animations ticked in %dms",
                int((qsg_render_timer.nsecsElapsed() - time_start)/1000000));

        Q_TRACE(QSG_animations_exit);
        Q_QUICK_SG_PROFILE_END(QQuickProfiler::SceneGraphWindowsAnimations, 1);

        // 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();
    }
}

/*
 * Render the contents of this window. First polish, then sync, render
 * then finally swap.
 *
 * Note: This render function does not implement aborting
 * the render call when sync step results in no scene graph changes,
 * like the threaded renderer does.
 */
void QSGWindowsRenderLoop::renderWindow(QQuickWindow *window)
{
    RLDEBUG("renderWindow");
    QQuickWindowPrivate *d = QQuickWindowPrivate::get(window);

    if (!d->isRenderable())
        return;

    if (!m_gl->makeCurrent(window)) {
        // Check for context loss.
        if (!m_gl->isValid()) {
            d->cleanupNodesOnShutdown();
            m_rc->invalidate();
            if (m_gl->create() && m_gl->makeCurrent(window)) {
                QSGDefaultRenderContext::InitParams rcParams;
                rcParams.sampleCount = qMax(1, m_gl->format().samples());
                rcParams.openGLContext = m_gl;
                rcParams.initialSurfacePixelSize = window->size() * window->effectiveDevicePixelRatio();
                rcParams.maybeSurface = window;
                m_rc->initialize(&rcParams);
            } else {
                return;
            }
        }
    }

    bool lastDirtyWindow = true;
    for (int i=0; i<m_windows.size(); ++i) {
        if ( m_windows[i].pendingUpdate) {
            lastDirtyWindow = false;
            break;
        }
    }

    d->flushFrameSynchronousEvents();
    // Event delivery or processing has caused the window to stop rendering.
    if (!windowData(window))
        return;

    Q_TRACE_SCOPE(QSG_renderWindow);

    QSG_LOG_TIME_SAMPLE(time_start);
    Q_QUICK_SG_PROFILE_START(QQuickProfiler::SceneGraphPolishFrame);
    Q_TRACE(QSG_polishItems_entry);

    RLDEBUG(" - polishing");
    d->polishItems();
    QSG_LOG_TIME_SAMPLE(time_polished);
    Q_TRACE(QSG_polishItems_exit);
    Q_QUICK_SG_PROFILE_SWITCH(QQuickProfiler::SceneGraphPolishFrame,
                              QQuickProfiler::SceneGraphRenderLoopFrame,
                              QQuickProfiler::SceneGraphPolishPolish);
    Q_TRACE(QSG_sync_entry);

    emit window->afterAnimating();

    RLDEBUG(" - syncing");
    d->syncSceneGraph();
    if (lastDirtyWindow)
        m_rc->endSync();
    Q_TRACE(QSG_sync_exit);
    QSG_RENDER_TIMING_SAMPLE(QQuickProfiler::SceneGraphRenderLoopFrame, time_synced,
                             QQuickProfiler::SceneGraphRenderLoopSync);
    Q_TRACE(QSG_render_entry);

    RLDEBUG(" - rendering");
    d->renderSceneGraph(window->size());
    Q_TRACE(QSG_render_exit);
    QSG_RENDER_TIMING_SAMPLE(QQuickProfiler::SceneGraphRenderLoopFrame, time_rendered,
                             QQuickProfiler::SceneGraphRenderLoopRender);
    Q_TRACE(QSG_swap_entry);

    RLDEBUG(" - swapping");
    if (!d->customRenderStage || !d->customRenderStage->swap())
        m_gl->swapBuffers(window);
    Q_TRACE(QSG_swap_exit);
    QSG_RENDER_TIMING_SAMPLE(QQuickProfiler::SceneGraphRenderLoopFrame, time_swapped,
                             QQuickProfiler::SceneGraphRenderLoopSwap);

    RLDEBUG(" - frameDone");
    d->fireFrameSwapped();

    qCDebug(QSG_LOG_TIME_RENDERLOOP()).nospace()
            << "Frame rendered with 'windows' renderloop in: " << (time_swapped - time_start) / 1000000 << "ms"
            << ", polish=" << (time_polished - time_start) / 1000000
            << ", sync=" << (time_synced - time_polished) / 1000000
            << ", render=" << (time_rendered - time_synced) / 1000000
            << ", swap=" << (time_swapped - time_rendered) / 1000000
            << " - " << window;

    Q_QUICK_SG_PROFILE_REPORT(QQuickProfiler::SceneGraphRenderLoopFrame,
                              QQuickProfiler::SceneGraphRenderLoopSwap);
}

void QSGWindowsRenderLoop::releaseResources(QQuickWindow *w)
{
    // No full invalidation of the rendercontext, just clear some caches.
    RLDEBUG("releaseResources");
    QQuickWindowPrivate *d = QQuickWindowPrivate::get(w);
    if (d->renderer)
        d->renderer->releaseCachedResources();
}

QT_END_NAMESPACE

#include "moc_qsgwindowsrenderloop_p.cpp"
