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

#include "waylandmodule.h"
#include <fcntl.h>
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <exception>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
#include <vector>
#include <fcitx-utils/handlertable.h>
#include <gio/gio.h>
#include <glib-object.h>
#include <glib.h>
#include <wayland-client-core.h>
#include <wayland-client-protocol.h>
#include "fcitx-config/iniparser.h"
#include "fcitx-config/rawconfig.h"
#include "fcitx-utils/dbus/message.h"
#include "fcitx-utils/environ.h"
#include "fcitx-utils/event.h"
#include "fcitx-utils/eventloopinterface.h"
#include "fcitx-utils/i18n.h"
#include "fcitx-utils/log.h"
#include "fcitx-utils/misc.h"
#include "fcitx-utils/misc_p.h"
#include "fcitx-utils/standardpaths.h"
#include "fcitx-utils/stringutils.h"
#include "fcitx-utils/trackableobject.h"
#include "fcitx-utils/unixfd.h"
#include "fcitx/addonfactory.h"
#include "fcitx/addoninstance.h"
#include "fcitx/event.h"
#include "fcitx/inputcontext.h"
#include "fcitx/instance.h"
#include "fcitx/misc_p.h"
#include "config.h"
#include "display.h"
#include "notifications_public.h"
#include "wayland_public.h"
#include "waylandeventreader.h"
#include "wl_seat.h"

#ifdef ENABLE_DBUS
#include "dbus_public.h"
#endif

#ifdef ENABLE_X11
#include "xcb_public.h"
#endif

namespace fcitx {

FCITX_DEFINE_LOG_CATEGORY(wayland_log, "wayland");

namespace {
bool isKDE5Plus() {
    static const DesktopType desktop = getDesktopType();
    return desktop == DesktopType::KDE5 || desktop == DesktopType::KDE6;
}

class ScopedEnvvar {
public:
    explicit ScopedEnvvar(std::string name, const char *value)
        : name_(std::move(name)) {
        oldValue_ = getEnvironment(name_.data());
        setenv(name_.data(), value, true);
    }

    ~ScopedEnvvar() {
        if (oldValue_) {
            setenv(name_.data(), oldValue_->data(), true);
        } else {
            unsetenv(name_.data());
        }
    }

private:
    std::string name_;
    std::optional<std::string> oldValue_ = std::nullopt;
};

} // namespace

WaylandConnection::WaylandConnection(WaylandModule *wayland, std::string name)
    : parent_(wayland), name_(std::move(name)) {
    wl_display *display = nullptr;
    {
        std::optional<ScopedEnvvar> env;
        if (wayland_log().checkLogLevel(Debug)) {
            env.emplace("WAYLAND_DEBUG", "1");
        }
        // When WAYLAND_SOCKET is set, wl_display_connect will use it
        // regardlessly, no matter it is valid or not.
        if (getEnvironment("WAYLAND_SOCKET")) {
            isWaylandSocket_ = true;
        }
        display = wl_display_connect(name_.empty() ? nullptr : name_.c_str());
    }
    if (!display) {
        throw std::runtime_error("Failed to open wayland connection");
    }
    if (!isWaylandSocket_ && name_.empty()) {
        // This is same as wl_display_connect(nullptr) logic.
        realName_ = "wayland-0";
        if (auto displayEnv = getEnvironment("WAYLAND_DISPLAY")) {
            realName_ = *displayEnv;
        }
    }
    init(display);
}

WaylandConnection::WaylandConnection(WaylandModule *wayland, std::string name,
                                     int fd, std::string realName)
    : parent_(wayland), name_(std::move(name)), realName_(std::move(realName)),
      isWaylandSocket_(true) {
    wl_display *display = nullptr;
    {
        std::unique_ptr<ScopedEnvvar> env;
        if (wayland_log().checkLogLevel(Debug)) {
            env = std::make_unique<ScopedEnvvar>("WAYLAND_DEBUG", "1");
        }
        display = wl_display_connect_to_fd(fd);
    }
    if (!display) {
        throw std::runtime_error("Failed to open wayland connection");
    }
    init(display);
}

WaylandConnection::~WaylandConnection() {}

void WaylandConnection::init(wl_display *display) {
    display_ = std::make_unique<wayland::Display>(display);

    group_ = std::make_unique<FocusGroup>(
        "wayland:" + name_, parent_->instance()->inputContextManager());

    panelConn_ = display_->globalCreated().connect(
        [this](const std::string &name, const std::shared_ptr<void> &seat) {
            if (name == wayland::WlSeat::interface) {
                setupKeyboard(static_cast<wayland::WlSeat *>(seat.get()));
            } else if (name == "zwp_input_method_v1") {
                // Do a deferred layout sync, since this is callback is called
                // before waylandim setup.
                parent_->instance()->eventDispatcher().scheduleWithContext(
                    watch(), [this]() { parent_->setLayoutToCompositor(); });
            }
        });
    panelRemovedConn_ = display_->globalRemoved().connect(
        [this](const std::string &name, const std::shared_ptr<void> &ptr) {
            if (name == wayland::WlSeat::interface) {
                keyboards_.erase(static_cast<wayland::WlSeat *>(ptr.get()));
            }
        });
    for (const auto &seat : display_->getGlobals<wayland::WlSeat>()) {
        setupKeyboard(seat.get());
    }
    eventReader_ = std::make_unique<WaylandEventReader>(this);
}

void WaylandConnection::finish() { parent_->removeConnection(name_); }

void WaylandConnection::setupKeyboard(wayland::WlSeat *seat) {
    auto &kbd = (keyboards_[seat] = std::make_unique<WaylandKeyboard>(seat));
    kbd->updateKeymap().connect([this]() {
        FCITX_WAYLAND_DEBUG() << "Update keymap";
        parent_->reloadXkbOption();
    });
}

std::optional<std::tuple<int32_t, int32_t>>
WaylandConnection::repeatInfo(wayland::WlSeat *seat) const {
    if (const auto *keyboard = findValue(keyboards_, seat)) {
        return keyboard->get()->repeatInfo();
    }
    return std::nullopt;
}

WaylandModule::WaylandModule(fcitx::Instance *instance)
    : instance_(instance), isWaylandSession_(isSessionType("wayland")) {

    delayedReloadXkbOption_ = instance_->eventLoop().addTimeEvent(
        CLOCK_MONOTONIC, now(CLOCK_MONOTONIC), 0,
        [this](EventSourceTime *, uint64_t) {
            reloadXkbOptionReal();
            return true;
        });
    delayedReloadXkbOption_->setEnabled(false);

    reloadConfig();
    if (!containerContains(instance->addonManager().addonOptions("wayland"),
                           "nodefault")) {
        openConnection("");
    }

    reloadXkbOption();

#ifdef ENABLE_DBUS
    eventHandlers_.emplace_back(instance_->watchEvent(
        EventType::InputMethodGroupChanged, EventWatcherPhase::Default,
        [this](Event &) { setLayoutToCompositor(); }));

    instance_->eventDispatcher().schedule([this]() {
        deferredDiagnose_ = instance_->eventLoop().addTimeEvent(
            CLOCK_MONOTONIC, now(CLOCK_MONOTONIC) + 10000000, 0,
            [this](EventSourceTime *, uint64_t) {
                selfDiagnose();
                return true;
            });
    });
#endif
}

void WaylandModule::reloadConfig() { readAsIni(config_, "conf/wayland.conf"); }

bool WaylandModule::openConnection(const std::string &name) {
    if (conns_.contains(name)) {
        return false;
    }

    if (auto *defaultConnection = findValue(conns_, "")) {
        if (name == defaultConnection->get()->realName()) {
            return false;
        }
    }

    WaylandConnection *newConnection = nullptr;
    try {
        auto connection = std::make_unique<WaylandConnection>(this, name);
        auto iter = conns_.emplace(
            std::piecewise_construct, std::forward_as_tuple(name),
            std::forward_as_tuple(std::move(connection)));
        newConnection = iter.first->second.get();
    } catch (const std::exception &e) {
        FCITX_ERROR() << e.what();
    }
    if (newConnection) {
        refreshCanRestart();
        onConnectionCreated(*newConnection);
        return true;
    }

    return false;
}

bool WaylandModule::openConnectionSocket(int fd) {
    auto name = stringutils::concat("socket:", fd);
    return openConnectionSocketWithName(fd, name, "");
}

bool WaylandModule::openConnectionSocketWithName(int fd,
                                                 const std::string &name,
                                                 const std::string &realName) {
    UnixFD guard = UnixFD::own(fd);

    if (conns_.contains(name)) {
        return false;
    }

    for (const auto &[name, connection] : conns_) {
        if (connection->display()->fd() == fd) {
            return false;
        }
    }

    WaylandConnection *newConnection = nullptr;
    try {
        auto connection =
            std::make_unique<WaylandConnection>(this, name, fd, realName);
        auto iter = conns_.emplace(
            std::piecewise_construct, std::forward_as_tuple(name),
            std::forward_as_tuple(std::move(connection)));
        guard.release();
        newConnection = iter.first->second.get();
    } catch (const std::exception &e) {
        FCITX_ERROR() << "Open wayland connection with socket failed: "
                      << e.what();
    }
    if (newConnection) {
        refreshCanRestart();
        onConnectionCreated(*newConnection);
        return true;
    }
    return false;
}

bool WaylandModule::reopenConnectionSocket(const std::string &displayName,
                                           int fd) {
    UnixFD guard = UnixFD::own(fd);
    std::string name = displayName;

    auto iter = conns_.find(name);
    if (iter == conns_.end()) {
        do {
            // If it's in flatpak, WAYLAND_DISPLAY may be overrrided, so don't
            // compare it with default connection.
            if (!isInFlatpak() && !name.empty()) {
                iter = conns_.find("");
                if (iter != conns_.end() && iter->second->realName() == name) {
                    name = "";
                    break;
                }

                if (auto displayEnv = getEnvironment("WAYLAND_DISPLAY")) {
                    if (name == displayEnv) {
                        name = "";
                    }
                }
            }
            return openConnectionSocketWithName(guard.release(), name,
                                                displayName);

        } while (0);
    }

    for (const auto &[name, connection] : conns_) {
        if (connection->display()->fd() == fd) {
            return false;
        }
    }

    // Record all the IC in the old connection.
    std::vector<TrackableObjectReference<InputContext>> ics;
    iter->second->focusGroup()->foreach([&ics](InputContext *ic) {
        ics.push_back(ic->watch());
        return true;
    });

    std::unique_ptr<WaylandConnection> newConnection;
    try {
        newConnection =
            std::make_unique<WaylandConnection>(this, name, fd, displayName);
        guard.release();
    } catch (const std::exception &e) {
        FCITX_ERROR() << "Open wayland connection: " << name
                      << " failed: " << e.what();
    }
    if (newConnection) {
        // At this point connection is already constructed, now we try to
        // replace it.
        onConnectionClosed(*iter->second);
        iter->second = std::move(newConnection);
        refreshCanRestart();
        onConnectionCreated(*iter->second);
        // Transfer it to new connection's IC group, we do create two focus
        // group with same name, but well, ic manager doesn't check that.
        for (const auto &icRef : ics) {
            if (auto *ic = icRef.get()) {
                if (!ic->focusGroup()) {
                    ic->setFocusGroup(iter->second->focusGroup());
                }
            }
        }
        return true;
    }
    return false;
}

void WaylandModule::removeConnection(const std::string &name) {
    FCITX_WAYLAND_INFO() << "Connection removed " << name;
    if (name.empty() && instance_->exitWhenMainDisplayDisconnected() &&
        isWaylandSession_) {
        instance_->exit();
    }
    auto iter = conns_.find(name);
    if (iter != conns_.end()) {
        onConnectionClosed(*iter->second);
        conns_.erase(iter);
        refreshCanRestart();
    }
}

std::unique_ptr<HandlerTableEntry<WaylandConnectionCreated>>
WaylandModule::addConnectionCreatedCallback(WaylandConnectionCreated callback) {
    auto result = createdCallbacks_.add(std::move(callback));

    for (auto &p : conns_) {
        auto &conn = p.second;
        (**result->handler())(conn->name(), *conn->display(),
                              conn->focusGroup());
    }
    return result;
}

std::unique_ptr<HandlerTableEntry<WaylandConnectionClosed>>
WaylandModule::addConnectionClosedCallback(WaylandConnectionClosed callback) {
    return closedCallbacks_.add(std::move(callback));
}

void WaylandModule::onConnectionCreated(WaylandConnection &conn) {
    for (auto &callback : createdCallbacks_.view()) {
        callback(conn.name(), *conn.display(), conn.focusGroup());
    }
}

void WaylandModule::onConnectionClosed(WaylandConnection &conn) {
    for (auto &callback : closedCallbacks_.view()) {
        callback(conn.name(), *conn.display());
    }
}

void WaylandModule::refreshCanRestart() {
    setCanRestart(std::ranges::all_of(
        conns_, [](const decltype(conns_)::value_type &conn) {
            return !conn.second->isWaylandSocket();
        }));
}

void WaylandModule::reloadXkbOption() {
    delayedReloadXkbOption_->setNextInterval(30000);
    delayedReloadXkbOption_->setOneShot();
}

void WaylandModule::reloadXkbOptionReal() {
#ifdef ENABLE_DBUS
    if (!isWaylandSession_) {
        return;
    }
    auto *connection = findValue(conns_, "");
    if (!connection) {
        return;
    }

    FCITX_WAYLAND_DEBUG() << "Try to reload Xkb option from desktop";
    std::optional<std::string> xkbOption = std::nullopt;
    if (isKDE5Plus()) {

        auto *dbusAddon = dbus();
        if (!dbusAddon) {
            return;
        }

        fcitx::RawConfig config;
        readAsIni(config, StandardPathsType::Config, "kxkbrc");
        const auto *model = config.valueByPath("Layout/Model");
        const auto *options = config.valueByPath("Layout/Options");
        xkbOption = (options ? *options : "");
        instance_->setXkbParameters((*connection)->focusGroup()->display(),
                                    DEFAULT_XKB_RULES, model ? *model : "",
                                    *xkbOption);
        FCITX_WAYLAND_DEBUG()
            << "KDE xkb options: model=" << (model ? *model : "")
            << " options=" << *xkbOption;
    } else if (getDesktopType() == DesktopType::GNOME) {
        UniqueCPtr<GSettings, g_object_unref> settings(
            g_settings_new("org.gnome.desktop.input-sources"));
        if (settings) {

            gchar **value = g_settings_get_strv(settings.get(), "xkb-options");
            if (value) {
                auto *options = g_strjoinv(",", value);
                xkbOption = (options ? options : "");
                instance_->setXkbParameters(
                    (*connection)->focusGroup()->display(), DEFAULT_XKB_RULES,
                    "", *xkbOption);
                FCITX_WAYLAND_DEBUG() << "GNOME xkb options=" << *xkbOption;
                g_free(options);
                g_strfreev(value);
            }
        }
    }
#ifdef ENABLE_X11
    if (auto *xcbAddon = xcb(); xcbAddon && xkbOption) {
        xcbAddon->call<IXCBModule::setXkbOption>(
            xcbAddon->call<IXCBModule::mainDisplay>(), *xkbOption);
    }
#endif
#endif
}

void WaylandModule::setLayoutToKDE() {
#ifdef ENABLE_DBUS
    auto *dbusAddon = dbus();
    if (!dbusAddon) {
        return;
    }

    if (!hasWaylandInputMethod()) {
        return;
    }

    auto layoutAndVariant = parseLayout(
        instance_->inputMethodManager().currentGroup().defaultLayout());

    if (layoutAndVariant.first.empty()) {
        return;
    }

    fcitx::RawConfig config;
    readAsIni(config, StandardPathsType::Config, "kxkbrc");
    config.setValueByPath("Layout/LayoutList", layoutAndVariant.first);
    config.setValueByPath("Layout/VariantList", layoutAndVariant.second);
    config.setValueByPath("Layout/DisplayNames", "");
    config.setValueByPath("Layout/Use", "true");

    // See: https://github.com/flatpak/flatpak/issues/5370
    // The write temp file and replace does not work with mount bind,
    // if the intention is to get the file populated outside the
    // sandbox.
    if (isInFlatpak()) {
        auto path =
            StandardPaths::global().userDirectory(StandardPathsType::Config) /
            "kxkbrc";
        auto file =
            StandardPaths::openPath(path, O_WRONLY | O_TRUNC | O_CREAT, 0644);
        if (file.isValid()) {
            writeAsIni(config, file.fd());
        } else {
            FCITX_WAYLAND_ERROR() << "Failed to write to kxkbrc.";
        }
    } else {
        safeSaveAsIni(config, StandardPathsType::Config, "kxkbrc");
    }

    auto *bus = dbusAddon->call<IDBusModule::bus>();
    // Prior KWin 6.5
    auto message =
        bus->createSignal("/Layouts", "org.kde.keyboard", "reloadConfig");
    message.send();
    // After KWin 6.5
    // https://github.com/KDE/kwin/commit/c75d26ff98fc40004923e4e0f9bb441c3b970ec3
    message =
        bus->createSignal("/kxkbrc", "org.kde.kconfig.notify", "ConfigChanged");
    // Example update from KCM a{saay}
    // array [
    //   dict entry(
    //      string "Layout"
    //      array [
    //         array of bytes "DisplayNames"
    //         array of bytes "LayoutList"
    //         array of bytes "VariantList"
    //      ]
    //   )
    // ]
    std::vector<dbus::DictEntry<std::string, std::vector<std::vector<uint8_t>>>>
        entries;
    entries.emplace_back("Layout", std::vector<std::vector<uint8_t>>());
    for (std::string_view key :
         {"LayoutList", "DisplayNames", "VariantList", "Use"}) {
        std::vector<uint8_t> bytes(key.begin(), key.end());
        entries.back().value().push_back(std::move(bytes));
    }
    message << entries;
    message.send();
#endif
}

void WaylandModule::setLayoutToGNOME() {
#ifdef ENABLE_DBUS
    auto layoutAndVariant = parseLayout(
        instance_->inputMethodManager().currentGroup().defaultLayout());

    if (layoutAndVariant.first.empty()) {
        return;
    }

    std::string xkbsource = layoutAndVariant.first;
    if (!layoutAndVariant.second.empty()) {
        xkbsource =
            stringutils::concat(xkbsource, "+", layoutAndVariant.second);
    }

    UniqueCPtr<GSettings, g_object_unref> settings(
        g_settings_new("org.gnome.desktop.input-sources"));
    if (settings) {
        GVariantBuilder builder;
        g_variant_builder_init(&builder, G_VARIANT_TYPE("a(ss)"));

        g_variant_builder_add(&builder, "(ss)", "xkb", xkbsource.data());
        UniqueCPtr<GVariant, &g_variant_unref> value(
            g_variant_ref_sink(g_variant_builder_end(&builder)));
        g_settings_set_value(settings.get(), "sources", value.get());
        g_settings_set_value(settings.get(), "mru-sources", value.get());
    }
#endif
}

void WaylandModule::setLayoutToCompositor() {
    if (!isWaylandSession_ || !*config_.allowOverrideXKB) {
        return;
    }

    auto *connection = findValue(conns_, "");
    if (!connection) {
        return;
    }

    if (isKDE5Plus()) {
        setLayoutToKDE();
    } else if (getDesktopType() == DesktopType::GNOME) {
        setLayoutToGNOME();
    }
}

bool WaylandModule::hasWaylandInputMethod() const {
    bool isWaylandIM = false;
    if (isInFlatpak()) {
        // In flatpak, ReopenWaylandConnection will not replace existing
        // connection.
        for (const auto &[_, conn] : conns_) {
            conn->focusGroup()->foreach([&isWaylandIM](InputContext *ic) {
                if (ic->frontendName().starts_with("wayland")) {
                    isWaylandIM = true;
                    return false;
                }
                return true;
            });
            if (isWaylandIM) {
                break;
            }
        }
    } else {
        const auto *connection = findValue(conns_, "");
        if (connection) {
            (*connection)
                ->focusGroup()
                ->foreach([&isWaylandIM](InputContext *ic) {
                    if (ic->frontendName().starts_with("wayland")) {
                        isWaylandIM = true;
                        return false;
                    }
                    return true;
                });
        }
    }
    return isWaylandIM;
}

void WaylandModule::selfDiagnose() {
    if (!isWaylandSession_) {
        return;
    }

    auto *connection = findValue(conns_, "");
    if (!connection) {
        return;
    }

    if (!notifications()) {
        return;
    }

    bool isWaylandIM = hasWaylandInputMethod();

    auto sendMessage = [this](const std::string &category,
                              const std::string &message) {
        notifications()->call<INotifications::showTip>(
            category, _("Fcitx"), "fcitx", _("Wayland Diagnose"), message,
            60000);
    };

    auto gtkIM = getEnvironmentOrEmpty("GTK_IM_MODULE");
    auto qtIM = getEnvironmentOrEmpty("QT_IM_MODULE");
    auto qtsIM = getEnvironmentOrEmpty("QT_IM_MODULES");

    FCITX_WAYLAND_INFO() << "Wayland self diagnose information";
    FCITX_WAYLAND_INFO() << "Using Wayland native input method protocol: "
                         << isWaylandIM;
    FCITX_WAYLAND_INFO()
        << "Toolkit specific environment variable detected by process:";
    FCITX_WAYLAND_INFO() << "GTK_IM_MODULE=" << gtkIM;
    FCITX_WAYLAND_INFO() << "QT_IM_MODULE=" << qtIM;
    FCITX_WAYLAND_INFO() << "QT_IM_MODULES=" << qtsIM;

    std::vector<std::string> messages;
    const auto desktop = getDesktopType();
    if (desktop == DesktopType::KDE5 || desktop == DesktopType::KDE6) {
        if (!isWaylandIM) {
            sendMessage(
                "wayland-diagnose-kde",
                _("Fcitx should be launched by KWin under KDE Wayland in order "
                  "to use Wayland input method frontend. This can improve the "
                  "experience when using Fcitx on Wayland. To "
                  "configure this, you need to go to \"System Settings\" > "
                  "\"Virtual "
                  "keyboard\" and select \"Fcitx 5\" from it. You may also "
                  "need to disable tools that launches input method, such as "
                  "imsettings on Fedora, or im-config on Debian/Ubuntu. For "
                  "more details see "
                  "https://fcitx-im.org/wiki/"
                  "Using_Fcitx_5_on_Wayland#KDE_Plasma"));
        } else if (desktop == DesktopType::KDE5 &&
                   (!gtkIM.empty() || !qtIM.empty())) {
            // On qt5 system, QT_IM_MODULE unset is most likely required.
            sendMessage("wayland-diagnose-kde",
                        _("Detect GTK_IM_MODULE and QT_IM_MODULE being set and "
                          "Wayland Input method frontend is working. It is "
                          "recommended to unset GTK_IM_MODULE and QT_IM_MODULE "
                          "and use Wayland input method frontend instead. For "
                          "more details see "
                          "https://fcitx-im.org/wiki/"
                          "Using_Fcitx_5_on_Wayland#KDE_Plasma"));
        } else if (desktop == DesktopType::KDE6) {
            // On qt6 system, QT_IM_MODULES can be used to override QT_IM_MODULE
            // if not empty.
            if (!gtkIM.empty() ||
                (!qtIM.empty() && qtsIM.find("wayland") == std::string::npos)) {

                sendMessage(
                    "wayland-diagnose-kde",
                    _("Detect GTK_IM_MODULE and QT_IM_MODULE being set and "
                      "Wayland Input method frontend is working. It is "
                      "recommended to use Wayland input method frontend. For "
                      "more details see "
                      "https://fcitx-im.org/wiki/"
                      "Using_Fcitx_5_on_Wayland#KDE_Plasma"));
            }
        }
    } else if (desktop == DesktopType::GNOME) {
        if (instance_->currentUI() != "kimpanel") {
            sendMessage(
                "wayland-diagnose-gnome",
                _("It is recommended to install Input Method Panel GNOME "
                  "Shell Extensions to provide the input method popup. "
                  "https://extensions.gnome.org/extension/261/kimpanel/ "
                  "Otherwise you may not be able to see input method popup "
                  "when typing in GNOME Shell's activities search box. For "
                  "more details "
                  "see "
                  "https://fcitx-im.org/wiki/Using_Fcitx_5_on_Wayland#GNOME"));
        }
    } else if (desktop == DesktopType::DEEPIN) {
        // Per Deepin upstream request, do not show this message for them.
    } else if (desktop == DesktopType::UKUI) {
        // Per UkUI upstream request, do not show this message for them.
    } else {
        // It is not clear whether compositor is supported, only warn if wayland
        // im is being used..
        if (isWaylandIM) {
            // Sway's input popup is not merged.
            if (!gtkIM.empty() && gtkIM != "wayland" &&
                desktop != DesktopType::Sway) {
                messages.push_back(
                    _("Detect GTK_IM_MODULE being set and "
                      "Wayland Input method frontend is working. It is "
                      "recommended to unset GTK_IM_MODULE "
                      "and use Wayland input method frontend instead."));
            }
        }

        // layout diagnosis only when overriding is enabled
        if (*config_.allowOverrideXKB) {
            std::optional<std::string> firstLayout;
            for (const auto &groupName :
                 instance_->inputMethodManager().groups()) {
                if (const auto *group =
                        instance_->inputMethodManager().group(groupName)) {
                    const auto &layout = group->defaultLayout();
                    if (!firstLayout) {
                        firstLayout = layout;
                    } else if (layout != *firstLayout) {
                        messages.push_back(_(
                            "Sending keyboard layout configuration to wayland "
                            "compositor from Fcitx is "
                            "not yet supported on current desktop. You may "
                            "still "
                            "use "
                            "Fcitx's internal layout conversion by adding "
                            "layout "
                            "as "
                            "input method to the input method group."));
                        break;
                    }
                }
            }
        }

        if (!messages.empty()) {
            messages.push_back(
                _("For more details see "
                  "https://fcitx-im.org/wiki/Using_Fcitx_5_on_Wayland"));

            sendMessage("wayland-diagnose-other",
                        stringutils::join(messages, "\n"));
        }
    }
}

std::optional<std::tuple<int32_t, int32_t>>
WaylandModule::repeatInfo(const std::string &name, wl_seat *seat) const {
    if (!seat) {
        return std::nullopt;
    }
    auto *wlSeat = static_cast<wayland::WlSeat *>(wl_seat_get_user_data(seat));
    if (!wlSeat) {
        return std::nullopt;
    }
    conns_.find(name);
    auto iter = conns_.find(name);
    if (iter == conns_.end()) {
        return std::nullopt;
    }
    return iter->second->repeatInfo(wlSeat);
}

class WaylandModuleFactory : public AddonFactory {
public:
    AddonInstance *create(AddonManager *manager) override {
        return new WaylandModule(manager->instance());
    }
};
} // namespace fcitx

FCITX_ADDON_FACTORY_V2(wayland, fcitx::WaylandModuleFactory);
