/*
    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 "effect/offscreenquickview.h"
#include "effect/effecthandler.h"

#include "compositor.h"
#include "core/drmdevice.h"
#include "core/inputdevice.h"
#include "core/renderbackend.h"
#include "input_event.h"
#include "logging_p.h"
#include "opengl/eglcontext.h"
#include "opengl/eglnativefence.h"
#include "opengl/eglswapchain.h"
#include "opengl/glrendertimequery.h"
#include "opengl/glutils.h"
#include "scene/imageitem.h"
#include "scene/itemrenderer.h"
#include "scene/surfaceitem.h"
#include "scene/texture.h"
#include "scene/workspacescene.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>
#include <private/qeventpoint_p.h> // for QMutableEventPoint

namespace KWin
{

class Q_DECL_HIDDEN OffscreenQuickView::Private
{
public:
    QPointingDevice *qtPointingDeviceForKWinDevice(InputDevice *device) const
    {
        if (device && device->isTouchpad()) {
            return touchpadDevice;
        } else {
            return mouseDevice;
        }
    }

    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::shared_ptr<EglSwapchain> m_swapchain;
    std::shared_ptr<EglSwapchainSlot> m_currentSlot;
    std::unique_ptr<ImageItem> m_imageItem;
    std::unique_ptr<SurfaceItem> m_surfaceItem;
    Item *m_item = nullptr;

    DrmDevice *m_scanoutDevice = nullptr;
    FormatModifierMap m_scanoutFormats;
    bool m_surfaceNeedsReallocation = false;

    std::unique_ptr<QTimer> m_repaintTimer;
    QImage m_image;
    std::unique_ptr<GLTexture> m_textureExport;
    // if we should capture a QImage after rendering into our BO.
    // Used for either software QtQuick rendering and nonGL kwin rendering
    bool m_useBlit = false;
    bool m_visible = true;
    bool m_hasAlphaChannel = true;
    bool m_automaticRepaint = true;

    std::optional<qreal> m_explicitDpr;

    QPointingDevice *mouseDevice;
    QPointingDevice *touchpadDevice;

    QList<QEventPoint> touchPoints;
    QSet<uint32_t> acceptedTouchPoints;
    QPointingDevice *touchDevice;

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

    void releaseResources();

    void updateTouchState(Qt::TouchPointState state, qint32 id, const QPointF &pos);
};

class Q_DECL_HIDDEN OffscreenQuickScene::Private
{
public:
    Private(OffscreenQuickScene *q)
        : q(q)
    {
    }

    void createItem(const QVariantMap &initialProperties);

    OffscreenQuickScene *q;
    std::unique_ptr<QQmlComponent> qmlComponent;
    std::unique_ptr<QQuickItem> quickItem;
};

class QuickViewItem : public SurfaceItem
{
public:
    explicit QuickViewItem(OffscreenQuickView *view, Item *parentItem)
        : SurfaceItem(parentItem)
        , m_view(view)
    {
    }

    RegionF shape() const override
    {
        return rect();
    }
    RegionF opaque() const override
    {
        return hasAlphaChannel() ? RegionF{} : rect();
    }
    void setScanoutHint(DrmDevice *device, const FormatModifierMap &drmFormats) override
    {
        if (m_view->d->m_scanoutDevice == device
            && m_view->d->m_scanoutFormats == drmFormats) {
            return;
        }
        m_view->d->m_scanoutDevice = device;
        m_view->d->m_scanoutFormats = drmFormats;
        m_view->d->m_surfaceNeedsReallocation = true;
    }

private:
    OffscreenQuickView *const m_view;
};

OffscreenQuickView::OffscreenQuickView(ExportMode exportMode, bool alpha)
    : d(new OffscreenQuickView::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);

    d->m_hasAlphaChannel = alpha;
    if (exportMode == ExportMode::Image) {
        d->m_useBlit = true;
    }

    const bool usingGl = d->m_view->rendererInterface()->graphicsApi() == QSGRendererInterface::OpenGL;

    if (!usingGl) {
        qCDebug(LIBKWINEFFECTS) << "QtQuick Software rendering mode detected";
        d->m_useBlit = true;
        d->m_imageItem = std::make_unique<ImageItem>(effects->scene()->overlayItem());
        d->m_item = d->m_imageItem.get();
        // explicitly do not call QQuickRenderControl::initialize, see Qt docs
    } else {
        QSurfaceFormat format;
        format.setOption(QSurfaceFormat::ResetNotification);
        format.setDepthBufferSize(16);
        format.setStencilBufferSize(8);
        if (alpha) {
            format.setAlphaBufferSize(8);
        }

        d->m_view->setFormat(format);

        d->m_glcontext = std::make_unique<QOpenGLContext>();
        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();
        d->m_surfaceItem = std::make_unique<QuickViewItem>(this, effects->scene()->overlayItem());
        d->m_item = d->m_surfaceItem.get();
    }
    connect(this, &OffscreenQuickView::geometryChanged, d->m_item, [this]() {
        d->m_item->setGeometry(geometry());
    });

    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, [this]() {
        update(nullptr);
    });
    connect(d->m_renderControl.get(), &QQuickRenderControl::renderRequested, this, &OffscreenQuickView::handleRenderRequested);
    connect(d->m_renderControl.get(), &QQuickRenderControl::sceneChanged, this, &OffscreenQuickView::handleSceneChanged);

    d->mouseDevice = new QPointingDevice(QStringLiteral("ForwardingMouseDevice"), {}, QInputDevice::DeviceType::Mouse, QPointingDevice::PointerType::Generic, QInputDevice::Capability::Position, 1, 0);
    d->touchpadDevice = new QPointingDevice(QStringLiteral("ForwardingTouchpadDevice"), {}, QInputDevice::DeviceType::TouchPad, QPointingDevice::PointerType::Finger, QInputDevice::Capability::Position, 10, 3);
    d->touchDevice = new QPointingDevice(QStringLiteral("ForwardingTouchDevice"), {}, QInputDevice::DeviceType::TouchScreen, QPointingDevice::PointerType::Finger, QInputDevice::Capability::Position, 10, {});
}

OffscreenQuickView::~OffscreenQuickView()
{
    disconnect(d->m_renderControl.get(), &QQuickRenderControl::renderRequested, this, &OffscreenQuickView::handleRenderRequested);
    disconnect(d->m_renderControl.get(), &QQuickRenderControl::sceneChanged, this, &OffscreenQuickView::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();
}

bool OffscreenQuickView::automaticRepaint() const
{
    return d->m_automaticRepaint;
}

void OffscreenQuickView::setAutomaticRepaint(bool set)
{
    if (d->m_automaticRepaint != set) {
        d->m_automaticRepaint = set;

        // If there's an in-flight update, disable it.
        if (!d->m_automaticRepaint) {
            d->m_repaintTimer->stop();
        }
    }
}

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

void OffscreenQuickView::handleSceneChanged()
{
    if (d->m_automaticRepaint) {
        d->m_repaintTimer->start();
    } else {
        d->m_item->scheduleFrame();
    }
    Q_EMIT sceneChanged();
}

void OffscreenQuickView::handleRenderRequested()
{
    if (d->m_automaticRepaint) {
        d->m_repaintTimer->start();
    } else {
        d->m_item->scheduleFrame();
    }
    Q_EMIT renderRequested();
}

void OffscreenQuickView::update(OutputFrame *frame)
{
    if (!d->m_visible) {
        return;
    }
    if (d->m_view->size().isEmpty()) {
        return;
    }

    bool usingGl = d->m_glcontext != nullptr;
    EglContext *previousContext = EglContext::currentContext();
    std::unique_ptr<GLRenderTimeQuery> renderTime;

    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;
        }
        if (frame) {
            renderTime = std::make_unique<GLRenderTimeQuery>();
        }

        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_swapchain || d->m_swapchain->size() != nativeSize || d->m_surfaceNeedsReallocation) {
            d->m_textureExport.reset(nullptr);

            QOpenGLFramebufferObjectFormat fboFormat;
            fboFormat.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
            fboFormat.setInternalTextureFormat(GL_RGBA8);

            const uint32_t format = d->m_hasAlphaChannel ? DRM_FORMAT_ARGB8888 : DRM_FORMAT_XRGB8888;
            if (d->m_scanoutDevice) {
                d->m_swapchain = EglSwapchain::create(d->m_scanoutDevice->allocator(),
                                                      EglContext::currentContext(), nativeSize,
                                                      format, d->m_scanoutFormats.value(format));
            }
            if (!d->m_swapchain) {
                // TODO add non-scanout feedback on the item for this?
                d->m_swapchain = EglSwapchain::create(Compositor::self()->backend()->drmDevice()->allocator(),
                                                      EglContext::currentContext(), nativeSize,
                                                      format, Compositor::self()->backend()->supportedFormats()[format]);
            }
            if (!d->m_swapchain) {
                d->m_glcontext->doneCurrent();
                qCWarning(LIBKWINEFFECTS, "Creating a swapchain for OffscreenQuickView failed!");
                return;
            }
            d->m_surfaceNeedsReallocation = false;
        }
        d->m_currentSlot = d->m_swapchain->acquire();
        if (!d->m_currentSlot) {
            d->m_glcontext->doneCurrent();
            qCWarning(LIBKWINEFFECTS, "Acquire for OffscreenQuickView failed!");
            return;
        }

        QQuickRenderTarget renderTarget = QQuickRenderTarget::fromOpenGLTexture(d->m_currentSlot->texture()->texture(), d->m_swapchain->size());
        renderTarget.setDevicePixelRatio(dpr);
        renderTarget.setMirrorVertically(true);
        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 (d->m_useBlit) {
        d->m_image = d->m_view->grabWindow();
        d->m_imageItem->setImage(d->m_image);
        d->m_imageItem->setSize(geometry().size());
    } else {
        d->m_surfaceItem->setBuffer(d->m_currentSlot->buffer());
        d->m_surfaceItem->setBufferReleasePoint(d->m_currentSlot->releasePoint());
        d->m_surfaceItem->setBufferSourceBox(Rect(QPoint(), d->m_swapchain->size()));
        d->m_surfaceItem->setDestinationSize(geometry().size());
        d->m_surfaceItem->addDamage(Rect(QPoint(), d->m_swapchain->size()));
    }
    d->m_item->setPosition(geometry().topLeft());

    if (usingGl) {
        EGLNativeFence fence(EglContext::currentContext()->displayObject());
        d->m_swapchain->release(d->m_currentSlot, fence.takeFileDescriptor());
        QOpenGLFramebufferObject::bindDefault();
        if (frame && renderTime) {
            renderTime->end();
            frame->addRenderTimeQuery(std::move(renderTime));
        }
        d->m_glcontext->doneCurrent();
        if (previousContext) {
            (void)previousContext->makeCurrent();
        }
    }
    d->m_item->scheduleRepaint(d->m_item->rect());
}

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

void OffscreenQuickView::forwardPointerMotionEvent(PointerMotionEvent *event)
{
    const QPointF localPosition = d->m_view->mapFromGlobal(event->position);
    QMouseEvent mouseEvent(QEvent::MouseMove,
                           localPosition,
                           event->position,
                           Qt::NoButton,
                           event->buttons,
                           event->modifiers,
                           d->qtPointingDeviceForKWinDevice(event->device));
    mouseEvent.setTimestamp(std::chrono::duration_cast<std::chrono::milliseconds>(event->timestamp).count());
    mouseEvent.setAccepted(false);
    QCoreApplication::sendEvent(d->m_view.get(), &mouseEvent);
}

void OffscreenQuickView::forwardPointerButtonEvent(PointerButtonEvent *event)
{
    const QPointF localPosition = d->m_view->mapFromGlobal(event->position);
    QMouseEvent mouseEvent(event->state == PointerButtonState::Pressed ? QEvent::MouseButtonPress : QEvent::MouseButtonRelease,
                           localPosition,
                           event->position,
                           event->button,
                           event->buttons,
                           event->modifiers,
                           d->qtPointingDeviceForKWinDevice(event->device));
    const auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(event->timestamp).count();
    mouseEvent.setTimestamp(timestamp);
    mouseEvent.setAccepted(false);
    QCoreApplication::sendEvent(d->m_view.get(), &mouseEvent);

    if (event->state == PointerButtonState::Pressed) {
        const ulong doubleClickInterval = static_cast<ulong>(QGuiApplication::styleHints()->mouseDoubleClickInterval());
        const bool doubleClick = (timestamp - d->lastMousePressTime < doubleClickInterval) && event->button == d->lastMousePressButton;
        d->lastMousePressTime = timestamp;
        d->lastMousePressButton = event->button;
        if (doubleClick) {
            d->lastMousePressButton = Qt::NoButton;
            QMouseEvent doubleClickEvent(QEvent::MouseButtonDblClick,
                                         localPosition,
                                         event->position,
                                         event->button,
                                         event->buttons,
                                         event->modifiers,
                                         d->qtPointingDeviceForKWinDevice(event->device));
            QCoreApplication::sendEvent(d->m_view.get(), &doubleClickEvent);
        }
    }
}

void OffscreenQuickView::forwardPointerAxisEvent(PointerAxisEvent *event)
{
    const QPointF localPosition = d->m_view->mapFromGlobal(event->position);
    QWheelEvent wheelEvent(localPosition,
                           event->position,
                           QPoint(),
                           // Qt expects angleDelta 120 to be a "click" whereas libinput uses 15, hence multiply by 8.
                           (event->orientation == Qt::Horizontal) ? QPoint(event->delta, 0) : QPoint(0, event->delta) * -8,
                           event->buttons,
                           event->modifiers,
                           Qt::NoScrollPhase,
                           event->inverted,
                           Qt::MouseEventNotSynthesized,
                           d->qtPointingDeviceForKWinDevice(event->device));
    wheelEvent.setTimestamp(std::chrono::duration_cast<std::chrono::milliseconds>(event->timestamp).count());
    wheelEvent.setAccepted(false);
    QCoreApplication::sendEvent(d->m_view.get(), &wheelEvent);
}

bool OffscreenQuickView::forwardTouchDown(qint32 id, const QPointF &pos, std::chrono::microseconds time)
{
    d->updateTouchState(Qt::TouchPointPressed, id, pos);

    QTouchEvent event(QEvent::TouchBegin, d->touchDevice, Qt::NoModifier, d->touchPoints);
    event.setTimestamp(std::chrono::duration_cast<std::chrono::milliseconds>(time).count());
    event.setAccepted(false);
    QCoreApplication::sendEvent(d->m_view.get(), &event);

    const bool ret = event.isAccepted();
    if (ret) {
        d->acceptedTouchPoints.insert(id);
    }
    return ret;
}

bool OffscreenQuickView::forwardTouchMotion(qint32 id, const QPointF &pos, std::chrono::microseconds time)
{
    d->updateTouchState(Qt::TouchPointMoved, id, pos);

    if (!d->acceptedTouchPoints.contains(id)) {
        return false;
    }

    QTouchEvent event(QEvent::TouchUpdate, d->touchDevice, Qt::NoModifier, d->touchPoints);
    event.setTimestamp(std::chrono::duration_cast<std::chrono::milliseconds>(time).count());
    event.setAccepted(false);
    QCoreApplication::sendEvent(d->m_view.get(), &event);

    return event.isAccepted();
}

bool OffscreenQuickView::forwardTouchUp(qint32 id, std::chrono::microseconds time)
{
    d->updateTouchState(Qt::TouchPointReleased, id, QPointF{});

    if (!d->acceptedTouchPoints.contains(id)) {
        return false;
    }

    QTouchEvent event(QEvent::TouchEnd, d->touchDevice, Qt::NoModifier, d->touchPoints);
    event.setTimestamp(std::chrono::duration_cast<std::chrono::milliseconds>(time).count());
    event.setAccepted(false);
    QCoreApplication::sendEvent(d->m_view.get(), &event);

    d->acceptedTouchPoints.remove(id);

    return event.isAccepted();
}

void OffscreenQuickView::forwardTouchCancel()
{
    d->acceptedTouchPoints.clear();
    d->touchPoints.clear();
    QTouchEvent event(QEvent::TouchCancel, d->touchDevice, Qt::NoModifier, d->touchPoints);
    event.setTimestamp(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch()).count());
    event.setAccepted(false);
    QCoreApplication::sendEvent(d->m_view.get(), &event);
}

Rect OffscreenQuickView::geometry() const
{
    return d->m_view->geometry();
}

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

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

bool OffscreenQuickView::hasAlphaChannel() const
{
    return d->m_hasAlphaChannel;
}

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

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

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

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

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

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

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

void OffscreenQuickView::scheduleFrame()
{
    d->m_item->scheduleFrame();
}

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

void OffscreenQuickView::setGeometry(const Rect &rect)
{
    const Rect 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 OffscreenQuickView::Private::releaseResources()
{
    if (m_glcontext) {
        m_glcontext->makeCurrent(m_offscreenSurface.get());
        m_view->releaseResources();
        m_glcontext->doneCurrent();
    } else {
        m_view->releaseResources();
    }
}

void OffscreenQuickView::Private::updateTouchState(Qt::TouchPointState state, qint32 id, const QPointF &pos)
{
    // Remove the points that were previously in a released state, since they
    // are no longer relevant. Additionally, reset the state of all remaining
    // points to Stationary so we only have one touch point with a different
    // state.
    touchPoints.erase(std::remove_if(touchPoints.begin(), touchPoints.end(), [](QTouchEvent::TouchPoint &point) {
        if (point.state() == QEventPoint::Released) {
            return true;
        }
        QMutableEventPoint::setState(point, QEventPoint::Stationary);
        return false;
    }),
                      touchPoints.end());

    // QtQuick Pointer Handlers incorrectly consider a touch point with ID 0
    // to be an invalid touch point. This has been fixed in Qt 6 but could not
    // be fixed for Qt 5. Instead, we offset kwin's internal IDs with this
    // offset to trick QtQuick into treating them as valid points.
    static const qint32 idOffset = 111;

    // Find the touch point that has changed. This is separate from the above
    // loop because removing the released touch points invalidates iterators.
    auto changed = std::find_if(touchPoints.begin(), touchPoints.end(), [id](const QTouchEvent::TouchPoint &point) {
        return point.id() == id + idOffset;
    });

    switch (state) {
    case Qt::TouchPointPressed: {
        if (changed != touchPoints.end()) {
            return;
        }

        QTouchEvent::TouchPoint point;
        QMutableEventPoint::setState(point, QEventPoint::Pressed);
        QMutableEventPoint::setId(point, id + idOffset);
        QMutableEventPoint::setGlobalPosition(point, pos);
        QMutableEventPoint::setScenePosition(point, m_view->mapFromGlobal(pos.toPoint()));
        QMutableEventPoint::setPosition(point, m_view->mapFromGlobal(pos.toPoint()));

        touchPoints.append(point);
    } break;
    case Qt::TouchPointMoved: {
        if (changed == touchPoints.end()) {
            return;
        }

        auto &point = *changed;
        QMutableEventPoint::setGlobalLastPosition(point, point.globalPosition());
        QMutableEventPoint::setState(point, QEventPoint::Updated);
        QMutableEventPoint::setScenePosition(point, m_view->mapFromGlobal(pos.toPoint()));
        QMutableEventPoint::setPosition(point, m_view->mapFromGlobal(pos.toPoint()));
        QMutableEventPoint::setGlobalPosition(point, pos);
    } break;
    case Qt::TouchPointReleased: {
        if (changed == touchPoints.end()) {
            return;
        }

        auto &point = *changed;
        QMutableEventPoint::setGlobalLastPosition(point, point.globalPosition());
        QMutableEventPoint::setState(point, QEventPoint::Released);
    } break;
    default:
        break;
    }
}

OffscreenQuickScene::OffscreenQuickScene(OffscreenQuickView::ExportMode exportMode, bool alpha)
    : OffscreenQuickView(exportMode, alpha)
    , d(new OffscreenQuickScene::Private(this))
{
}

OffscreenQuickScene::~OffscreenQuickScene() = default;

void OffscreenQuickScene::setSource(const QUrl &source)
{
    setSource(source, QVariantMap());
}

void OffscreenQuickScene::setSource(const QUrl &source, const QVariantMap &initialProperties)
{
    if (!d->qmlComponent) {
        d->qmlComponent = std::make_unique<QQmlComponent>(effects->qmlEngine());
    }

    d->qmlComponent->loadUrl(source);
    if (d->qmlComponent->isError()) {
        qCWarning(LIBKWINEFFECTS).nospace() << "Failed to load effect quick view " << source << ": " << d->qmlComponent->errors();
        d->qmlComponent.reset();
        return;
    }
    d->createItem(initialProperties);
}

void OffscreenQuickScene::loadFromModule(const QString &uri, const QString &typeName, const QVariantMap &initialProperties)
{
    if (!d->qmlComponent) {
        d->qmlComponent = std::make_unique<QQmlComponent>(effects->qmlEngine());
    }

    d->qmlComponent->loadFromModule(uri, typeName);
    if (d->qmlComponent->isError()) {
        qCWarning(LIBKWINEFFECTS).nospace() << "Failed to load effect quick view " << (uri + u'.' + typeName) << ": " << d->qmlComponent->errors();
        d->qmlComponent.reset();
        return;
    }
    d->createItem(initialProperties);
}

QQuickItem *OffscreenQuickScene::rootItem() const
{
    return d->quickItem.get();
}

void OffscreenQuickScene::Private::createItem(const QVariantMap &initialProperties)
{
    quickItem.reset();

    std::unique_ptr<QObject> qmlObject(qmlComponent->createWithInitialProperties(initialProperties));
    QQuickItem *item = qobject_cast<QQuickItem *>(qmlObject.get());
    if (!item) {
        qCWarning(LIBKWINEFFECTS) << "Root object of effect quick view" << qmlComponent->url() << "is not a QQuickItem";
        return;
    }

    qmlObject.release();
    quickItem.reset(item);

    item->setParentItem(q->contentItem());

    auto updateSize = [item, this]() {
        item->setSize(q->contentItem()->size());
    };
    updateSize();
    connect(q->contentItem(), &QQuickItem::widthChanged, item, updateSize);
    connect(q->contentItem(), &QQuickItem::heightChanged, item, updateSize);
}

} // namespace KWin

#include "moc_offscreenquickview.cpp"
