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

#include "pinyininputmethod_p.h"
#include "pinyindecoderservice_p.h"
#include <QtVirtualKeyboard/qvirtualkeyboardinputcontext.h>

#include <QLoggingCategory>
#include <QtCore/qpointer.h>

QT_BEGIN_NAMESPACE
namespace QtVirtualKeyboard {

Q_LOGGING_CATEGORY(lcPinyin, "qt.virtualkeyboard.pinyin")

class PinyinInputMethodPrivate
{
    Q_DECLARE_PUBLIC(PinyinInputMethod)

public:
    enum State
    {
        Idle,
        Input,
        Predict
    };

    PinyinInputMethodPrivate(PinyinInputMethod *q_ptr) :
        q_ptr(q_ptr),
        inputMode(QVirtualKeyboardInputEngine::InputMode::Pinyin),
        pinyinDecoderService(PinyinDecoderService::getInstance()),
        state(Idle),
        surface(),
        totalChoicesNum(0),
        candidatesList(),
        fixedLen(0),
        composingStr(),
        activeCmpsLen(0),
        finishSelection(true),
        posDelSpl(-1),
        isPosInSpl(false)
    {
    }

    void resetToIdleState()
    {
        Q_Q(PinyinInputMethod);

        QVirtualKeyboardInputContext *inputContext = q->inputContext();

        // Disable the user dictionary when entering sensitive data
        if (inputContext && pinyinDecoderService) {
            bool userDictionaryEnabled = !inputContext->inputMethodHints().testFlag(Qt::ImhSensitiveData);
            if (userDictionaryEnabled != pinyinDecoderService->isUserDictionaryEnabled())
                pinyinDecoderService->setUserDictionary(userDictionaryEnabled);
        }

        if (state == Idle)
            return;

        state = Idle;
        surface.clear();
        fixedLen = 0;
        finishSelection = true;
        composingStr.clear();
        if (inputContext)
            inputContext->setPreeditText(QString());
        activeCmpsLen = 0;
        posDelSpl = -1;
        isPosInSpl = false;

        resetCandidates();
    }

    bool addSpellingChar(QChar ch, bool reset)
    {
        if (reset) {
            surface.clear();
            pinyinDecoderService->resetSearch();
        }
        if (ch == u'\'') {
            if (surface.isEmpty())
                return false;
            if (surface.endsWith(ch))
                return true;
        }
        surface.append(ch);
        return true;
    }

    bool removeSpellingChar()
    {
        if (surface.isEmpty())
            return false;
        QList<int> splStart = pinyinDecoderService->spellingStartPositions();
        isPosInSpl = (surface.size() <= splStart[fixedLen + 1]);
        posDelSpl = isPosInSpl ? fixedLen - 1 : surface.size() - 1;
        return true;
    }

    void chooseAndUpdate(int candId)
    {
        Q_Q(PinyinInputMethod);

        if (state == Predict)
            choosePredictChoice(candId);
        else
            chooseDecodingCandidate(candId);

        if (composingStr.size() > 0) {
            if ((candId >= 0 || finishSelection) && composingStr.size() == fixedLen) {
                QString resultStr = getComposingStrActivePart();
                q->inputContext()->commit(resultStr);
                tryPredict();
            } else if (state == Idle) {
                state = Input;
            }
        } else {
            tryPredict();
        }
    }

    bool chooseAndFinish()
    {
        if (state == Predict || !totalChoicesNum)
            return false;

        chooseAndUpdate(0);
        if (state != Predict && totalChoicesNum > 0)
            chooseAndUpdate(0);

        return true;
    }

    int candidatesCount()
    {
        return totalChoicesNum;
    }

    QString candidateAt(int index)
    {
        if (index < 0 || index >= totalChoicesNum)
            return QString();
        if (index >= candidatesList.size()) {
            int fetchMore = qMin(index + 20, totalChoicesNum - candidatesList.size());
            candidatesList.append(pinyinDecoderService->fetchCandidates(candidatesList.size(), fetchMore, fixedLen));
            if (index == 0 && totalChoicesNum == 1) {
                int surfaceDecodedLen = pinyinDecoderService->pinyinStringLength(true);
                if (surfaceDecodedLen < surface.size())
                    candidatesList[0] = candidatesList[0] + surface.mid(surfaceDecodedLen).toLower();
            }
        }
        return index < candidatesList.size() ? candidatesList[index] : QString();
    }

    void chooseDecodingCandidate(int candId)
    {
        Q_Q(PinyinInputMethod);
        Q_ASSERT(state != Predict);

        int result = 0;
        if (candId < 0) {
            if (surface.size() > 0) {
                if (posDelSpl < 0) {
                    result = pinyinDecoderService->search(surface);
                } else {
                    result = pinyinDecoderService->deleteSearch(posDelSpl, isPosInSpl, false);
                    posDelSpl = -1;
                }
            }
        } else {
            if (totalChoicesNum > 1) {
                result = pinyinDecoderService->chooceCandidate(candId);
            } else {
                QString resultStr;
                if (totalChoicesNum == 1) {
                    QString undecodedStr = candId < candidatesList.size() ? candidatesList.at(candId) : QString();
                    resultStr = pinyinDecoderService->candidateAt(0).mid(0, fixedLen) + undecodedStr;
                }
                resetToIdleState();
                if (!resultStr.isEmpty())
                    q->inputContext()->commit(resultStr);
                return;
            }
        }

        resetCandidates();
        totalChoicesNum = result;

        surface = pinyinDecoderService->pinyinString(false);
        QList<int> splStart = pinyinDecoderService->spellingStartPositions();
        QString fullSent = pinyinDecoderService->candidateAt(0);
        fixedLen = pinyinDecoderService->fixedLength();
        composingStr = fullSent.mid(0, fixedLen) + surface.mid(splStart[fixedLen + 1]);
        activeCmpsLen = composingStr.size();

        // Prepare the display string.
        QString composingStrDisplay;
        int surfaceDecodedLen = pinyinDecoderService->pinyinStringLength(true);
        if (!surfaceDecodedLen) {
            composingStrDisplay = composingStr.toLower();
            if (!totalChoicesNum)
                totalChoicesNum = 1;
        } else {
            activeCmpsLen = activeCmpsLen - (surface.size() - surfaceDecodedLen);
            composingStrDisplay = fullSent.mid(0, fixedLen);
            for (int pos = fixedLen + 1; pos < splStart.size() - 1; pos++) {
                composingStrDisplay += surface.mid(splStart[pos], splStart[pos + 1] - splStart[pos]);
                if (splStart[pos + 1] < surfaceDecodedLen)
                    composingStrDisplay += QLatin1String(" ");
            }
            if (surfaceDecodedLen < surface.size())
                composingStrDisplay += surface.mid(surfaceDecodedLen);
        }
        q->inputContext()->setPreeditText(composingStrDisplay);

        finishSelection = splStart.size() == (fixedLen + 2);
        if (!finishSelection)
            candidateAt(0);
    }

    void choosePredictChoice(int choiceId)
    {
        Q_ASSERT(state == Predict);

        if (choiceId < 0 || choiceId >= totalChoicesNum)
            return;

        QString tmp = candidatesList.at(choiceId);

        resetCandidates();

        candidatesList.append(tmp);
        totalChoicesNum = 1;

        surface.clear();
        fixedLen = tmp.size();
        composingStr = tmp;
        activeCmpsLen = fixedLen;

        finishSelection = true;
    }

    QString getComposingStrActivePart()
    {
        return composingStr.mid(0, activeCmpsLen);
    }

    void resetCandidates()
    {
        candidatesList.clear();
        if (totalChoicesNum) {
            totalChoicesNum = 0;
        }
    }

    void updateCandidateList()
    {
        Q_Q(PinyinInputMethod);
        emit q->selectionListChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList);
        emit q->selectionListActiveItemChanged(QVirtualKeyboardSelectionListModel::Type::WordCandidateList,
                                               totalChoicesNum > 0 && state == PinyinInputMethodPrivate::Input ? 0 : -1);
    }

    bool canDoPrediction()
    {
        Q_Q(PinyinInputMethod);
        QVirtualKeyboardInputContext *inputContext = q->inputContext();
        return inputMode == QVirtualKeyboardInputEngine::InputMode::Pinyin &&
                composingStr.size() == fixedLen &&
                inputContext &&
                !inputContext->inputMethodHints().testFlag(Qt::ImhNoPredictiveText);
    }

    void tryPredict()
    {
        // Try to get the prediction list.
        if (canDoPrediction()) {
            Q_Q(PinyinInputMethod);
            if (state != Predict)
                resetToIdleState();
            QVirtualKeyboardInputContext *inputContext = q->inputContext();
            int cursorPosition = inputContext->cursorPosition();
            int historyStart = qMax(0, cursorPosition - 3);
            QString history = inputContext->surroundingText().mid(historyStart, cursorPosition - historyStart);
            candidatesList = pinyinDecoderService->predictionList(history);
            totalChoicesNum = candidatesList.size();
            finishSelection = false;
            state = Predict;
        } else {
            resetCandidates();
        }

        if (!candidatesCount())
            resetToIdleState();
    }

    PinyinInputMethod *q_ptr;
    QVirtualKeyboardInputEngine::InputMode inputMode;
    QPointer<PinyinDecoderService> pinyinDecoderService;
    State state;
    QString surface;
    int totalChoicesNum;
    QList<QString> candidatesList;
    int fixedLen;
    QString composingStr;
    int activeCmpsLen;
    bool finishSelection;
    int posDelSpl;
    bool isPosInSpl;
};

class ScopedCandidateListUpdate
{
    Q_DISABLE_COPY(ScopedCandidateListUpdate)
public:
    inline explicit ScopedCandidateListUpdate(PinyinInputMethodPrivate *d) :
        d(d),
        candidatesList(d->candidatesList),
        totalChoicesNum(d->totalChoicesNum),
        state(d->state)
    {
    }

    inline ~ScopedCandidateListUpdate()
    {
        if (totalChoicesNum != d->totalChoicesNum || state != d->state || candidatesList != d->candidatesList)
            d->updateCandidateList();
    }

private:
    PinyinInputMethodPrivate *d;
    QList<QString> candidatesList;
    int totalChoicesNum;
    PinyinInputMethodPrivate::State state;
};

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

PinyinInputMethod::PinyinInputMethod(QObject *parent) :
    QVirtualKeyboardAbstractInputMethod(parent),
    d_ptr(new PinyinInputMethodPrivate(this))
{
}

PinyinInputMethod::~PinyinInputMethod()
{
}

QList<QVirtualKeyboardInputEngine::InputMode> PinyinInputMethod::inputModes(const QString &locale)
{
    Q_UNUSED(locale);
    Q_D(PinyinInputMethod);
    QList<QVirtualKeyboardInputEngine::InputMode> result;
    if (d->pinyinDecoderService)
        result << QVirtualKeyboardInputEngine::InputMode::Pinyin;
    result << QVirtualKeyboardInputEngine::InputMode::Latin;
    return result;
}

bool PinyinInputMethod::setInputMode(const QString &locale, QVirtualKeyboardInputEngine::InputMode inputMode)
{
    Q_UNUSED(locale);
    Q_D(PinyinInputMethod);
    reset();
    if (inputMode == QVirtualKeyboardInputEngine::InputMode::Pinyin && !d->pinyinDecoderService)
        return false;
    d->inputMode = inputMode;
    return true;
}

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

bool PinyinInputMethod::keyEvent(Qt::Key key, const QString &text, Qt::KeyboardModifiers modifiers)
{
    Q_UNUSED(modifiers);
    Q_D(PinyinInputMethod);
    if (d->inputMode == QVirtualKeyboardInputEngine::InputMode::Pinyin) {
        ScopedCandidateListUpdate scopedCandidateListUpdate(d);
        Q_UNUSED(scopedCandidateListUpdate);
        if ((key >= Qt::Key_A && key <= Qt::Key_Z) || (key == Qt::Key_Apostrophe)) {
            if (d->state == PinyinInputMethodPrivate::Predict)
                d->resetToIdleState();
            if (d->addSpellingChar(text.at(0), d->state == PinyinInputMethodPrivate::Idle)) {
                d->chooseAndUpdate(-1);
                return true;
            }
        } else if (key == Qt::Key_Space) {
            if (d->state != PinyinInputMethodPrivate::Predict && d->candidatesCount() > 0) {
                d->chooseAndUpdate(0);
                return true;
            }
        } else if (key == Qt::Key_Return) {
            if (d->state != PinyinInputMethodPrivate::Predict && d->candidatesCount() > 0) {
                QString surface = d->surface;
                d->resetToIdleState();
                inputContext()->commit(surface);
                return true;
            }
        } else if (key == Qt::Key_Backspace) {
            if (d->removeSpellingChar()) {
                d->chooseAndUpdate(-1);
                return true;
            }
        } else if (key >= Qt::Key_0 && key <= Qt::Key_9) {
            if (d->state != PinyinInputMethodPrivate::Predict && d->candidatesCount() > 0) {
                QString surface = d->surface;
                d->resetToIdleState();
                inputContext()->commit(surface);
            }
        } else if (!text.isEmpty()) {
            d->chooseAndFinish();
        }
    }
    return false;
}

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

int PinyinInputMethod::selectionListItemCount(QVirtualKeyboardSelectionListModel::Type type)
{
    Q_UNUSED(type);
    Q_D(PinyinInputMethod);
    return d->candidatesCount();
}

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

void PinyinInputMethod::selectionListItemSelected(QVirtualKeyboardSelectionListModel::Type type, int index)
{
    Q_UNUSED(type);
    Q_D(PinyinInputMethod);
    ScopedCandidateListUpdate scopedCandidateListUpdate(d);
    Q_UNUSED(scopedCandidateListUpdate);
    d->chooseAndUpdate(index);
}

void PinyinInputMethod::reset()
{
    Q_D(PinyinInputMethod);
    ScopedCandidateListUpdate scopedCandidateListUpdate(d);
    Q_UNUSED(scopedCandidateListUpdate);
    d->resetToIdleState();
}

void PinyinInputMethod::update()
{
    Q_D(PinyinInputMethod);
    ScopedCandidateListUpdate scopedCandidateListUpdate(d);
    Q_UNUSED(scopedCandidateListUpdate);
    d->chooseAndFinish();
    d->tryPredict();
}

} // namespace QtVirtualKeyboard
QT_END_NAMESPACE
