/*
    KWin - the KDE window manager
    This file is part of the KDE project.

    SPDX-FileCopyrightText: 2019 David Edmundson <davidedmundson@kde.org>

    SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "renderer.h"

#include <QGuiApplication>
#include <QQmlComponent>
#include <QQmlContext>
#include <QQmlEngine>
#include <QQuickItem>
#include <QQuickRenderControl>
#include <QQuickView>
#include <QStyleHints>

#include <QOffscreenSurface>
#include <QOpenGLContext>
#include <QOpenGLFramebufferObject>
#include <QQuickGraphicsDevice>
#include <QQuickOpenGLUtils>
#include <QQuickRenderTarget>
#include <QTimer>

namespace Aurorae
{

class Renderer::Private
{
public:
    std::unique_ptr<QQuickWindow> m_view;
    std::unique_ptr<QQuickRenderControl> m_renderControl;
    std::unique_ptr<QOffscreenSurface> m_offscreenSurface;
    std::unique_ptr<QOpenGLContext> m_glcontext;
    std::unique_ptr<QOpenGLFramebufferObject> m_fbo;

    std::unique_ptr<QTimer> m_repaintTimer;
    QImage m_image;
    bool m_visible = true;

    std::optional<qreal> m_explicitDpr;

    ulong lastMousePressTime = 0;
    Qt::MouseButton lastMousePressButton = Qt::NoButton;

    void releaseResources();
};

Renderer::Renderer()
    : d(new Renderer::Private)
{
    d->m_renderControl = std::make_unique<QQuickRenderControl>();

    d->m_view = std::make_unique<QQuickWindow>(d->m_renderControl.get());
    Q_ASSERT(d->m_view->setProperty("_KWIN_WINDOW_IS_OFFSCREEN", true) || true);
    d->m_view->setFlags(Qt::FramelessWindowHint);
    d->m_view->setColor(Qt::transparent);

    const bool usingGl = d->m_view->rendererInterface()->graphicsApi() == QSGRendererInterface::OpenGL;
    if (usingGl) {
        QSurfaceFormat format;
        format.setOption(QSurfaceFormat::ResetNotification);
        format.setDepthBufferSize(16);
        format.setStencilBufferSize(8);
        format.setAlphaBufferSize(8);

        d->m_view->setFormat(format);

        auto shareContext = QOpenGLContext::globalShareContext();
        d->m_glcontext = std::make_unique<QOpenGLContext>();
        d->m_glcontext->setShareContext(shareContext);
        d->m_glcontext->setFormat(format);
        d->m_glcontext->create();

        // and the offscreen surface
        d->m_offscreenSurface = std::make_unique<QOffscreenSurface>();
        d->m_offscreenSurface->setFormat(d->m_glcontext->format());
        d->m_offscreenSurface->create();

        d->m_glcontext->makeCurrent(d->m_offscreenSurface.get());
        d->m_view->setGraphicsDevice(QQuickGraphicsDevice::fromOpenGLContext(d->m_glcontext.get()));
        d->m_renderControl->initialize();
        d->m_glcontext->doneCurrent();
    }

    auto updateSize = [this]() {
        contentItem()->setSize(d->m_view->size());
    };
    updateSize();
    connect(d->m_view.get(), &QWindow::widthChanged, this, updateSize);
    connect(d->m_view.get(), &QWindow::heightChanged, this, updateSize);

    d->m_repaintTimer = std::make_unique<QTimer>();
    d->m_repaintTimer->setSingleShot(true);
    d->m_repaintTimer->setInterval(10);

    connect(d->m_repaintTimer.get(), &QTimer::timeout, this, &Renderer::update);
    connect(d->m_renderControl.get(), &QQuickRenderControl::renderRequested, this, &Renderer::handleRenderRequested);
    connect(d->m_renderControl.get(), &QQuickRenderControl::sceneChanged, this, &Renderer::handleSceneChanged);
}

Renderer::~Renderer()
{
    disconnect(d->m_renderControl.get(), &QQuickRenderControl::renderRequested, this, &Renderer::handleRenderRequested);
    disconnect(d->m_renderControl.get(), &QQuickRenderControl::sceneChanged, this, &Renderer::handleSceneChanged);

    if (d->m_glcontext) {
        // close the view whilst we have an active GL context
        d->m_glcontext->makeCurrent(d->m_offscreenSurface.get());
    }

    d->m_view.reset();
    d->m_renderControl.reset();
}

void Renderer::setDevicePixelRatio(qreal dpr)
{
    d->m_explicitDpr = dpr;
}

void Renderer::handleSceneChanged()
{
    d->m_repaintTimer->start();
    Q_EMIT sceneChanged();
}

void Renderer::handleRenderRequested()
{
    d->m_repaintTimer->start();
    Q_EMIT renderRequested();
}

void Renderer::update()
{
    if (!d->m_visible) {
        return;
    }
    if (d->m_view->size().isEmpty()) {
        return;
    }

    const bool usingGl = d->m_glcontext != nullptr;
    if (usingGl) {
        if (!d->m_glcontext->makeCurrent(d->m_offscreenSurface.get())) {
            // probably a context loss event, kwin is about to reset all the effects anyway
            return;
        }

        qreal dpr = d->m_view->screen() ? d->m_view->screen()->devicePixelRatio() : 1.0;
        if (d->m_explicitDpr.has_value()) {
            dpr = d->m_explicitDpr.value();
        }

        const QSize nativeSize = d->m_view->size() * dpr;
        if (!d->m_fbo || d->m_fbo->size() != nativeSize) {
            QOpenGLFramebufferObjectFormat fboFormat;
            fboFormat.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
            fboFormat.setInternalTextureFormat(GL_RGBA8);

            d->m_fbo = std::make_unique<QOpenGLFramebufferObject>(nativeSize, fboFormat);
            if (!d->m_fbo->isValid()) {
                d->m_fbo.reset();
                d->m_glcontext->doneCurrent();
                return;
            }
        }

        QQuickRenderTarget renderTarget = QQuickRenderTarget::fromOpenGLTexture(d->m_fbo->texture(), d->m_fbo->size());
        renderTarget.setDevicePixelRatio(dpr);
        d->m_view->setRenderTarget(renderTarget);
    }

    d->m_renderControl->polishItems();
    if (usingGl) {
        d->m_renderControl->beginFrame();
    }
    d->m_renderControl->sync();
    d->m_renderControl->render();
    if (usingGl) {
        d->m_renderControl->endFrame();
    }

    if (usingGl) {
        QQuickOpenGLUtils::resetOpenGLState();
    }

    if (usingGl) {
        d->m_image = d->m_fbo->toImage();
        d->m_image.setDevicePixelRatio(d->m_view->effectiveDevicePixelRatio());
    } else {
        d->m_image = d->m_view->grabWindow();
    }

    Q_EMIT repaintNeeded();
}

void Renderer::forwardMouseEvent(QEvent *e)
{
    if (!d->m_visible) {
        return;
    }
    switch (e->type()) {
    case QEvent::MouseMove:
    case QEvent::MouseButtonPress:
    case QEvent::MouseButtonRelease: {
        QMouseEvent *me = static_cast<QMouseEvent *>(e);
        const QPoint widgetPos = d->m_view->mapFromGlobal(me->pos());
        QMouseEvent cloneEvent(me->type(), widgetPos, me->pos(), me->button(), me->buttons(), me->modifiers());
        cloneEvent.setAccepted(false);
        QCoreApplication::sendEvent(d->m_view.get(), &cloneEvent);
        e->setAccepted(cloneEvent.isAccepted());

        if (e->type() == QEvent::MouseButtonPress) {
            const ulong doubleClickInterval = static_cast<ulong>(QGuiApplication::styleHints()->mouseDoubleClickInterval());
            const bool doubleClick = (me->timestamp() - d->lastMousePressTime < doubleClickInterval) && me->button() == d->lastMousePressButton;
            d->lastMousePressTime = me->timestamp();
            d->lastMousePressButton = me->button();
            if (doubleClick) {
                d->lastMousePressButton = Qt::NoButton;
                QMouseEvent doubleClickEvent(QEvent::MouseButtonDblClick, me->position(), me->globalPosition(), me->button(), me->buttons(), me->modifiers());
                QCoreApplication::sendEvent(d->m_view.get(), &doubleClickEvent);
            }
        }

        return;
    }
    case QEvent::HoverEnter:
    case QEvent::HoverLeave:
    case QEvent::HoverMove: {
        QHoverEvent *he = static_cast<QHoverEvent *>(e);
        const QPointF widgetPos = d->m_view->mapFromGlobal(he->pos());
        const QPointF oldWidgetPos = d->m_view->mapFromGlobal(he->oldPos());
        QHoverEvent cloneEvent(he->type(), widgetPos, oldWidgetPos, he->modifiers());
        cloneEvent.setAccepted(false);
        QCoreApplication::sendEvent(d->m_view.get(), &cloneEvent);
        e->setAccepted(cloneEvent.isAccepted());
        return;
    }
    case QEvent::Wheel: {
        QWheelEvent *we = static_cast<QWheelEvent *>(e);
        const QPointF widgetPos = d->m_view->mapFromGlobal(we->position().toPoint());
        QWheelEvent cloneEvent(widgetPos, we->globalPosition(), we->pixelDelta(), we->angleDelta(), we->buttons(),
                               we->modifiers(), we->phase(), we->inverted());
        cloneEvent.setAccepted(false);
        QCoreApplication::sendEvent(d->m_view.get(), &cloneEvent);
        e->setAccepted(cloneEvent.isAccepted());
        return;
    }
    default:
        return;
    }
}

void Renderer::forwardKeyEvent(QKeyEvent *keyEvent)
{
    if (!d->m_visible) {
        return;
    }
    QCoreApplication::sendEvent(d->m_view.get(), keyEvent);
}

QRect Renderer::geometry() const
{
    return d->m_view->geometry();
}

void Renderer::setOpacity(qreal opacity)
{
    d->m_view->setOpacity(opacity);
}

qreal Renderer::opacity() const
{
    return d->m_view->opacity();
}

QQuickItem *Renderer::contentItem() const
{
    return d->m_view->contentItem();
}

QQuickWindow *Renderer::window() const
{
    return d->m_view.get();
}

void Renderer::setVisible(bool visible)
{
    if (d->m_visible == visible) {
        return;
    }
    d->m_visible = visible;

    if (visible) {
        Q_EMIT d->m_renderControl->renderRequested();
    } else {
        // deferred to not change GL context
        QTimer::singleShot(0, this, [this]() {
            d->releaseResources();
        });
    }
}

bool Renderer::isVisible() const
{
    return d->m_visible;
}

void Renderer::show()
{
    setVisible(true);
}

void Renderer::hide()
{
    setVisible(false);
}

QImage Renderer::bufferAsImage() const
{
    return d->m_image;
}

QSize Renderer::size() const
{
    return d->m_view->geometry().size();
}

void Renderer::setGeometry(const QRect &rect)
{
    const QRect oldGeometry = d->m_view->geometry();
    d->m_view->setGeometry(rect);
    // QWindow::setGeometry() won't sync output if there's no platform window.
    d->m_view->setScreen(QGuiApplication::screenAt(rect.center()));
    Q_EMIT geometryChanged(oldGeometry, rect);
}

void Renderer::Private::releaseResources()
{
    if (m_glcontext) {
        m_glcontext->makeCurrent(m_offscreenSurface.get());
        m_view->releaseResources();
        m_glcontext->doneCurrent();
    } else {
        m_view->releaseResources();
    }
}

} // namespace Aurorae

#include "moc_renderer.cpp"
