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

#include "tcinputmethod.h"
#include <QtVirtualKeyboard/qvirtualkeyboardinputengine.h>
#include <QtVirtualKeyboard/qvirtualkeyboardinputcontext.h>
#if QT_CONFIG(cangjie)
#include "cangjiedictionary.h"
#include "cangjietable.h"
#endif
#if QT_CONFIG(zhuyin)
#include "zhuyindictionary.h"
#include "zhuyintable.h"
#endif
#include "phrasedictionary.h"
#include <QLoggingCategory>

#include <QLibraryInfo>
#include <QFileInfo>

#include <array>

QT_BEGIN_NAMESPACE
namespace QtVirtualKeyboard {

Q_LOGGING_CATEGORY(lcTCIme, "qt.virtualkeyboard.tcime")

using namespace tcime;

class TCInputMethodPrivate
{
    Q_DECLARE_PUBLIC(TCInputMethod)
public:

    TCInputMethodPrivate(TCInputMethod *q_ptr) :
        q_ptr(q_ptr),
        inputMode(QVirtualKeyboardInputEngine::InputMode::Latin),
        wordDictionary(nullptr),
        highlightIndex(-1)
    {}

    bool setCandidates(const QStringList &values, bool highlightDefault)
    {
        bool candidatesChanged = candidates != values;
        candidates = values;
        highlightIndex = !candidates.isEmpty() && highlightDefault ? 0 : -1;
        return candidatesChanged;
    }

    bool clearCandidates()
    {
        if (candidates.isEmpty())
            return false;

        candidates.clear();
        highlightIndex = -1;
        return true;
    }

    QString pickHighlighted() const
    {
        return (highlightIndex >= 0 && highlightIndex < candidates.size()) ? candidates[highlightIndex] : QString();
    }

    void reset()
    {
        if (clearCandidates()) {
            Q_Q(TCInputMethod);
            emit q->selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
            emit q->selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, highlightIndex);
        }
        input.clear();
    }

    bool compose(const QChar &c)
    {
        bool accept;
        Q_Q(TCInputMethod);
        QVirtualKeyboardInputContext *ic = q->inputContext();
        switch (inputMode)
        {
#if QT_CONFIG(cangjie)
        case QVirtualKeyboardInputEngine::InputMode::Cangjie:
            accept = composeCangjie(ic, c);
            break;
#endif
#if QT_CONFIG(zhuyin)
        case QVirtualKeyboardInputEngine::InputMode::Zhuyin:
            accept = composeZhuyin(ic, c);
            break;
#endif
        default:
            accept = false;
            break;
        }
        return accept;
    }

#if QT_CONFIG(cangjie)
    bool composeCangjie(QVirtualKeyboardInputContext *ic, const QChar &c)
    {
        bool accept = false;
        if (!input.contains(QChar(0x91CD)) && CangjieTable::isLetter(c)) {
            if (input.size() < (cangjieDictionary.simplified() ? CangjieTable::MAX_SIMPLIFIED_CODE_LENGTH : CangjieTable::MAX_CODE_LENGTH)) {
                input.append(c);
                ic->setPreeditText(input);
                if (setCandidates(wordDictionary->getWords(input), true)) {
                    Q_Q(TCInputMethod);
                    emit q->selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
                    emit q->selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, highlightIndex);
                }
            }
            accept = true;
        } else if (c.unicode() == 0x91CD) {
            if (input.isEmpty()) {
                input.append(c);
                ic->setPreeditText(input);
                checkSpecialCharInput();
            }
            accept = true;
        } else if (c.unicode() == 0x96E3) {
            if (input.size() == 1) {
                Q_ASSERT(input.at(0).unicode() == 0x91CD);
                input.append(c);
                ic->setPreeditText(input);
                checkSpecialCharInput();
            }
            accept = true;
        }
        return accept;
    }

    bool checkSpecialCharInput()
    {
        if (input.size() == 1 && input.at(0).unicode() == 0x91CD) {
            static const QStringList specialChars1 = QStringList()
                    << QChar(0xFF01) << QChar(0x2018) << QChar(0x3000) << QChar(0xFF0C)
                    << QChar(0x3001) << QChar(0x3002) << QChar(0xFF0E) << QChar(0xFF1B)
                    << QChar(0xFF1A) << QChar(0xFF1F) << QChar(0x300E) << QChar(0x300F)
                    << QChar(0x3010) << QChar(0x3011) << QChar(0xFE57) << QChar(0x2026)
                    << QChar(0x2025) << QChar(0xFE50) << QChar(0xFE51) << QChar(0xFE52)
                    << QChar(0x00B7) << QChar(0xFE54) << QChar(0x2574) << QChar(0x2027)
                    << QChar(0x2032) << QChar(0x2035) << QChar(0x301E) << QChar(0x301D)
                    << QChar(0x201D) << QChar(0x201C) << QChar(0x2019) << QChar(0xFE55)
                    << QChar(0xFE5D) << QChar(0xFE5E) << QChar(0xFE59) << QChar(0xFE5A)
                    << QChar(0xFE5B) << QChar(0xFE5C) << QChar(0xFE43) << QChar(0xFE44);
            Q_Q(TCInputMethod);
            if (setCandidates(specialChars1, true)) {
                emit q->selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
                emit q->selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, highlightIndex);
            }
            q->inputContext()->setPreeditText(candidates[highlightIndex]);
            return true;
        } else if (input.size() == 2 && input.at(0).unicode() == 0x91CD && input.at(1).unicode() == 0x96E3) {
            static const QStringList specialChars2 = QStringList()
                    << QChar(0x3008) << QChar(0x3009) << QChar(0xFE31) << QChar(0x2013)
                    << QChar(0xFF5C) << QChar(0x300C) << QChar(0x300D) << QChar(0xFE40)
                    << QChar(0xFE3F) << QChar(0x2014) << QChar(0xFE3E) << QChar(0xFE3D)
                    << QChar(0x300A) << QChar(0x300B) << QChar(0xFE3B) << QChar(0xFE3C)
                    << QChar(0xFE56) << QChar(0xFE30) << QChar(0xFE39) << QChar(0xFE3A)
                    << QChar(0x3014) << QChar(0x3015) << QChar(0xFE37) << QChar(0xFE38)
                    << QChar(0xFE41) << QChar(0xFE42) << QChar(0xFF5B) << QChar(0xFF5D)
                    << QChar(0xFE35) << QChar(0xFE36) << QChar(0xFF08) << QChar(0xFF09)
                    << QChar(0xFE4F) << QChar(0xFE34) << QChar(0xFE33);
            Q_Q(TCInputMethod);
            if (setCandidates(specialChars2, true)) {
                emit q->selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
                emit q->selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, highlightIndex);
            }
            q->inputContext()->setPreeditText(candidates[highlightIndex]);
            return true;
        }
        return false;
    }
#endif

#if QT_CONFIG(zhuyin)
    bool composeZhuyin(QVirtualKeyboardInputContext *ic, const QChar &c)
    {
        if (ZhuyinTable::isTone(c)) {
            if (input.isEmpty())
                // Tones are accepted only when there's text in composing.
                return false;

            auto strippedTones = ZhuyinTable::stripTones(input);
            if (!strippedTones.ok)
                // Tones cannot be composed if there's no syllables.
                return false;

            // Replace the original tone with the new tone, but the default tone
            // character should not be composed into the composing text.
            QChar tone = strippedTones.pair[1].at(0);
            if (c == ZhuyinTable::DEFAULT_TONE) {
                if (tone != ZhuyinTable::DEFAULT_TONE)
                    input.remove(input.size() - 1, 1);
            } else {
                if (tone == ZhuyinTable::DEFAULT_TONE)
                    input.append(c);
                else
                    input.replace(input.size() - 1, 1, c);
            }
        } else if (ZhuyinTable::getInitials(c) > 0) {
            // Insert the initial or replace the original initial.
            if (input.isEmpty() || !ZhuyinTable::getInitials(input.at(0)))
                input.insert(0, c);
            else
                input.replace(0, 1, c);
        } else if (ZhuyinTable::getFinals(QStringView(&c, 1)) > 0) {
            // Replace the finals in the decomposed of syllables and tones.
            std::array<QChar, 4> decomposed = decomposeZhuyin();
            if (ZhuyinTable::isYiWuYuFinals(c)) {
                decomposed[1] = c;
            } else {
                decomposed[2] = c;
            }

            // Compose back the text after the finals replacement.
            input.clear();
            for (QChar ch : decomposed) {
                if (!ch.isNull())
                    input.append(ch);
            }
        } else {
            return false;
        }

        ic->setPreeditText(input);
        if (setCandidates(wordDictionary->getWords(input), true)) {
            Q_Q(TCInputMethod);
            emit q->selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
            emit q->selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, highlightIndex);
        }

        return true;
    }

    std::array<QChar, 4> decomposeZhuyin()
    {
        std::array<QChar, 4> results = {};
        auto strippedTones = ZhuyinTable::stripTones(input);
        if (strippedTones.ok) {
            // Decompose tones.
            QChar tone = strippedTones.pair[1].at(0);
            if (tone != ZhuyinTable::DEFAULT_TONE)
                results[3] = tone;

            // Decompose initials.
            QStringView syllables = strippedTones.pair[0];
            if (ZhuyinTable::getInitials(syllables.at(0)) > 0) {
                results[0] = syllables.at(0);
                syllables = syllables.mid(1);
            }

            // Decompose finals.
            if (!syllables.isEmpty()) {
                if (ZhuyinTable::isYiWuYuFinals(syllables.at(0))) {
                    results[1] = syllables.at(0);
                    if (syllables.size() > 1)
                        results[2] = syllables.at(1);
                } else {
                    results[2] = syllables.at(0);
                }
            }
        }
        return results;
    }
#endif

    TCInputMethod *q_ptr;
    QVirtualKeyboardInputEngine::InputMode inputMode;
#if QT_CONFIG(cangjie)
    CangjieDictionary cangjieDictionary;
#endif
#if QT_CONFIG(zhuyin)
    ZhuyinDictionary zhuyinDictionary;
#endif
    PhraseDictionary phraseDictionary;
    WordDictionary *wordDictionary;
    QString input;
    QStringList candidates;
    int highlightIndex;
};

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

TCInputMethod::TCInputMethod(QObject *parent) :
    QVirtualKeyboardAbstractInputMethod(parent),
    d_ptr(new TCInputMethodPrivate(this))
{
}

TCInputMethod::~TCInputMethod()
{
}

bool TCInputMethod::simplified() const
{
#if QT_CONFIG(cangjie)
    Q_D(const TCInputMethod);
    return d->cangjieDictionary.simplified();
#else
    return false;
#endif
}

void TCInputMethod::setSimplified(bool simplified)
{
    qCDebug(lcTCIme) << "TCInputMethod::setSimplified(): " << simplified;
#if QT_CONFIG(cangjie)
    Q_D(TCInputMethod);
    if (d->cangjieDictionary.simplified() != simplified) {
        d->reset();
        QVirtualKeyboardInputContext *ic = inputContext();
        if (ic)
            ic->clear();
        d->cangjieDictionary.setSimplified(simplified);
        emit simplifiedChanged();
    }
#else
    Q_UNUSED(simplified);
#endif
}

QList<QVirtualKeyboardInputEngine::InputMode> TCInputMethod::inputModes(const QString &locale)
{
    Q_UNUSED(locale);
    return QList<QVirtualKeyboardInputEngine::InputMode>()
#if QT_CONFIG(zhuyin)
            << QVirtualKeyboardInputEngine::InputMode::Zhuyin
#endif
#if QT_CONFIG(cangjie)
           << QVirtualKeyboardInputEngine::InputMode::Cangjie
#endif
               ;
}

bool TCInputMethod::setInputMode(const QString &locale, QVirtualKeyboardInputEngine::InputMode inputMode)
{
    Q_UNUSED(locale);
    Q_D(TCInputMethod);
    if (d->inputMode == inputMode)
        return true;
    update();
    bool result = false;
    d->inputMode = inputMode;
    d->wordDictionary = nullptr;
#if QT_CONFIG(cangjie)
    if (inputMode == QVirtualKeyboardInputEngine::InputMode::Cangjie) {
        if (d->cangjieDictionary.isEmpty()) {
            QString cangjieDictionary(qEnvironmentVariable("QT_VIRTUALKEYBOARD_CANGJIE_DICTIONARY"));
            if (!QFileInfo::exists(cangjieDictionary)) {
                cangjieDictionary = QLibraryInfo::path(QLibraryInfo::DataPath) + QLatin1String("/qtvirtualkeyboard/tcime/dict_cangjie.dat");
                if (!QFileInfo::exists(cangjieDictionary))
                    cangjieDictionary = QLatin1String(":/qt-project.org/imports/QtQuick/VirtualKeyboard/3rdparty/tcime/data/qt/dict_cangjie.dat");
            }
            d->cangjieDictionary.load(cangjieDictionary);
        }
        d->wordDictionary = &d->cangjieDictionary;
    }
#endif
#if QT_CONFIG(zhuyin)
    if (inputMode == QVirtualKeyboardInputEngine::InputMode::Zhuyin) {
        if (d->zhuyinDictionary.isEmpty()) {
            QString zhuyinDictionary(qEnvironmentVariable("QT_VIRTUALKEYBOARD_ZHUYIN_DICTIONARY"));
            if (!QFileInfo::exists(zhuyinDictionary)) {
                zhuyinDictionary = QLibraryInfo::path(QLibraryInfo::DataPath) + QLatin1String("/qtvirtualkeyboard/tcime/dict_zhuyin.dat");
                if (!QFileInfo::exists(zhuyinDictionary))
                    zhuyinDictionary = QLatin1String(":/qt-project.org/imports/QtQuick/VirtualKeyboard/3rdparty/tcime/data/qt/dict_zhuyin.dat");
            }
            d->zhuyinDictionary.load(zhuyinDictionary);
        }
        d->wordDictionary = &d->zhuyinDictionary;
    }
#endif
    result = d->wordDictionary && !d->wordDictionary->isEmpty();
    if (result && d->phraseDictionary.isEmpty()) {
        QString phraseDictionary(qEnvironmentVariable("QT_VIRTUALKEYBOARD_PHRASE_DICTIONARY"));
        if (!QFileInfo::exists(phraseDictionary)) {
            phraseDictionary = QLibraryInfo::path(QLibraryInfo::DataPath) + QLatin1String("/qtvirtualkeyboard/tcime/dict_phrases.dat");
            if (!QFileInfo::exists(phraseDictionary))
                phraseDictionary = QLatin1String(":/qt-project.org/imports/QtQuick/VirtualKeyboard/3rdparty/tcime/data/qt/dict_phrases.dat");
        }
        d->phraseDictionary.load(phraseDictionary);
    }
    if (!result)
        inputMode = QVirtualKeyboardInputEngine::InputMode::Latin;
    return result;
}

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

bool TCInputMethod::keyEvent(Qt::Key key, const QString &text, Qt::KeyboardModifiers modifiers)
{
    Q_UNUSED(key);
    Q_UNUSED(text);
    Q_UNUSED(modifiers);
    Q_D(TCInputMethod);
    QVirtualKeyboardInputContext *ic = inputContext();
    bool accept = false;
    switch (key) {
    case Qt::Key_Context1:
        // Do nothing on symbol mode switch
        accept = true;
        break;

    case Qt::Key_Enter:
    case Qt::Key_Return:
        update();
        break;

    case Qt::Key_Tab:
    case Qt::Key_Space:
        if (!d->input.isEmpty()) {
            accept = true;
            if (d->highlightIndex >= 0) {
                QString finalWord = d->pickHighlighted();
                d->reset();
                inputContext()->commit(finalWord);
                if (d->setCandidates(d->phraseDictionary.getWords(finalWord.left(1)), false)) {
                    emit selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
                    emit selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, d->highlightIndex);
                }
            }
        } else {
            update();
        }
        break;

    case Qt::Key_Backspace:
        if (!d->input.isEmpty()) {
            d->input.remove(d->input.size() - 1, 1);
            ic->setPreeditText(d->input);
#if QT_CONFIG(cangjie)
            if (!d->checkSpecialCharInput()) {
#endif
                if (d->setCandidates(d->wordDictionary->getWords(d->input), true)) {
                    emit selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
                    emit selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, d->highlightIndex);
                }
#if QT_CONFIG(cangjie)
            }
#endif
            accept = true;
        } else if (d->clearCandidates()) {
            emit selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
            emit selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, d->highlightIndex);
        }
        break;

    default:
        if (text.size() == 1)
            accept = d->compose(text.at(0));
        if (!accept)
            update();
        break;
    }
    return accept;
}

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

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

QVariant TCInputMethod::selectionListData(QVirtualKeyboardSelectionListModel::Type type, int index, QVirtualKeyboardSelectionListModel::Role role)
{
    QVariant result;
    Q_D(TCInputMethod);
    switch (role) {
    case QVirtualKeyboardSelectionListModel::Role::Display:
        result = QVariant(d->candidates.at(index));
        break;
    case QVirtualKeyboardSelectionListModel::Role::WordCompletionLength:
        result.setValue(0);
        break;
    default:
        result = QVirtualKeyboardAbstractInputMethod::selectionListData(type, index, role);
        break;
    }
    return result;
}

void TCInputMethod::selectionListItemSelected(QVirtualKeyboardSelectionListModel::Type type, int index)
{
    Q_UNUSED(type);
    Q_D(TCInputMethod);
    QString finalWord = d->candidates.at(index);
    reset();
    inputContext()->commit(finalWord);
    if (d->setCandidates(d->phraseDictionary.getWords(finalWord.left(1)), false)) {
        emit selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
        emit selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList, d->highlightIndex);
    }
}

void TCInputMethod::reset()
{
    Q_D(TCInputMethod);
    d->reset();
}

void TCInputMethod::update()
{
    Q_D(TCInputMethod);
    if (d->highlightIndex >= 0) {
        QString finalWord = d->pickHighlighted();
        d->reset();
        inputContext()->commit(finalWord);
    } else {
        inputContext()->clear();
        d->reset();
    }
}

} // namespace QtVirtualKeyboard
QT_END_NAMESPACE
