/****************************************************************************
**
** Copyright (C) 2021 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the Qt Virtual Keyboard module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 or (at your option) any later version
** approved by the KDE Free Qt Foundation. The licenses are as published by
** the Free Software Foundation and appearing in the file LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "t9writeinputmethod_p.h"
#include <QtVirtualKeyboard/qvirtualkeyboardinputengine.h>
#include <QtVirtualKeyboard/qvirtualkeyboardinputcontext.h>
#include <QtVirtualKeyboard/qvirtualkeyboardtrace.h>
#include "t9writeworker_p.h"
#include <QLoggingCategory>
#include <QDirIterator>
#include <QCryptographicHash>
#include <QTime>
#include <QMetaEnum>
#include <QtCore/QLibraryInfo>
#include <QtVirtualKeyboard/private/settings_p.h>
#include <QtVirtualKeyboard/private/handwritinggesturerecognizer_p.h>
#include <QtVirtualKeyboard/private/qvirtualkeyboardabstractinputmethod_p.h>
#ifdef QT_VIRTUALKEYBOARD_RECORD_TRACE_INPUT
#include <QtVirtualKeyboard/private/unipentrace_p.h>
#include <QStandardPaths>
#endif
#include <QtVirtualKeyboard/qvirtualkeyboarddictionary.h>
#include <QtVirtualKeyboard/qvirtualkeyboarddictionarymanager.h>

#include "decumaStatus.h"
#include "decumaSymbolCategories.h"
#include "decumaLanguages.h"
#include "xxchOem.h"
#include "xt9ldbmanager.h"
#include "t9writewordcandidate_p.h"
#ifdef HAVE_XT9
#include <xt9awime.h>
#include <xt9cpime.h>
#include <xt9jime.h>
#include <xt9kime.h>
#include <xt9callbacks.h>
#include <xt9languagemap.h>
#endif

/*  Set to 1 to enable T9 Write log.

    The log is routed to qDebug() and it can be enabled for troubleshooting
    and when reporting issues. The log must not to be enabled in production
    build.
*/
#define QT_VIRTUALKEYBOARD_CERENCE_HWR_LOG 0

#define CERENCE_HWR_DLM_MAX_WORDS 10000

QT_BEGIN_NAMESPACE
namespace QtVirtualKeyboard {

Q_LOGGING_CATEGORY(lcT9Write, "qt.virtualkeyboard.cerence-hwr")

class T9WriteCaseFormatter
{
public:
    T9WriteCaseFormatter() :
        preferLowercase(false)
    {
    }

    void clear()
    {
        textCaseList.clear();
    }

    void ensureLength(int length, QVirtualKeyboardInputEngine::TextCase textCase)
    {
        if (length <= 0) {
            textCaseList.clear();
            return;
        }
        while (length < textCaseList.length())
            textCaseList.removeLast();
        while (length > textCaseList.length())
            textCaseList.append(textCase);
    }

    QString formatString(const QString &str) const
    {
        QString result;
        QVirtualKeyboardInputEngine::TextCase textCase = QVirtualKeyboardInputEngine::TextCase::Lower;
        for (int i = 0; i < str.length(); ++i) {
            if (i < textCaseList.length())
                textCase = textCaseList.at(i);
            result.append(textCase == QVirtualKeyboardInputEngine::TextCase::Upper ? str.at(i).toUpper() : (preferLowercase ? str.at(i).toLower() : str.at(i)));
        }
        return result;
    }

    bool preferLowercase;

private:
    QList<QVirtualKeyboardInputEngine::TextCase> textCaseList;
};

class T9WriteInputMethodPrivate : public QVirtualKeyboardAbstractInputMethodPrivate
#ifdef HAVE_XT9
        , public Xt9RequestCallback
#endif
{
public:
    Q_DECLARE_PUBLIC(T9WriteInputMethod)

    T9WriteInputMethodPrivate(T9WriteInputMethod *q_ptr) :
        QVirtualKeyboardAbstractInputMethodPrivate(),
        q_ptr(q_ptr),
        cjk(false),
        engineMode(T9WriteInputMethod::EngineMode::Uninitialized),
        traceListHardLimit(32),
        attachedDictionary(nullptr),
        ldbManager(new Xt9LdbManager()),
        resultId(0),
        lastResultId(0),
        resultTimer(0),
        decumaSession(nullptr),
        activeWordIndex(-1),
        arcAdditionStarted(false),
        ignoreUpdate(false),
        textCase(QVirtualKeyboardInputEngine::TextCase::Lower)
#ifdef QT_VIRTUALKEYBOARD_RECORD_TRACE_INPUT
        , unipenTrace()
#endif
    {
        const QString pathListSep(
#if defined(Q_OS_WIN32)
            QStringLiteral(";")
#else
            QStringLiteral(":")
#endif
        );
        const QString userHwrDbPath(qEnvironmentVariable("QT_VIRTUALKEYBOARD_CERENCE_HWR_DB_PATH"));
        defaultHwrDbPaths = userHwrDbPath.split(pathListSep, Qt::SkipEmptyParts)
                << QDir(QStringLiteral("%1/qtvirtualkeyboard/cerence/handwriting/").arg(QLibraryInfo::path(QLibraryInfo::DataPath))).absolutePath()
                << QStringLiteral(":/QtQuick/VirtualKeyboard/Cerence/Handwriting/");
        ldbManager->setPhdEnabled(true);
        for (const QString &searchPath : defaultHwrDbPaths) {
            ldbManager->addSearchPath(searchPath);
        }
    }

    static void *decumaMalloc(size_t size, void *pPrivate)
    {
        Q_UNUSED(pPrivate);
        return malloc(size);
    }

    static void *decumaCalloc(size_t elements, size_t size, void *pPrivate)
    {
        Q_UNUSED(pPrivate);
        return calloc(elements, size);
    }

    static void decumaFree(void *ptr, void *pPrivate)
    {
        Q_UNUSED(pPrivate);
        free(ptr);
    }

#if QT_VIRTUALKEYBOARD_CERENCE_HWR_LOG
    static void decumaLogString(void *pUserData, const char *pLogString, DECUMA_UINT32 nLogStringLength)
    {
        static QMutex s_logMutex;
        static QByteArray s_logString;
        Q_UNUSED(pUserData);
        QMutexLocker guard(&s_logMutex);
        s_logString.append(pLogString, nLogStringLength);
        if (s_logString.endsWith('\n')) {
            while (s_logString.endsWith('\n'))
                s_logString.chop(1);
            qDebug() << (const char *)s_logString.constData();
            s_logString.clear();
        }
    }
#endif

    static const char *engineModeToString(T9WriteInputMethod::EngineMode mode)
    {
        return QMetaEnum::fromType<T9WriteInputMethod::EngineMode>().key(static_cast<int>(mode));
    }

    bool initEngine(T9WriteInputMethod::EngineMode newEngineMode)
    {
        if (engineMode == newEngineMode)
            return engineMode != T9WriteInputMethod::EngineMode::Uninitialized;

        qCDebug(lcT9Write) << "T9WriteInputMethodPrivate::initEngine()" << engineModeToString(newEngineMode);

        if (decumaSession)
            exitEngine();

        if (newEngineMode == T9WriteInputMethod::EngineMode::Uninitialized)
            return false;

        switch (newEngineMode) {
        case T9WriteInputMethod::EngineMode::Alphabetic:
        case T9WriteInputMethod::EngineMode::Arabic:
        case T9WriteInputMethod::EngineMode::Hebrew:
        case T9WriteInputMethod::EngineMode::Thai:
            cjk = false;
            break;
        case T9WriteInputMethod::EngineMode::SimplifiedChinese:
        case T9WriteInputMethod::EngineMode::TraditionalChinese:
        case T9WriteInputMethod::EngineMode::HongKongChinese:
        case T9WriteInputMethod::EngineMode::Japanese:
        case T9WriteInputMethod::EngineMode::Korean:
            cjk = true;
            break;
        default:
            Q_ASSERT(0 && "Invalid T9WriteInputMethod::EngineMode!");
            return false;
        }
        engineMode = newEngineMode;

        memset(&sessionSettings, 0, sizeof(sessionSettings));

        QString hwrDb = findHwrDb(engineMode);
        hwrDbFile.setFileName(hwrDb);
        if (!hwrDbFile.open(QIODevice::ReadOnly)) {
            qCCritical(lcT9Write) << "Could not open HWR database" << hwrDb;
            exitEngine();
            return false;
        }

        sessionSettings.pStaticDB = reinterpret_cast<DECUMA_STATIC_DB_PTR>(hwrDbFile.map(0, hwrDbFile.size(), QFile::NoOptions));
        if (!sessionSettings.pStaticDB) {
            qCCritical(lcT9Write) << "Could not read HWR database" << hwrDb;
            exitEngine();
            return false;
        }

        symbolCategories.append(DECUMA_CATEGORY_ANSI);
        languageCategories.append(DECUMA_LANG_EN);

        sessionSettings.recognitionMode = mcrMode;
        sessionSettings.writingDirection = unknownWriting;
        sessionSettings.charSet.pSymbolCategories = symbolCategories.data();
        sessionSettings.charSet.nSymbolCategories = static_cast<DECUMA_UINT8>(symbolCategories.size());
        sessionSettings.charSet.pLanguages = languageCategories.data();
        sessionSettings.charSet.nLanguages = static_cast<DECUMA_UINT8>(languageCategories.size());

        session = QByteArray(static_cast<int>(DECUMA_API(GetSessionSize)()), 0);
        decumaSession = reinterpret_cast<DECUMA_SESSION *>(!session.isEmpty() ? session.data() : nullptr);

        DECUMA_STATUS status = DECUMA_API(BeginSession)(decumaSession, &sessionSettings, &memFuncs);
        Q_ASSERT(status == decumaNoError);
        if (status != decumaNoError) {
            qCCritical(lcT9Write) << "Could not initialize engine" << status;
            exitEngine();
            return false;
        }

#if QT_VIRTUALKEYBOARD_CERENCE_HWR_LOG
        DECUMA_API(StartLogging)(decumaSession, 0, decumaLogString);
#endif

        worker.reset(new T9WriteWorker(decumaSession, cjk));
        worker->start();

        Q_Q(T9WriteInputMethod);
        resultListChangedConnection = QObjectPrivate::connect(
                    q, &T9WriteInputMethod::resultListChanged,
                    this, &T9WriteInputMethodPrivate::processResultCheckTimer,
                    Qt::QueuedConnection);
        availableDictionariesChangedConnection = QObjectPrivate::connect(QVirtualKeyboardDictionaryManager::instance(),
                   &QVirtualKeyboardDictionaryManager::availableDictionariesChanged,
                   this, &T9WriteInputMethodPrivate::onAvailableDynamicDictionariesChanged);
        activeDictionariesChangedConnection = QObjectPrivate::connect(QVirtualKeyboardDictionaryManager::instance(),
                   &QVirtualKeyboardDictionaryManager::activeDictionariesChanged,
                   this, &T9WriteInputMethodPrivate::onActiveDynamicDictionariesChanged);

#ifdef HAVE_XT9
        bindSettings();
#endif

        return true;
    }

    void exitEngine()
    {
        qCDebug(lcT9Write) << "T9WriteInputMethodPrivate::exitEngine()";

#ifdef HAVE_XT9
        unbindSettings();
#endif

        QObject::disconnect(resultListChangedConnection);
        QObject::disconnect(availableDictionariesChangedConnection);
        QObject::disconnect(activeDictionariesChangedConnection);

        worker.reset();

#ifdef HAVE_XT9
        xt9Exit();
#endif

        if (sessionSettings.pStaticDB) {
            hwrDbFile.unmap(const_cast<uchar *>(reinterpret_cast<const uchar *>(sessionSettings.pStaticDB)));
            hwrDbFile.close();
        }

        dlmDeactivate();

        if (attachedDictionary) {
            detachDictionary(attachedDictionary);
            attachedDictionary.reset();
        }
        loadedDictionary.reset();

        for (auto &dynamicDictionary : attachedDynamicDictionaries) {
            detachDictionary(dynamicDictionary);
        }
        attachedDynamicDictionaries.clear();
        dynamicDictionaries.clear();

        if (decumaSession) {
#if QT_VIRTUALKEYBOARD_CERENCE_HWR_LOG
            DECUMA_API(StopLogging)(decumaSession);
#endif
            DECUMA_API(EndSession)(decumaSession);
            decumaSession = nullptr;
            session.clear();
        }

        memset(&sessionSettings, 0, sizeof(sessionSettings));

        symbolCategories.clear();
        languageCategories.clear();

        ldbManager->closeAll();

        engineMode = T9WriteInputMethod::EngineMode::Uninitialized;
        cjk = false;
    }

#ifdef HAVE_XT9
    void xt9Init(const QLocale &locale, QVirtualKeyboardInputEngine::InputMode inputMode, T9WriteInputMethod::EngineMode newEngineMode)
    {
        ET9U32 dwFirstLdbNum = Xt9LanguageMap::languageId(locale);
        ET9U32 eInputMode = ET9AWInputMode_Default;

        xt9DetachAllDynamicDictionaries();

        switch (newEngineMode) {
        case T9WriteInputMethod::EngineMode::SimplifiedChinese:
        case T9WriteInputMethod::EngineMode::TraditionalChinese:
        case T9WriteInputMethod::EngineMode::HongKongChinese:
            if (inputMode == QVirtualKeyboardInputEngine::InputMode::ChineseHandwriting) {
                xt9Ime.reset(new Xt9CpIme(this));
                switch (locale.script()) {
                case QLocale::SimplifiedHanScript:
                    if (static_cast<Xt9CpIme *>(xt9Ime.data())) {
                        eInputMode = ET9CPMODE_PINYIN;
                    }
                    break;
                default:
                    break;
                }
            } else {
                if (xt9IsAlphabeticCoreDisabled()) {
                    xt9Exit();
                    return;
                }
                xt9Ime.reset(new Xt9AwIme(this));
                dwFirstLdbNum = ET9LIDEnglish_UK;
            }
            break;

        case T9WriteInputMethod::EngineMode::Japanese:
            xt9Ime.reset(new Xt9JIme(this));
            eInputMode = ET9AWInputMode_RomajiConversion;
            break;

        case T9WriteInputMethod::EngineMode::Korean:
            xt9Ime.reset(new Xt9KIme(this));
            break;

        default:
            if (xt9IsAlphabeticCoreDisabled()) {
                xt9Exit();
                return;
            }
            xt9Ime.reset(new Xt9AwIme(this));
            break;
        }

        xt9Ime->ldbManager = ldbManager;
        xt9Ime->sysInit();
        xt9Ime->setWorkingDirectory(Settings::instance()->userDataPath());
        if (xt9DynamicDictionaries.isEmpty())
            xt9Ime->removeAllIndexes();

        // PHD must be disabled during XT9 init to prevent finding
        // T9 Write specific dictionary files.
        ldbManager->setPhdEnabled(false);

        xt9Ime->ldbInit(dwFirstLdbNum, ET9PLIDNone, eInputMode);

        ldbManager->setPhdEnabled(true);

        onDefaultDictionaryDisabledChanged();
        onAvailableDynamicDictionariesChanged();
        onActiveDynamicDictionariesChanged();
    }

    void xt9Exit()
    {
        if (xt9Ime)
            xt9Ime->removeAllIndexes();

        Q_Q(T9WriteInputMethod);
        QVirtualKeyboardDictionaryManager *dictionaryManager = QVirtualKeyboardDictionaryManager::instance();
        const QStringList availableDictionaries = dictionaryManager->availableDictionaries();
        for (const QString &dictionaryName : availableDictionaries) {
            QVirtualKeyboardDictionary *dictionary = dictionaryManager->dictionary(dictionaryName);
            dictionary->disconnect(q);  // lambdas
        }

        xt9AttachedDynamicDictionaries.clear();
        xt9DynamicDictionaries.clear();
        xt9DynamicDictionaryNextId = 0;

        xt9Ime.reset();
    }

    void bindSettings()
    {
        if (!defaultInputMethodDisabledChangedConnection)
            defaultInputMethodDisabledChangedConnection = QObjectPrivate::connect(
                        Settings::instance(), &Settings::defaultInputMethodDisabledChanged,
                        this, &T9WriteInputMethodPrivate::onXt9AlphabeticCoreDisabledChanged);
        if (!defaultDictionaryDisabledChangedConnection)
            defaultDictionaryDisabledChangedConnection = QObjectPrivate::connect(
                        Settings::instance(), &Settings::defaultDictionaryDisabledChanged,
                        this, &T9WriteInputMethodPrivate::onDefaultDictionaryDisabledChanged);
        if (!userDataResetConnection)
            userDataResetConnection = QObjectPrivate::connect(
                        Settings::instance(), &Settings::userDataReset,
                        this, &T9WriteInputMethodPrivate::onUserDataReset);
    }

    void unbindSettings()
    {
        QObject::disconnect(defaultInputMethodDisabledChangedConnection);
        QObject::disconnect(defaultDictionaryDisabledChangedConnection);
        QObject::disconnect(userDataResetConnection);
    }

    void onXt9AlphabeticCoreDisabledChanged()
    {
        if (xt9IsAlphabeticCoreDisabled()) {
            xt9Exit();
        } else if (!xt9Ime) {
            Q_Q(T9WriteInputMethod);
            const QVirtualKeyboardInputEngine::InputMode inputMode = q->inputEngine()->inputMode();
            const QLocale locale = QLocale(q->inputContext()->locale());
            const DECUMA_UINT32 language = mapToDecumaLanguage(locale, inputMode);
            const T9WriteInputMethod::EngineMode newEngineMode = mapLocaleToEngineMode(locale, language);
            xt9Init(locale, inputMode, newEngineMode);
        }
    }

    static bool xt9IsAlphabeticCoreDisabled()
    {
        Settings *settings = Settings::instance();
        return settings->isDefaultInputMethodDisabled();
    }

    void onDefaultDictionaryDisabledChanged()
    {
        if (Xt9AwIme *xt9AwIme = dynamic_cast<Xt9AwIme *>(xt9Ime.data()))
            xt9AwIme->setLdbEnabled(!Settings::instance()->isDefaultDictionaryDisabled());
    }

    void onUserDataReset()
    {
        dlmDeactivate();
#ifdef HAVE_XT9
        xt9Exit();
#endif
    }

    ET9STATUS request(ET9_Request *const pRequest) override
    {
        Q_Q(T9WriteInputMethod);

        switch (pRequest->eType) {
        case ET9_REQ_BufferContext:
        {
            QVirtualKeyboardInputContext *ic = q->inputContext();
            if (!ic)
                break;

            const ET9U32 dwContextLen = static_cast<ET9U32>(ic->cursorPosition());
            const ET9U32 dwStartIndex =
                    dwContextLen <= pRequest->data.sBufferContextInfo.dwMaxBufLen ?
                        0 : dwContextLen - pRequest->data.sBufferContextInfo.dwMaxBufLen;

            pRequest->data.sBufferContextInfo.dwBufLen = dwContextLen - dwStartIndex;
            const QString surroundingText = ic->surroundingText();

            memcpy(pRequest->data.sBufferContextInfo.psBuf, surroundingText.utf16() + dwStartIndex,
                   pRequest->data.sBufferContextInfo.dwBufLen * sizeof(ET9SYMB));

            break;
        }

        default:
            break;
        }

        return ET9STATUS_NONE;
    }

    bool xt9AllSymbsArePinyin(const QString &symbs) const
    {
        bool allSymbsArePinyin = false;
        for (const QChar &symb : symbs) {
            const ushort ucs = symb.unicode();
            allSymbsArePinyin = ET9CPIsPinyinSymbol(ucs);
            if (!allSymbsArePinyin)
                break;
        }
        return allSymbsArePinyin;
    }

    bool xt9AttachDictionary(const QString &dictionaryName)
    {
        if (!xt9DynamicDictionaries.contains(dictionaryName))
            return false;

        const quint16 id = xt9DynamicDictionaries.value(dictionaryName);
        qCDebug(lcT9Write) << "T9WriteInputMethodPrivate::xt9AttachDictionary():" << dictionaryName << id;
        xt9Ime->mountIndex(id);
        xt9AttachedDynamicDictionaries[dictionaryName] = id;

        return true;
    }

    void xt9DetachDictionary(const QString &dictionaryName)
    {
        if (!xt9AttachedDynamicDictionaries.contains(dictionaryName))
            return;

        const quint16 id = xt9AttachedDynamicDictionaries[dictionaryName];
        qCDebug(lcT9Write) << "T9WriteInputMethodPrivate::xt9DetachDictionary():" << dictionaryName << id;
        xt9Ime->unmountIndex(id);
        xt9AttachedDynamicDictionaries.remove(dictionaryName);
    }
#endif

#ifdef HAVE_XT9
    void xt9DetachAllDynamicDictionaries()
    {
        if (xt9Ime) {
            const QStringList dictionaryNames = xt9AttachedDynamicDictionaries.keys();
            for (const QString &dictionaryName : dictionaryNames) {
                xt9DetachDictionary(dictionaryName);
            }
        }
        xt9AttachedDynamicDictionaries.clear();
    }
#endif

    QStringList xt9BuildSelectionList(const QString &exactWord, int *defaultListIndex)
    {
#ifdef HAVE_XT9
        if (!xt9Ime)
            return QStringList();

        xt9Ime->cursorMoved();

        Q_Q(T9WriteInputMethod);
        bool forceLowerCase =
                engineMode == T9WriteInputMethod::EngineMode::SimplifiedChinese &&
                q->inputEngine()->inputMode() == QVirtualKeyboardInputEngine::InputMode::ChineseHandwriting &&
                xt9AllSymbsArePinyin(exactWord);

        ET9ClearAllSymbs(&xt9Ime->sWordSymbInfo);
        for (const QChar &c : exactWord) {
            ET9AddExplicitSymb(&xt9Ime->sWordSymbInfo, c.unicode(), 0, !forceLowerCase && c.isUpper() ? ET9SHIFT : ET9NOSHIFT, ET9_NO_ACTIVE_INDEX);
        }

        ET9SYMB gestureValue = 0;
        return xt9Ime->buildSelectionList(defaultListIndex, &gestureValue);
#else
        return QStringList();
#endif
    }

    void appendWordCandidates(QList<T9WriteWordCandidate> &aWordCandidates, int &activeWordIndex, const QString &exactWord, const QStringList &wordCandidatesToAppend, int defaultListIndex, T9WriteWordCandidate::Origin origin) const
    {
        for (int index = 0; index < wordCandidatesToAppend.size(); ++index) {
            const T9WriteWordCandidate wordCandidate(wordCandidatesToAppend[index], index, origin);
            if (!aWordCandidates.contains(wordCandidate)) {
                bool isDefaultIndex = defaultListIndex > 0 &&
                        index == defaultListIndex && !cjk &&
                        origin == T9WriteWordCandidate::Origin::XT9;
                if (isDefaultIndex) {
                    activeWordIndex = qMin(aWordCandidates.size(), 1);
                    aWordCandidates.insert(activeWordIndex, wordCandidate);
                } else {
                    aWordCandidates.append(wordCandidate);
                }
            }
        }

        if (aWordCandidates.isEmpty()) {
            aWordCandidates.append(T9WriteWordCandidate(exactWord));
        }
    }

    QString findHwrDb(T9WriteInputMethod::EngineMode mode) const
    {
        for (const QString &defaultHwrDbPath : defaultHwrDbPaths) {
            const QString result = findHwrDb(mode, defaultHwrDbPath);
            if (!result.isEmpty())
                return result;
        }

        qCCritical(lcT9Write) << "Could not find HWR database for" << engineModeToString(mode);
        return QString();
    }

    QString findHwrDb(T9WriteInputMethod::EngineMode mode, const QString &dir) const
    {
        QString hwrDbPath(QDir::fromNativeSeparators(dir));
        if (!hwrDbPath.endsWith(QLatin1Char('/')))
            hwrDbPath.append(QLatin1Char('/'));
        switch (mode) {
        case T9WriteInputMethod::EngineMode::Alphabetic:
            hwrDbPath.append(QLatin1String("hwrDB_le.bin"));
            break;
        case T9WriteInputMethod::EngineMode::Arabic:
            hwrDbPath.append(QLatin1String("arabic/hwrDB_le.bin"));
            break;
        case T9WriteInputMethod::EngineMode::Hebrew:
            hwrDbPath.append(QLatin1String("hebrew/hwrDB_le.bin"));
            break;
        case T9WriteInputMethod::EngineMode::Thai:
            hwrDbPath.append(QLatin1String("thai/hwrDB_le.bin"));
            break;
        case T9WriteInputMethod::EngineMode::SimplifiedChinese:
            hwrDbPath.append(QLatin1String("cjk_S_gb18030_le.hdb"));
            break;
        case T9WriteInputMethod::EngineMode::TraditionalChinese:
        case T9WriteInputMethod::EngineMode::HongKongChinese:
            hwrDbPath.append(QLatin1String("cjk_HK_std_le.hdb"));
            break;
        case T9WriteInputMethod::EngineMode::Japanese:
            hwrDbPath.append(QLatin1String("cjk_J_std_le.hdb"));
            break;
        case T9WriteInputMethod::EngineMode::Korean:
            hwrDbPath.append(QLatin1String("cjk_K_mkt_le.hdb"));
            break;
        default:
            return QString();
        }
        if (!QFileInfo::exists(hwrDbPath)) {
            return QString();
        }
        return hwrDbPath;
    }

    QString findDictionary(const QLocale &locale, DECUMA_SRC_DICTIONARY_TYPE &srcType)
    {
        srcType = numberOfSrcDictionaryTypes;

        QString dictionary = ldbManager->findDictionary(locale);
        if (!dictionary.isEmpty()) {
            if (dictionary.endsWith(QLatin1String(".ldb"))) {
                srcType = decumaPortableHWRDictionary;
            } else if (dictionary.endsWith(QLatin1String(".phd"))) {
                srcType = decumaPortableHWRDictionary;
            } else {
                qCCritical(lcT9Write) << "Incompatible dictionary" << dictionary;
                dictionary.clear();
            }
        }

        return dictionary;
    }

    bool attachDictionary(const QSharedPointer<T9WriteAbstractDictionary> &dictionary)
    {
        const std::lock_guard<QRecursiveMutex> dictionaryGuard(dictionaryLock);
        Q_ASSERT(decumaSession != nullptr);
        Q_ASSERT(dictionary != nullptr);
        qCDebug(lcT9Write) << "T9WriteInputMethodPrivate::attachDictionary():" << dictionary->name();
        DECUMA_STATUS status = DECUMA_API(AttachDictionary)(decumaSession, dictionary->data(), static_cast<DECUMA_UINT32>(dictionary->size()));
        return status == decumaNoError;
    }

    void detachDictionary(const QSharedPointer<T9WriteAbstractDictionary> &dictionary)
    {
        const std::lock_guard<QRecursiveMutex> dictionaryGuard(dictionaryLock);
        if (!dictionary)
            return;

        qCDebug(lcT9Write) << "T9WriteInputMethodPrivate::detachDictionary():" << dictionary->name();

        Q_ASSERT(decumaSession != nullptr);
        DECUMA_STATUS status = DECUMA_API(DetachDictionary)(decumaSession, dictionary->data());
        Q_UNUSED(status);
        Q_ASSERT(status == decumaNoError);
    }

    bool setInputMode(const QLocale &locale, QVirtualKeyboardInputEngine::InputMode inputMode)
    {
        Q_Q(T9WriteInputMethod);
        qCDebug(lcT9Write) << "T9WriteInputMethodPrivate::setInputMode():" << locale << inputMode;

        finishRecognition();

        // All tasks must be completed before changing the session settings
        if (worker) {
            worker->waitForAllTasks();
            dictionaryTask.reset();
        }

        DECUMA_UINT32 language = mapToDecumaLanguage(locale, inputMode);
        if (language == DECUMA_LANG_GSMDEFAULT) {
            qCCritical(lcT9Write) << "Language is not supported" << locale.name();
            return false;
        }

        T9WriteInputMethod::EngineMode newEngineMode = mapLocaleToEngineMode(locale, language);

#ifdef HAVE_XT9
        xt9Init(locale, inputMode, newEngineMode);
#endif

        if (!initEngine(newEngineMode)) {
#ifdef HAVE_XT9
            xt9Exit();
#endif
            return false;
        }

        onAvailableDynamicDictionariesChanged();
        onActiveDynamicDictionariesChanged();

        const Qt::InputMethodHints inputMethodHints = q->inputContext()->inputMethodHints();
        if (!inputMethodHints.testFlag(Qt::ImhHiddenText) && !inputMethodHints.testFlag(Qt::ImhNoPredictiveText) &&
                !inputMethodHints.testFlag(Qt::ImhSensitiveData)) {
            dlmActivate();
        }

        int isLanguageSupported = 0;
        DECUMA_API(DatabaseIsLanguageSupported)(sessionSettings.pStaticDB, language, &isLanguageSupported);
        if (!isLanguageSupported) {
            qCCritical(lcT9Write) << "Language is not supported" << locale.name();
            return false;
        }

        bool languageChanged = languageCategories.isEmpty() || languageCategories.first() != language;
        languageCategories.clear();
        languageCategories.append(language);

        // Add English as secondary language for non-latin languages.
        // T9 Write requires it for punctuation and latin symbols if
        // included in the symbol categories.
        if (locale.script() != QLocale::LatinScript)
            languageCategories.append(DECUMA_LANG_EN);

        if (!updateSymbolCategories(language, locale, inputMode))
            return false;
        updateRecognitionMode(language, locale, inputMode);
        static const QList<DECUMA_UINT32> rtlLanguages = QList<DECUMA_UINT32>()
                << DECUMA_LANG_AR << DECUMA_LANG_IW << DECUMA_LANG_FA << DECUMA_LANG_UR;
        sessionSettings.writingDirection = rtlLanguages.contains(language) ? rightToLeft : leftToRight;

        // Enable multi-threaded recognition if available.
#ifdef DECUMA_USE_MULTI_THREAD
        // Note: This feature requires T9 Write v8.0.0 or later,
        //       and feature enabled in the SDK.
#if CERENCEHANDWRITINGAPIMAJORVERNUM > 28
        sessionSettings.bUseThreads = 1;
#else
        sessionSettings.nMaxThreads = qMax(QThread::idealThreadCount(), 0);
#endif
#endif

        qCDebug(lcT9Write) << " -> language categories:" << languageCategories;
        qCDebug(lcT9Write) << " -> symbol categories:" << symbolCategories;
        qCDebug(lcT9Write) << " -> recognition mode:" << sessionSettings.recognitionMode;

        // Change session settings
        sessionSettings.charSet.pSymbolCategories = symbolCategories.data();
        sessionSettings.charSet.nSymbolCategories = static_cast<DECUMA_UINT8>(symbolCategories.size());
        sessionSettings.charSet.pLanguages = languageCategories.data();
        sessionSettings.charSet.nLanguages = static_cast<DECUMA_UINT8>(languageCategories.size());
        DECUMA_STATUS status = DECUMA_API(ChangeSessionSettings)(decumaSession, &sessionSettings);

        if (status == decumaUnsupportedLanguageInUcrMode || status == decumaUnsupportedSymbolCategoryInUcrMode) {
            sessionSettings.recognitionMode = mcrMode;
            status = DECUMA_API(ChangeSessionSettings)(decumaSession, &sessionSettings);
        }

        if (status != decumaNoError)
            qCWarning(lcT9Write) << "Failed to configure HWR engine" << status;

        caseFormatter.preferLowercase = q->inputContext()->inputMethodHints().testFlag(Qt::ImhPreferLowercase);

        updateDictionary(language, locale, languageChanged);

        return status == decumaNoError;
    }

    T9WriteInputMethod::EngineMode mapLocaleToEngineMode(const QLocale &locale, DECUMA_UINT32 language = 0)
    {
#ifdef HAVE_CERENCE_HWR_CJK
        switch (locale.language()) {
        case QLocale::Chinese: {
            if (locale.script() == QLocale::TraditionalChineseScript)
                return locale.country() == QLocale::HongKong ? T9WriteInputMethod::EngineMode::HongKongChinese : T9WriteInputMethod::EngineMode::TraditionalChinese;
            return T9WriteInputMethod::EngineMode::SimplifiedChinese;
        }
        case QLocale::Japanese:
            return T9WriteInputMethod::EngineMode::Japanese;
        case QLocale::Korean:
            return T9WriteInputMethod::EngineMode::Korean;
        default:
            break;
        }
#else
        Q_UNUSED(locale);
        Q_UNUSED(language);
#endif

#ifdef HAVE_CERENCE_HWR_ALPHABETIC
        switch (locale.script()) {
        case QLocale::ArabicScript:
            return T9WriteInputMethod::EngineMode::Arabic;
        case QLocale::HebrewScript:
            return T9WriteInputMethod::EngineMode::Hebrew;
        case QLocale::ThaiScript:
            return language == DECUMA_LANG_EN ? T9WriteInputMethod::EngineMode::Alphabetic
                                              : T9WriteInputMethod::EngineMode::Thai;
        default:
            return T9WriteInputMethod::EngineMode::Alphabetic;
        }
#else
        return T9WriteInputMethod::EngineMode::Uninitialized;
#endif
    }

    DECUMA_UINT32 mapToDecumaLanguage(const QLocale &locale, QVirtualKeyboardInputEngine::InputMode inputMode)
    {
        struct LanguageMap {
            QLocale::Language localeLanguage;
            DECUMA_UINT16 decumaLanguage;
        };
        static const LanguageMap languageMap[] = {
            { QLocale::Language::Afrikaans,          DECUMA_LANG_AF },
            { QLocale::Language::Albanian,           DECUMA_LANG_SQ },
            { QLocale::Language::Arabic,             DECUMA_LANG_AR },
            { QLocale::Language::Azerbaijani,        DECUMA_LANG_AZ },
            { QLocale::Language::Basque,             DECUMA_LANG_EU },
            { QLocale::Language::Belarusian,         DECUMA_LANG_BE },
            { QLocale::Language::Bengali,            DECUMA_LANG_BN },
            { QLocale::Language::Bulgarian,          DECUMA_LANG_BG },
            { QLocale::Language::Catalan,            DECUMA_LANG_CA },
            { QLocale::Language::Chinese,            DECUMA_LANG_PRC },
            { QLocale::Language::Croatian,           DECUMA_LANG_HR },
            { QLocale::Language::Czech,              DECUMA_LANG_CS },
            { QLocale::Language::Danish,             DECUMA_LANG_DA },
            { QLocale::Language::Dutch,              DECUMA_LANG_NL },
            { QLocale::Language::English,            DECUMA_LANG_EN },
            { QLocale::Language::Estonian,           DECUMA_LANG_ET },
            { QLocale::Language::Finnish,            DECUMA_LANG_FI },
            { QLocale::Language::French,             DECUMA_LANG_FR },
            { QLocale::Language::Galician,           DECUMA_LANG_GL },
            { QLocale::Language::German,             DECUMA_LANG_DE },
            { QLocale::Language::Greek,              DECUMA_LANG_EL },
            { QLocale::Language::Gujarati,           DECUMA_LANG_GU },
            { QLocale::Language::Hausa,              DECUMA_LANG_HA },
            { QLocale::Language::Hebrew,             DECUMA_LANG_IW },
            { QLocale::Language::Hindi,              DECUMA_LANG_HI },
            { QLocale::Language::Hungarian,          DECUMA_LANG_HU },
            { QLocale::Language::Icelandic,          DECUMA_LANG_IS },
            { QLocale::Language::Indonesian,         DECUMA_LANG_IN },
            { QLocale::Language::Italian,            DECUMA_LANG_IT },
            { QLocale::Language::Japanese,           DECUMA_LANG_JP },
            { QLocale::Language::Kannada,            DECUMA_LANG_KN },
            { QLocale::Language::Kazakh,             DECUMA_LANG_KK },
            { QLocale::Language::Khmer,              DECUMA_LANG_KM },
            { QLocale::Language::Kirghiz,            DECUMA_LANG_KY },
            { QLocale::Language::Korean,             DECUMA_LANG_KO },
            { QLocale::Language::Latvian,            DECUMA_LANG_LV },
            { QLocale::Language::Lithuanian,         DECUMA_LANG_LT },
            { QLocale::Language::Macedonian,         DECUMA_LANG_MK },
            { QLocale::Language::Malay,              DECUMA_LANG_MS },
            { QLocale::Language::Malayalam,          DECUMA_LANG_ML },
            { QLocale::Language::Marathi,            DECUMA_LANG_MR },
            { QLocale::Language::Mongolian,          DECUMA_LANG_MN },
            { QLocale::Language::NorwegianBokmal,    DECUMA_LANG_NO },
            { QLocale::Language::Persian,            DECUMA_LANG_FA },
            { QLocale::Language::Polish,             DECUMA_LANG_PL },
            { QLocale::Language::Portuguese,         DECUMA_LANG_PT },
            { QLocale::Language::Punjabi,            DECUMA_LANG_PA },
            { QLocale::Language::Romanian,           DECUMA_LANG_RO },
            { QLocale::Language::Russian,            DECUMA_LANG_RU },
            { QLocale::Language::Serbian,            DECUMA_LANG_SRCY },
            { QLocale::Language::Sinhala,            DECUMA_LANG_SI },
            { QLocale::Language::Slovak,             DECUMA_LANG_SK },
            { QLocale::Language::Slovenian,          DECUMA_LANG_SL },
            { QLocale::Language::SouthernSotho,      DECUMA_LANG_ST },
            { QLocale::Language::Spanish,            DECUMA_LANG_ES },
            { QLocale::Language::Swahili,            DECUMA_LANG_SW },
            { QLocale::Language::Swedish,            DECUMA_LANG_SV },
            { QLocale::Language::Tajik,              DECUMA_LANG_TG },
            { QLocale::Language::Tamil,              DECUMA_LANG_TA },
            { QLocale::Language::Telugu,             DECUMA_LANG_TE },
            { QLocale::Language::Thai,               DECUMA_LANG_TH },
            { QLocale::Language::Turkish,            DECUMA_LANG_TR },
            { QLocale::Language::Ukrainian,          DECUMA_LANG_UK },
            { QLocale::Language::Urdu,               DECUMA_LANG_UR },
            { QLocale::Language::Uzbek,              DECUMA_LANG_UZ },
            { QLocale::Language::Vietnamese,         DECUMA_LANG_VI },
            // Last entry
            { QLocale::Language::AnyLanguage,        DECUMA_LANG_GSMDEFAULT }
        };

        const int localeLanguage = locale.language();
        const LanguageMap *languageMapIterator = languageMap;
        for (; languageMapIterator->localeLanguage != QLocale::Language::AnyLanguage &&
               languageMapIterator->localeLanguage != localeLanguage; languageMapIterator++) {}

        DECUMA_UINT32 language = languageMapIterator->decumaLanguage;
        if (language == DECUMA_LANG_PRC) {
            if (inputMode != QVirtualKeyboardInputEngine::InputMode::ChineseHandwriting)
                language = DECUMA_LANG_EN;
            else if (locale.script() == QLocale::TraditionalChineseScript)
                language = DECUMA_LANG_HK; // Common language for the traditional script
        } else if (language == DECUMA_LANG_JP) {
            if (inputMode != QVirtualKeyboardInputEngine::InputMode::JapaneseHandwriting)
                language = DECUMA_LANG_EN;
        } else if (language == DECUMA_LANG_KO) {
            if (inputMode != QVirtualKeyboardInputEngine::InputMode::KoreanHandwriting)
                language = DECUMA_LANG_EN;
        } else if (language == DECUMA_LANG_SRCY) {
            if (inputMode != QVirtualKeyboardInputEngine::InputMode::Cyrillic)
                language = DECUMA_LANG_SRLA;
        } else if (language == DECUMA_LANG_AR || language == DECUMA_LANG_FA) {
            if (inputMode != QVirtualKeyboardInputEngine::InputMode::Arabic && inputMode != QVirtualKeyboardInputEngine::InputMode::Numeric)
                language = DECUMA_LANG_EN;
        } else if (language == DECUMA_LANG_IW) {
            if (inputMode != QVirtualKeyboardInputEngine::InputMode::Hebrew)
                language = DECUMA_LANG_EN;
        } else if (language == DECUMA_LANG_TH) {
            if (inputMode != QVirtualKeyboardInputEngine::InputMode::Thai)
                language = DECUMA_LANG_EN;
        }

        return language;
    }

    void updateRecognitionMode(DECUMA_UINT32 language, const QLocale &locale,
                               QVirtualKeyboardInputEngine::InputMode inputMode)
    {
        Q_Q(T9WriteInputMethod);
        Q_UNUSED(language);
        Q_UNUSED(locale);

        // Select recognition mode
        // Note: MCR mode is preferred, as it does not require recognition
        //       timer and provides better user experience.
        sessionSettings.recognitionMode = mcrMode;

        // T9 Write Alphabetic v8.0.0 supports UCR mode for specific languages
        if (!cjk) {
            switch (inputMode) {
            case QVirtualKeyboardInputEngine::InputMode::Latin:
                sessionSettings.recognitionMode = ucrMode;
                break;
            case QVirtualKeyboardInputEngine::InputMode::Arabic:
                sessionSettings.recognitionMode = ucrMode;
                break;
            case QVirtualKeyboardInputEngine::InputMode::Cyrillic:
                sessionSettings.recognitionMode = ucrMode;
                break;
            case QVirtualKeyboardInputEngine::InputMode::Hebrew:
            case QVirtualKeyboardInputEngine::InputMode::Thai:
                sessionSettings.recognitionMode = ucrMode;
                break;
            default:
                break;
            }
        }

        // Use scrMode with hidden text or with no predictive mode
        if (inputMode != QVirtualKeyboardInputEngine::InputMode::ChineseHandwriting &&
                inputMode != QVirtualKeyboardInputEngine::InputMode::JapaneseHandwriting &&
                inputMode != QVirtualKeyboardInputEngine::InputMode::KoreanHandwriting) {
            const Qt::InputMethodHints inputMethodHints = q->inputContext()->inputMethodHints();
            if (inputMethodHints.testFlag(Qt::ImhHiddenText) || inputMethodHints.testFlag(Qt::ImhNoPredictiveText))
                sessionSettings.recognitionMode = scrMode;
        }
    }

    bool updateSymbolCategories(DECUMA_UINT32 language, const QLocale &locale,
                                QVirtualKeyboardInputEngine::InputMode inputMode)
    {
        // Handle CJK in separate method
        if (cjk)
            return updateSymbolCategoriesCjk(language, locale, inputMode);

        symbolCategories.clear();

        // Choose the symbol categories by input mode, script and input method hints
        bool leftToRightGestures = true;
        Q_Q(T9WriteInputMethod);
        const Qt::InputMethodHints inputMethodHints = q->inputContext()->inputMethodHints();
        switch (inputMode) {
        case QVirtualKeyboardInputEngine::InputMode::Latin:
            if (inputMethodHints.testFlag(Qt::ImhEmailCharactersOnly)) {
                symbolCategories.append(DECUMA_CATEGORY_EMAIL);
            } else if (inputMethodHints.testFlag(Qt::ImhUrlCharactersOnly)) {
                symbolCategories.append(DECUMA_CATEGORY_URL);
            } else {
                if (language == DECUMA_LANG_EN || language == DECUMA_LANG_NL ||
                        language == DECUMA_LANG_MS || language == DECUMA_LANG_IN)
                    symbolCategories.append(DECUMA_CATEGORY_ANSI);
                else
                    symbolCategories.append(DECUMA_CATEGORY_ISO8859_1);
                symbolCategories.append(DECUMA_CATEGORY_DIGIT);
                symbolCategories.append(DECUMA_CATEGORY_BASIC_PUNCTUATIONS);
                symbolCategories.append(DECUMA_CATEGORY_CONTRACTION_MARK);
                if (language == DECUMA_LANG_ES)
                    symbolCategories.append(DECUMA_CATEGORY_SPANISH_PUNCTUATIONS);
                else if (language == DECUMA_LANG_VI)
                    symbolCategories.append(DECUMA_CATEGORY_VIETNAMESE_SUPPLEMENTS);
            }
            break;

        case QVirtualKeyboardInputEngine::InputMode::Numeric:
            if (language == DECUMA_LANG_AR || language == DECUMA_LANG_FA) {
                symbolCategories.append(DECUMA_CATEGORY_ARABIC_NUM_MODE);
                symbolCategories.append(DECUMA_CATEGORY_ARABIC_GESTURES);
                leftToRightGestures = false;
                break;
            }
            symbolCategories.append(DECUMA_CATEGORY_DIGIT);
            if (!inputMethodHints.testFlag(Qt::ImhDigitsOnly))
                symbolCategories.append(DECUMA_CATEGORY_NUM_SUP);
            break;

        case QVirtualKeyboardInputEngine::InputMode::Dialable:
            symbolCategories.append(DECUMA_CATEGORY_PHONE_NUMBER);
            break;

        case QVirtualKeyboardInputEngine::InputMode::Greek:
            symbolCategories.append(DECUMA_CATEGORY_GREEK);
            symbolCategories.append(DECUMA_CATEGORY_QUEST_EXCL_MARK_PUNCTUATIONS);
            symbolCategories.append(DECUMA_CATEGORY_PERIOD_COMMA_PUNCTUATIONS);
            symbolCategories.append(DECUMA_CATEGORY_COLON_PUNCTUATIONS);
            symbolCategories.append(DECUMA_CATEGORY_CONTRACTION_MARK);
            symbolCategories.append(DECUMA_CATEGORY_CONTRACTION_MARK);
            break;

        case QVirtualKeyboardInputEngine::InputMode::Cyrillic:
            symbolCategories.append(DECUMA_CATEGORY_CYRILLIC);
            symbolCategories.append(DECUMA_CATEGORY_QUEST_EXCL_MARK_PUNCTUATIONS);
            symbolCategories.append(DECUMA_CATEGORY_PERIOD_COMMA_PUNCTUATIONS);
            // Ukrainian needs contraction mark, but not Russian or Bulgarian
            if (language == DECUMA_LANG_UK)
                symbolCategories.append(DECUMA_CATEGORY_CONTRACTION_MARK);
            break;

        case QVirtualKeyboardInputEngine::InputMode::Arabic:
            symbolCategories.append(DECUMA_CATEGORY_ARABIC_ISOLATED_LETTER_MODE);
            symbolCategories.append(DECUMA_CATEGORY_ARABIC_GESTURES);
            leftToRightGestures = false;
            break;

        case QVirtualKeyboardInputEngine::InputMode::Hebrew:
            symbolCategories.append(DECUMA_CATEGORY_HEBREW_GL_HEBREW_CURSIVE_MODE);
            symbolCategories.append(DECUMA_CATEGORY_HEBREW_GL_HEBREW_LETTERSYMBOLS);
            symbolCategories.append(DECUMA_CATEGORY_HEBREW_SHEQEL);
            symbolCategories.append(DECUMA_CATEGORY_ARABIC_GESTURES);
            leftToRightGestures = false;
            break;

        case QVirtualKeyboardInputEngine::InputMode::Thai:
            symbolCategories.append(DECUMA_CATEGORY_THAI_BASE);
            symbolCategories.append(DECUMA_CATEGORY_THAI_NON_BASE);
            break;

        default:
            qCCritical(lcT9Write)  << "Invalid input mode" << inputMode;
            return false;
        }

        if (leftToRightGestures) {
            symbolCategories.append(DECUMA_CATEGORY_BACKSPACE_STROKE);
            symbolCategories.append(DECUMA_CATEGORY_RETURN_STROKE);
            symbolCategories.append(DECUMA_CATEGORY_WHITESPACE_STROKE);
        }

        return true;
    }

    bool updateSymbolCategoriesCjk(DECUMA_UINT32 language, const QLocale &locale,
                                   QVirtualKeyboardInputEngine::InputMode inputMode)
    {
        Q_ASSERT(cjk);

        symbolCategories.clear();

        switch (inputMode) {
        case QVirtualKeyboardInputEngine::InputMode::Latin:
            symbolCategories.append(DECUMA_CATEGORY_ANSI);
            symbolCategories.append(DECUMA_CATEGORY_CJK_SYMBOL);
            symbolCategories.append(DECUMA_CATEGORY_PUNCTUATIONS);
            break;

        case QVirtualKeyboardInputEngine::InputMode::Numeric:
            symbolCategories.append(DECUMA_CATEGORY_DIGIT);
            symbolCategories.append(DECUMA_CATEGORY_CJK_SYMBOL);
            symbolCategories.append(DECUMA_CATEGORY_PUNCTUATIONS);
            break;

        case QVirtualKeyboardInputEngine::InputMode::Dialable:
            symbolCategories.append(DECUMA_CATEGORY_DIGIT);
            symbolCategories.append(DECUMA_CATEGORY_CJK_SYMBOL);
            break;

        case QVirtualKeyboardInputEngine::InputMode::ChineseHandwriting:
            switch (locale.script()) {
            case QLocale::SimplifiedChineseScript:
                symbolCategories.append(DECUMA_CATEGORY_GB2312_A);
                symbolCategories.append(DECUMA_CATEGORY_GB2312_B_CHARS_ONLY);
                symbolCategories.append(DECUMA_CATEGORY_GBK_3);
                symbolCategories.append(DECUMA_CATEGORY_GBK_4);
                symbolCategories.append(DECUMA_CATEGORY_CJK_SYMBOL);
                symbolCategories.append(DECUMA_CATEGORY_CJK_GENERAL_PUNCTUATIONS);
                symbolCategories.append(DECUMA_CATEGORY_PUNCTUATIONS);
#ifdef HAVE_XT9
                if (xt9Ime) {
                    symbolCategories.append(DECUMA_CATEGORY_ANSI);
                }
#endif
                break;

            case QLocale::TraditionalChineseScript:
                symbolCategories.append(DECUMA_CATEGORY_BIGFIVE);
                if (language == DECUMA_LANG_HK)
                    symbolCategories.append(DECUMA_CATEGORY_HKSCS_CHARS_ONLY);
                symbolCategories.append(DECUMA_CATEGORY_CJK_SYMBOL);
                symbolCategories.append(DECUMA_CATEGORY_CJK_GENERAL_PUNCTUATIONS);
                symbolCategories.append(DECUMA_CATEGORY_PUNCTUATIONS);
                break;

            default:
                qCCritical(lcT9Write)  << "Invalid locale" << locale << "for" << engineModeToString(engineMode);
                return false;
            }
            break;

        case QVirtualKeyboardInputEngine::InputMode::JapaneseHandwriting:
            symbolCategories.append(DECUMA_CATEGORY_JIS_LEVEL_1);
            symbolCategories.append(DECUMA_CATEGORY_JIS_LEVEL_2);
            symbolCategories.append(DECUMA_CATEGORY_HIRAGANA);
            symbolCategories.append(DECUMA_CATEGORY_KATAKANA);
            symbolCategories.append(DECUMA_CATEGORY_HIRAGANASMALL);
            symbolCategories.append(DECUMA_CATEGORY_KATAKANASMALL);
            symbolCategories.append(DECUMA_CATEGORY_CJK_SYMBOL);
            symbolCategories.append(DECUMA_CATEGORY_CJK_GENERAL_PUNCTUATIONS);
            symbolCategories.append(DECUMA_CATEGORY_PUNCTUATIONS);
#ifdef HAVE_XT9
            if (xt9Ime) {
                symbolCategories.append(DECUMA_CATEGORY_ANSI);
            }
#endif
            break;

        case QVirtualKeyboardInputEngine::InputMode::KoreanHandwriting:
            symbolCategories.append(DECUMA_CATEGORY_HANGUL_1001_A);
            symbolCategories.append(DECUMA_CATEGORY_HANGUL_1001_B);
            symbolCategories.append(DECUMA_CATEGORY_CJK_SYMBOL);
            symbolCategories.append(DECUMA_CATEGORY_CJK_GENERAL_PUNCTUATIONS);
            symbolCategories.append(DECUMA_CATEGORY_PUNCTUATIONS);
            break;

        default:
            return false;
        }

        return true;
    }

    void updateDictionary(DECUMA_UINT32 language, const QLocale &locale, bool languageChanged)
    {
        Q_Q(T9WriteInputMethod);

        /*  The dictionary is loaded in the background thread. Once the loading is
            complete the dictionary will be attached to the current session. The
            attachment happens in the worker thread context, thus the direct
            connection for the signal handler and the mutex protecting the
            converted dictionary for concurrent access.
            The loading operation is blocking for the main thread only if the
            user starts handwriting input before the operation is complete.
        */
        const std::lock_guard<QRecursiveMutex> dictionaryGuard(dictionaryLock);

        // Detach previous dictionary if the language is being changed
        // or the recognizer mode is single-character mode
        const Qt::InputMethodHints inputMethodHints = q->inputContext()->inputMethodHints();
        if ((languageChanged || inputMethodHints.testFlag(Qt::ImhNoPredictiveText) || sessionSettings.recognitionMode == scrMode) && attachedDictionary) {
            detachDictionary(attachedDictionary);
            attachedDictionary.reset();
        }

        // Check if a dictionary needs to be loaded
        if (languageChanged || !loadedDictionary) {
            loadedDictionary.reset();

            DECUMA_SRC_DICTIONARY_INFO dictionaryInfo;
            memset(&dictionaryInfo, 0, sizeof(dictionaryInfo));

            QList<QLocale> decumaLocales;
            decumaLocales.append(locale);

            // CJK: No dictionary for latin input
            if (cjk && language == DECUMA_LANG_EN)
                decumaLocales.clear();

            dictionaryFileName.clear();
            QLocale decumaLocale;
            for (const QLocale &tryLocale : decumaLocales) {
                dictionaryFileName = findDictionary(tryLocale, dictionaryInfo.srcType);
                if (!dictionaryFileName.isEmpty()) {
                    decumaLocale = tryLocale;
                    break;
                }
            }
            if (!dictionaryFileName.isEmpty()) {
                if (dictionaryTask.isNull() || dictionaryTask->dictionaryFileName != dictionaryFileName) {
                    qCDebug(lcT9Write) << " -> load dictionary:" << dictionaryFileName;

                    bool convertDictionary = true;
#if defined(HAVE_CERENCE_HWR_CJK)
                    // Chinese dictionary cannot be converted (PHD)
                    if (dictionaryInfo.srcType == decumaPortableHWRDictionary && decumaLocale.language() == QLocale::Chinese)
                        convertDictionary = false;
#endif

                    QSharedPointer<T9WriteAbstractSource> sourceDictionary(new T9WriteFileSource(dictionaryInfo, dictionaryFileName));

                    QSharedPointer<T9WriteDictionary> newDictionary(new T9WriteDictionary(sourceDictionary, decumaSession, memFuncs, cjk));
                    dictionaryTask.reset(new T9WriteDictionaryTask(newDictionary, convertDictionary));

                    QObjectPrivate::connect(dictionaryTask.data(), &T9WriteDictionaryTask::completed,
                                            this, &T9WriteInputMethodPrivate::dictionaryLoadCompleted, Qt::DirectConnection);
                    worker->addTask(dictionaryTask);
                }
            }
        }

        // Attach existing dictionary, if available
        if (sessionSettings.recognitionMode != scrMode && !inputMethodHints.testFlag(Qt::ImhNoPredictiveText) &&
                loadedDictionary && !attachedDictionary) {
            if (attachDictionary(loadedDictionary))
                attachedDictionary = loadedDictionary;
        }
    }

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

        hash.addData(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(lcT9Write) << "T9WriteInputMethodPrivate::setContext():" << QLatin1String((context.toHex()));

        // Finish recognition, but preserve current input
        Q_Q(T9WriteInputMethod);
        QString preeditText = q->inputContext()->preeditText();
        bool preserveCurrentInput = !preeditText.isEmpty();
        T9WriteCaseFormatter oldCaseFormatter(caseFormatter);
        finishRecognition(!preserveCurrentInput);

        if (preserveCurrentInput) {
            caseFormatter = oldCaseFormatter;
            stringStart = preeditText;
            wordCandidates.append(T9WriteWordCandidate(preeditText));
            activeWordIndex = 0;
            emit q->selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
            emit q->selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, activeWordIndex);
        }

        const int dpi = traceCaptureDeviceInfo.value(QLatin1String("dpi"), 96).toInt();
        static const int INSTANT_GESTURE_WIDTH_THRESHOLD_MM = 25;
        gestureWidthThreshold = static_cast<DECUMA_UINT32>(INSTANT_GESTURE_WIDTH_THRESHOLD_MM / 25.4 * dpi);

        gestureRecognizer.setDpi(dpi);

        sessionSettings.baseline = 0;
        sessionSettings.helpline = 0;
        sessionSettings.topline = 0;
        sessionSettings.supportLineSet = baselineAndHelpline;
        sessionSettings.UIInputGuide = none;

        DECUMA_STATUS status = DECUMA_API(ChangeSessionSettings)(decumaSession, &sessionSettings);
        Q_ASSERT(status == decumaNoError);
    }

    QVirtualKeyboardTrace *traceBegin(
            int traceId, QVirtualKeyboardInputEngine::PatternRecognitionMode patternRecognitionMode,
            const QVariantMap &traceCaptureDeviceInfo, const QVariantMap &traceScreenInfo)
    {
        if (!worker)
            return nullptr;

        // The result id follows the trace id so that the (previous)
        // results completed during the handwriting can be rejected.
        resultId = traceId;

        stopResultTimer();

        // Dictionary must be completed before the arc addition can begin
        worker->waitForAllTasksOfType<T9WriteDictionaryTask>();
        dictionaryTask.reset();

        // Cancel the current recognition task
        worker->removeAllTasks<T9WriteRecognitionResultsTask>();
        worker->removeAllTasks<T9WriteRecognitionTask>();
        if (recognitionTask) {
            recognitionTask->cancelRecognition();
            recognitionTask.reset();
        }

        // Check for hard limit on the size the trace list
        if (traceList.count() >= traceListHardLimit) {
            worker->waitForAllTasksOfType<T9WriteAddArcTask>();
            while (traceListHardLimit < traceList.count())
                delete traceList.takeFirst();
        }

#ifdef QT_VIRTUALKEYBOARD_RECORD_TRACE_INPUT
        if (!unipenTrace)
            unipenTrace.reset(new UnipenTrace(traceCaptureDeviceInfo, traceScreenInfo));
#endif

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

        DECUMA_STATUS status;

        if (!arcAdditionStarted) {
            worker->waitForAllTasks();

            DECUMA_RECOGNITION_SETTINGS recSettings;
            memset(&recSettings, 0, sizeof(recSettings));

            // Boost dictionary words by default
            recSettings.boostLevel = attachedDictionary || attachedDynamicDictionaries.size() > 0 ? boostDictWords : noBoost;

            // Disable dictionary boost in UCR mode for URL and E-mail input
            // Otherwise it will completely mess input
            Q_Q(T9WriteInputMethod);
            const Qt::InputMethodHints inputMethodHints = q->inputContext()->inputMethodHints();
            if (sessionSettings.recognitionMode == ucrMode && (inputMethodHints & (Qt::ImhUrlCharactersOnly | Qt::ImhEmailCharactersOnly)))
                recSettings.boostLevel = noBoost;

            recSettings.stringCompleteness = canBeContinued;
            if (!stringStart.isEmpty())
                recSettings.pStringStart = const_cast<DECUMA_UNICODE *>(stringStart.utf16());

            status = DECUMA_API(BeginArcAddition)(decumaSession, &recSettings);
            Q_ASSERT(status == decumaNoError);
            arcAdditionStarted = true;
        }

        Q_Q(T9WriteInputMethod);
        QVirtualKeyboardTrace *trace = new QVirtualKeyboardTrace(q);
#ifdef QT_VIRTUALKEYBOARD_RECORD_TRACE_INPUT
        trace->setChannels(QStringList(QLatin1String("t")));
#endif
        traceList.append(trace);

        return trace;
    }

    void traceEnd(QVirtualKeyboardTrace *trace)
    {
        if (trace->isCanceled()) {
            traceList.removeOne(trace);
            delete trace;
        } else {
            if (cjk && countActiveTraces() == 0) {
                // For some reason gestures don't seem to work in CJK mode
                // Using our own gesture recognizer as fallback
                if (handleGesture())
                    return;
            }
            worker->addTask(QSharedPointer<T9WriteAddArcTask>(new T9WriteAddArcTask(trace)));
        }
        if (!traceList.isEmpty()) {
            Q_ASSERT(arcAdditionStarted);
            if (countActiveTraces() == 0)
                restartRecognition();
        }
    }

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

    void clearTraces()
    {
        worker->waitForAllTasks();
        qDeleteAll(traceList);
        traceList.clear();
    }

    void noteSelected(int index)
    {
        if (index < 0 || index >= wordCandidates.size())
            return;

        qCDebug(lcT9Write) << "T9WriteInputMethodPrivate::noteSelected():" << index;
        const T9WriteWordCandidate &wordCandidate = wordCandidates[index];
        switch (wordCandidate.origin) {
        case T9WriteWordCandidate::Origin::T9Write:
            DECUMA_API(NoteSelectedCandidate)(decumaSession, wordCandidate.resultIndex);
            break;
#ifdef HAVE_XT9
        case T9WriteWordCandidate::Origin::XT9:
        {
            Xt9AwIme *xt9AwIme = static_cast<Xt9AwIme *>(xt9Ime.data());
            if (xt9AwIme) {
                xt9AwIme->noteWordDone(wordCandidate.symbs);
                xt9AwIme->selectWord(wordCandidate.resultIndex, true);
            }
            break;
        }
#endif
        default:
            break;
        }
    }

    void restartRecognition()
    {
        qCDebug(lcT9Write) << "T9WriteInputMethodPrivate::restartRecognition()";

        worker->removeAllTasks<T9WriteRecognitionResultsTask>();
        if (recognitionTask) {
            recognitionTask->cancelRecognition();
            recognitionTask.reset();
        }

        QSharedPointer<T9WriteRecognitionResult> recognitionResult(new T9WriteRecognitionResult(resultId, 9, 64));
        recognitionTask.reset(new T9WriteRecognitionTask(recognitionResult));
        worker->addTask(recognitionTask);

        QSharedPointer<T9WriteRecognitionResultsTask> resultsTask(new T9WriteRecognitionResultsTask(recognitionResult));
        QObjectPrivate::connect(resultsTask.data(), &T9WriteRecognitionResultsTask::resultsAvailable, this, &T9WriteInputMethodPrivate::setResultList, Qt::DirectConnection);
        worker->addTask(resultsTask);

        resetResultTimer(cjk ? Settings::instance()->hwrTimeoutForCjk() : Settings::instance()->hwrTimeoutForAlphabetic());
    }

    void waitForRecognitionResults()
    {
        if (!worker)
            return;

        qCDebug(lcT9Write) << "T9WriteInputMethodPrivate::waitForRecognitionResults()";
        worker->waitForAllTasks();
        processResultCheckTimer();
    }

    bool finishRecognition(bool emitSelectionListChanged = true)
    {
        qCDebug(lcT9Write) << "T9WriteInputMethodPrivate::finishRecognition()";
        if (!worker)
            return false;

        bool result = !traceList.isEmpty();

        Q_ASSERT(decumaSession != nullptr);

        stopResultTimer();

        worker->removeAllTasks<T9WriteAddArcTask>();
        worker->removeAllTasks<T9WriteRecognitionResultsTask>();
        if (recognitionTask) {
            recognitionTask->cancelRecognition();
            recognitionTask.reset();
            result = true;
        }
        worker->waitForAllTasks();

        clearTraces();

        if (arcAdditionStarted) {
            DECUMA_API(EndArcAddition)(decumaSession);
            arcAdditionStarted = false;
        }

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

        stringStart.clear();
        scrResult.clear();
        caseFormatter.clear();

#ifdef QT_VIRTUALKEYBOARD_RECORD_TRACE_INPUT
        unipenTrace.reset();
#endif

        return result;
    }

    bool select(int index = -1)
    {
        if (!worker)
            return false;

        if (sessionSettings.recognitionMode != scrMode && wordCandidates.isEmpty()) {
            finishRecognition();
            return false;
        }
        if (sessionSettings.recognitionMode == scrMode && scrResult.isEmpty()) {
            finishRecognition();
            return false;
        }

        qCDebug(lcT9Write) << "T9WriteInputMethodPrivate::select():" << index;

        Q_Q(T9WriteInputMethod);
        if (sessionSettings.recognitionMode != scrMode) {
            index = index >= 0 ? index : activeWordIndex;
            noteSelected(index);
            QString finalWord = wordCandidates.at(index).symbs;
            dlmAddWord(finalWord);

#ifdef QT_VIRTUALKEYBOARD_RECORD_TRACE_INPUT
            // Record trace
            if (unipenTrace) {
                if (finalWord.length() == 1) {
                    // In recording mode, the text case must match with the current text case
                    QChar ch(finalWord.at(0));
                    if (!ch.isLetter() || (ch.isUpper() == (textCase == QVirtualKeyboardInputEngine::TextCase::Upper))) {
                        QStringList homeLocations = QStandardPaths::standardLocations(QStandardPaths::HomeLocation);
                        if (!homeLocations.isEmpty()) {
                            unipenTrace->setDirectory(QStringLiteral("%1/%2").arg(homeLocations.at(0)).arg(QLatin1String("VIRTUAL_KEYBOARD_TRACES")));
                            unipenTrace->record(traceList);
                            unipenTrace->save(ch.unicode(), 100);
                        }
                    }
                }
            }
#endif

            finishRecognition();
            QChar gesture = T9WriteInputMethodPrivate::mapSymbolToGesture(finalWord.right(1).at(0));
            if (!gesture.isNull())
                finalWord.chop(1);
            q->inputContext()->commit(finalWord);
            applyGesture(gesture);
        } else if (sessionSettings.recognitionMode == scrMode) {
            QString finalWord = scrResult;
            finishRecognition();
            q->inputContext()->inputEngine()->virtualKeyClick(static_cast<Qt::Key>(finalWord.at(0).unicode()), finalWord, Qt::NoModifier);
        }

        return true;
    }

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

    void stopResultTimer()
    {
        if (resultTimer) {
            qCDebug(lcT9Write) << "T9WriteInputMethodPrivate::stopResultTimer()";
            Q_Q(T9WriteInputMethod);
            q->killTimer(resultTimer);
            resultTimer = 0;
        }
    }

    void dictionaryLoadCompleted(QSharedPointer<T9WriteAbstractDictionary> dictionary)
    {
        // Note: This method is called in worker thread context
        const std::lock_guard<QRecursiveMutex> dictionaryGuard(dictionaryLock);

        if (!dictionary)
            return;

        qCDebug(lcT9Write) << "T9WriteInputMethodPrivate::dictionaryLoadCompleted():"
                           << dictionary->name() << dictionary->data() << dictionary->size();

        Q_Q(T9WriteInputMethod);
        QVirtualKeyboardInputContext *ic = q->inputContext();
        if (ic && dictionary->name() == dictionaryFileName) {
            loadedDictionary = dictionary;
            if (sessionSettings.recognitionMode != scrMode &&
                    !ic->inputMethodHints().testFlag(Qt::ImhNoPredictiveText) &&
                    !attachedDictionary) {
                if (attachDictionary(loadedDictionary))
                    attachedDictionary = loadedDictionary;
            }
        }
    }

    void recognitionError(int status)
    {
        qCDebug(lcT9Write) << "T9WriteInputMethodPrivate::recognitionError():" << status;
        Q_Q(T9WriteInputMethod);
        q->reset();
    }

    // Note: Called from T9WriteWorker thread!
    void setResultList(const QVariantList &resultList)
    {
        {
            const std::lock_guard<QRecursiveMutex> ListGuard(resultListLock);
            this->resultList = resultList;
        }

        Q_Q(T9WriteInputMethod);
        emit q->resultListChanged();
    }

    void processResultCheckTimer()
    {
        bool resultTimerWasRunning = resultTimer != 0;

        processResult();

        // Restart the result timer now if it stopped before the results were completed
        if (!resultTimerWasRunning && (!scrResult.isEmpty() || !wordCandidates.isEmpty()))
            resetResultTimer(0);
    }

    void processResult()
    {
        qCDebug(lcT9Write) << "T9WriteInputMethodPrivate::processResult()";

#ifdef SENSITIVE_DEBUG
        if (lcT9Write().isDebugEnabled()) {
            const std::lock_guard<QRecursiveMutex> resultListGuard(resultListLock);
            for (int i = 0; i < resultList.size(); i++) {
                QVariantMap result = resultList.at(i).toMap();
                QString resultPrint = QStringLiteral("%1: ").arg(i + 1);
                QString resultChars = result.value(QLatin1String("chars")).toString();
                if (!resultChars.isEmpty())
                    resultPrint.append(resultChars);
                if (result.contains(QLatin1String("gesture"))) {
                    if (!resultChars.isEmpty())
                        resultPrint.append(QLatin1String(", "));
                    QString gesture = result[QLatin1String("gesture")].toString();
                    resultPrint.append(QLatin1String("gesture ="));
                    for (const QChar &chr : gesture) {
                        resultPrint.append(QString::fromLatin1(" 0x%1").arg(chr.unicode(), 0, 16));
                    }
                }
                qCDebug(lcT9Write) << resultPrint.toUtf8().constData();
            }
        }
#endif

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

#ifdef HAVE_XT9
        int xt9DefaultListIndex = 0;
        QStringList xt9Candidates;
#endif

        QList<T9WriteWordCandidate> newWordCandidates;
        int newActiveWordIndex = -1;
        QString resultString;
        QString gesture;
        QVariantList symbolStrokes;
        {
            const std::lock_guard<QRecursiveMutex> resultListGuard(resultListLock);
            if (resultList.isEmpty())
                return;

            if (resultList.first().toMap()[QLatin1String("resultId")] != resultId) {
                qCDebug(lcT9Write) << "T9WriteInputMethodPrivate::processResult(): resultId mismatch" << resultList.first().toMap()[QLatin1String("resultId")] << "(" << resultId << ")";
                resultList.clear();
                return;
            }
            lastResultId = resultId;

            for (int i = 0; i < resultList.size(); i++) {
                QVariantMap result = resultList.at(i).toMap();
                QString resultChars = result[QLatin1String("chars")].toString();
                if (i == 0) {
                    if (ic->isShiftActive()) {
                        caseFormatter.ensureLength(1, textCase);
                        caseFormatter.ensureLength(resultChars.length(), QVirtualKeyboardInputEngine::TextCase::Lower);
                    } else {
                        caseFormatter.ensureLength(resultChars.length(), textCase);
                    }
                }
                if (!resultChars.isEmpty()) {
                    resultChars = caseFormatter.formatString(resultChars);
                    if (sessionSettings.recognitionMode != scrMode && !newWordCandidates.contains(T9WriteWordCandidate(resultChars))) {
                        newWordCandidates.append(T9WriteWordCandidate(resultChars, i, T9WriteWordCandidate::Origin::T9Write));
                    }
                }
                if (i == 0) {
                    resultString = resultChars;
                    if (result.contains(QLatin1String("gesture")))
                        gesture = result[QLatin1String("gesture")].toString();
                    if (sessionSettings.recognitionMode != scrMode && result.contains(QLatin1String("symbolStrokes")))
                        symbolStrokes = result[QLatin1String("symbolStrokes")].toList();
                    if (sessionSettings.recognitionMode == scrMode)
                        break;
#ifdef HAVE_XT9
                    xt9Candidates = xt9BuildSelectionList(resultString, &xt9DefaultListIndex);
#endif
                } else {
                    // Add a gesture symbol to the secondary candidate
                    if (sessionSettings.recognitionMode != scrMode && result.contains(QLatin1String("gesture"))) {
                        QString gesture2 = result[QLatin1String("gesture")].toString();
                        if (gesture2.length() == 1) {
                            QChar symbol = T9WriteInputMethodPrivate::mapGestureToSymbol(gesture2.at(0).unicode());
                            if (!symbol.isNull()) {
                                // Check for duplicates
                                bool duplicateFound = false;
                                for (const T9WriteWordCandidate &wordCandidate : newWordCandidates) {
                                    duplicateFound = wordCandidate.symbs.size() == 1 && wordCandidate.symbs.at(0) == symbol;
                                    if (duplicateFound)
                                        break;
                                }
                                if (!duplicateFound) {
                                    if (!resultChars.isEmpty()) {
                                        newWordCandidates.last().symbs.append(symbol);
                                    } else {
                                        newWordCandidates.append(T9WriteWordCandidate(symbol, i, T9WriteWordCandidate::Origin::T9Write));
                                    }
                                }
                            }
                        }
                    }
#ifdef HAVE_XT9
                    if (i >= 2 && !xt9Candidates.isEmpty())
                        break;
#endif
                }
            }

            resultList.clear();

            if (!newWordCandidates.isEmpty())
                newActiveWordIndex = 0;
#ifdef HAVE_XT9
            if (!xt9Candidates.isEmpty())
                appendWordCandidates(newWordCandidates, newActiveWordIndex, resultString, xt9Candidates, xt9DefaultListIndex, T9WriteWordCandidate::Origin::XT9);
#endif
        }

        bool wordCandidatesChanged = true;

#ifndef QT_VIRTUALKEYBOARD_RECORD_TRACE_INPUT
        // Delete trace history
        // Note: We have to be sure there are no background tasks
        //       running since the QVirtualKeyboardTrace objects consumed there.
        if (worker->numberOfPendingTasks() == 0) {

            const QVirtualKeyboardInputEngine::InputMode inputMode = q->inputEngine()->inputMode();
            if (sessionSettings.recognitionMode != scrMode &&
                    inputMode != QVirtualKeyboardInputEngine::InputMode::ChineseHandwriting &&
                    inputMode != QVirtualKeyboardInputEngine::InputMode::JapaneseHandwriting &&
                    inputMode != QVirtualKeyboardInputEngine::InputMode::KoreanHandwriting) {
                const int hwrTimeout = cjk ?
                            Settings::instance()->hwrTimeoutForCjk() :
                            Settings::instance()->hwrTimeoutForAlphabetic();
                for (int traceIndex = 0; traceIndex < traceList.size();) {
                    QVirtualKeyboardTrace *trace = traceList.at(traceIndex);
                    if (trace->opacity() > 0) {
                        trace->startHideTimer(hwrTimeout);
                        ++traceIndex;
                    } else {
                        traceList.removeAt(traceIndex);
                        delete trace;
                    }
                }
            }

            // Enforce hard limit for number of traces
            if (traceList.count() >= traceListHardLimit) {
                qCDebug(lcT9Write) << "T9WriteInputMethodPrivate::processResult(): Clearing traces (hard limit):" << traceList.count();
                clearTraces();
            }
        }
#endif

        // Find a gesture at the end of the first result
        if (!gesture.isEmpty()) {

            DECUMA_UNICODE gestureSymbol = gesture.at(0).unicode();
            if (!applyGesture(gestureSymbol)) {
                ic->commit(ic->preeditText());
                finishRecognition();
            }

            return;
        }

        if (sessionSettings.recognitionMode != scrMode) {
            ignoreUpdate = true;
            ic->setPreeditText(resultString);
            ignoreUpdate = false;
        } else {
            scrResult = resultString;
        }

        if (wordCandidatesChanged) {
            wordCandidates = newWordCandidates;
            activeWordIndex = wordCandidates.isEmpty() ? -1 : newActiveWordIndex;
            emit q->selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
            emit q->selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, activeWordIndex);
        }

        if (arcAdditionStarted && traceList.isEmpty() && worker->numberOfPendingTasks() == 0) {
            DECUMA_API(EndArcAddition)(decumaSession);
            arcAdditionStarted = false;
        }
    }

    static QChar mapGestureToSymbol(const QChar &gesture)
    {
        switch (gesture.unicode()) {
        case '\r':
            return QChar(0x23CE);
        case ' ':
            return QChar(0x2423);
        default:
            return QChar();
        }
    }

    static QChar mapSymbolToGesture(const QChar &symbol)
    {
        switch (symbol.unicode()) {
        case 0x23CE:
            return QLatin1Char('\r');
        case 0x2423:
            return QLatin1Char(' ');
        default:
            return QChar();
        }
    }

    bool applyGesture(const QChar &gesture)
    {
        Q_Q(T9WriteInputMethod);
        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.length() - 1, 1)));
        if (gesture.isEmpty())
            return false;

        qCDebug(lcT9Write) << "T9WriteInputMethodPrivate::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(T9WriteInputMethod);
                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
                const QVirtualKeyboardInputEngine::InputMode inputMode = q->inputEngine()->inputMode();
                if (inputMode != QVirtualKeyboardInputEngine::InputMode::ChineseHandwriting &&
                        inputMode != QVirtualKeyboardInputEngine::InputMode::JapaneseHandwriting &&
                        inputMode != QVirtualKeyboardInputEngine::InputMode::KoreanHandwriting) {
                    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<const int>(QVirtualKeyboardInputEngine::InputMode::Numeric));
                            int indexOfDialableInputMode = inputModes.indexOf(static_cast<const int>(QVirtualKeyboardInputEngine::InputMode::Dialable));
                            if (indexOfNumericInputMode != -1 && indexOfDialableInputMode != -1)
                                inputModes.removeAt(inputMode != QVirtualKeyboardInputEngine::InputMode::Dialable ?
                                            indexOfDialableInputMode :
                                            indexOfNumericInputMode);
                            if (inputModes.count() > 1) {
                                int inputModeIndex = inputModes.indexOf(static_cast<const int>(inputMode)) + 1;
                                if (inputModeIndex >= inputModes.count())
                                    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 T9WriteInputMethod);
            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 onAvailableDynamicDictionariesChanged()
    {
        if (!worker)
            return;

        const std::lock_guard<QRecursiveMutex> dictionaryGuard(dictionaryLock);
        QVirtualKeyboardDictionaryManager *dictionaryManager = QVirtualKeyboardDictionaryManager::instance();

        const QStringList availableDictionaries = dictionaryManager->availableDictionaries();
        for (const QString &dictionaryName : availableDictionaries) {

            if (!dynamicDictionaries.contains(dictionaryName)) {

                DECUMA_SRC_DICTIONARY_INFO sourceInfo;
                sourceInfo.srcType = decumaTextList;
                sourceInfo.bNoFrequencyRanking = 1;
                sourceInfo.multiStepWordSeparator = 0;

                QSharedPointer<T9WriteAbstractSource> sourceDictionary(
                            new T9WriteStringSource(
                                sourceInfo,
                                dictionaryManager->dictionary(dictionaryName)->contents(),
                                dictionaryName));

                QSharedPointer<T9WriteDictionary> dynamicDictionary(
                            new T9WriteDictionary(
                                sourceDictionary,
                                decumaSession,
                                memFuncs,
                                cjk));

                dynamicDictionaries[dictionaryName] = dynamicDictionary;

                QSharedPointer<T9WriteDictionaryTask> dynamicDictionaryTask(
                            new T9WriteDictionaryTask(
                                dynamicDictionary,
                                true));

                Q_Q(T9WriteInputMethod);
                q->connect(dynamicDictionaryTask.data(),
                           &T9WriteDictionaryTask::completed,
                           [=](QSharedPointer<T9WriteAbstractDictionary> dynamicDictionary) {

                    const std::lock_guard<QRecursiveMutex> dictionaryGuard(dictionaryLock);

                    if (dynamicDictionary->data()) {
                        if (dictionaryManager->activeDictionaries().contains(dictionaryName) &&
                                !attachedDynamicDictionaries.contains(dictionaryName) &&
                                attachDictionary(dynamicDictionary))
                            attachedDynamicDictionaries[dictionaryName] = dynamicDictionary;
                    } else {
                        dynamicDictionaries.remove(dictionaryName);
                    }
                });

                worker->addTask(dynamicDictionaryTask);
            }

#ifdef HAVE_XT9
            if (!xt9DynamicDictionaries.contains(dictionaryName)) {
                if (xt9Ime) {
                    QVirtualKeyboardDictionary *dictionary = dictionaryManager->dictionary(dictionaryName);
                    const quint16 id = static_cast<quint16>(xt9DynamicDictionaryNextId.fetchAndAddRelaxed(1));
                    xt9DynamicDictionaries[dictionaryName] = id;

                    xt9Ime->updateIndex(id, dictionary->contents());

                    Q_Q(T9WriteInputMethod);
                    q->connect(dictionary, &QVirtualKeyboardDictionary::contentsChanged, q, [=]() {
                        xt9Ime->updateIndex(id, dictionary->contents());
                        if (xt9AttachedDynamicDictionaries.contains(dictionaryName))
                            xt9Ime->mountIndex(id);
                    });
                }
            }
#endif
        }
    }

    void onActiveDynamicDictionariesChanged()
    {
        if (!worker)
            return;

        const std::lock_guard<QRecursiveMutex> dictionaryGuard(dictionaryLock);
        QVirtualKeyboardDictionaryManager *dictionaryManager = QVirtualKeyboardDictionaryManager::instance();

        // Attach
        const QStringList activeDictionaries = dictionaryManager->activeDictionaries();
        for (const QString &dictionaryName : activeDictionaries) {

            QSharedPointer<T9WriteAbstractDictionary> dynamicDictionary = dynamicDictionaries.value(dictionaryName);
            if (dynamicDictionary && dynamicDictionary->data() &&
                    !attachedDynamicDictionaries.contains(dictionaryName) &&
                    dynamicDictionary->isCompleted() &&
                    attachDictionary(dynamicDictionary)) {
                attachedDynamicDictionaries[dictionaryName] = dynamicDictionary;
            }
#ifdef HAVE_XT9
            if (xt9Ime) {
                if (!xt9AttachedDynamicDictionaries.contains(dictionaryName)) {
                    xt9AttachDictionary(dictionaryName);
                }
            }
#endif
        }

        // Detach
        const QStringList attachedDynamicDictionariesKeys = attachedDynamicDictionaries.keys();
        for (const QString &dictionaryName : attachedDynamicDictionariesKeys) {
            if (!activeDictionaries.contains(dictionaryName)) {
                if (attachedDynamicDictionaries.contains(dictionaryName)) {
                    detachDictionary(attachedDynamicDictionaries[dictionaryName]);
                    attachedDynamicDictionaries.remove(dictionaryName);
                }
            }
        }
#ifdef HAVE_XT9
        // Detach (XT9)
        if (xt9Ime) {
            const QStringList xt9AttachedDynamicDictionariesKeys = xt9AttachedDynamicDictionaries.keys();
            for (const QString &dictionaryName : xt9AttachedDynamicDictionariesKeys) {
                if (!activeDictionaries.contains(dictionaryName)) {
                    xt9DetachDictionary(dictionaryName);
                }
            }
        }
#endif
    }

    bool isDlmActive()
    {
        const std::lock_guard<QRecursiveMutex> dictionaryGuard(dictionaryLock);
        return !loadedDlmDictionary.isNull();
    }

    QString dlmFileName() const
    {
        QString suffix;
        switch (engineMode) {
        case T9WriteInputMethod::EngineMode::Alphabetic:
        case T9WriteInputMethod::EngineMode::Arabic:
        case T9WriteInputMethod::EngineMode::Hebrew:
        case T9WriteInputMethod::EngineMode::Thai:
            suffix = QStringLiteral("aw");
            break;
        case T9WriteInputMethod::EngineMode::SimplifiedChinese:
        case T9WriteInputMethod::EngineMode::TraditionalChinese:
        case T9WriteInputMethod::EngineMode::HongKongChinese:
            suffix = QStringLiteral("cp");
            break;
        case T9WriteInputMethod::EngineMode::Japanese:
            suffix = QStringLiteral("j");
            break;
        case T9WriteInputMethod::EngineMode::Korean:
            suffix = QStringLiteral("k");
            break;
        default:
            break;
        }
        return QStringLiteral("cerence-hwr%1.dlm").arg(suffix);
    }

    void dlmActivate()
    {
        qCDebug(lcT9Write) << "T9WriteInputMethodPrivate::dlmActivate()";

        const std::lock_guard<QRecursiveMutex> dictionaryGuard(dictionaryLock);
        if (loadedDlmDictionary)
            return;

        DECUMA_SRC_DICTIONARY_INFO sourceInfo;
        sourceInfo.srcType = decumaPortableHWRDictionary;
        sourceInfo.bNoFrequencyRanking = 1;
        sourceInfo.multiStepWordSeparator = 0;

        QString dictionaryName(QStringLiteral("%1/%2").arg(Settings::instance()->userDataPath(), dlmFileName()));
        QSharedPointer<T9WriteAbstractSource> sourceDictionary(
                    new T9WriteFileSource(
                        sourceInfo,
                        dictionaryName));

        loadedDlmDictionary.reset(new T9WriteDynamicDictionary(
                        sourceDictionary,
                        CERENCE_HWR_DLM_MAX_WORDS,
                        memFuncs,
                        cjk));

        QSharedPointer<T9WriteDictionaryTask> dynamicDictionaryTask(
                    new T9WriteDictionaryTask(
                        loadedDlmDictionary,
                        false));

        Q_Q(T9WriteInputMethod);
        q->connect(dynamicDictionaryTask.data(),
                   &T9WriteDictionaryTask::completed,
                   [=](QSharedPointer<T9WriteAbstractDictionary> dynamicDictionary) {

            const std::lock_guard<QRecursiveMutex> dictionaryGuard(dictionaryLock);

            if (!loadedDlmDictionary || dynamicDictionary != loadedDlmDictionary)
                return;

            if (dynamicDictionary->data()) {
                if (attachedDlmDictionary != dynamicDictionary &&
                        attachDictionary(dynamicDictionary)) {
                    attachedDlmDictionary = dynamicDictionary;
                }
            } else {
                dlmDeactivate();
            }
        });

        worker->addTask(dynamicDictionaryTask);
    }

    void dlmDeactivate()
    {
        const std::lock_guard<QRecursiveMutex> dictionaryGuard(dictionaryLock);

        if (loadedDlmDictionary) {
            qCDebug(lcT9Write) << "T9WriteInputMethodPrivate::dlmDeactivate()";
            loadedDlmDictionary.reset();
        }

        if (attachedDlmDictionary) {
            detachDictionary(attachedDlmDictionary);
            attachedDlmDictionary.reset();
        }
    }

    void dlmAddWord(const QString &word)
    {
        if (!isDlmActive() || !worker)
            return;

        qCDebug(lcT9Write) << "T9WriteInputMethodPrivate::dlmAddWord()";

        QSharedPointer<T9WriteDlmWordTask> learnWordTask(new T9WriteDlmWordTask(loadedDlmDictionary, word, stringStart));
        worker->addTask(learnWordTask);
    }

    bool dlmHasWord(const QString &word)
    {
        if (!isDlmActive() || !worker)
            return false;

        worker->waitForAllTasks();

        const std::lock_guard<QRecursiveMutex> dictionaryGuard(dictionaryLock);
        T9WriteDynamicDictionary *dictionary = static_cast<T9WriteDynamicDictionary *>(loadedDlmDictionary.data());

        return dictionary->hasWord(word);
    }

    void dlmRemoveWord(const QString &word)
    {
        qCDebug(lcT9Write) << "T9WriteInputMethodPrivate::dlmRemoveWord()";

        QSharedPointer<T9WriteDlmRemoveWordTask> removeWordTask(new T9WriteDlmRemoveWordTask(loadedDlmDictionary, word));
        worker->addTask(removeWordTask);
    }

    T9WriteInputMethod *q_ptr;
    static const DECUMA_MEM_FUNCTIONS memFuncs;
    bool cjk;
    T9WriteInputMethod::EngineMode engineMode;
    QByteArray currentContext;
    DECUMA_SESSION_SETTINGS sessionSettings;
    DECUMA_UINT32 gestureWidthThreshold;
    QStringList defaultHwrDbPaths;
    QFile hwrDbFile;
    QList<DECUMA_UINT32> languageCategories;
    QList<DECUMA_UINT32> symbolCategories;
    QScopedPointer<T9WriteWorker> worker;
    QList<QVirtualKeyboardTrace *> traceList;
    int traceListHardLimit;
    QRecursiveMutex dictionaryLock;
    QString dictionaryFileName;
    QSharedPointer<T9WriteAbstractDictionary> loadedDictionary;
    QSharedPointer<T9WriteAbstractDictionary> attachedDictionary;
    QSharedPointer<Xt9LdbManager> ldbManager;
    QSharedPointer<T9WriteDictionaryTask> dictionaryTask;
    QMap<QString, QSharedPointer<T9WriteAbstractDictionary>> dynamicDictionaries;
    QMap<QString, QSharedPointer<T9WriteAbstractDictionary>> attachedDynamicDictionaries;
    QSharedPointer<T9WriteAbstractDictionary> loadedDlmDictionary;
    QSharedPointer<T9WriteAbstractDictionary> attachedDlmDictionary;
    QMetaObject::Connection availableDictionariesChangedConnection;
    QMetaObject::Connection activeDictionariesChangedConnection;
    QSharedPointer<T9WriteRecognitionTask> recognitionTask;
    QRecursiveMutex resultListLock;
    QVariantList resultList;
    QMetaObject::Connection resultListChangedConnection;
    int resultId;
    int lastResultId;
    int resultTimer;
    QByteArray session;
    DECUMA_SESSION *decumaSession;
    QList<T9WriteWordCandidate> wordCandidates;
    QString stringStart;
    QString scrResult;
    int activeWordIndex;
    bool arcAdditionStarted;
    bool ignoreUpdate;
    QVirtualKeyboardInputEngine::TextCase textCase;
    T9WriteCaseFormatter caseFormatter;
    HandwritingGestureRecognizer gestureRecognizer;
#ifdef QT_VIRTUALKEYBOARD_RECORD_TRACE_INPUT
    QScopedPointer<UnipenTrace> unipenTrace;
#endif
#ifdef HAVE_XT9
    QScopedPointer<Xt9Ime> xt9Ime;
    QMap<QString, quint16> xt9DynamicDictionaries;
    QMap<QString, quint16> xt9AttachedDynamicDictionaries;
    QAtomicInt xt9DynamicDictionaryNextId;
    QMetaObject::Connection defaultInputMethodDisabledChangedConnection;
    QMetaObject::Connection defaultDictionaryDisabledChangedConnection;
#endif
    QMetaObject::Connection userDataResetConnection;
};

const DECUMA_MEM_FUNCTIONS T9WriteInputMethodPrivate::memFuncs = {
    T9WriteInputMethodPrivate::decumaMalloc,
    T9WriteInputMethodPrivate::decumaCalloc,
    T9WriteInputMethodPrivate::decumaFree,
    nullptr
};

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

T9WriteInputMethod::T9WriteInputMethod(QObject *parent) :
    QVirtualKeyboardAbstractInputMethod(*new T9WriteInputMethodPrivate(this), parent)
{
}

T9WriteInputMethod::~T9WriteInputMethod()
{
}

void T9WriteInputMethod::clearInputMode()
{
    Q_D(T9WriteInputMethod);
    d->exitEngine();
}

QList<QVirtualKeyboardInputEngine::InputMode> T9WriteInputMethod::inputModes(const QString &locale)
{
    Q_D(T9WriteInputMethod);
    QList<QVirtualKeyboardInputEngine::InputMode> availableInputModes;
    const Qt::InputMethodHints inputMethodHints(inputContext()->inputMethodHints());
    const QLocale loc(locale);
    T9WriteInputMethod::EngineMode mode = d->mapLocaleToEngineMode(loc);

    // Add primary input mode
    switch (mode) {
#ifdef HAVE_CERENCE_HWR_ALPHABETIC
    case T9WriteInputMethod::EngineMode::Alphabetic:
        if (d->findHwrDb(T9WriteInputMethod::EngineMode::Alphabetic).isEmpty())
            return availableInputModes;
        if (!(inputMethodHints & (Qt::ImhDialableCharactersOnly | Qt::ImhFormattedNumbersOnly | Qt::ImhDigitsOnly | Qt::ImhLatinOnly))) {
            switch (loc.script()) {
            case QLocale::GreekScript:
                availableInputModes.append(QVirtualKeyboardInputEngine::InputMode::Greek);
                break;
            case QLocale::CyrillicScript:
                availableInputModes.append(QVirtualKeyboardInputEngine::InputMode::Cyrillic);
                break;
            case QLocale::ThaiScript:
                availableInputModes.append(QVirtualKeyboardInputEngine::InputMode::Thai);
                break;
            default:
                break;
            }
            availableInputModes.append(QVirtualKeyboardInputEngine::InputMode::Latin);
        }
        break;
    case T9WriteInputMethod::EngineMode::Arabic:
        if (d->findHwrDb(T9WriteInputMethod::EngineMode::Arabic).isEmpty())
            return availableInputModes;
        if (!(inputMethodHints & (Qt::ImhDialableCharactersOnly | Qt::ImhFormattedNumbersOnly | Qt::ImhDigitsOnly | Qt::ImhLatinOnly)))
            availableInputModes.append(QVirtualKeyboardInputEngine::InputMode::Arabic);
        break;
    case T9WriteInputMethod::EngineMode::Hebrew:
        if (d->findHwrDb(T9WriteInputMethod::EngineMode::Hebrew).isEmpty())
            return availableInputModes;
        if (!(inputMethodHints & (Qt::ImhDialableCharactersOnly | Qt::ImhFormattedNumbersOnly | Qt::ImhDigitsOnly | Qt::ImhLatinOnly)))
            availableInputModes.append(QVirtualKeyboardInputEngine::InputMode::Hebrew);
        break;
    case T9WriteInputMethod::EngineMode::Thai:
        if (d->findHwrDb(T9WriteInputMethod::EngineMode::Thai).isEmpty())
            return availableInputModes;
        if (!(inputMethodHints & (Qt::ImhDialableCharactersOnly | Qt::ImhFormattedNumbersOnly | Qt::ImhDigitsOnly | Qt::ImhLatinOnly)))
            availableInputModes.append(QVirtualKeyboardInputEngine::InputMode::Thai);
        break;
#endif
#ifdef HAVE_CERENCE_HWR_CJK
    case T9WriteInputMethod::EngineMode::SimplifiedChinese:
    case T9WriteInputMethod::EngineMode::TraditionalChinese:
    case T9WriteInputMethod::EngineMode::HongKongChinese:
        if (d->findHwrDb(mode).isEmpty())
            return availableInputModes;
        if (!(inputMethodHints & (Qt::ImhDialableCharactersOnly | Qt::ImhFormattedNumbersOnly | Qt::ImhDigitsOnly | Qt::ImhLatinOnly)))
            availableInputModes.append(QVirtualKeyboardInputEngine::InputMode::ChineseHandwriting);
        break;
    case T9WriteInputMethod::EngineMode::Japanese:
        if (d->findHwrDb(T9WriteInputMethod::EngineMode::Japanese).isEmpty())
            return availableInputModes;
        if (!(inputMethodHints & (Qt::ImhDialableCharactersOnly | Qt::ImhFormattedNumbersOnly | Qt::ImhDigitsOnly | Qt::ImhLatinOnly)))
            availableInputModes.append(QVirtualKeyboardInputEngine::InputMode::JapaneseHandwriting);
        break;
    case T9WriteInputMethod::EngineMode::Korean:
        if (d->findHwrDb(T9WriteInputMethod::EngineMode::Korean).isEmpty())
            return availableInputModes;
        if (!(inputMethodHints & (Qt::ImhDialableCharactersOnly | Qt::ImhFormattedNumbersOnly | Qt::ImhDigitsOnly | Qt::ImhLatinOnly)))
            availableInputModes.append(QVirtualKeyboardInputEngine::InputMode::KoreanHandwriting);
        break;
#endif
    default:
        return availableInputModes;
    }

    // Add exclusive input modes
    if (inputMethodHints.testFlag(Qt::ImhDialableCharactersOnly) || inputMethodHints.testFlag(Qt::ImhDigitsOnly)) {
        availableInputModes.append(QVirtualKeyboardInputEngine::InputMode::Dialable);
    } else if (inputMethodHints.testFlag(Qt::ImhFormattedNumbersOnly)) {
        availableInputModes.append(QVirtualKeyboardInputEngine::InputMode::Numeric);
    } else if (inputMethodHints.testFlag(Qt::ImhLatinOnly)) {
        availableInputModes.append(QVirtualKeyboardInputEngine::InputMode::Latin);
    } else {
        // Add other input modes
        Q_ASSERT(!availableInputModes.isEmpty());
        if (!availableInputModes.contains(QVirtualKeyboardInputEngine::InputMode::Latin))
            availableInputModes.append(QVirtualKeyboardInputEngine::InputMode::Latin);
        availableInputModes.append(QVirtualKeyboardInputEngine::InputMode::Numeric);
    }

    return availableInputModes;
}

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

bool T9WriteInputMethod::setTextCase(QVirtualKeyboardInputEngine::TextCase textCase)
{
    Q_D(T9WriteInputMethod);
    d->textCase = textCase;
    return true;
}

bool T9WriteInputMethod::keyEvent(Qt::Key key, const QString &text, Qt::KeyboardModifiers modifiers)
{
    Q_UNUSED(modifiers);
    Q_D(T9WriteInputMethod);
    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);
                d->caseFormatter.ensureLength(preeditText.length(), d->textCase);
                T9WriteCaseFormatter caseFormatter(d->caseFormatter);
                d->finishRecognition(false);
                d->caseFormatter = caseFormatter;
                d->stringStart = preeditText;
                int xt9DefaultListIndex = 0;
                d->activeWordIndex = 0;
                d->appendWordCandidates(d->wordCandidates, d->activeWordIndex, d->stringStart, d->xt9BuildSelectionList(d->stringStart, &xt9DefaultListIndex), xt9DefaultListIndex, T9WriteWordCandidate::Origin::XT9);
                emit selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
                emit selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, d->activeWordIndex);
                return true;
            } else {
                bool result = !preeditText.isEmpty();
                if (result)
                    ic->clear();
                else
                    result = !d->scrResult.isEmpty();
                d->finishRecognition();
                return result;
            }
        }

    default:
        if (d->sessionSettings.recognitionMode != scrMode && text.length() > 0) {
            d->waitForRecognitionResults();
            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->caseFormatter.ensureLength(preeditText.length(), d->textCase);
                T9WriteCaseFormatter caseFormatter(d->caseFormatter);
                d->finishRecognition(false);
                d->caseFormatter = caseFormatter;
                d->stringStart = preeditText;
                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;
        } else if (d->sessionSettings.recognitionMode == scrMode) {
            d->finishRecognition();
        }
    }
    return false;
}

void T9WriteInputMethod::reset()
{
    Q_D(T9WriteInputMethod);
    d->finishRecognition();
    d->setInputMode(QLocale(inputContext()->locale()), inputEngine()->inputMode());
}

void T9WriteInputMethod::update()
{
    Q_D(T9WriteInputMethod);
    if (d->ignoreUpdate)
        return;
    d->select();
}

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

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

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

void T9WriteInputMethod::selectionListItemSelected(QVirtualKeyboardSelectionListModel::Type type, int index)
{
    Q_UNUSED(type);
    Q_D(T9WriteInputMethod);

#ifdef HAVE_XT9
    switch (d->engineMode) {
    case T9WriteInputMethod::EngineMode::SimplifiedChinese:
        if (inputEngine()->inputMode() != QVirtualKeyboardInputEngine::InputMode::ChineseHandwriting)
            break;

    [[fallthrough]]; case T9WriteInputMethod::EngineMode::Japanese:
        if (d->wordCandidates[index].origin == T9WriteWordCandidate::Origin::T9Write) {
            if (d->xt9AllSymbsArePinyin(d->wordCandidates[index].symbs)) {
                int indexOfFirstXt9Candidate;
                for (indexOfFirstXt9Candidate = 0; indexOfFirstXt9Candidate < d->wordCandidates.size(); ++indexOfFirstXt9Candidate) {
                    if (d->wordCandidates[indexOfFirstXt9Candidate].origin == T9WriteWordCandidate::Origin::XT9)
                        break;
                }

                while (indexOfFirstXt9Candidate < d->wordCandidates.size()) {
                    d->wordCandidates.removeAt(indexOfFirstXt9Candidate);
                }

                int xt9DefaultListIndex = 0;
                d->appendWordCandidates(d->wordCandidates, d->activeWordIndex, d->wordCandidates[index].symbs, d->xt9BuildSelectionList(d->wordCandidates[index].symbs, &xt9DefaultListIndex), xt9DefaultListIndex, T9WriteWordCandidate::Origin::XT9);
                d->activeWordIndex = index;

                emit selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
                emit selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, d->activeWordIndex);
                return;
            }
        }
        break;

    default:
        break;
    }
#endif

    d->select(index);
}

bool T9WriteInputMethod::selectionListRemoveItem(QVirtualKeyboardSelectionListModel::Type type, int index)
{
    Q_UNUSED(type)
    Q_D(T9WriteInputMethod);
    if (index < 0 || index >= d->wordCandidates.size())
        return false;
    d->dlmRemoveWord(d->wordCandidates.at(index).symbs);
    if (d->wordCandidates.size() > 1) {
        d->wordCandidates.removeAt(index);
        emit selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
        emit selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, d->activeWordIndex);
    } else {
        inputContext()->clear();
        reset();
    }
    return true;
}

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

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

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

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

    if (d->sessionSettings.recognitionMode == scrMode)
        return false;

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

    const QVirtualKeyboardInputEngine::InputMode inputMode = inputEngine()->inputMode();
    const int maxLength = (inputMode == QVirtualKeyboardInputEngine::InputMode::ChineseHandwriting ||
            inputMode == QVirtualKeyboardInputEngine::InputMode::JapaneseHandwriting ||
            inputMode == QVirtualKeyboardInputEngine::InputMode::KoreanHandwriting) ? 16 : 32;
    const QString surroundingText = ic->surroundingText();
    int replaceFrom = 0;

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

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

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

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

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

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

    if (d->stringStart.isEmpty())
        return false;

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

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

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

    ic->setPreeditText(d->stringStart, QList<QInputMethodEvent::Attribute>(), replaceFrom, d->stringStart.length());
    for (int i = 0; i < d->stringStart.length(); ++i)
        d->caseFormatter.ensureLength(i + 1, d->stringStart.at(i).isUpper() ? QVirtualKeyboardInputEngine::TextCase::Upper : QVirtualKeyboardInputEngine::TextCase::Lower);
    int xt9DefaultListIndex = 0;
    d->activeWordIndex = 0;
    d->appendWordCandidates(d->wordCandidates, d->activeWordIndex, d->stringStart, d->xt9BuildSelectionList(d->stringStart, &xt9DefaultListIndex), xt9DefaultListIndex, T9WriteWordCandidate::Origin::XT9);
    emit selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
    emit selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, d->activeWordIndex);

    return true;
}

void T9WriteInputMethod::timerEvent(QTimerEvent *timerEvent)
{
    Q_D(T9WriteInputMethod);
    int timerId = timerEvent->timerId();
    qCDebug(lcT9Write) << "T9WriteInputMethod::timerEvent():" << timerId;
    if (timerId == d->resultTimer) {
        d->stopResultTimer();

        // Ignore if the result is not yet available
        if (d->resultId != d->lastResultId) {
            qCDebug(lcT9Write) << "T9WriteInputMethod::timerEvent(): Result not yet available";
            return;
        }

        if (d->sessionSettings.recognitionMode != scrMode) {
#ifndef QT_VIRTUALKEYBOARD_RECORD_TRACE_INPUT
            // Don't clear traces in UCR mode if dictionary is loaded.
            // In UCR mode the whole purpose is to write the word with
            // one or few strokes.
            if (d->sessionSettings.recognitionMode == ucrMode) {
                const std::lock_guard<QRecursiveMutex> dictionaryGuard(d->dictionaryLock);
                if (d->attachedDictionary)
                    return;
            }

            const QVirtualKeyboardInputEngine::InputMode inputMode = inputEngine()->inputMode();
            if (inputMode != QVirtualKeyboardInputEngine::InputMode::ChineseHandwriting &&
                    inputMode != QVirtualKeyboardInputEngine::InputMode::JapaneseHandwriting &&
                    inputMode != QVirtualKeyboardInputEngine::InputMode::KoreanHandwriting) {
                d->clearTraces();
            }
#endif
        } else {
            d->select();
        }
    }
}

} // namespace QtVirtualKeyboard
QT_END_NAMESPACE
