// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant

#include "qquickeventreplayservice.h"

#include <QtQuick/qquickwindow.h>

#include <QtGui/qtguiglobal.h>
#include <QtGui/qtestsupport_gui.h>
#include <QtGui/qwindow.h>

QT_BEGIN_NAMESPACE

Q_GUI_EXPORT void qt_handleMouseEvent(
        QWindow *window, const QPointF &local, const QPointF &global, Qt::MouseButtons state,
        Qt::MouseButton button, QEvent::Type type, Qt::KeyboardModifiers mods, int timestamp);

Q_GUI_EXPORT void qt_handleKeyEvent(
        QWindow *w, QEvent::Type t, int k, Qt::KeyboardModifiers mods,
        const QString &text = QString(), bool autorep = false, ushort count = 1);

Q_GUI_EXPORT void qt_handleWheelEvent(
        QWindow *window, const QPointF &local, const QPointF &global, QPoint pixelDelta,
        QPoint angleDelta, Qt::KeyboardModifiers mods, Qt::ScrollPhase phase);

QQuickEventReplayServiceImpl::QQuickEventReplayServiceImpl(QObject *parent)
    : QQuickEventReplayService(1, parent)
{
    m_elapsed.start();
    start();
}

void QQuickEventReplayServiceImpl::messageReceived(const QByteArray &message)
{
    QQmlDebugPacket stream(message);
    QQuickProfilerData data;

    stream >> data.time >> data.messageType >> data.detailType;

    if (data.messageType != Message::Event)
        return;

    switch (data.detailType) {
    case EventType::Key:
    case EventType::Mouse:
        break;
    default:
        return;
    }

    stream >> data.inputType >> data.inputA >> data.inputB;

    bool isFirstEvent = false;
    {
        QMutexLocker lock(&m_dataMutex);
        isFirstEvent = m_data.isEmpty();
        m_data.enqueue(std::move(data));
    }

    if (isFirstEvent)
        emit dataAvailable();
}

static QWindow *targetWindow()
{
    // If we have an exposed focus window, that's the one to receive events.
    if (QWindow *focusWindow = QGuiApplication::focusWindow()) {
        if (focusWindow->isExposed() && focusWindow->width() != 0 && focusWindow->height() != 0)
            return focusWindow;
    }

    // Otherwise, if we have exactly one QQuickWindow, use that.
    // Otherwise, we don't know what to do.
    QWindow *found = nullptr;
    const QWindowList windowList = QGuiApplication::allWindows();
    for (QWindow *window : windowList) {
        if (!qobject_cast<QQuickWindow *>(window))
            continue;
        if (!window->isExposed() || window->width() == 0 || window->height() == 0)
            continue;
        if (found)
            return nullptr;
        found = window;
    }

    return found;
}

void QQuickEventReplayServiceImpl::start()
{
    if (!targetWindow()) {
        // Poll on a timer since focusing the window is not the only way to
        // produce a unique target. Having exactly one window also works.
        QTimer::singleShot(16, this, &QQuickEventReplayServiceImpl::start);

        QMutexLocker lock(&m_dataMutex);
        if (!m_data.isEmpty()) {
            qWarning() << "Cannot determine target window for event replay. "
                          "Focus a window to use it.";
        }
        return;
    }

    QObject::connect(
            &m_schedule, &QTimer::timeout,
            this, &QQuickEventReplayServiceImpl::sendNextEvent);
    QObject::connect(this, &QQuickEventReplayServiceImpl::dataAvailable, this, [this]() {
        Q_ASSERT(this); // avoids some confusion in clang-sa believing that this might be null
        QMutexLocker lock(&m_dataMutex);
        scheduleNextEvent(m_data.head());
    });

    QMutexLocker lock(&m_dataMutex);
    if (!m_data.isEmpty())
        scheduleNextEvent(m_data.head());
}

static QEvent::Type eventType(int profilerEventType)
{
    using InputEventType = QQmlProfilerDefinitions::InputEventType;
    switch (profilerEventType) {
    case InputEventType::InputKeyPress:
        return QEvent::KeyPress;
    case InputEventType::InputKeyRelease:
        return QEvent::KeyRelease;
    case InputEventType::InputMousePress:
        return QEvent::MouseButtonPress;
    case InputEventType::InputMouseDoubleClick:
        return QEvent::MouseButtonDblClick;
    case InputEventType::InputMouseMove:
        return QEvent::MouseMove;
    case InputEventType::InputMouseRelease:
        return QEvent::MouseButtonRelease;
    case InputEventType::InputMouseWheel:
        return QEvent::Wheel;
    default:
        return QEvent::None;
    }
}

QQuickProfilerData QQuickEventReplayServiceImpl::takeNextEvent()
{
    QMutexLocker lock(&m_dataMutex);
    const QQuickProfilerData data = m_data.dequeue();
    if (!m_data.isEmpty())
        scheduleNextEvent(m_data.head());
    else
        m_schedule.stop();
    return data;
}

void QQuickEventReplayServiceImpl::sendNextEvent()
{
    QWindow *window = targetWindow();
    if (!window) {
        qWarning() << "Target window has disappeared during event replay";
        return;
    }

    const QQuickProfilerData data = takeNextEvent();
    Q_ASSERT(data.messageType == Message::Event);

    const QEvent::Type type = eventType(data.inputType);
    switch (data.detailType) {
    case EventType::Key: {
        qt_handleKeyEvent(window, type, data.inputA, Qt::KeyboardModifiers(data.inputB));
        break;
    }
    case EventType::Mouse: {
        switch (data.inputType) {
        case InputEventType::InputMouseMove: {
            m_currentPos = QPoint(data.inputA, data.inputB);
            qt_handleMouseEvent(
                    window, m_currentPos, window->mapToGlobal(m_currentPos), m_currentButtons,
                    Qt::NoButton, type, m_currentModifiers, m_elapsed.elapsed());
            break;
        }
        case InputEventType::InputMouseWheel: {
            qt_handleWheelEvent(
                    window, m_currentPos, window->mapToGlobal(m_currentPos), QPoint(),
                    QPoint(data.inputA, data.inputB), m_currentModifiers, Qt::ScrollUpdate);
            break;
        }
        case InputEventType::InputMouseDoubleClick:
            // Ingore double clicks. We consider the constituent low level events instead.
            break;
        default: {
            m_currentButtons = Qt::MouseButtons(data.inputB);
            qt_handleMouseEvent(
                    window, m_currentPos, window->mapToGlobal(m_currentPos), m_currentButtons,
                    Qt::MouseButton(data.inputA), type, m_currentModifiers, m_elapsed.elapsed());
        }
        }
    }
    }
}

void QQuickEventReplayServiceImpl::scheduleNextEvent(const QQuickProfilerData &nextEvent)
{
    // We assume that our timer starts at the same time as the timer of the program that
    // recorded the events. That's really the best thing we can do because both of them
    // effectively start at an arbitrary point in time. Trying to calculate a clock skew
    // here is guesswork at best. The client can front-pad the timestamps if the transfer
    // mechanism is so slow that it influences the accuracy of the playback.
    // QTimer only has millisecond resolution but that should be good enough for input events.
    m_schedule.start(std::max((nextEvent.time - m_elapsed.nsecsElapsed()) / 1000000ll, 1ll));
}

QT_END_NAMESPACE
