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

#include "examplehwrinputmethod_p.h"
#include <QtVirtualKeyboard/qvirtualkeyboardinputengine.h>
#include <QtVirtualKeyboard/qvirtualkeyboardinputcontext.h>
#include <QtVirtualKeyboard/qvirtualkeyboardtrace.h>
#include <QtVirtualKeyboard/private/handwritinggesturerecognizer_p.h>
#include <QtVirtualKeyboard/private/settings_p.h>
#include <QtVirtualKeyboard/private/qvirtualkeyboardabstractinputmethod_p.h>
#include <QCryptographicHash>
#include <QRandomGenerator>
#include <QLoggingCategory>
#include <QLocale>

QT_BEGIN_NAMESPACE
namespace QtVirtualKeyboard {

Q_LOGGING_CATEGORY(lcExampleHwr, "qt.virtualkeyboard.example.hwr")

class ExampleHwrInputMethodPrivate : public QVirtualKeyboardAbstractInputMethodPrivate
{
public:
    Q_DECLARE_PUBLIC(ExampleHwrInputMethod)

    ExampleHwrInputMethodPrivate(ExampleHwrInputMethod *q_ptr) :
        QVirtualKeyboardAbstractInputMethodPrivate(),
        q_ptr(q_ptr)
    {
    }

    bool setInputMode(const QLocale &locale, QVirtualKeyboardInputEngine::InputMode inputMode)
    {
        Q_UNUSED(locale);
        finishRecognition();
        this->inputMode = inputMode;
        return true;
    }

    QByteArray getContext(QVirtualKeyboardInputEngine::PatternRecognitionMode patternRecognitionMode,
                          const QVariantMap &traceCaptureDeviceInfo,
                          const QVariantMap &traceScreenInfo) const
    {
        QCryptographicHash hash(QCryptographicHash::Md5);

        hash.addData(QByteArrayView(reinterpret_cast<const char *>(&patternRecognitionMode), sizeof(patternRecognitionMode)));

        QByteArray mapData;
        QDataStream ds(&mapData, QIODevice::WriteOnly);
        ds << traceCaptureDeviceInfo;
        ds << traceScreenInfo;
        hash.addData(mapData);

        return hash.result();
    }

    void setContext(QVirtualKeyboardInputEngine::PatternRecognitionMode patternRecognitionMode,
                    const QVariantMap &traceCaptureDeviceInfo,
                    const QVariantMap &traceScreenInfo,
                    const QByteArray &context)
    {
        Q_UNUSED(patternRecognitionMode);
        Q_UNUSED(traceScreenInfo);
        if (context == currentContext)
            return;
        currentContext = context;

        qCDebug(lcExampleHwr) << "setContext:" << QLatin1String((context.toHex()));

        // Finish recognition, but preserve current input
        const int dpi = traceCaptureDeviceInfo.value(QLatin1String("dpi"), 96).toInt();
        static const int INSTANT_GESTURE_WIDTH_THRESHOLD_MM = 25;
        gestureWidthThreshold = qRound(INSTANT_GESTURE_WIDTH_THRESHOLD_MM / 25.4 * dpi);

        gestureRecognizer.setDpi(dpi);
    }

    QVirtualKeyboardTrace *traceBegin(
            int traceId, QVirtualKeyboardInputEngine::PatternRecognitionMode patternRecognitionMode,
            const QVariantMap &traceCaptureDeviceInfo, const QVariantMap &traceScreenInfo)
    {
        // The result id follows the trace id so that the (previous)
        // results completed during the handwriting can be rejected.
        resultId = traceId;

        QByteArray context = getContext(patternRecognitionMode, traceCaptureDeviceInfo, traceScreenInfo);
        if (context != currentContext) {
            setContext(patternRecognitionMode, traceCaptureDeviceInfo, traceScreenInfo, context);
        }

        Q_Q(ExampleHwrInputMethod);
        QVirtualKeyboardTrace *trace = new QVirtualKeyboardTrace(q);
        traceList.append(trace);

        return trace;
    }

    void traceEnd(QVirtualKeyboardTrace *trace)
    {
        if (trace->isCanceled()) {
            traceList.removeOne(trace);
            delete trace;
        } else if (handleGesture()) {
            finishRecognition();
            return;
        }
        if (!traceList.isEmpty()) {
            if (countActiveTraces() == 0)
                restartRecognition();
        }
    }

    int countActiveTraces() const
    {
        int count = 0;
        for (QVirtualKeyboardTrace *trace : std::as_const(traceList)) {
            if (!trace->isFinal())
                count++;
        }
        return count;
    }

    void clearTraces()
    {
        qDeleteAll(traceList);
        traceList.clear();
    }

    bool applyGesture(const QChar &gesture)
    {
        Q_Q(ExampleHwrInputMethod);
        QVirtualKeyboardInputContext *ic = q->inputContext();
        switch (gesture.unicode()) {
        case '\b':
            return ic->inputEngine()->virtualKeyClick(Qt::Key_Backspace, QString(), Qt::NoModifier);
        case '\r':
            return ic->inputEngine()->virtualKeyClick(Qt::Key_Return, QLatin1String("\n"), Qt::NoModifier);
        case ' ':
            return ic->inputEngine()->virtualKeyClick(Qt::Key_Space, QLatin1String(" "), Qt::NoModifier);
        default:
            return false;
        }
    }

    bool handleGesture()
    {
        if (countActiveTraces() > 0)
            return false;

        QVariantMap gesture(gestureRecognizer.recognize(traceList.mid(traceList.size() - 1, 1)));
        if (gesture.isEmpty())
            return false;

        qCDebug(lcExampleHwr) << "handleGesture:" << gesture;

        if (gesture[QLatin1String("type")].toString() == QLatin1String("swipe")) {

            static const int SWIPE_ANGLE_THRESHOLD = 15;    // degrees +-

            qreal swipeLength = gesture[QLatin1String("length")].toReal();
            if (swipeLength >= gestureWidthThreshold) {

                Q_Q(ExampleHwrInputMethod);
                QVirtualKeyboardInputContext *ic = q->inputContext();
                if (!ic)
                    return false;

                qreal swipeAngle = gesture[QLatin1String("angle_degrees")].toReal();
                int swipeTouchCount = gesture[QLatin1String("touch_count")].toInt();

                // Swipe left
                if (swipeAngle <= 180 + SWIPE_ANGLE_THRESHOLD && swipeAngle >= 180 - SWIPE_ANGLE_THRESHOLD) {
                    if (swipeTouchCount == 1) {
                        // Single swipe: backspace
                        ic->inputEngine()->virtualKeyClick(Qt::Key_Backspace, QString(), Qt::NoModifier);
                        return true;
                    }
                    return false;
                }

                // Swipe right
                if (swipeAngle <= SWIPE_ANGLE_THRESHOLD || swipeAngle >= 360 - SWIPE_ANGLE_THRESHOLD) {
                    if (swipeTouchCount == 1) {
                        // Single swipe: space
                        ic->inputEngine()->virtualKeyClick(Qt::Key_Space, QLatin1String(" "), Qt::NoModifier);
                        return true;
                    }
                    return false;
                }

                // Swipe up
                if (swipeAngle <= 270 + SWIPE_ANGLE_THRESHOLD && swipeAngle >= 270 - SWIPE_ANGLE_THRESHOLD) {
                    if (swipeTouchCount == 1) {
                        // Single swipe: toggle input mode
                        select();
                        if (!(ic->inputMethodHints() & (Qt::ImhDialableCharactersOnly | Qt::ImhFormattedNumbersOnly | Qt::ImhDigitsOnly))) {
                            QList<int> inputModes = ic->inputEngine()->inputModes();
                            // Filter out duplicate numeric mode (in favor of Numeric)
                            int indexOfNumericInputMode = inputModes.indexOf(static_cast<int>(QVirtualKeyboardInputEngine::InputMode::Numeric));
                            int indexOfDialableInputMode = inputModes.indexOf(static_cast<int>(QVirtualKeyboardInputEngine::InputMode::Dialable));
                            if (indexOfNumericInputMode != -1 && indexOfDialableInputMode != -1)
                                inputModes.removeAt(inputMode != QVirtualKeyboardInputEngine::InputMode::Dialable ?
                                            indexOfDialableInputMode :
                                            indexOfNumericInputMode);
                            if (inputModes.size() > 1) {
                                int inputModeIndex = inputModes.indexOf(static_cast<int>(inputMode)) + 1;
                                if (inputModeIndex >= inputModes.size())
                                    inputModeIndex = 0;
                                ic->inputEngine()->setInputMode(static_cast<QVirtualKeyboardInputEngine::InputMode>(inputModes.at(inputModeIndex)));
                            }
                        }
                        return true;
                    }
                }
            }
        }

        return false;
    }

    bool isValidInputChar(const QChar &c) const
    {
        if (c.isLetterOrNumber())
            return true;
        if (isJoiner(c))
            return true;
        return false;
    }

    bool isJoiner(const QChar &c) const
    {
        if (c.isPunct() || c.isSymbol()) {
            Q_Q(const ExampleHwrInputMethod);
            QVirtualKeyboardInputContext *ic = q->inputContext();
            if (ic) {
                Qt::InputMethodHints inputMethodHints = ic->inputMethodHints();
                if (inputMethodHints.testFlag(Qt::ImhUrlCharactersOnly) || inputMethodHints.testFlag(Qt::ImhEmailCharactersOnly))
                    return QString(QStringLiteral(":/?#[]@!$&'()*+,;=-_.%")).contains(c);
            }
            ushort unicode = c.unicode();
            if (unicode == Qt::Key_Apostrophe || unicode == Qt::Key_Minus)
                return true;
        }
        return false;
    }

    void restartRecognition()
    {
        qCDebug(lcExampleHwr) << "restartRecognition";

        resetResultTimer(Settings::instance()->hwrTimeoutForAlphabetic());
    }

    bool finishRecognition(bool emitSelectionListChanged = true)
    {
        qCDebug(lcExampleHwr) << "finishRecognition";
        bool result = !traceList.isEmpty();

        stopResultTimer();
        clearTraces();

        if (!wordCandidates.isEmpty()) {
            wordCandidates.clear();
            activeWordIndex = -1;
            if (emitSelectionListChanged) {
                Q_Q(ExampleHwrInputMethod);
                emit q->selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
                emit q->selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, activeWordIndex);
            }
            result = true;
        }

        return result;
    }

    void select(int index = -1)
    {
        Q_Q(ExampleHwrInputMethod);
        QVirtualKeyboardInputContext *ic = q->inputContext();
        if (!ic)
            return;

        if (!wordCandidates.isEmpty())
            ic->commit(wordCandidates.at(index != -1 ? index : 0));

        finishRecognition();
    }

    void processResult()
    {
        qCDebug(lcExampleHwr) << "processResult";

        Q_Q(ExampleHwrInputMethod);
        QVirtualKeyboardInputContext *ic = q->inputContext();
        if (!ic)
            return;

        QStringList newWordCandidates;
        QString word = !wordCandidates.isEmpty() ? wordCandidates.at(0) : QString();
        switch (inputMode) {
        case QVirtualKeyboardInputEngine::InputMode::Latin:
            appendRandomChar(word);
            break;
        case QVirtualKeyboardInputEngine::InputMode::Numeric:
        case QVirtualKeyboardInputEngine::InputMode::Dialable:
            appendRandomDigit(word);
            break;
        default:
            break;
        }
        newWordCandidates.append(word);
        activeWordIndex = 0;
        wordCandidates = newWordCandidates;
        qCDebug(lcExampleHwr) << "wordCandidates" << wordCandidates;
        ic->setPreeditText(word);

        emit q->selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
        emit q->selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, activeWordIndex);
    }

    static void appendRandomChar(QString& word)
    {
        word.append(QChar('a' + QRandomGenerator::global()->bounded(26)));
    }

    static void appendRandomDigit(QString& word)
    {
        word.append(QChar('0' + QRandomGenerator::global()->bounded(10)));
    }

    void resetResultTimer(int interval = 500)
    {
        qCDebug(lcExampleHwr) << "resetResultTimer:" << interval;
        Q_Q(ExampleHwrInputMethod);
        stopResultTimer();
        resultTimer = q->startTimer(interval);
    }

    void stopResultTimer()
    {
        if (resultTimer) {
            qCDebug(lcExampleHwr) << "stopResultTimer";
            Q_Q(ExampleHwrInputMethod);
            q->killTimer(resultTimer);
            resultTimer = 0;
        }
    }

    ExampleHwrInputMethod *q_ptr = nullptr;
    QVirtualKeyboardInputEngine::InputMode inputMode = QVirtualKeyboardInputEngine::InputMode::Latin;
    QByteArray currentContext;
    int gestureWidthThreshold = 0;
    int resultId = 0;
    int lastResultId = 0;
    int resultTimer = 0;
    QList<QVirtualKeyboardTrace *> traceList;
    HandwritingGestureRecognizer gestureRecognizer;
    QStringList wordCandidates;
    int activeWordIndex = -1;
};

/*!
    \class QtVirtualKeyboard::ExampleHwrInputMethod
    \internal
*/

ExampleHwrInputMethod::ExampleHwrInputMethod(QObject *parent) :
    QVirtualKeyboardAbstractInputMethod(*new ExampleHwrInputMethodPrivate(this), parent)
{
}

ExampleHwrInputMethod::~ExampleHwrInputMethod()
{
}

QList<QVirtualKeyboardInputEngine::InputMode> ExampleHwrInputMethod::inputModes(const QString &locale)
{
    Q_UNUSED(locale);
    QList<QVirtualKeyboardInputEngine::InputMode> availableInputModes = {
        QVirtualKeyboardInputEngine::InputMode::Latin,
        QVirtualKeyboardInputEngine::InputMode::Numeric,
    };
    return availableInputModes;
}

bool ExampleHwrInputMethod::setInputMode(const QString &locale, QVirtualKeyboardInputEngine::InputMode inputMode)
{
    Q_D(ExampleHwrInputMethod);
    d->select();
    return d->setInputMode(QLocale(locale), inputMode);
}

bool ExampleHwrInputMethod::setTextCase(QVirtualKeyboardInputEngine::TextCase textCase)
{
    Q_UNUSED(textCase);
    return true;
}

bool ExampleHwrInputMethod::keyEvent(Qt::Key key, const QString &text, Qt::KeyboardModifiers modifiers)
{
    Q_UNUSED(modifiers);
    Q_D(ExampleHwrInputMethod);
    switch (key) {
    case Qt::Key_Enter:
    case Qt::Key_Return:
    case Qt::Key_Tab:
    case Qt::Key_Space:
        d->select();
        update();
        break;

    case Qt::Key_Backspace:
        {
            QVirtualKeyboardInputContext *ic = inputContext();
            QString preeditText = ic->preeditText();
            if (preeditText.length() > 1) {
                preeditText.chop(1);
                ic->setPreeditText(preeditText);
                emit selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
                emit selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, d->activeWordIndex);
                return true;
            } else {
                bool result = !preeditText.isEmpty();
                if (result)
                    ic->clear();
                d->finishRecognition();
                return result;
            }
        }

    default:
        if (text.length() > 0) {
            QVirtualKeyboardInputContext *ic = inputContext();
            QString preeditText = ic->preeditText();
            QChar c = text.at(0);
            bool addToWord = d->isValidInputChar(c) && (!preeditText.isEmpty() || !d->isJoiner(c));
            if (addToWord) {
                preeditText.append(text);
                ic->setPreeditText(preeditText);
                d->finishRecognition(false);
                d->wordCandidates.append(preeditText);
                d->activeWordIndex = 0;
                emit selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
                emit selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, d->activeWordIndex);
                return true;
            } else {
                ic->commit();
                d->finishRecognition();
            }
            break;
        }
    }
    return false;
}

void ExampleHwrInputMethod::reset()
{
    Q_D(ExampleHwrInputMethod);
    qCDebug(lcExampleHwr) << "reset";
    d->finishRecognition();
}

void ExampleHwrInputMethod::update()
{
    Q_D(ExampleHwrInputMethod);
    qCDebug(lcExampleHwr) << "update";
    d->select();
}

QList<QVirtualKeyboardSelectionListModel::Type> ExampleHwrInputMethod::selectionLists()
{
    return QList<QVirtualKeyboardSelectionListModel::Type>() << QVirtualKeyboardSelectionListModel::Type::WordCandidateList;
}

int ExampleHwrInputMethod::selectionListItemCount(QVirtualKeyboardSelectionListModel::Type type)
{
    Q_UNUSED(type);
    Q_D(ExampleHwrInputMethod);
    return d->wordCandidates.size();
}

QVariant ExampleHwrInputMethod::selectionListData(QVirtualKeyboardSelectionListModel::Type type, int index, QVirtualKeyboardSelectionListModel::Role role)
{
    QVariant result;
    Q_D(ExampleHwrInputMethod);
    switch (role) {
    case QVirtualKeyboardSelectionListModel::Role::Display:
        result = QVariant(d->wordCandidates.at(index));
        break;
    case QVirtualKeyboardSelectionListModel::Role::WordCompletionLength:
        result.setValue(0);
        break;
    case QVirtualKeyboardSelectionListModel::Role::Dictionary:
    {
        QVirtualKeyboardSelectionListModel::DictionaryType dictionaryType =
                QVirtualKeyboardSelectionListModel::DictionaryType::Default;
        result = QVariant(static_cast<int>(dictionaryType));
        break;
    }
    case QVirtualKeyboardSelectionListModel::Role::CanRemoveSuggestion:
        result = QVariant(false);
        break;
    default:
        result = QVirtualKeyboardAbstractInputMethod::selectionListData(type, index, role);
        break;
    }
    return result;
}

void ExampleHwrInputMethod::selectionListItemSelected(QVirtualKeyboardSelectionListModel::Type type, int index)
{
    Q_UNUSED(type);
    Q_D(ExampleHwrInputMethod);
    d->select(index);
}

QList<QVirtualKeyboardInputEngine::PatternRecognitionMode> ExampleHwrInputMethod::patternRecognitionModes() const
{
    return QList<QVirtualKeyboardInputEngine::PatternRecognitionMode>()
            << QVirtualKeyboardInputEngine::PatternRecognitionMode::Handwriting;
}

QVirtualKeyboardTrace *ExampleHwrInputMethod::traceBegin(
        int traceId, QVirtualKeyboardInputEngine::PatternRecognitionMode patternRecognitionMode,
        const QVariantMap &traceCaptureDeviceInfo, const QVariantMap &traceScreenInfo)
{
    Q_D(ExampleHwrInputMethod);
    return d->traceBegin(traceId, patternRecognitionMode, traceCaptureDeviceInfo, traceScreenInfo);
}

bool ExampleHwrInputMethod::traceEnd(QVirtualKeyboardTrace *trace)
{
    Q_D(ExampleHwrInputMethod);
    d->traceEnd(trace);
    return true;
}

bool ExampleHwrInputMethod::reselect(int cursorPosition, const QVirtualKeyboardInputEngine::ReselectFlags &reselectFlags)
{
    Q_D(ExampleHwrInputMethod);

    QVirtualKeyboardInputContext *ic = inputContext();
    if (!ic)
        return false;

    const int maxLength = 32;
    const QString surroundingText = ic->surroundingText();
    int replaceFrom = 0;
    QString stringStart;

    if (cursorPosition > surroundingText.length())
        return false;

    if (reselectFlags.testFlag(QVirtualKeyboardInputEngine::ReselectFlag::WordBeforeCursor)) {
        for (int i = cursorPosition - 1; i >= 0 && stringStart.length() < maxLength; --i) {
            QChar c = surroundingText.at(i);
            if (!d->isValidInputChar(c))
                break;
            stringStart.insert(0, c);
            --replaceFrom;
        }

        while (replaceFrom < 0 && d->isJoiner(stringStart.at(0))) {
            stringStart.remove(0, 1);
            ++replaceFrom;
        }
    }

    if (reselectFlags.testFlag(QVirtualKeyboardInputEngine::ReselectFlag::WordAtCursor) && replaceFrom == 0) {
        stringStart.clear();
        return false;
    }

    if (reselectFlags.testFlag(QVirtualKeyboardInputEngine::ReselectFlag::WordAfterCursor)) {
        for (int i = cursorPosition; i < surroundingText.length() && stringStart.length() < maxLength; ++i) {
            QChar c = surroundingText.at(i);
            if (!d->isValidInputChar(c))
                break;
            stringStart.append(c);
        }

        while (replaceFrom > -stringStart.length()) {
            int lastPos = stringStart.length() - 1;
            if (!d->isJoiner(stringStart.at(lastPos)))
                break;
            stringStart.remove(lastPos, 1);
        }
    }

    if (stringStart.isEmpty())
        return false;

    if (reselectFlags.testFlag(QVirtualKeyboardInputEngine::ReselectFlag::WordAtCursor) && replaceFrom == -stringStart.length() && stringStart.length() < maxLength) {
        stringStart.clear();
        return false;
    }

    if (d->isJoiner(stringStart.at(0))) {
        stringStart.clear();
        return false;
    }

    if (d->isJoiner(stringStart.at(stringStart.length() - 1))) {
        stringStart.clear();
        return false;
    }

    ic->setPreeditText(stringStart, QList<QInputMethodEvent::Attribute>(), replaceFrom, stringStart.length());
    d->activeWordIndex = 0;
    d->wordCandidates = {stringStart};
    emit selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
    emit selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, d->activeWordIndex);

    return true;
}

void ExampleHwrInputMethod::timerEvent(QTimerEvent *timerEvent)
{
    Q_D(ExampleHwrInputMethod);
    int timerId = timerEvent->timerId();
    qCDebug(lcExampleHwr) << "timerEvent():" << timerId;
    if (timerId == d->resultTimer) {
        if (!d->countActiveTraces()) {
            d->stopResultTimer();
            d->processResult();
            d->clearTraces();
        }
    }
}

} // namespace QtVirtualKeyboard
QT_END_NAMESPACE
