/*
 * SPDX-FileCopyrightText: 2017-2017 CSSlayer <wengxt@gmail.com>
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 */

#include "pinyin.h"

// Use relative path so we don't need import export target.
// We want to keep cloudpinyin logic but don't call it.
#include "../../modules/cloudpinyin/cloudpinyin_public.h"
#include "config.h"
#include "customphrase.h"
#include "notifications_public.h"
#include "pinyincandidate.h"
#include "pinyinhelper_public.h"
#include "punctuation_public.h"
#include "spell_public.h"
#include "workerthread.h"
#include <algorithm>
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <ctime>
#include <exception>
#include <fcitx-config/iniparser.h>
#include <fcitx-config/rawconfig.h>
#include <fcitx-utils/capabilityflags.h>
#include <fcitx-utils/charutils.h>
#include <fcitx-utils/event.h>
#include <fcitx-utils/eventloopinterface.h>
#include <fcitx-utils/fdstreambuf.h>
#include <fcitx-utils/i18n.h>
#include <fcitx-utils/key.h>
#include <fcitx-utils/keysym.h>
#include <fcitx-utils/log.h>
#include <fcitx-utils/macros.h>
#include <fcitx-utils/misc.h>
#include <fcitx-utils/standardpaths.h>
#include <fcitx-utils/stringutils.h>
#include <fcitx-utils/textformatflags.h>
#include <fcitx-utils/unixfd.h>
#include <fcitx-utils/utf8.h>
#include <fcitx/action.h>
#include <fcitx/addoninstance.h>
#include <fcitx/candidatelist.h>
#include <fcitx/event.h>
#include <fcitx/inputcontext.h>
#include <fcitx/inputcontextmanager.h>
#include <fcitx/inputcontextproperty.h>
#include <fcitx/inputmethodengine.h>
#include <fcitx/inputmethodentry.h>
#include <fcitx/inputpanel.h>
#include <fcitx/instance.h>
#include <fcitx/statusarea.h>
#include <fcitx/text.h>
#include <fcitx/userinterface.h>
#include <fcitx/userinterfacemanager.h>
#include <fcntl.h>
#include <filesystem>
#include <fstream>
#include <functional>
#include <future>
#include <ios>
#include <istream>
#include <iterator>
#include <libime/core/historybigram.h>
#include <libime/core/languagemodel.h>
#include <libime/core/lattice.h>
#include <libime/core/triedictionary.h>
#include <libime/core/userlanguagemodel.h>
#include <libime/pinyin/pinyincontext.h>
#include <libime/pinyin/pinyincorrectionprofile.h>
#include <libime/pinyin/pinyindecoder.h>
#include <libime/pinyin/pinyindictionary.h>
#include <libime/pinyin/pinyinencoder.h>
#include <libime/pinyin/pinyinime.h>
#include <libime/pinyin/pinyinprediction.h>
#include <libime/pinyin/shuangpinprofile.h>
#include <list>
#include <memory>
#include <optional>
#include <ostream>
#include <quickphrase_public.h>
#include <ranges>
#include <regex>
#include <stdexcept>
#include <string>
#include <string_view>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>

#ifdef FCITX_HAS_LUA
#include <luaaddon_public.h>
#endif

namespace fcitx {

namespace {

FCITX_DEFINE_LOG_CATEGORY(pinyin, "pinyin");

#define PINYIN_DEBUG() FCITX_LOGC(pinyin, Debug)
#define PINYIN_ERROR() FCITX_LOGC(pinyin, Error)

template <typename T>
std::unique_ptr<CandidateList>
predictCandidateList(PinyinEngine *engine, const std::vector<T> &words) {
    if (words.empty()) {
        return nullptr;
    }
    auto candidateList = std::make_unique<CommonCandidateList>();
    for (const auto &word : words) {
        if constexpr (std::is_same_v<T, std::string>) {
            candidateList->append<PinyinPredictCandidateWord>(engine, word);
        } else if constexpr (std::is_same_v<
                                 T,
                                 std::pair<std::string,
                                           libime::PinyinPredictionSource>>) {

            if (word.second == libime::PinyinPredictionSource::Model) {
                candidateList->append<PinyinPredictCandidateWord>(engine,
                                                                  word.first);
            } else if (word.second ==
                       libime::PinyinPredictionSource::Dictionary) {
                candidateList->append<PinyinPredictDictCandidateWord>(
                    engine, word.first);
            }
        }
    }
    candidateList->setSelectionKey(engine->selectionKeys());
    candidateList->setPageSize(*engine->config().pageSize);
    if (!candidateList->empty()) {
        candidateList->setGlobalCursorIndex(0);
    }
    return candidateList;
}

std::tuple<bool, int> englishNess(const std::string &input, bool sp) {
    const auto pys = stringutils::split(input, " ");
    constexpr int fullWeight = -2;
    constexpr int shortWeight = 3;
    constexpr int invalidWeight = 6;
    constexpr int defaultWeight = shortWeight;
    int weight = 0;
    if (std::ranges::any_of(input, charutils::isupper)) {
        return {true,
                std::max<size_t>(1, ((invalidWeight * pys.size()) + 7) / 10)};
    }

    for (const auto &py : pys) {
        if (sp) {
            if (py.size() == 2) {
                weight += fullWeight / 2;
            } else {
                weight += invalidWeight;
            }
        } else {
            if (py == "ng") {
                weight += fullWeight;
            } else {
                auto firstChr = py[0];
                if (firstChr == '\'') {
                    return {false, 0};
                }
                if (firstChr == 'i' || firstChr == 'u' || firstChr == 'v') {
                    weight += invalidWeight;
                } else if (py.size() <= 2) {
                    weight += shortWeight;
                } else if (py.find_first_of("aeiou") != std::string::npos) {
                    weight += fullWeight;
                } else {
                    weight += defaultWeight;
                }
            }
        }
    }

    if (weight < 0) {
        return {false, 0};
    }
    return {false, (weight + 7) / 10};
}

bool isStroke(const std::string &input) {
    static const std::unordered_set<char> py{'h', 'p', 's', 'z', 'n'};
    return std::all_of(input.begin(), input.end(),
                       [](char c) { return py.count(c); });
}

PinyinTabbedCandidateList *currentPinyinTabbed(InputContext *inputContext) {
    auto candidateList = inputContext->inputPanel().candidateList();
    if (!candidateList) {
        return nullptr;
    }
    return dynamic_cast<PinyinTabbedCandidateList *>(candidateList->toTabbed());
}

std::string getEncodedPinyin(const libime::SentenceResult &result) {
    std::string encodedPinyin;
    bool validPinyin = std::all_of(
        result.sentence().begin(), result.sentence().end(),
        [](const libime::LatticeNode *node) {
            if (node->word().empty()) {
                return true;
            }
            const auto *pinyinNode =
                static_cast<const libime::PinyinLatticeNode *>(node);
            return !pinyinNode->encodedPinyin().empty() &&
                   pinyinNode->encodedPinyin().size() % 2 == 0;
        });
    if (validPinyin) {
        for (const auto *node : result.sentence()) {
            const auto *pinyinNode =
                static_cast<const libime::PinyinLatticeNode *>(node);
            encodedPinyin.insert(encodedPinyin.end(),
                                 pinyinNode->encodedPinyin().begin(),
                                 pinyinNode->encodedPinyin().end());
        }
    }
    return encodedPinyin;
}

std::optional<int> selectCharFromCandidateIndex(const Key &key,
                                                PinyinEngineConfig &config) {
    if (key.check(*config.selectLastCharFromPhrase)) {
        return -1;
    }
    if (int idx = key.keyListIndex(*config.selectCharFromPhrase); idx >= 0) {
        return idx;
    }
    return std::nullopt;
}

struct CloudPinyinMatchResult {
    std::vector<libime::HistoryBigram::WordWithCode> context;
    std::string_view word;
    std::string fullPinyin;
};

std::optional<CloudPinyinMatchResult>
matchCloudPinyin(const libime::PinyinContext &context,
                 const std::string &selected, const std::string &word) {
    // This ensure us to convert pinyin to the right one.
    auto sentence = context.preedit(libime::PinyinPreeditMode::RawText);
    std::string_view preedit = sentence;
    // preedit is "selected sentence" + Pinyin string.
    // Validate selected is still the same.
    if (!stringutils::consumePrefix(preedit, selected)) {
        return std::nullopt;
    }
    auto words = context.selectedWordsWithPinyin();
    auto originalSize = words.size();
    auto pinyins = stringutils::split(preedit, " '");
    std::string_view wordView = word;
    // Check word length matches pinyin length.
    // If this doesn't match, then our segmentation doesn't match the cloud
    // pinyin, so doesn't make sense to match further.
    if (pinyins.empty() || pinyins.size() != utf8::length(word)) {
        return std::nullopt;
    }
    if (context.useShuangpin()) {
        for (auto &sppinyin : pinyins) {
            // Likely something goes wrong.
            if (sppinyin.size() > 2) {
                return std::nullopt;
            }
            sppinyin = libime::PinyinEncoder::shuangpinToPinyin(
                sppinyin, *context.ime()->shuangpinProfile());
            if (sppinyin.empty()) {
                return std::nullopt;
            }
        }
    }
    // If our engine based sentence can match the selected sentence, we can
    // reuse the pinyin result from engine to try to match as much as possible.
    // Ideally we can also traverse the lattice, but that's too much work.
    const auto &candidates = context.candidates();
    if (candidates.empty()) {
        return CloudPinyinMatchResult{.context = std::move(words),
                                      .word = wordView,
                                      .fullPinyin =
                                          stringutils::join(pinyins, "\'")};
    }
    auto pinyinsIter = pinyins.begin();
    auto pinyinsEnd = pinyins.end();
    const auto &bestSentence = candidates[0].sentence();
    for (const auto *segment : bestSentence) {
        auto consumed = wordView;
        if (!stringutils::consumePrefix(consumed, segment->word())) {
            break;
        }
        if (!segment->word().empty()) {
            PINYIN_DEBUG() << "Cloud Pinyin can reuse segment "
                           << segment->word();
            const auto *pinyinNode =
                static_cast<const libime::PinyinLatticeNode *>(segment);
            auto pinyinSize = pinyinNode->encodedPinyin().size() / 2;
            if (pinyinSize && static_cast<size_t>(std::distance(
                                  pinyinsIter, pinyinsEnd)) >= pinyinSize) {
                pinyinsIter += pinyinSize;
                words.push_back(libime::HistoryBigram::WordWithCode{
                    segment->word(), pinyinNode->encodedPinyin()});
            } else {
                break;
            }
        }
        wordView = consumed;
    }
    // Special case for all single character, if that happens, try to learn as
    // original word.
    if (utf8::length(wordView) == 1 &&
        std::ranges::all_of(words,
                            [](const libime::HistoryBigram::WordWithCode &w) {
                                return utf8::length(w.first) == 1;
                            })) {
        words.resize(originalSize);
        wordView = word;
        pinyinsIter = pinyins.begin();
    }
    return CloudPinyinMatchResult{
        .context = std::move(words),
        .word = wordView,
        .fullPinyin = stringutils::join(pinyinsIter, pinyinsEnd, "\'")};
}

} // namespace

PinyinState::PinyinState(PinyinEngine *engine) : context_(engine->ime()) {
    context_.setMaxSentenceLength(35);
}

void PinyinEngine::initPredict(InputContext *inputContext) {
    auto *state = inputContext->propertyFor(&factory_);
    // clear state no matter what.
    state->predictWords_.reset();
    auto &context = state->context_;
    auto lmState = context.state();
    auto selected = context.selectedWordsWithPinyin();
    if (*config_.keepCurrentContext) {
        context.appendContextWordsWithPinyin(context.selectedWordsWithPinyin());
    }
    if (selected.empty()) {
        return;
    }
    if (!*config_.predictionEnabled) {
        return;
    }

    const auto selectPinyin = context.selectedWordsWithPinyin();

    auto words =
        prediction_.predict(lmState, selected, *config_.predictionSize);
    if (auto candidateList = predictCandidateList(this, words)) {
        auto &inputPanel = inputContext->inputPanel();
        state->predictWords_ = std::move(selected);
        inputPanel.setCandidateList(std::move(candidateList));
    } else {
        return;
    }
    inputContext->updatePreedit();
    inputContext->updateUserInterface(UserInterfaceComponent::InputPanel);
}

void PinyinEngine::updatePredict(InputContext *inputContext) {
    inputContext->inputPanel().reset();

    auto *state = inputContext->propertyFor(&factory_);
    assert(state->predictWords_.has_value());
    if (*config_.keepCurrentContext) {
        state->context_.setContextWordsWithPinyin(*state->predictWords_);
    }
    auto words =
        prediction_.predict(ime_->model()->nullState(), *state->predictWords_,
                            *config_.predictionSize);
    if (auto candidateList = predictCandidateList(this, words)) {
        auto &inputPanel = inputContext->inputPanel();
        inputPanel.setCandidateList(std::move(candidateList));
    } else {
        // Clear if we can't do predict.
        // This help other code to detect whether we are in predict.
        state->predictWords_.reset();
    }
    inputContext->updatePreedit();
    inputContext->updateUserInterface(UserInterfaceComponent::InputPanel);
}

#ifdef FCITX_HAS_LUA
std::vector<std::string>
PinyinEngine::luaCandidateTrigger(InputContext *ic,
                                  const std::string &candidateString) {
    std::vector<std::string> result;
    RawConfig arg;
    arg.setValue(candidateString);
    auto ret = imeapi()->call<ILuaAddon::invokeLuaFunction>(
        ic, "candidateTrigger", arg);
    const auto *length = ret.valueByPath("Length");
    try {
        if (length) {
            auto n = std::stoi(*length);
            for (int i = 0; i < n; i++) {
                const auto *candidate = ret.valueByPath(std::to_string(i));
                if (candidate && !candidate->empty()) {
                    result.push_back(*candidate);
                }
            }
        }
    } catch (...) {
    }
    return result;
}
#endif

std::pair<Text, Text> PinyinEngine::preedit(InputContext *inputContext) const {
    auto *state = inputContext->propertyFor(&factory_);
    PreeditMode mode =
        inputContext->capabilityFlags().test(CapabilityFlag::Preedit)
            ? *config_.preeditMode
            : PreeditMode::No;
    // Use const ref to avoid accidentally change anything.
    const auto &context = state->context_;
    auto preeditWithCursor = context.preeditWithCursor();
    // client preedit can be empty/pinyin/preview depends on config
    Text clientPreedit;
    Text preedit;
    switch (mode) {
    case PreeditMode::ComposingPinyin:
        if (*config_.preeditCursorPositionAtBeginning) {
            clientPreedit.append(
                preeditWithCursor.first.substr(0, preeditWithCursor.second),
                {TextFormatFlag::HighLight, TextFormatFlag::Underline});
            clientPreedit.append(
                preeditWithCursor.first.substr(preeditWithCursor.second),
                TextFormatFlag::Underline);
            clientPreedit.setCursor(0);
        } else {
            clientPreedit.append(preeditWithCursor.first,
                                 TextFormatFlag::Underline);
            clientPreedit.setCursor(preeditWithCursor.second);
        }
        break;
    case PreeditMode::CommitPreview:
        clientPreedit.append(context.sentence(), TextFormatFlag::Underline);
        if (*config_.preeditCursorPositionAtBeginning) {
            clientPreedit.setCursor(0);
        } else {
            clientPreedit.setCursor(context.selectedSentence().size());
        }
        [[fallthrough]];
    case PreeditMode::No:
        preedit.append(preeditWithCursor.first);
        preedit.setCursor(preeditWithCursor.second);
        break;
    }
    return {std::move(clientPreedit), std::move(preedit)};
}

std::string
PinyinEngine::preeditCommitString(InputContext *inputContext) const {
    auto *state = inputContext->propertyFor(&factory_);
    // Use const ref to avoid accidentally change anything.
    const auto &context = state->context_;

    const auto &userInput = context.userInput();
    const auto selectedLength = context.selectedLength();

    return context.selectedSentence() +
           userInput.substr(selectedLength,
                            userInput.length() - selectedLength);
}

void PinyinEngine::updatePreedit(InputContext *inputContext) const {
    auto &inputPanel = inputContext->inputPanel();
    auto [clientPreedit, preedit] = this->preedit(inputContext);
    inputPanel.setClientPreedit(clientPreedit);
    inputPanel.setPreedit(preedit);
}

void PinyinEngine::updatePuncPreedit(InputContext *inputContext) const {
    auto candidateList = inputContext->inputPanel().candidateList();

    if (inputContext->capabilityFlags().test(CapabilityFlag::Preedit)) {
        if (candidateList->cursorIndex() >= 0) {
            Text preedit;

            const auto &candidate =
                candidateList->candidate(candidateList->cursorIndex());
            if (const auto *puncCandidate =
                    dynamic_cast<const PinyinPunctuationCandidateWord *>(
                        &candidate)) {
                preedit.append(puncCandidate->word());
            }

            preedit.setCursor(preedit.textLength());
            inputContext->inputPanel().setClientPreedit(preedit);
        }
        inputContext->updatePreedit();
    }
}

void PinyinEngine::updatePuncCandidate(
    InputContext *inputContext, const std::string &original,
    const std::vector<std::string> &candidates) const {
    inputContext->inputPanel().reset();
    auto *state = inputContext->propertyFor(&factory_);
    auto puncCandidateList = std::make_unique<CommonCandidateList>();
    puncCandidateList->setPageSize(*config_.pageSize);
    puncCandidateList->setCursorPositionAfterPaging(
        CursorPositionAfterPaging::ResetToFirst);
    for (const auto &result : candidates) {
        puncCandidateList->append<PinyinPunctuationCandidateWord>(
            this, result, original == result);
    }
    puncCandidateList->setCursorIncludeUnselected(false);
    puncCandidateList->setCursorKeepInSamePage(false);
    puncCandidateList->setGlobalCursorIndex(0);
    puncCandidateList->setSelectionKey(selectionKeys_);
    state->mode_ = PinyinMode::Punctuation;
    inputContext->inputPanel().setCandidateList(std::move(puncCandidateList));
    updatePuncPreedit(inputContext);
    inputContext->updateUserInterface(UserInterfaceComponent::InputPanel);
}

void PinyinEngine::updateUI(InputContext *inputContext) {
    auto *state = inputContext->propertyFor(&factory_);
    inputContext->inputPanel().reset();
    // Use const ref to avoid accidentally change anything.
    const auto &context = state->context_;
    if (context.selected()) {
        auto sentence = context.sentence();
        if (shouldLearn(inputContext)) {
            state->context_.learn();
        }
        inputContext->commitString(sentence);
        inputContext->updatePreedit();
        inputContext->updateUserInterface(UserInterfaceComponent::InputPanel);
        initPredict(inputContext);
        state->context_.clear();
        return;
    }

    do {
        if (context.userInput().empty()) {
            break;
        }
        // Update Preedit.
        updatePreedit(inputContext);
        auto &inputPanel = inputContext->inputPanel();
        // Update candidate
        const auto &pinyinCandidates = context.candidatesToCursor();
        if (pinyinCandidates.empty()) {
            break;
        }

        // Setup candidate list.
        auto candidateList = std::make_unique<CommonCandidateList>();
        candidateList->setPageSize(*config_.pageSize);
        candidateList->setCursorPositionAfterPaging(
            CursorPositionAfterPaging::ResetToFirst);

        // Precompute some values.
        const auto selectedLength = context.selectedLength();
        const auto selectedSentence = context.selectedSentence();

        const bool fullResult = (context.cursor() == context.size() ||
                                 context.cursor() == context.selectedLength());

        const auto pyLength = context.cursor() > selectedLength
                                  ? context.cursor() - selectedLength
                                  : std::string::npos;
        const auto pyBeforeCursor =
            context.userInput().substr(selectedLength, pyLength);

        std::unordered_map<std::string,
                           std::unique_ptr<PinyinAbstractCandidateWord>>
            customCandidateMap;
        /// Create custom phrase candidate {{{
        do {
            const auto *results = customPhrase_.lookup(pyBeforeCursor);
            if (!results) {
                break;
            }
            for (const auto &result : *results) {
                if (result.order() <= 0) {
                    continue;
                }
                auto phrase =
                    result.evaluate([this, inputContext](std::string_view key) {
                        return evaluateCustomPhrase(inputContext, key);
                    });
                if (customCandidateMap.contains(phrase)) {
                    continue;
                }
                std::string customPhrase =
                    result.isDynamic() ? result.value() : phrase;
                customCandidateMap.emplace(
                    phrase, std::make_unique<CustomPhraseCandidateWord>(
                                this, pyBeforeCursor.size(),
                                CandidateOrder{result.order() - 1,
                                               customCandidateMap.size()},
                                phrase, std::move(customPhrase)));
            }
        } while (false);
        /// }}}

        /// Create cloud candidate. {{{
        if (*config_.cloudPinyinEnabled && cloudpinyin() &&
            !inputContext->capabilityFlags().testAny(
                CapabilityFlag::PasswordOrSensitive) &&
            fullResult) {
            using namespace std::placeholders;
            auto fullPinyin = context.useShuangpin()
                                  ? context.candidateFullPinyin(0)
                                  : context.userInput().substr(selectedLength);
            auto cand = std::make_unique<CustomCloudPinyinCandidateWord>(
                this, fullPinyin, selectedSentence, inputContext,
                [this](InputContext *ic, const std::string &selected,
                       const std::string &word) {
                    cloudPinyinSelected(ic, selected, word);
                },
                CandidateOrder{*config_.cloudPinyinIndex - 1,
                               customCandidateMap.size()});
            if (!cand->filled() ||
                (!cand->word().empty() &&
                 !customCandidateMap.contains(cand->word()) &&
                 !context.candidatesToCursorSet().contains(cand->word()))) {
                customCandidateMap.emplace(cand->word(), std::move(cand));
            }
        }
        /// }}}

        /// Create spell candidate {{{
        auto [parsedPy, parsedPyCursor] = state->context_.preeditWithCursor(
            libime::PinyinPreeditMode::RawText);
        if (*config_.spellEnabled && spell() &&
            parsedPyCursor >= selectedSentence.size() &&
            selectedLength <= context.cursor()) {
            auto [hasUpper, engNess] =
                englishNess(parsedPy, context.useShuangpin());
            if (engNess) {
                parsedPyCursor -= selectedSentence.length();
                parsedPy = parsedPy.substr(
                    selectedSentence.size(),
                    parsedPyCursor > selectedSentence.length()
                        ? parsedPyCursor - selectedSentence.length()
                        : std::string::npos);
                auto results = spell()->call<ISpell::hintWithProvider>(
                    "en", SpellProvider::Custom, pyBeforeCursor, engNess);

                // Our hint doesn't work well with mixed case, so, always put a
                // word as is.
                if (hasUpper && !pyBeforeCursor.empty()) {
                    if (std::find(results.begin(), results.end(),
                                  pyBeforeCursor) == results.end()) {
                        if (!charutils::isupper(pyBeforeCursor[0])) {
                            results.insert(results.begin(), pyBeforeCursor);
                        } else {
                            results.push_back(pyBeforeCursor);
                        }
                    }
                }

                int position = hasUpper ? 0 : 1;
                for (const auto &result : results) {
                    if (customCandidateMap.contains(result)) {
                        continue;
                    }
                    customCandidateMap.emplace(
                        result, std::make_unique<SpellCandidateWord>(
                                    this, result, pyBeforeCursor.size(),
                                    CandidateOrder{position++,
                                                   customCandidateMap.size()}));
                }
            }
        }
        /// }}}

        /// Create stroke candidate {{{
        if (*config_.strokeCandidateEnabled && pinyinhelper() &&
            context.selectedLength() == 0 && fullResult &&
            isStroke(context.userInput())) {
            int limit = (context.userInput().size() + 4) / 5;
            limit = std::min(limit, 3);
            auto results = pinyinhelper()->call<IPinyinHelper::lookupStroke>(
                context.userInput(), limit);

            int strokeCandsPos =
                *config_.pageSize - static_cast<int>(results.size());
            if (strokeCandsPos < 0) {
                strokeCandsPos = *config_.pageSize - 1;
            }
            for (auto &result : results) {
                utf8::getChar(result.first);
                auto py = pinyinhelper()->call<IPinyinHelper::lookup>(
                    utf8::getChar(result.first));
                auto pystr = stringutils::join(py, " ");
                customCandidateMap.try_emplace(
                    result.first,
                    std::make_unique<StrokeCandidateWord>(
                        this, result.first, pystr, context.userInput().size(),
                        CandidateOrder{strokeCandsPos++,
                                       customCandidateMap.size()}));
            }
        }
        /// }}}

        const auto candidateCompare = [](const auto &lhs, const auto &rhs) {
            return lhs->sortOrder() < rhs->sortOrder();
        };

        // Save the middle point for inplace_merge.
        std::vector<std::unique_ptr<PinyinAbstractCandidateWord>> candidates;
        std::unordered_map<std::string, std::vector<PinyinCandidateWord *>>
            candidateSet;

        candidates.reserve(customCandidateMap.size() + pinyinCandidates.size());
        // Reserve the space for custom candidate and start to lay them out.
        candidates.resize(customCandidateMap.size());

        for (size_t idx = 0; idx < pinyinCandidates.size(); ++idx) {
            const auto &candidate = pinyinCandidates[idx];
            auto candidateString = candidate.toString();
            auto iter = customCandidateMap.find(candidateString);
            CandidateOrder order{candidates.size() - customCandidateMap.size(),
                                 customCandidateMap.size()};
            if (iter != customCandidateMap.end()) {
                if (iter->second->selectLength() !=
                    candidate.sentence().back()->to()->index()) {
                    continue;
                }
                order = iter->second->sortOrder();
            }
            auto pinyinCandidate = std::make_unique<PinyinCandidateWord>(
                this, inputContext, Text(std::move(candidateString)),
                candidate.sentence().back()->to()->index(), idx, order);
            candidateSet[pinyinCandidate->text().toString()].push_back(
                pinyinCandidate.get());
            if (iter != customCandidateMap.end()) {
                if (dynamic_cast<CustomPhraseCandidateWord *>(
                        iter->second.get())) {
                    pinyinCandidate->setCustomPhrase();
                }
                iter->second = std::move(pinyinCandidate);
            } else {
                candidates.push_back(std::move(pinyinCandidate));
            }
        }
        for (const auto &[_, candidates] : candidateSet) {
            for (auto *candidate : candidates | std::views::drop(1)) {
                candidate->setPinyinInComment();
            }
        }
        // Put the custom candidate in the first half.
        size_t middle = 0;
        for (auto &[_, candidate] : customCandidateMap) {
            candidates[middle++] = std::move(candidate);
        }
        // Sort the custom candidate half.
        std::sort(candidates.begin(),
                  std::ranges::next(candidates.begin(), middle),
                  candidateCompare);

        // Normalize the order
        // E.g. we have sort order 1, 1, 3, 6.
        // Since we would try to fit custom candidate as close as possible to
        // the position, In the final result would be like (x is pinyin
        // candidate)
        //
        // {x0, 1, 1, 3, x1, x2, 6, x3, x4, x5, ...}
        //
        // To achieve this, we'd re-number the order to be like
        //
        // {x0, 1, 1, 1, x1, x2, 3, x3, x4, x5, ...}
        //
        // the new number is number of "x" before it.
        std::optional<size_t> currentSegement = std::nullopt;
        for (size_t i = 0; i < middle; ++i) {
            const CandidateOrder current = candidates[i]->sortOrder();
            if (!currentSegement || current.first > *currentSegement + i) {
                currentSegement = current.first - i;
            }
            candidates[i]->setSortOrder({*currentSegement, i});
        }

        std::ranges::inplace_merge(
            candidates, std::ranges::next(candidates.begin(), middle),
            candidateCompare);

        // Apply the candidate to candidate generation.
        for (auto &candidatePtr : candidates) {
            // Candidate pointer shall still valid here.
            auto *candidate = candidatePtr.get();
            auto candidateString = candidate->text().toString();

            std::vector<std::string> luaExtraCandidates;
#ifdef FCITX_HAS_LUA
            // Only trigger lua for top N candidates to avoid too much overhead.
            if (candidate->order() <
                    std::max<size_t>(*config_.nbest, *config_.pageSize) &&
                imeapi()) {
                luaExtraCandidates =
                    luaCandidateTrigger(inputContext, candidateString);
            }
#endif

            // use candidateString before it is moved.
            const std::vector<std::string> *symbols = nullptr;
            if (*config_.symbolsEnabled) {
                symbols = symbols_.lookup(candidateString);
            }

            candidateList->append(std::move(candidatePtr));
            for (auto &extraCandidate : luaExtraCandidates) {
                candidateList->append<LuaCandidateWord>(
                    this, candidate->selectLength(), std::move(extraCandidate));
            }

            if (symbols) {
                std::string encodedPinyin;
                int pinyinCandidateIndex = -1;
                if (candidate->isPinyinCandidate()) {
                    auto *pinyinCandidate =
                        static_cast<PinyinCandidateWord *>(candidate);
                    pinyinCandidateIndex = pinyinCandidate->candidateIndex();
                    encodedPinyin = getEncodedPinyin(
                        pinyinCandidates[pinyinCandidateIndex]);
                }
                for (const auto &symbol : *symbols) {
                    const bool isFull =
                        candidate->selectLength() == pyBeforeCursor.size();
                    candidateList->append<SymbolCandidateWord>(
                        this, symbol, encodedPinyin, candidate->selectLength(),
                        isFull, pinyinCandidateIndex);
                }
            }
        }

        candidateList->setSelectionKey(selectionKeys_);
        if (!candidateList->empty()) {
            candidateList->setGlobalCursorIndex(0);
        }
        candidateList->setActionableImpl(
            std::make_unique<PinyinActionableCandidateList>(this,
                                                            inputContext));
        candidateList->setTabbedImpl(
            std::make_unique<PinyinTabbedCandidateList>(this, inputContext,
                                                        candidateList.get()));
        inputPanel.setCandidateList(std::move(candidateList));
    } while (0);
    inputContext->updatePreedit();
    inputContext->updateUserInterface(UserInterfaceComponent::InputPanel);
}

bool PinyinEngine::shouldLearn(const InputContext *inputContext) const {
    return *config_.learning && !inputContext->capabilityFlags().testAny(
                                    CapabilityFlag::PasswordOrSensitive);
}

std::string PinyinEngine::evaluateCustomPhrase(InputContext *inputContext,
                                               std::string_view key) {
    FCITX_UNUSED(inputContext);
    auto result = CustomPhrase::builtinEvaluator(key);
    if (!result.empty()) {
        return result;
    }

#ifdef FCITX_HAS_LUA
    if (key.starts_with("lua:")) {
        RawConfig config;
        auto ret = imeapi()->call<ILuaAddon::invokeLuaFunction>(
            inputContext, std::string(key.substr(4)), config);
        if (!ret.value().empty()) {
            return ret.value();
        }
    }
#endif
    return "";
}

PinyinEngine::PinyinEngine(Instance *instance)
    : instance_(instance),
      factory_([this](InputContext &) { return new PinyinState(this); }),
      worker_(instance->eventDispatcher()) {
    ime_ = std::make_unique<libime::PinyinIME>(
        std::make_unique<libime::PinyinDictionary>(),
        std::make_unique<libime::UserLanguageModel>(
            libime::DefaultLanguageModelResolver::instance()
                .languageModelFileForLanguage("zh_CN")));

    const auto &standardPath = StandardPaths::global();
    auto systemDictFile =
        standardPath.open(StandardPathsType::Data, "libime/sc.dict");
    if (systemDictFile.isValid()) {
        IFDStreamBuf buffer(systemDictFile.fd());
        std::istream in(&buffer);
        ime_->dict()->load(libime::PinyinDictionary::SystemDict, in,
                           libime::PinyinDictFormat::Binary);
    } else {
        ime_->dict()->load(libime::PinyinDictionary::SystemDict,
                           LIBIME_INSTALL_PKGDATADIR "/sc.dict",
                           libime::PinyinDictFormat::Binary);
    }
    prediction_.setUserLanguageModel(ime_->model());
    prediction_.setPinyinDictionary(ime_->dict());

    do {
        auto file =
            standardPath.open(StandardPathsType::PkgData, "pinyin/user.dict");

        if (!file.isValid()) {
            break;
        }

        try {
            IFDStreamBuf buffer(file.fd());
            std::istream in(&buffer);
            ime_->dict()->load(libime::PinyinDictionary::UserDict, in,
                               libime::PinyinDictFormat::Binary);
        } catch (const std::exception &e) {
            PINYIN_ERROR() << "Failed to load pinyin dict: " << e.what();
        }
    } while (0);
    do {
        auto file =
            standardPath.open(StandardPathsType::PkgData, "pinyin/user.history",
                              StandardPathsMode::User);

        try {
            IFDStreamBuf buffer(file.fd());
            std::istream in(&buffer);
            ime_->model()->load(in);
        } catch (const std::exception &e) {
            PINYIN_ERROR() << "Failed to load pinyin history: " << e.what();
        }
    } while (0);

    ime_->setScoreFilter(1);
    loadBuiltInDict();
    reloadConfig();
    loadExtraDict();
    loadCustomPhrase();
    instance_->inputContextManager().registerProperty("pinyinState", &factory_);
    KeySym syms[] = {
        FcitxKey_1, FcitxKey_2, FcitxKey_3, FcitxKey_4, FcitxKey_5,
        FcitxKey_6, FcitxKey_7, FcitxKey_8, FcitxKey_9, FcitxKey_0,
    };

    KeyStates states;
    for (auto sym : syms) {
        selectionKeys_.emplace_back(sym, states);
    }

    KeySym numpadsyms[] = {
        FcitxKey_KP_1, FcitxKey_KP_2, FcitxKey_KP_3, FcitxKey_KP_4,
        FcitxKey_KP_5, FcitxKey_KP_6, FcitxKey_KP_7, FcitxKey_KP_8,
        FcitxKey_KP_9, FcitxKey_KP_0,
    };

    KeyStates numpadStates;
    for (auto sym : numpadsyms) {
        numpadSelectionKeys_.emplace_back(sym, numpadStates);
    }

    predictionAction_.setShortText(*config_.predictionEnabled
                                       ? _("Prediction Enabled")
                                       : _("Prediction Disabled"));
    predictionAction_.setLongText(_("Show prediction words"));
    predictionAction_.setIcon(*config_.predictionEnabled
                                  ? "fcitx-remind-active"
                                  : "fcitx-remind-inactive");
    predictionAction_.connect<SimpleAction::Activated>(
        [this](InputContext *ic) {
            config_.predictionEnabled.setValue(!(*config_.predictionEnabled));
            predictionAction_.setShortText(*config_.predictionEnabled
                                               ? _("Prediction Enabled")
                                               : _("Prediction Disabled"));
            predictionAction_.setIcon(*config_.predictionEnabled
                                          ? "fcitx-remind-active"
                                          : "fcitx-remind-inactive");
            predictionAction_.update(ic);
        });
    instance_->userInterfaceManager().registerAction("pinyin-prediction",
                                                     &predictionAction_);
    event_ = instance_->watchEvent(
        EventType::InputContextKeyEvent, EventWatcherPhase::PreInputMethod,
        [this](Event &event) {
            auto &keyEvent = static_cast<KeyEvent &>(event);
            auto *inputContext = keyEvent.inputContext();
            const auto *entry = instance_->inputMethodEntry(inputContext);
            if (!entry || entry->addon() != "pinyin") {
                return;
            }
            handle2nd3rdSelection(keyEvent);
        });

    pyConfig_.shuangpinProfile.annotation().setHidden(true);
    pyConfig_.showShuangpinMode.annotation().setHidden(true);
    pyConfig_.fuzzyConfig->partialSp.annotation().setHidden(true);

    deferredPreload_ = instance_->eventLoop().addDeferEvent([this](
                                                                EventSource *) {
        bool hasCloudPinyin = cloudpinyin() != nullptr;
        for (auto *configPtr : {&config_, &pyConfig_}) {
            configPtr->cloudPinyinEnabled.annotation().setHidden(
                !hasCloudPinyin);
            configPtr->cloudPinyinIndex.annotation().setHidden(!hasCloudPinyin);
            configPtr->cloudPinyinAnimation.annotation().setHidden(
                !hasCloudPinyin);
            configPtr->keepCloudPinyinPlaceHolder.annotation().setHidden(
                !hasCloudPinyin);
            configPtr->cloudpinyin.setHidden(!hasCloudPinyin);
        }
        deferredPreload_.reset();
        return true;
    });
}

PinyinEngine::~PinyinEngine() {}

void PinyinEngine::loadSymbols(const UnixFD &file) {
    if (!file.isValid()) {
        return;
    }
    IFDStreamBuf buffer(file.fd());
    std::istream in(&buffer);
    try {
        PINYIN_DEBUG() << "Loading symbol dict.";
        symbols_.load(in);
    } catch (const std::exception &e) {
        PINYIN_ERROR() << "Failed to load symbol dict: " << e.what();
    }
}

void PinyinEngine::loadDict(const std::string &fullPath,
                            std::list<std::unique_ptr<TaskToken>> &taskTokens) {
    if (fullPath.empty()) {
        return;
    }
    ime_->dict()->addEmptyDict();
    PINYIN_DEBUG() << "Loading pinyin dict " << fullPath;
    std::packaged_task<libime::PinyinDictionary::TrieType()> task([fullPath]() {
        std::ifstream in(fullPath, std::ios::in | std::ios::binary);
        auto trie = libime::PinyinDictionary::load(
            in, libime::PinyinDictFormat::Binary);
        return trie;
    });
    taskTokens.push_back(worker_.addTask(
        std::move(task),
        [this, index = ime_->dict()->dictSize() - 1, fullPath](
            std::shared_future<libime::PinyinDictionary::TrieType> &future) {
            try {
                PINYIN_DEBUG()
                    << "Load pinyin dict " << fullPath << " finished.";
                ime_->dict()->setTrie(index, future.get());
            } catch (const std::exception &e) {
                PINYIN_ERROR() << "Failed to load pinyin dict " << fullPath
                               << ": " << e.what();
            }
        }));
}

void PinyinEngine::loadBuiltInDict() {
    const auto &standardPath = StandardPaths::global();
    {
        auto file =
            standardPath.open(StandardPathsType::PkgData, "pinyin/symbols");
        loadSymbols(file);
    }
    {
        auto file = standardPath.locate(StandardPathsType::PkgData,
                                        "pinyin/chaizi.dict");
        loadDict(file, persistentTask_);
    }
    {
        auto file =
            standardPath.locate(StandardPathsType::Data, "libime/extb.dict");
        // Try again with absolute libime path.
        if (file.empty()) {
            file = standardPath.locate(StandardPathsType::Data,
                                       LIBIME_INSTALL_PKGDATADIR "/extb.dict");
        }
        loadDict(file, persistentTask_);
    }
    if (ime_->dict()->dictSize() !=
        libime::TrieDictionary::UserDict + 1 + NumBuiltInDict) {
        throw std::runtime_error("Failed to load built-in dictionary");
    }
}

void PinyinEngine::loadExtraDict() {
    const auto &standardPath = StandardPaths::global();
    auto files =
        standardPath.locate(StandardPathsType::PkgData, "pinyin/dictionaries",
                            pathfilter::extension(".dict"));
    std::unordered_set<std::filesystem::path> disableFilesSet;
    for (const auto &item :
         standardPath.locate(StandardPathsType::PkgData, "pinyin/dictionaries",
                             pathfilter::extension(".disable")) |
             std::views::transform(
                 [](const auto &item) { return item.first.stem(); })) {
        disableFilesSet.insert(item);
    }
    FCITX_ASSERT(ime_->dict()->dictSize() >=
                 libime::TrieDictionary::UserDict + NumBuiltInDict + 1)
        << "Dict size: " << ime_->dict()->dictSize();
    tasks_.clear();
    ime_->dict()->removeFrom(libime::TrieDictionary::UserDict + NumBuiltInDict +
                             1);
    for (auto &file : files) {
        if (disableFilesSet.contains(file.first)) {
            PINYIN_DEBUG() << "Dictionary: " << file.first << " is disabled.";
            continue;
        }
        PINYIN_DEBUG() << "Loading extra dictionary: " << file.first;
        loadDict(file.second, tasks_);
    }
}

void PinyinEngine::loadCustomPhrase() {
    const auto &standardPath = StandardPaths::global();
    auto file =
        standardPath.open(StandardPathsType::PkgData, "pinyin/customphrase");
    if (!file.isValid()) {
        customPhrase_.clear();
        return;
    }

    try {
        IFDStreamBuf buffer(file.fd());
        std::istream in(&buffer);
        customPhrase_.load(in, true);
    } catch (const std::exception &e) {
        PINYIN_ERROR() << e.what();
    }
}

void PinyinEngine::populateConfig() {
    if (*config_.firstRun) {
        config_.firstRun.setValue(false);
        safeSaveAsIni(config_, "conf/pinyin.conf");
        deferEvent_ = instance_->eventLoop().addDeferEvent([this](
                                                               EventSource *) {
            if (cloudpinyin() && !*config_.cloudPinyinEnabled &&
                notifications()) {
                const auto &key =
                    cloudpinyin()->call<ICloudPinyin::toggleKey>();

                std::string msg;
                if (key.empty()) {
                    msg = _("Do you want to enable cloudpinyin now for better "
                            "prediction? You can always toggle it later in "
                            "configuration.");
                } else {
                    msg = _(
                        "Do you want to enable cloudpinyin now for better "
                        "prediction? You can always toggle it later in "
                        "configuration or by pressing {}.",
                        Key::keyListToString(key, KeyStringFormat::Localized));
                }
                std::vector<std::string> actions = {"yes", _("Yes"), "no",
                                                    _("No")};

                notifications()->call<INotifications::sendNotification>(
                    _("Pinyin"), 0, "fcitx-pinyin", _("Enable Cloudpinyin"),
                    msg, actions, -1,
                    [this](const std::string &action) {
                        if (action == "yes") {
                            for (auto *configPtr : {&config_, &pyConfig_}) {
                                configPtr->cloudPinyinEnabled.setValue(true);
                            }
                            save();
                        }
                    },
                    nullptr);
            }
            deferEvent_.reset();
            return true;
        });
    }
    ime_->setNBest(*config_.nbest);
    ime_->setWordCandidateLimit(*config_.wordCandidateLimit);
    ime_->setPartialLongWordLimit(*config_.longWordLimit);
    ime_->setPreeditMode(*config_.showActualPinyinInPreedit
                             ? libime::PinyinPreeditMode::Pinyin
                             : libime::PinyinPreeditMode::RawText);

    // Setup fuzzy flags.
    libime::PinyinFuzzyFlags flags;
    const auto &fuzzyConfig = *config_.fuzzyConfig;
#define SET_FUZZY_FLAG(VAR, ENUM)                                              \
    if (*fuzzyConfig.VAR) {                                                    \
        flags |= libime::PinyinFuzzyFlag::ENUM;                                \
    }
    SET_FUZZY_FLAG(ue, VE_UE)
    SET_FUZZY_FLAG(commonTypo, CommonTypo)
    SET_FUZZY_FLAG(commonTypo, AdvancedTypo)
    SET_FUZZY_FLAG(lowerCaseMatchLetter, Letter)
    SET_FUZZY_FLAG(inner, Inner)
    SET_FUZZY_FLAG(innerShort, InnerShort)
    SET_FUZZY_FLAG(partialFinal, PartialFinal)
    SET_FUZZY_FLAG(partialSp, PartialSp)
    SET_FUZZY_FLAG(v, V_U)
    SET_FUZZY_FLAG(an, AN_ANG)
    SET_FUZZY_FLAG(en, EN_ENG)
    SET_FUZZY_FLAG(ian, IAN_IANG)
    SET_FUZZY_FLAG(in, IN_ING)
    SET_FUZZY_FLAG(ou, U_OU)
    SET_FUZZY_FLAG(uan, UAN_UANG)
    SET_FUZZY_FLAG(c, C_CH)
    SET_FUZZY_FLAG(f, F_H)
    SET_FUZZY_FLAG(l, L_N)
    SET_FUZZY_FLAG(r, L_R)
    SET_FUZZY_FLAG(s, S_SH)
    SET_FUZZY_FLAG(z, Z_ZH)

    {
        std::shared_ptr<libime::PinyinCorrectionProfile> correctionProfile;
        switch (*fuzzyConfig.correction) {
        case CorrectionLayout::None:
            break;
        case CorrectionLayout::Qwerty:
            correctionProfile =
                std::make_shared<libime::PinyinCorrectionProfile>(
                    libime::BuiltinPinyinCorrectionProfile::Qwerty);
            break;
        }
        ime_->setCorrectionProfile(std::move(correctionProfile));
    }

    if (ime_->correctionProfile()) {
        flags |= libime::PinyinFuzzyFlag::Correction;
    }
    ime_->setFuzzyFlags(flags);

    if (*config_.shuangpinProfile == ShuangpinProfileEnum::Custom) {
        auto file = StandardPaths::global().open(StandardPathsType::PkgConfig,
                                                 "pinyin/sp.dat");
        if (file.isValid()) {
            try {
                IFDStreamBuf buffer(file.fd());
                std::istream in(&buffer);
                ime_->setShuangpinProfile(
                    std::make_shared<libime::ShuangpinProfile>(
                        in, ime_->correctionProfile().get()));
            } catch (const std::exception &e) {
                PINYIN_ERROR() << e.what();
            }
        } else {
            PINYIN_ERROR() << "Failed to open shuangpin profile.";
        }
    } else {
        libime::ShuangpinBuiltinProfile profile;
#define TRANS_SP_PROFILE(PROFILE)                                              \
    case ShuangpinProfileEnum::PROFILE:                                        \
        profile = libime::ShuangpinBuiltinProfile::PROFILE;                    \
        break;
        switch (*config_.shuangpinProfile) {
            TRANS_SP_PROFILE(Ziranma)
            TRANS_SP_PROFILE(MS)
            TRANS_SP_PROFILE(Ziguang)
            TRANS_SP_PROFILE(ABC)
            TRANS_SP_PROFILE(Zhongwenzhixing)
            TRANS_SP_PROFILE(PinyinJiajia)
            TRANS_SP_PROFILE(Xiaohe)
            TRANS_SP_PROFILE(GB)
        default:
            profile = libime::ShuangpinBuiltinProfile::Ziranma;
            break;
        }
        ime_->setShuangpinProfile(std::make_shared<libime::ShuangpinProfile>(
            profile, ime_->correctionProfile().get()));
    }

    // Always set a profile to avoid crash.
    if (!ime_->shuangpinProfile()) {
        ime_->setShuangpinProfile(std::make_shared<libime::ShuangpinProfile>(
            libime::ShuangpinBuiltinProfile::Ziranma,
            ime_->correctionProfile().get()));
    }

    quickphraseTriggerRegex_.clear();
    for (const std::string &regStr : *config_.quickphraseTriggerRegex) {
        if (regStr.empty()) {
            continue;
        }
        try {
            std::regex reg{regStr};
            quickphraseTriggerRegex_.push_back(std::move(reg));
        } catch (const std::exception &e) {
            PINYIN_DEBUG() << "Invalid regular expression: \"" << regStr
                           << "\", " << e.what();
        }
    }
    PINYIN_DEBUG() << "Quick Phrase Trigger Regex size: "
                   << quickphraseTriggerRegex_.size();

    ime_->dict()->setFlags(libime::TrieDictionary::UserDict + 1,
                           *config_.chaiziEnabled
                               ? libime::PinyinDictFlag::FullMatch
                               : libime::PinyinDictFlag::Disabled);
    ime_->dict()->setFlags(libime::TrieDictionary::UserDict + 2,
                           *config_.extBEnabled
                               ? libime::PinyinDictFlag::NoFlag
                               : libime::PinyinDictFlag::Disabled);

    pyConfig_ = config_;
}

void PinyinEngine::reloadConfig() {
    PINYIN_DEBUG() << "Reload pinyin config.";
    readAsIni(config_, "conf/pinyin.conf");
    populateConfig();
}
void PinyinEngine::activate(const fcitx::InputMethodEntry &entry,
                            fcitx::InputContextEvent &event) {
    auto *inputContext = event.inputContext();
    // Request addons that gonna be used but not listed in optional
    // dependencies.
    fullwidth();
    chttrans();
    if (*config_.spellEnabled) {
        spell();
    }
    if (pinyinhelper()) {
        // Preload stroke data, since we gonna use it anyway.
        pinyinhelper()->call<IPinyinHelper::loadStroke>();
    }
    for (const auto *actionName : {"chttrans", "punctuation", "fullwidth"}) {
        if (auto *action =
                instance_->userInterfaceManager().lookupAction(actionName)) {
            inputContext->statusArea().addAction(StatusGroup::InputMethod,
                                                 action);
        }
    }
    inputContext->statusArea().addAction(StatusGroup::InputMethod,
                                         &predictionAction_);
    auto *state = inputContext->propertyFor(&factory_);
    state->context_.setUseShuangpin(entry.uniqueName() == "shuangpin");
    // TODO: use surrouding to re-build context.
    state->context_.clearContextWords();
}

void PinyinEngine::deactivate(const fcitx::InputMethodEntry &entry,
                              fcitx::InputContextEvent &event) {
    auto *inputContext = event.inputContext();
    do {
        if (event.type() != EventType::InputContextSwitchInputMethod) {
            break;
        }
        auto *state = inputContext->propertyFor(&factory_);
        if (state->mode_ == PinyinMode::Punctuation) {
            auto candidateList = inputContext->inputPanel().candidateList();
            if (!candidateList) {
                break;
            }
            auto index = candidateList->cursorIndex();
            if (index >= 0) {
                candidateList->candidate(index).select(inputContext);
            }
            break;
        }

        if (state->context_.empty()) {
            break;
        }

        switch (*config_.switchInputMethodBehavior) {
        case SwitchInputMethodBehavior::CommitPreedit:
            inputContext->commitString(preeditCommitString(inputContext));
            break;
        case SwitchInputMethodBehavior::CommitDefault: {
            inputContext->commitString(state->context_.sentence());
            break;
        }
        case SwitchInputMethodBehavior::Clear:
            break;
        }
    } while (0);
    reset(entry, event);
}

bool PinyinEngine::handlePuncCandidate(KeyEvent &event) {
    auto *inputContext = event.inputContext();
    auto *state = inputContext->propertyFor(&factory_);
    if (state->mode_ != PinyinMode::Punctuation) {
        return false;
    }
    auto candidateList = inputContext->inputPanel().candidateList();
    if (!candidateList) {
        doReset(inputContext);
        return false;
    }
    if (event.key().check(FcitxKey_BackSpace)) {
        event.filterAndAccept();
        doReset(inputContext);
        return true;
    }
    if (!event.isVirtual()) {
        int idx = event.key().keyListIndex(selectionKeys_);
        if (idx == -1 && *config_.useKeypadAsSelectionKey) {
            idx = event.key().keyListIndex(numpadSelectionKeys_);
        }
        if (idx >= 0) {
            event.filterAndAccept();
            if (idx < candidateList->size()) {
                candidateList->candidate(idx).select(inputContext);
            }
            return true;
        }

        if (auto *movable = candidateList->toCursorMovable()) {
            if (event.key().checkKeyList(*config_.nextCandidate)) {
                movable->nextCandidate();
                updatePuncPreedit(inputContext);
                inputContext->updateUserInterface(
                    UserInterfaceComponent::InputPanel);
                event.filterAndAccept();
                return true;
            }
            if (event.key().checkKeyList(*config_.prevCandidate)) {
                movable->prevCandidate();
                updatePuncPreedit(inputContext);
                inputContext->updateUserInterface(
                    UserInterfaceComponent::InputPanel);
                event.filterAndAccept();
                return true;
            }
        }
    }

    auto index = candidateList->cursorIndex();
    if (index >= 0) {
        candidateList->candidate(index).select(inputContext);
    }

    doReset(inputContext);
    return false;
}

bool PinyinEngine::handleCloudpinyinTrigger(KeyEvent &event) {
    if (cloudpinyin() && event.key().checkKeyList(
                             cloudpinyin()->call<ICloudPinyin::toggleKey>())) {
        config_.cloudPinyinEnabled.setValue(!*config_.cloudPinyinEnabled);
        safeSaveAsIni(config_, "conf/pinyin.conf");

        if (notifications()) {
            notifications()->call<INotifications::showTip>(
                "fcitx-cloudpinyin-toggle", _("Pinyin"), "",
                _("Cloud Pinyin Status"),
                *config_.cloudPinyinEnabled ? _("Cloud Pinyin is enabled.")
                                            : _("Cloud Pinyin is disabled."),
                1000);
        }
        if (*config_.cloudPinyinEnabled) {
            cloudpinyin()->call<ICloudPinyin::resetError>();
        }
        event.filterAndAccept();
        return true;
    }
    return false;
}

bool PinyinEngine::handle2nd3rdSelection(KeyEvent &event) {
    auto *inputContext = event.inputContext();
    auto candidateList = inputContext->inputPanel().candidateList();
    if (!candidateList) {
        return false;
    }

    struct {
        const KeyList &list;
        int selection;
    } keyHandlers[] = {
        // Index starts with 0
        {.list = *config_.secondCandidate, .selection = 1},
        {.list = *config_.thirdCandidate, .selection = 2},
    };

    auto *state = inputContext->propertyFor(&factory_);
    int keyReleased = state->keyReleased_;
    int keyReleasedIndex = state->keyReleasedIndex_;
    // Keep these two values, and reset them in the state
    state->keyReleased_ = -1;
    state->keyReleasedIndex_ = -2;
    const bool isModifier = event.key().isModifier();
    if (event.isRelease()) {
        int idx = 0;
        for (auto &keyHandler : keyHandlers) {
            if (keyReleased == idx &&
                keyReleasedIndex == event.key().keyListIndex(keyHandler.list)) {
                if (isModifier) {
                    if (instance_->globalConfig().checkModifierOnlyKeyTimeout(
                            state->lastKeyPressedTime_) &&
                        keyHandler.selection < candidateList->size()) {
                        candidateList->candidate(keyHandler.selection)
                            .select(inputContext);
                    }
                    state->lastKeyPressedTime_ = 0;
                    event.filterAndAccept();
                    return true;
                }
                event.filter();
                return true;
            }
            idx++;
        }
    }

    if (!event.filtered() && !event.isRelease()) {
        int idx = 0;
        for (auto &keyHandler : keyHandlers) {
            auto keyIdx = event.key().keyListIndex(keyHandler.list);
            if (keyIdx >= 0) {
                state->keyReleased_ = idx;
                state->keyReleasedIndex_ = keyIdx;
                if (isModifier) {
                    state->lastKeyPressedTime_ = now(CLOCK_MONOTONIC);
                    // don't forward to input method, but make it pass
                    // through to client.
                    event.filter();
                    return true;
                }
                if (keyHandler.selection < candidateList->size()) {
                    candidateList->candidate(keyHandler.selection)
                        .select(inputContext);
                }
                event.filterAndAccept();
                return true;
            }
            idx++;
        }
    }
    return false;
}

bool PinyinEngine::handleCandidateList(
    KeyEvent &event, const std::shared_future<uint32_t> &keyChr) {
    auto *inputContext = event.inputContext();
    auto candidateList = inputContext->inputPanel().candidateList();
    if (!candidateList) {
        return false;
    }
    auto *state = inputContext->propertyFor(&factory_);
    if (event.key().checkKeyList(*config_.currentCandidate) &&
        !state->predictWords_) {
        if (!candidateList->empty()) {
            event.filterAndAccept();
            int idx = std::max(candidateList->cursorIndex(), 0);
            candidateList->candidate(idx).select(inputContext);
            return true;
        }
    }

    if (event.isVirtual()) {
        return false;
    }

    int idx = event.key().keyListIndex(selectionKeys_);
    if (idx == -1 && *config_.useKeypadAsSelectionKey) {
        idx = event.key().keyListIndex(numpadSelectionKeys_);
    }
    if (idx >= 0) {
        event.filterAndAccept();
        if (idx < candidateList->size()) {
            candidateList->candidate(idx).select(inputContext);
        }
        return true;
    }

    if (event.key().checkKeyList(*config_.prevPage)) {
        auto *pageable = candidateList->toPageable();
        if (!pageable->hasPrev()) {
            if (pageable->usedNextBefore()) {
                event.filterAndAccept();
                return true;
            }
            // Only let key go through if it can reach handlePunc.
            auto c = keyChr.get();
            if (event.key().hasModifier() || !c) {
                event.filterAndAccept();
                return true;
            }
        } else {
            event.filterAndAccept();
            pageable->prev();
            inputContext->updateUserInterface(
                UserInterfaceComponent::InputPanel);
            return true;
        }
    }

    if (handleNextPage(event)) {
        return true;
    }

    if (auto *movable = candidateList->toCursorMovable()) {
        if (event.key().checkKeyList(*config_.nextCandidate)) {
            movable->nextCandidate();
            inputContext->updateUserInterface(
                UserInterfaceComponent::InputPanel);
            event.filterAndAccept();
            return true;
        }
        if (event.key().checkKeyList(*config_.prevCandidate)) {
            movable->prevCandidate();
            inputContext->updateUserInterface(
                UserInterfaceComponent::InputPanel);
            event.filterAndAccept();
            return true;
        }
    }
    return false;
}

bool PinyinEngine::handleNextPage(KeyEvent &event) const {
    auto *inputContext = event.inputContext();
    auto candidateList = inputContext->inputPanel().candidateList();
    if (event.key().checkKeyList(*config_.nextPage)) {
        event.filterAndAccept();
        candidateList->toPageable()->next();
        inputContext->updateUserInterface(UserInterfaceComponent::InputPanel);
        return true;
    }
    return false;
}

void PinyinEngine::updateFilter(InputContext *inputContext) {
    auto &inputPanel = inputContext->inputPanel();
    auto *pinyinTabbed = currentPinyinTabbed(inputContext);

    updatePreedit(inputContext);
    Text aux;
    if (pinyinTabbed && pinyinTabbed->inStrokeFilterMode() && pinyinhelper()) {
        aux.append(_("[Stroke Filtering]"));
        aux.append(pinyinhelper()->call<IPinyinHelper::prettyStrokeString>(
            pinyinTabbed->strokeBuffer()));
    }
    inputPanel.setAuxUp(aux);
    inputPanel.setAuxDown(Text());

    auto *candidateList =
        dynamic_cast<CommonCandidateList *>(inputPanel.candidateList().get());
    if (candidateList) {
        if (!pinyinTabbed || !pinyinTabbed->hasFilter()) {
            candidateList->clearFilter();
        } else {
            candidateList->setFilter(
                [pinyinTabbed](const CandidateWord &candidate) -> bool {
                    return pinyinTabbed->filter(candidate);
                });
        }

        if (candidateList->totalPages() > 0) {
            candidateList->setPage(0);
        }
    }
    inputContext->updatePreedit();
    inputContext->updateUserInterface(UserInterfaceComponent::InputPanel);
}

void PinyinEngine::updateForgetCandidate(InputContext *inputContext) {
    auto *state = inputContext->propertyFor(&factory_);
    auto &inputPanel = inputContext->inputPanel();
    inputPanel.reset();

    updatePreedit(inputContext);
    Text aux(_("[Select the word to remove from history]"));
    inputPanel.setAuxUp(aux);

    auto candidateList = std::make_unique<CommonCandidateList>();
    candidateList->setPageSize(*config_.pageSize);
    candidateList->setCursorPositionAfterPaging(
        CursorPositionAfterPaging::ResetToFirst);

    auto *origCandidateList = state->forgetCandidateList_->toBulk();
    for (int i = 0; i < origCandidateList->totalSize(); i++) {
        const auto &candidate = origCandidateList->candidateFromAll(i);
        if (const auto *pyCandidate =
                dynamic_cast<const PinyinCandidateWord *>(&candidate)) {
            if (static_cast<size_t>(pyCandidate->candidateIndex()) >=
                    state->context_.candidatesToCursor().size() ||
                state->context_
                    .candidateFullPinyin(state->context_.candidatesToCursor()
                                             [pyCandidate->candidateIndex()])
                    .empty()) {
                continue;
            }

            candidateList->append<ForgetCandidateWord>(
                this, pyCandidate->text(), pyCandidate->candidateIndex());
        }
    }
    candidateList->setSelectionKey(selectionKeys_);
    if (!candidateList->empty()) {
        candidateList->setGlobalCursorIndex(0);
    }
    inputContext->inputPanel().setCandidateList(std::move(candidateList));
    inputContext->updatePreedit();
    inputContext->updateUserInterface(UserInterfaceComponent::InputPanel);
}

void PinyinEngine::resetForgetCandidate(InputContext *inputContext) const {
    auto *state = inputContext->propertyFor(&factory_);
    state->forgetCandidateList_.reset();
    if (state->mode_ == PinyinMode::ForgetCandidate) {
        state->mode_ = PinyinMode::Normal;
    }
}

void PinyinEngine::forgetCandidate(InputContext *inputContext, size_t index) {
    auto *state = inputContext->propertyFor(&factory_);

    const std::string currentInput = state->context_.userInput();

    if (index < state->context_.candidatesToCursor().size()) {
        const auto &sentence = state->context_.candidatesToCursor()[index];
        // If this is a word, remove it from user dict.
        if (sentence.size() == 1) {
            auto py = state->context_.candidateFullPinyin(index);
            state->context_.ime()->dict()->removeWord(
                libime::PinyinDictionary::UserDict, py, sentence.toString());
        }
        for (const auto &word : sentence.sentence()) {
            state->context_.ime()->model()->history().forget(word->word());
        }
    }
    resetForgetCandidate(inputContext);
    doReset(inputContext);

    state->context_.type(currentInput);
    updateUI(inputContext);
}

void PinyinEngine::saveCustomPhrase() {
    instance_->eventDispatcher().scheduleWithContext(watch(), [this]() {
        StandardPaths::global().safeSave(
            StandardPathsType::PkgData, "pinyin/customphrase", [this](int fd) {
                OFDStreamBuf buffer(fd);
                std::ostream out(&buffer);
                try {
                    customPhrase_.save(out);
                    return static_cast<bool>(out);
                } catch (const std::exception &e) {
                    PINYIN_ERROR()
                        << "Failed to save custom phrase: " << e.what();
                    return false;
                }
            });
    });
}

void PinyinEngine::pinCustomPhrase(InputContext *inputContext,
                                   const std::string &customPhrase) {
    auto *state = inputContext->propertyFor(&factory_);
    auto &context = state->context_;
    // Precompute some values.
    const auto selectedLength = context.selectedLength();
    const auto pyLength = context.cursor() > selectedLength
                              ? context.cursor() - selectedLength
                              : std::string::npos;
    const auto py = context.userInput().substr(selectedLength, pyLength);
    customPhrase_.pinPhrase(py, customPhrase);

    updateUI(inputContext);
    saveCustomPhrase();
}

void PinyinEngine::deleteCustomPhrase(InputContext *inputContext,
                                      const std::string &customPhrase) {
    auto *state = inputContext->propertyFor(&factory_);
    auto &context = state->context_;
    // Precompute some values.
    const auto selectedLength = context.selectedLength();
    const auto pyLength = context.cursor() > selectedLength
                              ? context.cursor() - selectedLength
                              : std::string::npos;
    const auto py = context.userInput().substr(selectedLength, pyLength);
    customPhrase_.removePhrase(py, customPhrase);

    updateUI(inputContext);
    saveCustomPhrase();
}

bool PinyinEngine::handleStrokeFilter(
    KeyEvent &event, const std::shared_future<uint32_t> &keyChr) {
    auto *inputContext = event.inputContext();
    auto candidateList = inputContext->inputPanel().candidateList();
    auto *state = inputContext->propertyFor(&factory_);
    auto *pinyinTabbed = currentPinyinTabbed(inputContext);

    // Any candidate list without PinyinTabbedCandidateList will not be able to
    // enter stroke filter mode.
    if (!pinyinTabbed) {
        return false;
    }

    // Stroke relies on PinyinHelper.
    if (!pinyinhelper()) {
        return false;
    }

    // Only available in normal mode.
    if (state->mode_ != PinyinMode::Normal) {
        return false;
    }

    if (!pinyinTabbed->inStrokeFilterMode()) {
        assert(candidateList);
        if (!candidateList->empty() && candidateList->toBulk() &&
            event.key().checkKeyList(*config_.selectByStroke)) {
            pinyinTabbed->setStrokeFilterMode();
            updateFilter(inputContext);
            handleNextPage(event);

            event.filterAndAccept();
            return true;
        }
        return false;
    }

    // If we are still not in stroke filter mode, return.
    if (!pinyinTabbed->inStrokeFilterMode()) {
        return false;
    }

    event.filterAndAccept();
    // A special case that allow prev page to quit stroke filtering.
    if ((pinyinTabbed->strokeBuffer().empty() &&
         event.key().checkKeyList(*config_.prevPage))) {
        auto candidateList = inputContext->inputPanel().candidateList();
        if (candidateList && candidateList->toPageable() &&
            candidateList->toPageable()->currentPage() <= 1) {
            pinyinTabbed->resetStrokeFilterMode();
            return true;
        }
    }

    if (handleCandidateList(event, keyChr)) {
        return true;
    }
    // Skip all key combination.
    if (event.key().states().testAny(KeyState::SimpleMask)) {
        return true;
    }

    if (event.key().check(FcitxKey_Escape)) {
        pinyinTabbed->resetStrokeFilterMode();
        return true;
    }
    if (event.key().check(FcitxKey_BackSpace)) {
        // Do backspace is stroke is not empty.
        if (!pinyinTabbed->popStroke()) {
            // Exit stroke mode when stroke buffer is empty.
            updateUI(inputContext);
        }
        return true;
    }
    // if it gonna commit something
    auto c = keyChr.get();
    if (!c) {
        return true;
    }

    if (event.key().check(FcitxKey_h) || event.key().check(FcitxKey_p) ||
        event.key().check(FcitxKey_s) || event.key().check(FcitxKey_n) ||
        event.key().check(FcitxKey_z)) {
        static const std::unordered_map<FcitxKeySym, char> strokeMap{
            {FcitxKey_h, '1'},
            {FcitxKey_s, '2'},
            {FcitxKey_p, '3'},
            {FcitxKey_n, '4'},
            {FcitxKey_z, '5'}};
        if (auto iter = strokeMap.find(event.key().sym());
            iter != strokeMap.end()) {
            pinyinTabbed->pushStroke(iter->second);
        }
    }

    return true;
}

bool PinyinEngine::handleForgetCandidate(KeyEvent &event) {
    auto *inputContext = event.inputContext();
    auto candidateList = inputContext->inputPanel().candidateList();
    auto *state = inputContext->propertyFor(&factory_);
    if (state->mode_ == PinyinMode::Normal) {
        if (!state->predictWords_ && candidateList && !candidateList->empty() &&
            candidateList->toBulk() &&
            event.key().checkKeyList(*config_.forgetWord)) {
            resetForgetCandidate(inputContext);
            state->forgetCandidateList_ = std::move(candidateList);
            state->mode_ = PinyinMode::ForgetCandidate;
            updateForgetCandidate(inputContext);
            event.filterAndAccept();
            return true;
        }
        return false;
    }

    if (state->mode_ != PinyinMode::ForgetCandidate) {
        return false;
    }

    event.filterAndAccept();
    // Skip all key combination.
    if (event.key().states().testAny(KeyState::SimpleMask)) {
        return true;
    }

    if (event.key().check(FcitxKey_Escape)) {
        resetForgetCandidate(inputContext);
        updateUI(inputContext);
        return true;
    }
    return true;
}

bool PinyinEngine::handlePunc(KeyEvent &event,
                              const std::shared_future<uint32_t> &keyChr) {
    auto *inputContext = event.inputContext();
    auto candidateList = inputContext->inputPanel().candidateList();
    auto *state = inputContext->propertyFor(&factory_);
    if (event.filtered()) {
        return false;
    }
    // if it gonna commit something
    auto c = keyChr.get();
    if (event.key().hasModifier() || !c) {
        // Handle quick phrase with modifier
        if (event.key().check(*config_.quickphraseKey) && quickphrase()) {
            quickphrase()->call<IQuickPhrase::trigger>(inputContext, "", "", "",
                                                       "", Key());
            event.filterAndAccept();
            return true;
        }
        return false;
    }
    if (candidateList && !candidateList->empty()) {
        int idx = std::max(candidateList->cursorIndex(), 0);
        candidateList->candidate(idx).select(inputContext);
    }

    // We can't emit punctuation right now, since it's not yet fully selected.
    // There is an option to split the current input, however, it may not be
    // sane to do so based on the partial input and cursor location. E.g.
    // current input is n|i hao, and you press ",". Candidate will be filtered
    // until "ni", instead of "n". which means you are not inserting "," between
    // n & i. Thus, let's just make it select the default candidate.
    // Also, it can not work with punctuation candidate, or type punc in pair.
    if (!state->context_.empty()) {
        event.filterAndAccept();
        return true;
    }
    // It's a key not handled by engine, just clear the word context.
    // Need to do after candidate selection, so the context is properly reset.
    state->context_.clearContextWords();

    std::string punc;
    std::string puncAfter;
    // skip key pad
    if (c && !event.key().isKeyPad()) {
        auto candidates =
            punctuation()->call<IPunctuation::getPunctuationCandidates>("zh_CN",
                                                                        c);
        auto pushResult = punctuation()->call<IPunctuation::pushPunctuationV2>(
            "zh_CN", inputContext, c);
        if (candidates.size() == 1) {
            std::tie(punc, puncAfter) = pushResult;
        } else if (candidates.size() > 1) {
            updatePuncCandidate(inputContext, utf8::UCS4ToUTF8(c), candidates);
            event.filterAndAccept();
            return true;
        }
    }
    if (!event.isVirtual() && event.key().check(*config_.quickphraseKey) &&
        quickphrase()) {
        auto keyString = utf8::UCS4ToUTF8(c);
        // s is punc or key
        auto output = !punc.empty() ? (punc + puncAfter) : keyString;
        // alt is key or empty
        auto altOutput = !punc.empty() ? keyString : "";
        // if no punc: key -> key (s = key, alt = empty)
        // if there's punc: key -> punc, return -> key (s = punc, alt =
        // key)
        std::string text;
        if (!output.empty()) {
            if (!altOutput.empty()) {
                text = _("Press {} for {} and Return for {}", keyString, output,
                         altOutput);
            } else {
                text = _("Press {} for {}", keyString, output);
            }
        }
        quickphrase()->call<IQuickPhrase::trigger>(
            inputContext, text, "", output, altOutput, *config_.quickphraseKey);
        event.filterAndAccept();
        return true;
    }
    if (!punc.empty()) {
        event.filterAndAccept();
        auto paired = punc + puncAfter;
        if (inputContext->capabilityFlags().test(
                CapabilityFlag::CommitStringWithCursor)) {
            if (size_t length = utf8::lengthValidated(punc);
                length != 0 && length != utf8::INVALID_LENGTH) {
                inputContext->commitStringWithCursor(paired, length);
            } else {
                inputContext->commitString(paired);
            }
        } else {
            inputContext->commitString(paired);
            if (size_t length = utf8::lengthValidated(puncAfter);
                length != 0 && length != utf8::INVALID_LENGTH) {
                for (size_t i = 0; i < length; i++) {
                    inputContext->forwardKey(Key(FcitxKey_Left));
                }
            }
        }
    }
    state->lastIsPunc_ = true;
    return false;
}

bool PinyinEngine::handleCompose(KeyEvent &event) {
    auto *inputContext = event.inputContext();
    auto *state = inputContext->propertyFor(&factory_);
    if (event.key().states().testAny(
            KeyStates{KeyState::Ctrl, KeyState::Super}) ||
        state->mode_ != PinyinMode::Normal) {
        return false;
    }
    auto candidateList = inputContext->inputPanel().candidateList();
    if (event.filtered()) {
        return false;
    }
    auto compose =
        instance_->processComposeString(inputContext, event.key().sym());
    if (!compose) {
        // invalid compose or in the middle of compose.
        event.filterAndAccept();
        return true;
    }
    // Handle the key just like punc select.
    if (!compose->empty()) {
        // Reset predict in case we are in predict.
        resetPredict(inputContext);
        // punc like auto selection.
        auto candidateList = inputContext->inputPanel().candidateList();
        if (candidateList && !candidateList->empty()) {
            candidateList->candidate(0).select(inputContext);
        }
        inputContext->commitString(*compose);
        event.filterAndAccept();
        return true;
    }
    return false;
}

void PinyinEngine::resetPredict(InputContext *inputContext) {
    auto *state = inputContext->propertyFor(&factory_);
    if (!state->predictWords_) {
        return;
    }
    state->predictWords_.reset();
    inputContext->inputPanel().reset();
    inputContext->updatePreedit();
    inputContext->updateUserInterface(UserInterfaceComponent::InputPanel);
}

void PinyinEngine::keyEvent(const InputMethodEntry &entry, KeyEvent &event) {
    FCITX_UNUSED(entry);
    PINYIN_DEBUG() << "Pinyin receive key: " << event.key() << " "
                   << event.isRelease();
    auto *inputContext = event.inputContext();
    auto *state = inputContext->propertyFor(&factory_);

    std::shared_future<uint32_t> keyChr =
        std::async(std::launch::deferred, [sym = event.key().sym()] {
            return Key::keySymToUnicode(sym);
        }).share();
    std::shared_future<std::string> keyStr =
        std::async(std::launch::deferred, [&keyChr] {
            return utf8::UCS4ToUTF8(keyChr.get());
        }).share();

    // 2nd/3rd selection is allowed to be modifier only, handle them before we
    // skip the release.
    if (handle2nd3rdSelection(event)) {
        return;
    }

    // by pass all key release
    if (event.isRelease()) {
        return;
    }

    // Small hack to debug tabbed candidate on desktop.
#if 0
    KeyList list;
    list.push_back(Key(FcitxKey_1, KeyState::Alt));
    list.push_back(Key(FcitxKey_2, KeyState::Alt));
    list.push_back(Key(FcitxKey_3, KeyState::Alt));
    list.push_back(Key(FcitxKey_4, KeyState::Alt));
    list.push_back(Key(FcitxKey_5, KeyState::Alt));
    list.push_back(Key(FcitxKey_6, KeyState::Alt));
    if (auto candidateList = inputContext->inputPanel().candidateList()) {
        if (auto *tabbed = candidateList->toTabbed()) {
            auto actions = tabbed->tabActions();
            if (auto idx = event.key().keyListIndex(list); idx >= 0 && idx < actions.size()) {
                tabbed->triggerTabAction(actions[idx].id());
                event.filterAndAccept();
                return;
            }
        }
    }
#endif

    if (handlePuncCandidate(event)) {
        return;
    }

    // and by pass all modifier
    if (event.key().isModifier()) {
        return;
    }

    if (handleCloudpinyinTrigger(event)) {
        return;
    }

    auto candidateList = inputContext->inputPanel().candidateList();
    bool lastIsPunc = state->lastIsPunc_;
    state->lastIsPunc_ = false;

    if (handleStrokeFilter(event, keyChr)) {
        return;
    }

    if (handleCompose(event)) {
        return;
    }

    // handle number key selection and prev/next page/candidate.
    if (handleCandidateList(event, keyChr)) {
        return;
    }

    if (handleForgetCandidate(event)) {
        return;
    }

    // In prediction, as long as it's not candidate selection, clear, then
    // fallback
    // to remaining operation.
    if (state->predictWords_) {
        resetPredict(inputContext);
        if (event.key().check(FcitxKey_Escape) ||
            (event.key().check(FcitxKey_BackSpace) &&
             (*config_.backspaceBehaviorOnPrediction ==
                  BackspaceBehaviorOnPrediction::OnlyClearCandidates ||
              (*config_.backspaceBehaviorOnPrediction ==
                   BackspaceBehaviorOnPrediction::
                       BackspaceWhenNotUsingOnScreenKeyboard &&
               event.isVirtual()))) ||
            event.key().check(FcitxKey_Delete)) {
            event.filterAndAccept();
            return;
        }
    }

    auto checkSp = [this, &keyChr](const KeyEvent &event, PinyinState *state) {
        auto shuangpinProfile = ime_->shuangpinProfile();
        if (!state->context_.useShuangpin() || !shuangpinProfile ||
            !event.key().isSimple()) {
            return false;
        }
        // event.key().isSimple() make sure the return value is within range of
        // char.
        char chr = keyChr.get();
        return (!state->context_.empty() &&
                shuangpinProfile->validInput().contains(chr)) ||
               (state->context_.empty() &&
                shuangpinProfile->validInitial().contains(chr));
    };

    if (!event.key().hasModifier() && quickphrase() &&
        !quickphraseTriggerRegex_.empty() && !keyStr.get().empty() &&
        state->context_.selectedLength() == 0 &&
        state->context_.cursor() == state->context_.size()) {
        std::string text =
            stringutils::concat(state->context_.userInput(), keyStr.get());
        for (const auto &reg : quickphraseTriggerRegex_) {
            if (std::regex_search(text, reg,
                                  std::regex_constants::match_default)) {
                // Keep the current state before reset.
                const std::string origin = state->context_.userInput();
                doReset(inputContext);
                quickphrase()->call<IQuickPhrase::trigger>(inputContext, "", "",
                                                           "", "", Key());
                quickphrase()->call<IQuickPhrase::setBufferWithRestoreCallback>(
                    inputContext, text, origin,
                    [this](InputContext *ic, const std::string &origin) {
                        if (this->instance()->inputMethodEngine(ic) == this) {
                            auto *state = ic->propertyFor(&factory_);
                            doReset(ic);
                            state->context_.type(origin);
                            updateUI(ic);
                        }
                    });
                event.filterAndAccept();
                return;
            }
        }
    }

    if (event.key().isLAZ() || event.key().isUAZ() ||
        (event.key().check(FcitxKey_apostrophe) && !state->context_.empty()) ||
        checkSp(event, state)) {
        // first v, use it to trigger quickphrase
        if (*config_.useVAsQuickphrase && quickphrase() &&
            state->context_.empty()) {
            const bool isSp = state->context_.useShuangpin();
            if ((event.key().check(FcitxKey_v) && !isSp) ||
                (event.key().check(FcitxKey_V) && isSp)) {
                const std::string leadingString =
                    Key::keySymToUTF8(event.key().sym());

                quickphrase()->call<IQuickPhrase::trigger>(
                    inputContext, "", leadingString, "", "",
                    Key(FcitxKey_None));
                event.filterAndAccept();
                return;
            }
        }
        event.filterAndAccept();
        if (!state->context_.type(keyStr.get())) {
            return;
        }
    } else if (!state->context_.empty()) {
        // key to handle when it is not empty.
        if (event.key().check(FcitxKey_BackSpace)) {
            if (*config_.useBackSpaceToUnselect &&
                state->context_.selectedLength()) {
                state->context_.cancel();
            } else {
                state->context_.backspace();
            }
            event.filterAndAccept();
        } else if (event.key().check(FcitxKey_Delete) ||
                   event.key().check(FcitxKey_KP_Delete)) {
            state->context_.del();
            event.filterAndAccept();
        } else if (event.key().check(FcitxKey_BackSpace, KeyState::Ctrl)) {
            if (state->context_.cursor() == state->context_.selectedLength()) {
                state->context_.cancel();
            }
            auto cursor = state->context_.pinyinBeforeCursor();
            if (cursor >= 0) {
                state->context_.erase(cursor, state->context_.cursor());
            }
            event.filterAndAccept();
        } else if (event.key().check(FcitxKey_Delete, KeyState::Ctrl) ||
                   event.key().check(FcitxKey_KP_Delete, KeyState::Ctrl)) {
            auto cursor = state->context_.pinyinAfterCursor();
            if (cursor >= 0 &&
                static_cast<size_t>(cursor) <= state->context_.size()) {
                state->context_.erase(state->context_.cursor(), cursor);
            }
            event.filterAndAccept();
        } else if (event.key().check(FcitxKey_Home) ||
                   event.key().check(FcitxKey_KP_Home)) {
            state->context_.setCursor(state->context_.selectedLength());
            event.filterAndAccept();
        } else if (event.key().check(FcitxKey_End) ||
                   event.key().check(FcitxKey_KP_End)) {
            state->context_.setCursor(state->context_.size());
            event.filterAndAccept();
        } else if (event.key().check(FcitxKey_Left) ||
                   event.key().check(FcitxKey_KP_Left)) {
            if (state->context_.cursor() == state->context_.selectedLength()) {
                state->context_.cancel();
            }
            auto cursor = state->context_.cursor();
            if (cursor > 0) {
                state->context_.setCursor(cursor - 1);
            }
            event.filterAndAccept();
        } else if (event.key().check(FcitxKey_Right) ||
                   event.key().check(FcitxKey_KP_Right)) {
            auto cursor = state->context_.cursor();
            if (cursor < state->context_.size()) {
                state->context_.setCursor(cursor + 1);
            }
            event.filterAndAccept();
        } else if (event.key().check(FcitxKey_Left, KeyState::Ctrl) ||
                   event.key().check(FcitxKey_KP_Left, KeyState::Ctrl)) {
            if (state->context_.cursor() == state->context_.selectedLength()) {
                state->context_.cancel();
            }
            auto cursor = state->context_.pinyinBeforeCursor();
            if (cursor >= 0) {
                state->context_.setCursor(cursor);
            }
            event.filterAndAccept();
        } else if (event.key().check(FcitxKey_Right, KeyState::Ctrl) ||
                   event.key().check(FcitxKey_KP_Right, KeyState::Ctrl)) {
            auto cursor = state->context_.pinyinAfterCursor();
            if (cursor >= 0 &&
                static_cast<size_t>(cursor) <= state->context_.size()) {
                state->context_.setCursor(cursor);
            }
            event.filterAndAccept();
        } else if (event.key().check(FcitxKey_Escape)) {
            state->context_.clear();
            event.filterAndAccept();
        } else if (event.key().checkKeyList(*config_.commitRawInput)) {
            inputContext->commitString(preeditCommitString(inputContext));
            state->context_.clear();
            state->context_.clearContextWords();
            event.filterAndAccept();
        } else if (auto maybeIdx =
                       selectCharFromCandidateIndex(event.key(), config_)) {
            do {
                if (!candidateList || candidateList->empty() ||
                    candidateList->cursorIndex() < 0) {
                    break;
                }
                auto idx = maybeIdx.value();
                const auto &candidate =
                    candidateList->candidate(candidateList->cursorIndex());
                auto str = candidate.text().toStringForCommit();
                // Validate string and length.
                auto len = utf8::lengthValidated(str);
                if (len == utf8::INVALID_LENGTH || len == 0) {
                    break;
                }
                if (idx < 0) {
                    idx = len - 1;
                }
                if (static_cast<size_t>(idx) >= len) {
                    break;
                }
                // Get idx-th char.
                const std::string_view chr =
                    std::next(utf8::MakeUTF8CharRange(str).begin(), idx).view();
                auto segmentLength =
                    state->context_.size() - state->context_.selectedLength();
                const auto *pyCandidate =
                    dynamic_cast<const PinyinCandidateWord *>(&candidate);
                if (pyCandidate) {
                    const auto &contextCandidates =
                        state->context_.candidatesToCursor();
                    if (static_cast<size_t>(pyCandidate->candidateIndex()) <
                        contextCandidates.size()) {
                        const auto &sentence =
                            contextCandidates[pyCandidate->candidateIndex()]
                                .sentence();
                        const auto candidateSegmentLength =
                            sentence.back()->to()->index();
                        segmentLength =
                            std::min(segmentLength, candidateSegmentLength);
                    }
                }
                state->context_.selectCustom(segmentLength, chr);
                event.filterAndAccept();
            } while (false);
        }
    } else if (event.key().check(FcitxKey_BackSpace)) {
        if (lastIsPunc) {
            const std::string &puncStr =
                punctuation()->call<IPunctuation::cancelLast>("zh_CN",
                                                              inputContext);
            if (!puncStr.empty()) {
                // forward the original key is the best choice.
                state->cancelLastEvent_ = instance()->eventLoop().addTimeEvent(
                    CLOCK_MONOTONIC, now(CLOCK_MONOTONIC) + 3000, 1,
                    [this, ref = inputContext->watch(),
                     puncStr = puncStr](EventSourceTime *, uint64_t) {
                        if (auto *inputContext = ref.get()) {
                            inputContext->commitString(puncStr);
                            auto *state = inputContext->propertyFor(&factory_);
                            state->cancelLastEvent_.reset();
                        }
                        return true;
                    });
                event.filter();
                return;
            }
        }
    }
    if (handlePunc(event, keyChr)) {
        return;
    }

    if (event.filtered() && event.accepted()) {
        updateUI(inputContext);
    }
}

void PinyinEngine::setSubConfig(const std::string &path,
                                const RawConfig &config) {
    FCITX_UNUSED(config);
    if (path == "dictmanager") {
        loadExtraDict();
    } else if (path == "clearuserdict") {
        ime_->dict()->clear(libime::PinyinDictionary::UserDict);
    } else if (path == "clearalldict") {
        ime_->dict()->clear(libime::PinyinDictionary::UserDict);
        ime_->model()->history().clear();
    } else if (path == "customphrase") {
        loadCustomPhrase();
    }
}

void PinyinEngine::reset(const InputMethodEntry & /*entry*/,
                         InputContextEvent &event) {
    auto *inputContext = event.inputContext();
    doReset(inputContext);
}

void PinyinEngine::doReset(InputContext *inputContext) const {
    auto *state = inputContext->propertyFor(&factory_);
    resetForgetCandidate(inputContext);
    state->mode_ = PinyinMode::Normal;
    state->context_.clear();
    state->context_.clearContextWords();
    state->predictWords_.reset();
    inputContext->inputPanel().reset();
    inputContext->updatePreedit();
    inputContext->updateUserInterface(UserInterfaceComponent::InputPanel);
    // state->lastIsPunc_ = false;

    state->keyReleased_ = -1;
    state->keyReleasedIndex_ = -2;
    instance_->resetCompose(inputContext);
}

void PinyinEngine::save() {
    safeSaveAsIni(config_, "conf/pinyin.conf");
    const auto &standardPath = StandardPaths::global();
    standardPath.safeSave(
        StandardPathsType::PkgData, "pinyin/user.dict", [this](int fd) {
            OFDStreamBuf buffer(fd);
            std::ostream out(&buffer);
            try {
                ime_->dict()->save(libime::PinyinDictionary::UserDict, out,
                                   libime::PinyinDictFormat::Binary);
                return static_cast<bool>(out);
            } catch (const std::exception &e) {
                PINYIN_ERROR() << "Failed to save pinyin dict: " << e.what();
                return false;
            }
        });
    standardPath.safeSave(
        StandardPathsType::PkgData, "pinyin/user.history", [this](int fd) {
            OFDStreamBuf buffer(fd);
            std::ostream out(&buffer);
            try {
                ime_->model()->save(out);
                return true;
            } catch (const std::exception &e) {
                PINYIN_ERROR() << "Failed to save pinyin history: " << e.what();
                return false;
            }
        });
}

std::string PinyinEngine::subMode(const InputMethodEntry &entry,
                                  InputContext &inputContext) {
    FCITX_UNUSED(inputContext);
    if (entry.uniqueName() == "shuangpin" && *config_.showShuangpinMode &&
        *config_.shuangpinProfile != ShuangpinProfileEnum::Custom) {
        return ShuangpinProfileEnumI18NAnnotation::toString(
            *config_.shuangpinProfile);
    }
    return {};
}

const Configuration *PinyinEngine::getConfig() const { return &config_; }

const Configuration *
PinyinEngine::getConfigForInputMethod(const InputMethodEntry &entry) const {
    // Hide shuangpin options.
    if (entry.uniqueName() == "pinyin") {
        return &pyConfig_;
    }
    return &config_;
}

void PinyinEngine::invokeActionImpl(const InputMethodEntry &entry,
                                    InvokeActionEvent &event) {
    auto *inputContext = event.inputContext();
    auto *state = inputContext->propertyFor(&factory_);
    auto &context = state->context_;
    auto &inputPanel = inputContext->inputPanel();
    if (event.cursor() < 0 ||
        event.action() != InvokeActionEvent::Action::LeftClick ||
        !inputContext->capabilityFlags().test(CapabilityFlag::Preedit)) {
        InputMethodEngineV3::invokeActionImpl(entry, event);
        return;
    }
    auto [clientPreedit, _] = this->preedit(inputContext);

    auto preedit = clientPreedit.toString();
    size_t cursor = event.cursor();
    if (inputPanel.clientPreedit().toString() != clientPreedit.toString() ||
        inputPanel.clientPreedit().cursor() != clientPreedit.cursor() ||
        cursor > utf8::length(preedit)) {
        InputMethodEngineV3::invokeActionImpl(entry, event);
        return;
    }

    event.filter();

    auto preeditWithCursor = context.preeditWithCursor();
    auto selectedSentence = context.selectedSentence();
    // The logic here need to match ::preedit()
    switch (*config_.preeditMode) {
    case PreeditMode::No:
        break;
    case PreeditMode::ComposingPinyin:
        if (utf8::length(selectedSentence) > cursor) {
            // If cursor is with in selected sentence range, cancel until cursor
            // is covered.
            do {
                context.cancel();
            } while (utf8::length(context.selectedSentence()) > cursor);
            context.setCursor(context.selectedLength());
        } else {
            // If cursor is with in the pinyin range, move to the left most and
            // then move it around. This is a easy way to cover Shuangpin and
            // Preedit Mode difference. setCursor should be cheap operation if
            // it does not cancel any selection.
            context.setCursor(context.selectedLength());
            while (context.cursor() < context.size()) {
                auto [preeditText, preeditCursor] = context.preeditWithCursor();
                if (utf8::length(preeditText.begin(),
                                 preeditText.begin() + preeditCursor) <
                    cursor) {
                    state->context_.setCursor(context.cursor() + 1);
                } else {
                    break;
                }
            }
            auto [preeditText, preeditCursor] = context.preeditWithCursor();
            if (utf8::length(preeditText.begin(),
                             preeditText.begin() + preeditCursor) > cursor) {
                state->context_.setCursor(context.cursor() - 1);
            }
        }
        break;
    case PreeditMode::CommitPreview:
        if (utf8::length(selectedSentence) > cursor) {
            do {
                context.cancel();
            } while (utf8::length(context.selectedSentence()) > cursor);
        }
        break;
    }
    updateUI(inputContext);
}

void PinyinEngine::cloudPinyinSelected(InputContext *inputContext,
                                       const std::string &selected,
                                       const std::string &word) {
    auto *state = inputContext->propertyFor(&factory_);
    auto result = matchCloudPinyin(state->context_, selected, word);
    if (result.has_value()) {
        // if pinyin is not valid, it may throw
        try {
            // If somehow all segments are consumed by matchCloudPinyin.
            // Though it's quite unlikely, but we should still handle it.
            if (!result->word.empty() && !result->fullPinyin.empty()) {
                auto encodedPinyin =
                    libime::PinyinEncoder::encodeFullPinyinWithFlags(
                        result->fullPinyin, libime::PinyinFuzzyFlag::VE_UE);
                PINYIN_DEBUG() << "Cloud pinyin saves word: " << result->word
                               << " " << result->fullPinyin;
                result->context.push_back(libime::HistoryBigram::WordWithCode{
                    std::string(result->word),
                    std::string(encodedPinyin.data(), encodedPinyin.size())});
            }
            if (shouldLearn(inputContext)) {
                if (!result->word.empty() && !result->fullPinyin.empty()) {
                    ime_->dict()->addWord(libime::PinyinDictionary::UserDict,
                                          result->fullPinyin, result->word);
                }
                ime_->model()->history().addWithCode(result->context);
            }
        } catch (const std::exception &e) {
            PINYIN_DEBUG() << "Failed to save cloudpinyin: " << e.what();
            result.reset();
        }
    }
    state->context_.clear();
    inputContext->commitString(selected + word);
    inputContext->inputPanel().reset();
    if (result.has_value()) {
        if (*config_.keepCurrentContext) {
            state->context_.appendContextWordsWithPinyin(result->context);
        }
        if (*config_.predictionEnabled) {
            state->predictWords_ = std::move(result->context);
            updatePredict(inputContext);
        }
    } else {
        state->context_.clearContextWords();
        resetPredict(inputContext);
    }
    inputContext->updatePreedit();
    inputContext->updateUserInterface(UserInterfaceComponent::InputPanel);
}

} // namespace fcitx
