// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-FileCopyrightText: 2020 The Qt Company Ltd.
// SPDX-License-Identifier: GPL-2.0-or-later

// based on Flickable, but heavily simplified

#include "swipearea.h"

#include <QMouseEvent>
#include <QObject>
#include <QTabletEvent>
#include <QTouchEvent>
#include <QWheelEvent>

// how many pixels to move before it starts being registered as a swipe
const int SWIPE_REGISTER_THRESHOLD = 10;
const qreal WHEEL_STEP_PIXEL_DELTA = 48.0;
const int TOUCHPAD_SCROLL_END_TIMEOUT = 160;

SwipeArea::SwipeArea(QQuickItem *parent)
    : QQuickItem{parent}
{
    setAcceptTouchEvents(true);
    setAcceptedMouseButtons(Qt::LeftButton);
    setFiltersChildMouseEvents(true);

    m_touchpadScrollEndTimer.setSingleShot(true);
    m_touchpadScrollEndTimer.setInterval(TOUCHPAD_SCROLL_END_TIMEOUT);
    connect(&m_touchpadScrollEndTimer, &QTimer::timeout, this, &SwipeArea::endTouchpadScroll);
}

SwipeArea::Mode SwipeArea::mode() const
{
    return m_mode;
}

void SwipeArea::setMode(Mode mode)
{
    m_mode = mode;
    Q_EMIT modeChanged();
}

bool SwipeArea::interactive() const
{
    return m_interactive;
}

void SwipeArea::setInteractive(bool interactive)
{
    m_interactive = interactive;
    Q_EMIT interactiveChanged();
}

bool SwipeArea::moving() const
{
    return m_moving;
}

bool SwipeArea::pressed() const
{
    return m_pressed;
}

void SwipeArea::setSkipSwipeThreshold(bool value)
{
    m_skipSwipeThreshold = value;
}

bool SwipeArea::childMouseEventFilter(QQuickItem *item, QEvent *event)
{
    if (!isVisible() || !isEnabled() || !m_interactive) {
        resetSwipe();
        return QQuickItem::childMouseEventFilter(item, event);
    }

    if (event->isPointerEvent() && event->type() != QEvent::UngrabMouse) {
        return filterPointerEvent(item, static_cast<QPointerEvent *>(event));
    }

    return QQuickItem::childMouseEventFilter(item, event);
}

// take exclusive grab from children
bool SwipeArea::filterPointerEvent(QQuickItem *receiver, QPointerEvent *event)
{
    // only filter mouse, touch or tablet events
    if (!dynamic_cast<QMouseEvent *>(event) && !dynamic_cast<QTabletEvent *>(event) && !dynamic_cast<QTouchEvent *>(event)) {
        return false;
    }

    const auto &firstPoint = event->points().first();

    if (event->pointCount() == 1 && event->exclusiveGrabber(firstPoint) == this) {
        // We have an exclusive grab (since we're e.g dragging), but at the same time, we have
        // a child with a passive grab (which is why this filter is being called). And because
        // of that, we end up getting the same pointer events twice; First in our own event
        // handlers (because of the grab), then once more in here, since we filter the child.
        // To avoid processing the event twice (e.g avoid calling handleReleaseEvent once more
        // from below), we mark the event as filtered, and simply return.
        event->setAccepted(true);
        return true;
    }

    QPointF localPos = mapFromScene(firstPoint.scenePosition());
    bool receiverDisabled = receiver && !receiver->isEnabled();
    bool receiverKeepsGrab = receiver && (receiver->keepMouseGrab() || receiver->keepTouchGrab());

    if ((m_stealMouse || contains(localPos)) && (!receiver || !receiverKeepsGrab || receiverDisabled)) {
        // clone the event, and set the first point's local position
        // HACK: we can't change QPointerEvent's points since it's const, so we have to pass localPos into the handlers
        QPointerEvent *localizedEvent = event->clone();
        localizedEvent->setAccepted(false);

        switch (firstPoint.state()) {
        case QEventPoint::State::Updated:
            handleMoveEvent(localizedEvent, localPos);
            break;
        case QEventPoint::State::Pressed:
            handlePressEvent(localizedEvent, localPos);
            break;
        case QEventPoint::State::Released:
            handleReleaseEvent(localizedEvent, localPos);
            break;
        case QEventPoint::State::Stationary:
        case QEventPoint::State::Unknown:
            break;
        }

        if ((receiver && m_stealMouse && !receiverKeepsGrab && receiver != this) || receiverDisabled) {
            event->setExclusiveGrabber(firstPoint, this);
        }

        bool filtered = m_stealMouse || receiverDisabled;
        if (filtered) {
            event->setAccepted(true);
        }

        return filtered;
    }

    if (firstPoint.state() == QEventPoint::State::Released || (receiverKeepsGrab && !receiverDisabled)) {
        // mouse released, or another item has claimed the grab
        resetSwipe();
    }

    return false;
}

void SwipeArea::mouseMoveEvent(QMouseEvent *event)
{
    if (m_interactive) {
        handleMoveEvent(event, event->points().first().position());
        event->accept();
    } else {
        QQuickItem::mouseMoveEvent(event);
    }
}

void SwipeArea::mousePressEvent(QMouseEvent *event)
{
    if (m_interactive) {
        handlePressEvent(event, event->points().first().position());
        event->accept();
    } else {
        QQuickItem::mousePressEvent(event);
    }
}

void SwipeArea::mouseReleaseEvent(QMouseEvent *event)
{
    if (m_interactive) {
        handleReleaseEvent(event, event->points().first().position());
        event->accept();
    } else {
        QQuickItem::mouseReleaseEvent(event);
    }
}

void SwipeArea::mouseUngrabEvent()
{
    QQuickItem::mouseUngrabEvent();
}

void SwipeArea::touchEvent(QTouchEvent *event)
{
    bool unhandled = true;
    const auto &firstPoint = event->points().first();

    switch (firstPoint.state()) {
    case QEventPoint::State::Pressed:
        if (m_interactive) {
            handlePressEvent(event, firstPoint.position());
            event->accept();
            unhandled = false;
        }
        break;
    case QEventPoint::State::Updated:
        if (m_interactive) {
            handleMoveEvent(event, firstPoint.position());
            event->accept();
            unhandled = false;
        }
        break;
    case QEventPoint::State::Released:
        if (m_interactive) {
            handleReleaseEvent(event, firstPoint.position());
            event->accept();
            unhandled = false;
        }
        break;
    case QEventPoint::State::Stationary:
    case QEventPoint::State::Unknown:
        break;
    }

    if (unhandled) {
        QQuickItem::touchEvent(event);
    }
}

void SwipeArea::touchUngrabEvent()
{
    QQuickItem::touchUngrabEvent();
}

void SwipeArea::wheelEvent(QWheelEvent *event)
{
    if (!m_interactive) {
        QQuickItem::wheelEvent(event);
        return;
    }

    event->setAccepted(false);

    const bool isTouchpad = event->deviceType() == QInputDevice::DeviceType::TouchPad;
    QPointF scrollDelta = event->pixelDelta();
    if (scrollDelta.isNull() && !event->angleDelta().isNull()) {
        scrollDelta = QPointF(event->angleDelta()) / QWheelEvent::DefaultDeltasPerStep * WHEEL_STEP_PIXEL_DELTA;
    }

    switch (event->phase()) {
    case Qt::ScrollBegin:
        if (isTouchpad && !m_touchpadScrolling) {
            event->accept();
            startTouchpadScroll(event->points().first().position());
        }
        break;
    case Qt::ScrollEnd:
        if (m_touchpadScrolling) {
            endTouchpadScroll();
            event->accept();
        }
        return;
    default:
        break;
    }

    if (!m_touchpadScrolling) {
        if (!isTouchpad || scrollDelta.isNull()) {
            return;
        }
        startTouchpadScroll(event->points().first().position());
    }

    for (auto &point : event->points()) {
        event->addPassiveGrabber(point, this);
    }

    m_totalScrollDelta = QPointF{m_totalScrollDelta + scrollDelta};
    Q_EMIT touchpadScrollMove(m_totalScrollDelta.x(), m_totalScrollDelta.y(), scrollDelta.x(), scrollDelta.y());

    m_touchpadScrollEndTimer.start();

    event->accept();
}

void SwipeArea::startTouchpadScroll(QPointF point)
{
    if (m_touchpadScrolling) {
        return;
    }

    m_touchpadScrolling = true;
    m_totalScrollDelta = QPointF{0, 0};
    Q_EMIT touchpadScrollStarted(point);
}

void SwipeArea::endTouchpadScroll()
{
    if (!m_touchpadScrolling) {
        return;
    }

    m_touchpadScrollEndTimer.stop();
    m_touchpadScrolling = false;
    m_totalScrollDelta = QPointF{0, 0};
    Q_EMIT touchpadScrollEnded();
}

void SwipeArea::setMoving(bool moving)
{
    m_moving = moving;
    Q_EMIT movingChanged();
}

void SwipeArea::setPressed(bool pressed)
{
    m_pressed = pressed;
    Q_EMIT pressedChanged();
}

void SwipeArea::resetSwipe()
{
    m_skipSwipeThreshold = false;
    m_stealMouse = false;
    if (m_pressed) {
        setPressed(false);
    }
    if (m_moving) {
        setMoving(false);
    }
}

void SwipeArea::handlePressEvent(QPointerEvent *event, QPointF point)
{
    Q_UNUSED(event)

    // ignore more touch events
    if (m_pressed) {
        return;
    }

    setPressed(true);
    m_stealMouse = false;
    m_pressPos = point;
    m_lastPos = m_pressPos;
}

void SwipeArea::handleReleaseEvent(QPointerEvent *event, QPointF point)
{
    Q_UNUSED(event)
    Q_UNUSED(point)

    // if we are in a swipe
    if (m_moving) {
        Q_EMIT swipeEnded();
    } else if (m_pressed) {
        // No swipe occurred — this was a click/tap
        Q_EMIT clicked();
    }

    resetSwipe();
}

void SwipeArea::handleMoveEvent(QPointerEvent *event, QPointF point)
{
    Q_UNUSED(event)

    if (!m_stealMouse) {
        if (!m_skipSwipeThreshold) {
            // if we haven't reached the swipe registering threshold yet, don't start the swipe
            if (m_mode == Mode::VerticalOnly && qAbs(point.y() - m_pressPos.y()) < SWIPE_REGISTER_THRESHOLD) {
                return;
            } else if (m_mode == Mode::HorizontalOnly && qAbs(point.x() - m_pressPos.x()) < SWIPE_REGISTER_THRESHOLD) {
                return;
            } else if (m_mode == Mode::BothAxis && qAbs(point.manhattanLength() - m_pressPos.manhattanLength()) < SWIPE_REGISTER_THRESHOLD) {
                return;
            }
        }
        m_skipSwipeThreshold = false;

        // we now start the swipe, stealing it from children

        m_startPos = point;
        m_lastPos = point;
        m_stealMouse = true;
        setMoving(true);
        Q_EMIT swipeStarted(m_startPos, m_pressPos);
    }

    const QVector2D totalDelta = QVector2D(point - m_startPos);
    const QVector2D delta = QVector2D(point - m_lastPos);
    m_lastPos = point;

    // ensure it's called AFTER swipeStarted()
    Q_EMIT swipeMove(totalDelta.x(), totalDelta.y(), delta.x(), delta.y());
}
