// Copyright (C) 2016 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 reason:default

#include "qquickmultipointhandler_p.h"
#include "qquickmultipointhandler_p_p.h"
#include <private/qquickitem_p.h>
#include <QLineF>
#include <QMouseEvent>
#include <QDebug>

QT_BEGIN_NAMESPACE

/*!
    \qmltype MultiPointHandler
    \since 5.10
    \preliminary
    \nativetype QQuickMultiPointHandler
    \inherits PointerDeviceHandler
    \inqmlmodule QtQuick
    \brief Abstract handler for multi-point Pointer Events.

    An intermediate class (not registered as a QML type)
    for any type of handler which requires and acts upon a specific number
    of multiple touchpoints.
*/

/*!
    \class QQuickMultiPointHandler
    \inmodule QtQuick
    \internal
*/
QQuickMultiPointHandler::QQuickMultiPointHandler(QQuickItem *parent, int minimumPointCount, int maximumPointCount)
    : QQuickPointerDeviceHandler(*(new QQuickMultiPointHandlerPrivate(minimumPointCount, maximumPointCount)), parent)
{
}

bool QQuickMultiPointHandler::wantsPointerEvent(QPointerEvent *event)
{
    Q_D(QQuickMultiPointHandler);
    if (!QQuickPointerDeviceHandler::wantsPointerEvent(event))
        return false;

    if (event->type() == QEvent::Wheel)
        return false;

    bool ret = false;
#if QT_CONFIG(gestures)
    if (event->type() == QEvent::NativeGesture && event->point(0).state() != QEventPoint::Released)
        ret = true;
#endif

    // If points were pressed or released within parentItem, reset stored state
    // and check eligible points again. This class of handlers is intended to
    // handle a specific number of points, so a differing number of points will
    // usually result in different behavior. But otherwise if the currentPoints
    // are all still there in the event, we're good to go (do not reset
    // currentPoints, because we don't want to lose the pressPosition, and do
    // not want to reshuffle the order either).
    const auto candidatePoints = eligiblePoints(event);
    if (candidatePoints.size() != d->currentPoints.size()) {
        d->currentPoints.clear();
        if (active()) {
            setActive(false);
            d->centroid.reset();
            emit centroidChanged();
        }
    } else if (hasCurrentPoints(event)) {
        return true;
    }

    ret = ret || (candidatePoints.size() >= minimumPointCount() && candidatePoints.size() <= maximumPointCount());
    if (ret) {
        const int c = candidatePoints.size();
        d->currentPoints.resize(c);
        for (int i = 0; i < c; ++i) {
            d->currentPoints[i].reset(event, candidatePoints[i]);
            if (auto par = parentItem())
                d->currentPoints[i].localize(par);
        }
    } else {
        d->currentPoints.clear();
    }
    return ret;
}

void QQuickMultiPointHandler::handlePointerEventImpl(QPointerEvent *event)
{
    Q_D(QQuickMultiPointHandler);
    QQuickPointerHandler::handlePointerEventImpl(event);
    // event's points can be reordered since the previous event, which is why currentPoints
    // is _not_ a shallow copy of the QQuickPointerTouchEvent::m_touchPoints vector.
    // So we have to update our currentPoints instances based on the given event.
    for (QQuickHandlerPoint &p : d->currentPoints) {
        if (const QEventPoint *ep = event->pointById(p.id()))
            p.reset(event, *ep);
    }
    QPointF sceneGrabPos = d->centroid.sceneGrabPosition();
    d->centroid.reset(d->currentPoints);
    d->centroid.m_sceneGrabPosition = sceneGrabPos; // preserve as it was
    emit centroidChanged();
}

void QQuickMultiPointHandler::onActiveChanged()
{
    Q_D(QQuickMultiPointHandler);
    if (active()) {
        d->centroid.m_sceneGrabPosition = d->centroid.m_scenePosition;
    } else {
        // Don't call centroid.reset() here, because in a QML onActiveChanged
        // callback, we'd like to see what the position _was_, what the velocity _was_, etc.
        // (having them undefined is not useful)
        // But pressedButtons and pressedModifiers are meant to be more real-time than those
        // (which seems a bit inconsistent, from one side).
        d->centroid.m_pressedButtons = Qt::NoButton;
        d->centroid.m_pressedModifiers = Qt::NoModifier;
    }
}

void QQuickMultiPointHandler::onGrabChanged(QQuickPointerHandler *grabber, QPointingDevice::GrabTransition transition, QPointerEvent *event, QEventPoint &point)
{
    Q_D(QQuickMultiPointHandler);
    // If another handler or item takes over this set of points, assume it has
    // decided that it's the better fit for them. Don't immediately re-grab
    // at the next opportunity. This should help to avoid grab cycles
    // (e.g. between DragHandler and PinchHandler).
    if (transition == QPointingDevice::UngrabExclusive || transition == QPointingDevice::CancelGrabExclusive)
        d->currentPoints.clear();
    if (grabber != this)
        return;
    switch (transition) {
    case QPointingDevice::GrabExclusive:
        for (auto &pt : d->currentPoints)
            if (pt.id() == point.id()) {
                pt.m_sceneGrabPosition = point.scenePosition();
                break;
            }
        QQuickPointerHandler::onGrabChanged(grabber, transition, event, point);
        break;
    case QPointingDevice::GrabPassive:
    case QPointingDevice::UngrabPassive:
    case QPointingDevice::UngrabExclusive:
    case QPointingDevice::CancelGrabPassive:
    case QPointingDevice::CancelGrabExclusive:
        QQuickPointerHandler::onGrabChanged(grabber, transition, event, point);
        break;
    case QPointingDevice::OverrideGrabPassive:
        return; // don't emit
    }
}

QList<QEventPoint> QQuickMultiPointHandler::eligiblePoints(QPointerEvent *event)
{
    QList<QEventPoint> ret;
    // If one or more points are newly pressed or released, all non-released points are candidates for this handler.
    // In other cases however, check whether it would be OK to steal the grab if the handler chooses to do that.
    bool stealingAllowed = event->isBeginEvent() || event->isEndEvent();
    for (int i = 0; i < event->pointCount(); ++i) {
        auto &p = event->point(i);
        if (QQuickDeliveryAgentPrivate::isMouseEvent(event)) {
            if (static_cast<QMouseEvent *>(event)->buttons() == Qt::NoButton)
                continue;
        }
        if (!stealingAllowed) {
            QObject *exclusiveGrabber = event->exclusiveGrabber(p);
            if (exclusiveGrabber && exclusiveGrabber != this && !canGrab(event, p))
                continue;
        }
        if (p.state() != QEventPoint::Released && wantsEventPoint(event, p))
            ret << p;
    }
    return ret;
}

/*!
     \qmlproperty int MultiPointHandler::minimumPointCount

     The minimum number of touchpoints required to activate this handler.

     If a smaller number of touchpoints are in contact with the
     \l {PointerHandler::parent}{parent}, they will be ignored.

     Any ignored points are eligible to activate other Input Handlers that
     have different constraints, on the same Item or on other Items.

     The default value is 2.
*/
int QQuickMultiPointHandler::minimumPointCount() const
{
    Q_D(const QQuickMultiPointHandler);
    return d->minimumPointCount;
}

void QQuickMultiPointHandler::setMinimumPointCount(int c)
{
    Q_D(QQuickMultiPointHandler);
    if (d->minimumPointCount == c)
        return;

    d->minimumPointCount = c;
    emit minimumPointCountChanged();
    if (d->maximumPointCount < 0)
        emit maximumPointCountChanged();
}

/*!
     \qmlproperty int MultiPointHandler::maximumPointCount

     The maximum number of touchpoints this handler can utilize.

     If a larger number of touchpoints are in contact with the
     \l {PointerHandler::parent}{parent}, the required number of points will be
     chosen in the order that they are pressed, and the remaining points will
     be ignored.

     Any ignored points are eligible to activate other Input Handlers that
     have different constraints, on the same Item or on other Items.

     The default value is the same as \l minimumPointCount.
*/
int QQuickMultiPointHandler::maximumPointCount() const
{
    Q_D(const QQuickMultiPointHandler);
    return d->maximumPointCount >= 0 ? d->maximumPointCount : d->minimumPointCount;
}

void QQuickMultiPointHandler::setMaximumPointCount(int maximumPointCount)
{
    Q_D(QQuickMultiPointHandler);
    if (d->maximumPointCount == maximumPointCount)
        return;

    d->maximumPointCount = maximumPointCount;
    emit maximumPointCountChanged();
}

/*!
    \readonly
    \qmlproperty QtQuick::handlerPoint QtQuick::MultiPointHandler::centroid

    A point exactly in the middle of the currently-pressed touch points.
    If only one point is pressed, it's the same as that point.
    A handler that has a \l target will normally transform it relative to this point.
*/
const QQuickHandlerPoint &QQuickMultiPointHandler::centroid() const
{
    Q_D(const QQuickMultiPointHandler);
    return d->centroid;
}

/*!
    Returns a modifiable reference to the point that will be returned by the
    \l centroid property. If you modify it, you are responsible to emit
    \l centroidChanged.
*/
QQuickHandlerPoint &QQuickMultiPointHandler::mutableCentroid()
{
    Q_D(QQuickMultiPointHandler);
    return d->centroid;
}

QList<QQuickHandlerPoint> &QQuickMultiPointHandler::currentPoints()
{
    Q_D(QQuickMultiPointHandler);
    return d->currentPoints;
}

bool QQuickMultiPointHandler::hasCurrentPoints(QPointerEvent *event)
{
    Q_D(const QQuickMultiPointHandler);
    if (event->pointCount() < d->currentPoints.size() || d->currentPoints.size() == 0)
        return false;
    // TODO optimize: either ensure the points are sorted,
    // or use std::equal with a predicate
    for (const QQuickHandlerPoint &p : std::as_const(d->currentPoints)) {
        const QEventPoint *ep = event->pointById(p.id());
        if (!ep)
            return false;
        if (ep->state() == QEventPoint::Released)
            return false;
    }
    return true;
}

qreal QQuickMultiPointHandler::averageTouchPointDistance(const QPointF &ref)
{
    Q_D(const QQuickMultiPointHandler);
    qreal ret = 0;
    if (Q_UNLIKELY(d->currentPoints.size() == 0))
        return ret;
    for (const QQuickHandlerPoint &p : d->currentPoints)
        ret += QVector2D(p.scenePosition() - ref).length();
    return ret / d->currentPoints.size();
}

qreal QQuickMultiPointHandler::averageStartingDistance(const QPointF &ref)
{
    Q_D(const QQuickMultiPointHandler);
    // TODO cache it in setActive()?
    qreal ret = 0;
    if (Q_UNLIKELY(d->currentPoints.size() == 0))
        return ret;
    for (const QQuickHandlerPoint &p : d->currentPoints)
        ret += QVector2D(p.sceneGrabPosition() - ref).length();
    return ret / d->currentPoints.size();
}

QList<QQuickMultiPointHandler::PointData> QQuickMultiPointHandler::angles(const QPointF &ref) const
{
    Q_D(const QQuickMultiPointHandler);
    QList<PointData> angles;
    angles.reserve(d->currentPoints.size());
    for (const QQuickHandlerPoint &p : d->currentPoints) {
        qreal angle = QLineF(ref, p.scenePosition()).angle();
        angles.append(PointData(p.id(), -angle));     // convert to clockwise, to be consistent with QQuickItem::rotation
    }
    return angles;
}

qreal QQuickMultiPointHandler::averageAngleDelta(const QList<PointData> &old, const QList<PointData> &newAngles)
{
    qreal avgAngleDelta = 0;
    int numSamples = 0;

    auto oldBegin = old.constBegin();

    for (PointData newData : newAngles) {
        quint64 id = newData.id;
        auto it = std::find_if(oldBegin, old.constEnd(), [id] (PointData pd) { return pd.id == id; });
        qreal angleD = 0;
        if (it != old.constEnd()) {
            PointData oldData = *it;
            // We might rotate from 359 degrees to 1 degree. However, this
            // should be interpreted as a rotation of +2 degrees instead of
            // -358 degrees. Therefore, we call remainder() to translate the angle
            // to be in the range [-180, 180] (-350 to +10 etc)
            angleD = remainder(newData.angle - oldData.angle, qreal(360));
            // optimization: narrow down the O(n^2) search to optimally O(n)
            // if both vectors have the same points and they are in the same order
            if (it == oldBegin)
                ++oldBegin;
            numSamples++;
        }
        avgAngleDelta += angleD;
    }
    if (numSamples > 1)
        avgAngleDelta /= numSamples;

    return avgAngleDelta;
}

void QQuickMultiPointHandler::acceptPoints(const QList<QEventPoint> &points)
{
    // "auto point" is a copy, but it's OK because
    // setAccepted() changes QEventPointPrivate::accept via the shared d-pointer
    for (auto point : points)
        point.setAccepted();
}

bool QQuickMultiPointHandler::grabPoints(QPointerEvent *event, const QList<QEventPoint> &points)
{
    if (points.isEmpty())
        return false;
    bool allowed = true;
    for (auto &point : points) {
        if (event->exclusiveGrabber(point) != this && !canGrab(event, point)) {
            allowed = false;
            break;
        }
    }
    if (allowed) {
        for (const auto &point : std::as_const(points))
            setExclusiveGrab(event, point);
    }
    return allowed;
}

void QQuickMultiPointHandler::moveTarget(QPointF pos)
{
    Q_D(QQuickMultiPointHandler);
    if (QQuickItem *t = target()) {
        d->xMetaProperty().write(t, pos.x());
        d->yMetaProperty().write(t, pos.y());
        d->centroid.m_position = t->mapFromScene(d->centroid.m_scenePosition);
    } else {
        qWarning() << "moveTarget: target is null";
    }
}

QQuickMultiPointHandlerPrivate::QQuickMultiPointHandlerPrivate(int minPointCount, int maxPointCount)
  : QQuickPointerDeviceHandlerPrivate()
  , minimumPointCount(minPointCount)
  , maximumPointCount(maxPointCount)
{
}

QMetaProperty &QQuickMultiPointHandlerPrivate::xMetaProperty() const
{
    Q_Q(const QQuickMultiPointHandler);
    if (!xProperty.isValid() && q->target()) {
        const QMetaObject *targetMeta = q->target()->metaObject();
        xProperty = targetMeta->property(targetMeta->indexOfProperty("x"));
    }
    return xProperty;
}

QMetaProperty &QQuickMultiPointHandlerPrivate::yMetaProperty() const
{
    Q_Q(const QQuickMultiPointHandler);
    if (!yProperty.isValid() && q->target()) {
        const QMetaObject *targetMeta = q->target()->metaObject();
        yProperty = targetMeta->property(targetMeta->indexOfProperty("y"));
    }
    return yProperty;
}

QT_END_NAMESPACE

#include "moc_qquickmultipointhandler_p.cpp"
