// Copyright (C) 2017 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

#include <QtVirtualKeyboard/private/handwritinggesturerecognizer_p.h>

#include <QtCore/qmath.h>
#include <QVector2D>

QT_BEGIN_NAMESPACE
namespace QtVirtualKeyboard {

HandwritingGestureRecognizer::HandwritingGestureRecognizer(QObject *parent) :
    GestureRecognizer(parent),
    m_dpi(96)
{
}

void HandwritingGestureRecognizer::setDpi(int value)
{
    m_dpi = value >= 0 ? value : 96;
}

int HandwritingGestureRecognizer::dpi() const
{
    return m_dpi;
}

QVariantMap HandwritingGestureRecognizer::recognize(const QList<QVirtualKeyboardTrace *> traceList)
{
    if (traceList.size() > 0 && traceList.size() < 3) {

        // Swipe gesture detection
        // =======================
        //
        // The following algorithm is based on the assumption that a
        // vector composed of two arbitrary selected, but consecutive
        // measuring points, and a vector composed of the first and last
        // of the measuring points, are approximately in the same angle.
        //
        // If the measuring points are located very close to each other,
        // the angle can fluctuate a lot. This has been taken into account
        // by setting a minimum Euclidean distance between the measuring
        // points.
        //

        // Minimum euclidean distance of a segment (in millimeters)
        static const int MINIMUM_EUCLIDEAN_DISTANCE = 8;

        // Maximum theta variance (in degrees)
        static const qreal THETA_THRESHOLD = 25.0;

        // Maximum width variance in multitouch swipe (+- in percent)
        static const int MAXIMUM_WIDTH_VARIANCE = 20;

        const qreal minimumEuclideanDistance = MINIMUM_EUCLIDEAN_DISTANCE / 25.4 * m_dpi;
        static const qreal thetaThreshold = qDegreesToRadians(THETA_THRESHOLD);

        QList<QVector2D> swipeVectors;

        int traceIndex;
        const int traceCount = traceList.size();
        for (traceIndex = 0; traceIndex < traceCount; ++traceIndex) {

            const QVirtualKeyboardTrace *trace = traceList.at(traceIndex);
            const QVariantList &points = trace->points();
            QVector2D swipeVector;
            const int pointCount = points.size();
            int pointIndex = 0;
            if (pointCount >= 2) {

                QPointF startPosition = points.first().toPointF();
                swipeVector = QVector2D(points.last().toPointF() - startPosition);
                const qreal swipeLength = swipeVector.length();

                if (swipeLength >= minimumEuclideanDistance) {

                    QPointF previousPosition = startPosition;
                    qreal euclideanDistance = 0;
                    for (pointIndex = 1; pointIndex < pointCount; ++pointIndex) {

                        QPointF currentPosition(points.at(pointIndex).toPointF());

                        euclideanDistance += QVector2D(currentPosition - previousPosition).length();
                        if (euclideanDistance >= minimumEuclideanDistance) {

                            // Set the angle (theta) between the sample vector and the swipe vector
                            const QVector2D sampleVector(currentPosition - startPosition);
                            const qreal theta = qAcos(QVector2D::dotProduct(swipeVector, sampleVector) / (swipeLength * sampleVector.length()));

                            // Rejected when theta above threshold
                            if (theta >= thetaThreshold) {
                                swipeVector = QVector2D();
                                break;
                            }

                            startPosition = currentPosition;
                            euclideanDistance = 0;
                        }

                        previousPosition = currentPosition;
                    }

                    if (pointIndex < pointCount) {
                        swipeVector = QVector2D();
                        break;
                    }

                    // Check to see if angle and length matches to existing touch points
                    if (!swipeVectors.isEmpty()) {
                        bool matchesToExisting = true;
                        const qreal minimumSwipeLength = (swipeLength * (100.0 - MAXIMUM_WIDTH_VARIANCE) / 100.0);
                        const qreal maximumSwipeLength = (swipeLength * (100.0 + MAXIMUM_WIDTH_VARIANCE) / 100.0);
                        for (const QVector2D &otherSwipeVector : std::as_const(swipeVectors)) {
                            const qreal otherSwipeLength = otherSwipeVector.length();
                            const qreal theta = qAcos(QVector2D::dotProduct(swipeVector, otherSwipeVector) / (swipeLength * otherSwipeLength));

                            if (theta >= thetaThreshold) {
                                matchesToExisting = false;
                                break;
                            }

                            if (otherSwipeLength < minimumSwipeLength || otherSwipeLength > maximumSwipeLength) {
                                matchesToExisting = false;
                                break;
                            }
                        }

                        if (!matchesToExisting) {
                            swipeVector = QVector2D();
                            break;
                        }
                    }
                } else {
                    swipeVector = QVector2D();
                }
            }

            if (swipeVector.isNull())
                break;

            swipeVectors.append(swipeVector);
        }

        if (swipeVectors.size() == traceCount) {

            QVariantMap swipeGesture;

            // Get swipe angle from the first vector:
            //    0 degrees == right
            //   90 degrees == down
            //  180 degrees == left
            //  270 degrees == up
            QList<QVector2D>::ConstIterator swipeVector = swipeVectors.constBegin();
            qreal swipeLength = swipeVector->length();
            qreal swipeAngle = qAcos(swipeVector->x() / swipeLength);
            if (swipeVector->y() < 0)
                swipeAngle = 2 * M_PI - swipeAngle;

            // Calculate an average length of the vector
            ++swipeVector;
            for (const auto cend = swipeVectors.cend(); swipeVector != cend; ++swipeVector)
                swipeLength += swipeVector->length();
            swipeLength /= traceCount;

            swipeGesture[QLatin1String("type")] = QLatin1String("swipe");
            swipeGesture[QLatin1String("angle")] = swipeAngle;
            swipeGesture[QLatin1String("angle_degrees")] = qRadiansToDegrees(swipeAngle);
            swipeGesture[QLatin1String("length")] = swipeLength;
            swipeGesture[QLatin1String("length_mm")] = swipeLength / m_dpi * 25.4;
            swipeGesture[QLatin1String("touch_count")] = traceCount;

            return swipeGesture;
        }
    }

    return QVariantMap();
}

} // namespace QtVirtualKeyboard
QT_END_NAMESPACE
