/*
 * SPDX-FileCopyrightText: 2012-2017 CSSlayer <wengxt@gmail.com>
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 */
#include "clipboard.h"
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <iterator>
#include <limits>
#include <memory>
#include <string>
#include <unordered_set>
#include <utility>
#include "fcitx-config/iniparser.h"
#include "fcitx-utils/event.h"
#include "fcitx-utils/eventloopinterface.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_p.h"
#include "fcitx-utils/utf8.h"
#include "fcitx/addonfactory.h"
#include "fcitx/addoninstance.h"
#include "fcitx/addonmanager.h"
#include "fcitx/candidatelist.h"
#include "fcitx/event.h"
#include "fcitx/inputcontext.h"
#include "fcitx/inputcontextmanager.h"
#include "fcitx/inputpanel.h"
#include "fcitx/instance.h"
#include "fcitx/tempmodemanager.h"
#include "fcitx/text.h"
#include "fcitx/userinterface.h"
#include "clipboardentry.h"
#include "clipboardtempmode.h"

#ifdef ENABLE_X11
#include <xcb/xcb.h>
#include "xcb_public.h"
#include "xcbclipboard.h"
#endif
#ifdef WAYLAND_FOUND
#include <wayland-client-core.h>
#include "wayland_public.h"
#include "waylandclipboard.h"
#endif

namespace fcitx {

namespace {

constexpr uint64_t oneSecond = 1000000ULL;
constexpr char threeDot[] = "\xe2\x80\xa6";

bool shouldClearPassword(const ClipboardEntry &entry, uint64_t life) {
    if (entry.passwordTimestamp == 0 || life == 0) {
        // Not password.
        return false;
    }
    // Allow 0.5 second skew.
    return (entry.passwordTimestamp + (life * oneSecond) - (oneSecond / 2)) <=
           now(CLOCK_MONOTONIC);
}

std::string ClipboardSelectionStrip(const std::string &text) {
    if (!utf8::validate(text)) {
        return text;
    }
    std::string result;
    result.reserve(text.size());
    auto iter = text.begin();
    constexpr int maxCharCount = 43;
    int count = 0;
    while (iter != text.end()) {
        auto next = utf8::nextChar(iter);
        if (std::distance(iter, next) == 1) {
            switch (*iter) {
            case '\t':
            case '\b':
            case '\f':
            case '\v':
                result += ' ';
                break;
            case '\n':
                result += "\xe2\x8f\x8e";
            case '\r':
                break;
            default:
                result += *iter;
                break;
            }
        } else {
            result.append(iter, next);
        }
        count++;
        if (count > maxCharCount) {
            result += threeDot;
            break;
        }

        iter = next;
    }
    return result;
}

} // namespace

FCITX_DEFINE_LOG_CATEGORY(clipboard_log, "clipboard");

class ClipboardCandidateWord : public CandidateWord {
public:
    ClipboardCandidateWord(Clipboard *q, const std::string &str, bool password)
        : q_(q), str_(str) {
        Text text;
        if (password && !*q->config().showPassword) {
            auto length = utf8::length(str);
            length = std::min(length, static_cast<size_t>(8));
            std::string dot;
            dot.reserve(length * 3);
            while (length != 0) {
                dot += "\xe2\x80\xa2";
                length -= 1;
            }
            text.append(dot);
            setComment(Text{_("<Password>")});
        } else {
            text.append(ClipboardSelectionStrip(str));
        }
        setText(std::move(text));
    }

    void select(InputContext *inputContext) const override {
        inputContext->commitString(str_);
        q_->reset(inputContext);
    }

    Clipboard *q_;
    std::string str_;
};

Clipboard::Clipboard(Instance *instance) : instance_(instance) {
#ifdef ENABLE_X11
    if (auto *xcb = this->xcb()) {
        xcbCreatedCallback_ =
            xcb->call<IXCBModule::addConnectionCreatedCallback>(
                [this](const std::string &name, xcb_connection_t *, int,
                       FocusGroup *) {
                    xcbClipboards_[name].reset(new XcbClipboard(this, name));
                });
        xcbClosedCallback_ = xcb->call<IXCBModule::addConnectionClosedCallback>(
            [this](const std::string &name, xcb_connection_t *) {
                xcbClipboards_.erase(name);
            });
    }
#endif
#ifdef WAYLAND_FOUND
    if (auto *wayland = this->wayland()) {
        waylandCreatedCallback_ =
            wayland->call<IWaylandModule::addConnectionCreatedCallback>(
                [this](const std::string &name, wl_display *display,
                       FocusGroup *) {
                    waylandClipboards_[name] =
                        std::make_unique<WaylandClipboard>(this, name, display);
                });
        waylandClosedCallback_ =
            wayland->call<IWaylandModule::addConnectionClosedCallback>(
                [this](const std::string &name, wl_display *) {
                    waylandClipboards_.erase(name);
                });
    }
#endif

    constexpr KeySym syms[] = {
        FcitxKey_1, FcitxKey_2, FcitxKey_3, FcitxKey_4, FcitxKey_5,
        FcitxKey_6, FcitxKey_7, FcitxKey_8, FcitxKey_9, FcitxKey_0,
    };

    KeyStates states = KeyState::NoState;

    for (auto sym : syms) {
        selectionKeys_.emplace_back(sym, states);
    }
    tempMode_ = std::make_unique<ClipboardTempMode>(this);
    instance_->tempModeManager().registerTempMode(*tempMode_);
    eventHandlers_.emplace_back(
        instance_->watchEvent<EventType::InputContextKeyEvent>(
            EventWatcherPhase::Default, [this](KeyEvent &keyEvent) {
                if (!keyEvent.isRelease() &&
                    keyEvent.key().checkKeyList(
                        config_.pastePrimaryKey.value())) {
                    keyEvent.inputContext()->commitString(
                        primary(keyEvent.inputContext()));
                    keyEvent.filterAndAccept();
                }
            }));
    clearPasswordTimer_ = instance_->eventLoop().addTimeEvent(
        CLOCK_MONOTONIC, now(CLOCK_MONOTONIC), 0,
        [this](EventSourceTime *, uint64_t) {
            refreshPasswordTimer();
            return true;
        });
    reloadConfig();
}

Clipboard::~Clipboard() {}

void Clipboard::reloadConfig() {
    readAsIni(config_, configFile);
    refreshPasswordTimer();
}

void Clipboard::clear() {
    history_.clear();
    primary_.clear();
}

void Clipboard::reset(InputContext *inputContext) {
    tempMode_->reset(inputContext);
}
void Clipboard::updateUI(InputContext *inputContext) {
    inputContext->inputPanel().reset();

    auto candidateList = std::make_unique<CommonCandidateList>();
    candidateList->setPageSize(instance_->globalConfig().defaultPageSize());

    // Append first item from history_.
    auto iter = history_.begin();
    if (iter != history_.end()) {
        candidateList->append<ClipboardCandidateWord>(this, iter->text,
                                                      iter->passwordTimestamp);
        iter++;
    }
    // Append primary_, but check duplication first.
    if (!primary_.empty()) {
        if (!history_.contains(primary_)) {
            candidateList->append<ClipboardCandidateWord>(
                this, primary_.text, primary_.passwordTimestamp);
        }
    }
    // If primary_ is appended, it might squeeze one space out.
    for (; iter != history_.end(); iter++) {
        if (candidateList->totalSize() >= config_.numOfEntries.value()) {
            break;
        }
        candidateList->append<ClipboardCandidateWord>(this, iter->text,
                                                      iter->passwordTimestamp);
    }
    candidateList->setSelectionKey(selectionKeys_);
    candidateList->setLayoutHint(CandidateLayoutHint::Vertical);

    Text auxUp(_("Clipboard (Press BackSpace/Delete to clear history):"));
    if (!candidateList->totalSize()) {
        Text auxDown(_("No clipboard history."));
        inputContext->inputPanel().setAuxDown(auxDown);
    } else {
        candidateList->setGlobalCursorIndex(0);
    }
    inputContext->inputPanel().setCandidateList(std::move(candidateList));
    inputContext->inputPanel().setAuxUp(auxUp);
    inputContext->updatePreedit();
    inputContext->updateUserInterface(UserInterfaceComponent::InputPanel);
}

void Clipboard::setPrimary(const std::string &name, const std::string &str) {
    setPrimaryV2(name, str, false);
}

void Clipboard::setClipboard(const std::string &name, const std::string &str) {
    setClipboardV2(name, str, false);
}

void Clipboard::setPrimaryV2(const std::string &name, const std::string &str,
                             bool password) {
    setPrimaryEntry(name,
                    ClipboardEntry{.text = str,
                                   .passwordTimestamp =
                                       (password ? now(CLOCK_MONOTONIC) : 0)});
}

void Clipboard::setClipboardV2(const std::string &name, const std::string &str,
                               bool password) {
    setClipboardEntry(
        name, ClipboardEntry{.text = str,
                             .passwordTimestamp =
                                 (password ? now(CLOCK_MONOTONIC) : 0)});
}

void Clipboard::setPrimaryEntry(const std::string &name, ClipboardEntry entry) {
    FCITX_UNUSED(name);
    if (!utf8::validate(entry.text)) {
        return;
    }
    primary_ = std::move(entry);
    if (entry.passwordTimestamp) {
        refreshPasswordTimer();
    }
}

void Clipboard::setClipboardEntry(const std::string &name,
                                  const ClipboardEntry &entry) {
    FCITX_UNUSED(name);
    if (entry.text.empty() || !utf8::validate(entry.text)) {
        return;
    }

    if (!history_.pushFront(entry)) {
        history_.moveToTop(entry);
    }
    if (history_.front().passwordTimestamp || entry.passwordTimestamp) {
        history_.front().passwordTimestamp = std::max(
            entry.passwordTimestamp, history_.front().passwordTimestamp);
    }
    while (!history_.empty() &&
           static_cast<int>(history_.size()) > config_.numOfEntries.value()) {
        history_.pop();
    }
    if (entry.passwordTimestamp) {
        refreshPasswordTimer();
    }
}

std::string Clipboard::primary(const InputContext * /*unused*/) const {
    // TODO: per ic
    return primary_.text;
}

std::string Clipboard::clipboard(const InputContext * /*unused*/) const {
    // TODO: per ic
    if (history_.empty()) {
        return "";
    }
    return history_.front().text;
}

void Clipboard::refreshPasswordTimer() {
    if (*config_.clearPasswordAfter == 0) {
        FCITX_CLIPBOARD_DEBUG() << "Disable Password Clearing Timer.";
        clearPasswordTimer_->setEnabled(false);
        return;
    }

    uint64_t minTimestamp = std::numeric_limits<uint64_t>::max();

    if (shouldClearPassword(primary_, *config_.clearPasswordAfter)) {
        FCITX_CLIPBOARD_DEBUG() << "Clear password in primary.";
        primary_.clear();
    } else if (primary_.passwordTimestamp) {
        minTimestamp = std::min(minTimestamp, primary_.passwordTimestamp);
    }

    // Not efficient, but we don't have lots of entries anyway.
    std::unordered_set<ClipboardEntry> needRemove;
    for (const auto &entry : history_) {
        if (shouldClearPassword(entry, *config_.clearPasswordAfter)) {
            needRemove.insert(entry);
        } else if (entry.passwordTimestamp) {
            minTimestamp = std::min(minTimestamp, entry.passwordTimestamp);
        }
    }
    FCITX_CLIPBOARD_DEBUG() << "Clear " << needRemove.size()
                            << " password(s) in clipboard history.";
    for (const auto &entry : needRemove) {
        history_.remove(entry);
    }

    if (minTimestamp != std::numeric_limits<uint64_t>::max()) {
        clearPasswordTimer_->setTime(
            minTimestamp + (oneSecond * (*config_.clearPasswordAfter)));
        FCITX_CLIPBOARD_DEBUG()
            << "Password Clearing Timer will be triggered after: "
            << clearPasswordTimer_->time() - now(CLOCK_MONOTONIC);
        clearPasswordTimer_->setOneShot();
    }
}

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

FCITX_ADDON_FACTORY_V2(clipboard, fcitx::ClipboardModuleFactory);
