/*
 * SPDX-FileCopyrightText: 2020~2020 CSSlayer <wengxt@gmail.com>
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 */
#include "waylandimserverv2.h"
#include <sys/mman.h>
#include "fcitx-utils/unixfd.h"
#include "fcitx-utils/utf8.h"
#include "wayland-text-input-unstable-v3-client-protocol.h"
#include "waylandim.h"
#include "wl_seat.h"

namespace fcitx {

WaylandIMServerV2::WaylandIMServerV2(wl_display *display, FocusGroup *group,
                                     const std::string &name,
                                     WaylandIMModule *waylandim)
    : group_(group), name_(name), parent_(waylandim),
      inputMethodManagerV2_(nullptr), display_(static_cast<wayland::Display *>(
                                          wl_display_get_user_data(display))) {
    display_->requestGlobals<wayland::ZwpInputMethodManagerV2>();
    display_->requestGlobals<wayland::ZwpVirtualKeyboardManagerV1>();
    display_->requestGlobals<wayland::WlSeat>();
    WAYLANDIM_DEBUG() << "WAYLANDIM V2";
    globalConn_ = display_->globalCreated().connect(
        [this](const std::string &interface, const std::shared_ptr<void> &) {
            if (interface == wayland::ZwpInputMethodManagerV2::interface) {
                WAYLANDIM_DEBUG() << "WAYLAND IM INTERFACE: " << interface;
                inputMethodManagerV2_ =
                    display_->getGlobal<wayland::ZwpInputMethodManagerV2>();
            }
            if (interface == wayland::ZwpVirtualKeyboardManagerV1::interface) {
                WAYLANDIM_DEBUG() << "WAYLAND VK INTERFACE: " << interface;
                virtualKeyboardManagerV1_ =
                    display_->getGlobal<wayland::ZwpVirtualKeyboardManagerV1>();
            }
            if (interface == wayland::WlSeat::interface) {
                refreshSeat();
            }
            init();
        });

    if (auto im = display_->getGlobal<wayland::ZwpInputMethodManagerV2>()) {
        inputMethodManagerV2_ = im;
    }

    if (auto vk = display_->getGlobal<wayland::ZwpVirtualKeyboardManagerV1>()) {
        virtualKeyboardManagerV1_ = vk;
    }
    init();
}

WaylandIMServerV2::~WaylandIMServerV2() {
    // Delete all input context when server goes away.
    while (!icMap_.empty()) {
        delete icMap_.begin()->second;
    }
}
InputContextManager &WaylandIMServerV2::inputContextManager() {
    return parent_->instance()->inputContextManager();
}

Instance *WaylandIMServerV2::instance() { return parent_->instance(); }

void WaylandIMServerV2::init() {
    if (init_) {
        return;
    }
    if (inputMethodManagerV2_ && virtualKeyboardManagerV1_) {
        init_ = true;
    } else {
        return;
    }
    WAYLANDIM_DEBUG() << "INIT IM V2";
    refreshSeat();

    display_->flush();
}

void WaylandIMServerV2::refreshSeat() {
    if (!init_) {
        return;
    }
    auto seats = display_->getGlobals<wayland::WlSeat>();
    for (const auto &seat : seats) {
        if (icMap_.count(seat.get())) {
            continue;
        }
        auto *ic = new WaylandIMInputContextV2(
            inputContextManager(), this, seat,
            virtualKeyboardManagerV1_->createVirtualKeyboard(seat.get()));
        ic->setFocusGroup(group_);
    }
}

void WaylandIMServerV2::add(WaylandIMInputContextV2 *ic, wayland::WlSeat *id) {
    icMap_[id] = ic;
}

void WaylandIMServerV2::remove(wayland::WlSeat *id) {
    auto iter = icMap_.find(id);
    if (iter != icMap_.end()) {
        icMap_.erase(iter);
    }
}

WaylandIMInputContextV2::WaylandIMInputContextV2(
    InputContextManager &inputContextManager, WaylandIMServerV2 *server,
    std::shared_ptr<wayland::WlSeat> seat, wayland::ZwpVirtualKeyboardV1 *vk)
    : InputContext(inputContextManager), server_(server),
      seat_(std::move(seat)),
      ic_(server->inputMethodManagerV2()->getInputMethod(seat_.get())),
      vk_(vk) {
    server->add(this, seat_.get());
    ic_->surroundingText().connect(
        [this](const char *text, uint32_t cursor, uint32_t anchor) {
            surroundingTextCallback(text, cursor, anchor);
        });
    ic_->activate().connect([this]() {
        WAYLANDIM_DEBUG() << "ACTIVATE";
        pendingActivate_ = true;
    });
    ic_->deactivate().connect([this]() {
        WAYLANDIM_DEBUG() << "DEACTIVATE";
        pendingDeactivate_ = true;
    });
    ic_->done().connect([this]() {
        WAYLANDIM_DEBUG() << "DONE";
        ++serial_;
        if (pendingDeactivate_) {
            pendingDeactivate_ = false;
            keyboardGrab_.reset();
            server_->display_->sync();
            focusOut();
        }
        if (pendingActivate_) {
            pendingActivate_ = false;
            keyboardGrab_.reset(ic_->grabKeyboard());
            if (!keyboardGrab_) {
                WAYLANDIM_DEBUG() << "Failed to grab keyboard";
            } else {
                keyboardGrab_->keymap().connect(
                    [this](uint32_t format, int32_t fd, uint32_t size) {
                        keymapCallback(format, fd, size);
                    });
                keyboardGrab_->key().connect([this](uint32_t serial,
                                                    uint32_t time, uint32_t key,
                                                    uint32_t state) {
                    keyCallback(serial, time, key, state);
                });
                keyboardGrab_->modifiers().connect(
                    [this](uint32_t serial, uint32_t mods_depressed,
                           uint32_t mods_latched, uint32_t mods_locked,
                           uint32_t group) {
                        modifiersCallback(serial, mods_depressed, mods_latched,
                                          mods_locked, group);
                    });
                keyboardGrab_->repeatInfo().connect(
                    [this](int32_t rate, int32_t delay) {
                        repeatInfoCallback(rate, delay);
                    });
                repeatInfoCallback(repeatRate_, repeatDelay_);
                focusIn();
                server_->display_->sync();
            }
        }
    });
    ic_->contentType().connect([this](uint32_t hint, uint32_t purpose) {
        WAYLANDIM_DEBUG() << "contentTypeCallback:" << hint << purpose;
        contentTypeCallback(hint, purpose);
    });
    ic_->textChangeCause().connect([this](uint32_t cause) {
        WAYLANDIM_DEBUG() << "TEXTCHANGECAUSE:" << cause << keyboardGrab_.get();
    });
    ic_->unavailable().connect([]() { WAYLANDIM_DEBUG() << "UNAVAILABLE"; });
    timeEvent_ = server_->instance()->eventLoop().addTimeEvent(
        CLOCK_MONOTONIC, now(CLOCK_MONOTONIC), 0,
        [this](EventSourceTime *, uint64_t) {
            repeat();
            return true;
        });
    timeEvent_->setEnabled(false);
    created();
}

WaylandIMInputContextV2::~WaylandIMInputContextV2() {
    server_->remove(seat_.get());
    destroy();
}

void WaylandIMInputContextV2::repeat() {
    KeyEvent event(
        this,
        Key(repeatSym_, server_->modifiers_ | KeyState::Repeat, repeatKey_ + 8),
        false, repeatTime_);
    if (!keyEvent(event)) {
        vk_->key(repeatTime_, event.rawKey().code() - 8,
                 event.isRelease() ? WL_KEYBOARD_KEY_STATE_RELEASED
                                   : WL_KEYBOARD_KEY_STATE_PRESSED);
    }

    timeEvent_->setNextInterval(1000000 / repeatRate_);
    timeEvent_->setOneShot();
    server_->display_->flush();
}

void WaylandIMInputContextV2::surroundingTextCallback(const char *text,
                                                      uint32_t cursor,
                                                      uint32_t anchor) {
    surroundingText().setText(text, cursor, anchor);
    updateSurroundingText();
}
void WaylandIMInputContextV2::resetCallback() { reset(ResetReason::Client); }
void WaylandIMInputContextV2::contentTypeCallback(uint32_t hint,
                                                  uint32_t purpose) {
    CapabilityFlags flags;

    if (hint & ZWP_TEXT_INPUT_V3_CONTENT_HINT_COMPLETION) {
        flags |= CapabilityFlag::WordCompletion;
    }
    if (hint & ZWP_TEXT_INPUT_V3_CONTENT_HINT_SPELLCHECK) {
        flags |= CapabilityFlag::SpellCheck;
    }
    if (hint & ZWP_TEXT_INPUT_V3_CONTENT_HINT_AUTO_CAPITALIZATION) {
        flags |= CapabilityFlag::UppercaseWords;
    }
    if (hint & ZWP_TEXT_INPUT_V3_CONTENT_HINT_LOWERCASE) {
        flags |= CapabilityFlag::Lowercase;
    }
    if (hint & ZWP_TEXT_INPUT_V3_CONTENT_HINT_UPPERCASE) {
        flags |= CapabilityFlag::Uppercase;
    }
    if (hint & ZWP_TEXT_INPUT_V3_CONTENT_HINT_TITLECASE) {
        // ??
    }
    if (hint & ZWP_TEXT_INPUT_V3_CONTENT_HINT_HIDDEN_TEXT) {
        flags |= CapabilityFlag::Password;
    }
    if (hint & ZWP_TEXT_INPUT_V3_CONTENT_HINT_SENSITIVE_DATA) {
        flags |= CapabilityFlag::Sensitive;
    }
    if (hint & ZWP_TEXT_INPUT_V3_CONTENT_HINT_LATIN) {
        flags |= CapabilityFlag::Alpha;
    }
    if (hint & ZWP_TEXT_INPUT_V3_CONTENT_HINT_MULTILINE) {
        flags |= CapabilityFlag::Multiline;
    }
    if (purpose == ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ALPHA) {
        flags |= CapabilityFlag::Alpha;
    }
    if (purpose == ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DIGITS) {
        flags |= CapabilityFlag::Digit;
    }
    if (purpose == ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NUMBER) {
        flags |= CapabilityFlag::Number;
    }
    if (purpose == ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PHONE) {
        flags |= CapabilityFlag::Dialable;
    }
    if (purpose == ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_URL) {
        flags |= CapabilityFlag::Url;
    }
    if (purpose == ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_EMAIL) {
        flags |= CapabilityFlag::Email;
    }
    if (purpose == ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NAME) {
        flags |= CapabilityFlag::Name;
    }
    if (purpose == ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PASSWORD) {
        flags |= CapabilityFlag::Password;
    }
    if (purpose == ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PIN) {
        flags |= CapabilityFlag::Password;
        flags |= CapabilityFlag::Digit;
    }
    if (purpose == ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DATE) {
        flags |= CapabilityFlag::Date;
    }
    if (purpose == ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TIME) {
        flags |= CapabilityFlag::Time;
    }
    if (purpose == ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DATETIME) {
        flags |= CapabilityFlag::Date;
        flags |= CapabilityFlag::Time;
    }
    if (purpose == ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TERMINAL) {
        flags |= CapabilityFlag::Terminal;
    }
    setCapabilityFlags(flags);
}

void WaylandIMInputContextV2::keymapCallback(uint32_t format, int32_t fd,
                                             uint32_t size) {
    WAYLANDIM_DEBUG() << "keymapCallback";
    if (!server_->context_) {
        server_->context_.reset(xkb_context_new(XKB_CONTEXT_NO_FLAGS));
        xkb_context_set_log_level(server_->context_.get(),
                                  XKB_LOG_LEVEL_CRITICAL);
    }

    UnixFD scopeFD = UnixFD::own(fd);

    if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) {
        return;
    }

    if (server_->keymap_) {
        server_->keymap_.reset();
    }

    auto *mapStr = mmap(nullptr, size, PROT_READ, MAP_SHARED, fd, 0);
    if (mapStr == MAP_FAILED) {
        return;
    }

    server_->keymap_.reset(xkb_keymap_new_from_string(
        server_->context_.get(), static_cast<const char *>(mapStr),
        XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS));

    munmap(mapStr, size);

    if (!server_->keymap_) {
        return;
    }

    server_->state_.reset(xkb_state_new(server_->keymap_.get()));
    if (!server_->state_) {
        server_->keymap_.reset();
        return;
    }

    server_->stateMask_.shift_mask =
        1 << xkb_keymap_mod_get_index(server_->keymap_.get(), "Shift");
    server_->stateMask_.lock_mask =
        1 << xkb_keymap_mod_get_index(server_->keymap_.get(), "Lock");
    server_->stateMask_.control_mask =
        1 << xkb_keymap_mod_get_index(server_->keymap_.get(), "Control");
    server_->stateMask_.mod1_mask =
        1 << xkb_keymap_mod_get_index(server_->keymap_.get(), "Mod1");
    server_->stateMask_.mod2_mask =
        1 << xkb_keymap_mod_get_index(server_->keymap_.get(), "Mod2");
    server_->stateMask_.mod3_mask =
        1 << xkb_keymap_mod_get_index(server_->keymap_.get(), "Mod3");
    server_->stateMask_.mod4_mask =
        1 << xkb_keymap_mod_get_index(server_->keymap_.get(), "Mod4");
    server_->stateMask_.mod5_mask =
        1 << xkb_keymap_mod_get_index(server_->keymap_.get(), "Mod5");
    server_->stateMask_.super_mask =
        1 << xkb_keymap_mod_get_index(server_->keymap_.get(), "Super");
    server_->stateMask_.hyper_mask =
        1 << xkb_keymap_mod_get_index(server_->keymap_.get(), "Hyper");
    server_->stateMask_.meta_mask =
        1 << xkb_keymap_mod_get_index(server_->keymap_.get(), "Meta");

    vk_->keymap(format, scopeFD.fd(), size);
}

void WaylandIMInputContextV2::keyCallback(uint32_t, uint32_t time, uint32_t key,
                                          uint32_t state) {
    time_ = time;
    if (!server_->state_) {
        return;
    }

    WAYLANDIM_DEBUG() << "RECEIVE KEY";
    // EVDEV OFFSET
    uint32_t code = key + 8;

    KeyEvent event(this,
                   Key(static_cast<KeySym>(xkb_state_key_get_one_sym(
                           server_->state_.get(), code)),
                       server_->modifiers_, code),
                   state == WL_KEYBOARD_KEY_STATE_RELEASED, time);

    if (state == WL_KEYBOARD_KEY_STATE_RELEASED && key == repeatKey_) {
        timeEvent_->setEnabled(false);
    } else if (state == WL_KEYBOARD_KEY_STATE_PRESSED &&
               xkb_keymap_key_repeats(server_->keymap_.get(), code)) {
        if (repeatRate_) {
            repeatKey_ = key;
            repeatTime_ = time;
            repeatSym_ = event.rawKey().sym();
            timeEvent_->setNextInterval(repeatDelay_ * 1000);
            timeEvent_->setOneShot();
        }
    }

    WAYLANDIM_DEBUG() << event.key().toString()
                      << " IsRelease=" << event.isRelease();
    if (!keyEvent(event)) {
        vk_->key(time, event.rawKey().code() - 8,
                 event.isRelease() ? WL_KEYBOARD_KEY_STATE_RELEASED
                                   : WL_KEYBOARD_KEY_STATE_PRESSED);
    }
    server_->display_->flush();
}
void WaylandIMInputContextV2::modifiersCallback(uint32_t,
                                                uint32_t mods_depressed,
                                                uint32_t mods_latched,
                                                uint32_t mods_locked,
                                                uint32_t group) {
    if (!server_->state_) {
        return;
    }

    xkb_mod_mask_t mask;

    xkb_state_update_mask(server_->state_.get(), mods_depressed, mods_latched,
                          mods_locked, 0, 0, group);
    server_->instance()->updateXkbStateMask(
        server_->group()->display(), mods_depressed, mods_latched, mods_locked);
    mask = xkb_state_serialize_mods(
        server_->state_.get(), static_cast<xkb_state_component>(
                                   XKB_STATE_DEPRESSED | XKB_STATE_LATCHED));

    server_->modifiers_ = 0;
    if (mask & server_->stateMask_.shift_mask) {
        server_->modifiers_ |= KeyState::Shift;
    }
    if (mask & server_->stateMask_.lock_mask) {
        server_->modifiers_ |= KeyState::CapsLock;
    }
    if (mask & server_->stateMask_.control_mask) {
        server_->modifiers_ |= KeyState::Ctrl;
    }
    if (mask & server_->stateMask_.mod1_mask) {
        server_->modifiers_ |= KeyState::Alt;
    }
    if (mask & server_->stateMask_.super_mask) {
        server_->modifiers_ |= KeyState::Super;
    }
    if (mask & server_->stateMask_.hyper_mask) {
        server_->modifiers_ |= KeyState::Hyper;
    }
    if (mask & server_->stateMask_.meta_mask) {
        server_->modifiers_ |= KeyState::Meta;
    }

    vk_->modifiers(mods_depressed, mods_depressed, mods_latched, group);
}

void WaylandIMInputContextV2::repeatInfoCallback(int32_t rate, int32_t delay) {
    repeatRate_ = rate;
    repeatDelay_ = delay;
    timeEvent_->setAccuracy(std::min(delay * 1000, 1000000 / rate));
}

void WaylandIMInputContextV2::updatePreeditImpl() {
    auto preedit =
        server_->instance()->outputFilter(this, inputPanel().clientPreedit());

    for (int i = 0, e = preedit.size(); i < e; i++) {
        if (!utf8::validate(preedit.stringAt(i))) {
            return;
        }
    }

    ic_->setPreeditString(preedit.toString().data(), preedit.cursor(),
                          preedit.cursor());
    ic_->commit(serial_);
}
} // namespace fcitx
