/*
    This file is part of the KDE libraries
    SPDX-FileCopyrightText: 1999 Matthias Ettrich <ettrich@kde.org>
    SPDX-FileCopyrightText: 2007 Lubos Lunak <l.lunak@kde.org>
    SPDX-FileCopyrightText: 2014 Martin Gräßlin <mgraesslin@kde.org>

    SPDX-License-Identifier: LGPL-2.1-or-later
*/

#include "kx11extras.h"

// clang-format off
#include <kxerrorhandler_p.h>
#include <fixx11h.h>
#include <kxutils_p.h>
// clang-format on

#include "cptr_p.h"
#include "kwindowsystem.h"
#include "kwindowsystem_debug.h"
#include "kxcbevent_p.h"
#include "netwm.h"

#include <QAbstractNativeEventFilter>
#include <QGuiApplication>
#include <QMetaMethod>
#include <QRect>
#include <QScreen>
#include <private/qtx11extras_p.h>

#include <X11/Xatom.h>
#include <X11/Xutil.h>
#include <X11/extensions/Xfixes.h>
#include <xcb/xcb.h>
#include <xcb/xfixes.h>

// QPoint and QSize all have handy / operators which are useful for scaling, positions and sizes for high DPI support
// QRect does not, so we create one for internal purposes within this class
inline QRect operator/(const QRect &rectangle, qreal factor)
{
    return QRect(rectangle.topLeft() / factor, rectangle.size() / factor);
}

class MainThreadInstantiator : public QObject
{
    Q_OBJECT

public:
    MainThreadInstantiator(KX11Extras::FilterInfo _what);
    Q_INVOKABLE NETEventFilter *createNETEventFilter();

private:
    KX11Extras::FilterInfo m_what;
};

class NETEventFilter : public NETRootInfo, public QAbstractNativeEventFilter
{
public:
    NETEventFilter(KX11Extras::FilterInfo _what);
    ~NETEventFilter() override;
    void activate();
    QList<WId> windows;
    QList<WId> stackingOrder;

    struct StrutData {
        StrutData(WId window_, const NETStrut &strut_, int desktop_)
            : window(window_)
            , strut(strut_)
            , desktop(desktop_)
        {
        }
        WId window;
        NETStrut strut;
        int desktop;
    };
    QList<StrutData> strutWindows;
    QList<WId> possibleStrutWindows;
    bool strutSignalConnected;
    bool compositingEnabled;
    bool haveXfixes;
    KX11Extras::FilterInfo what;
    int xfixesEventBase;
    bool mapViewport();

    bool nativeEventFilter(const QByteArray &eventType, void *message, qintptr *) override;

    void updateStackingOrder();
    bool removeStrutWindow(WId);

protected:
    void addClient(xcb_window_t) override;
    void removeClient(xcb_window_t) override;

private:
    bool nativeEventFilter(xcb_generic_event_t *event);
    xcb_window_t winId;
    xcb_window_t m_appRootWindow;
};

static Atom net_wm_cm;
static void create_atoms();

static inline const QRect &displayGeometry()
{
    static QRect displayGeometry;
    static bool isDirty = true;

    if (isDirty) {
        static QList<QMetaObject::Connection> connections;
        auto dirtify = [&] {
            isDirty = true;
            for (const QMetaObject::Connection &con : std::as_const(connections)) {
                QObject::disconnect(con);
            }
            connections.clear();
        };

        QObject::connect(qApp, &QGuiApplication::screenAdded, dirtify);
        QObject::connect(qApp, &QGuiApplication::screenRemoved, dirtify);
        const QList<QScreen *> screenList = QGuiApplication::screens();
        QRegion region;
        for (int i = 0; i < screenList.count(); ++i) {
            const QScreen *screen = screenList.at(i);
            connections << QObject::connect(screen, &QScreen::geometryChanged, dirtify);
            const QRect geometry = screen->geometry();
            const qreal dpr = screen->devicePixelRatio();
            region += QRect(geometry.topLeft(), geometry.size() * dpr);
        }
        displayGeometry = region.boundingRect();
        isDirty = false;
    }

    return displayGeometry;
}

static inline int displayWidth()
{
    return displayGeometry().width();
}

static inline int displayHeight()
{
    return displayGeometry().height();
}

// clang-format off
static const NET::Properties windowsProperties = NET::ClientList | NET::ClientListStacking |
                                                 NET::Supported |
                                                 NET::NumberOfDesktops |
                                                 NET::DesktopGeometry |
                                                 NET::DesktopViewport |
                                                 NET::CurrentDesktop |
                                                 NET::DesktopNames |
                                                 NET::ActiveWindow |
                                                 NET::WorkArea;
static const NET::Properties2 windowsProperties2 = NET::WM2ShowingDesktop;

// ClientList and ClientListStacking is not per-window information, but a desktop information,
// so track it even with only INFO_BASIC
static const NET::Properties desktopProperties = NET::ClientList | NET::ClientListStacking |
                                                 NET::Supported |
                                                 NET::NumberOfDesktops |
                                                 NET::DesktopGeometry |
                                                 NET::DesktopViewport |
                                                 NET::CurrentDesktop |
                                                 NET::DesktopNames |
                                                 NET::ActiveWindow |
                                                 NET::WorkArea;
static const NET::Properties2 desktopProperties2 = NET::WM2ShowingDesktop;
// clang-format on

MainThreadInstantiator::MainThreadInstantiator(KX11Extras::FilterInfo _what)
    : QObject()
    , m_what(_what)
{
}

NETEventFilter *MainThreadInstantiator::createNETEventFilter()
{
    return new NETEventFilter(m_what);
}

NETEventFilter::NETEventFilter(KX11Extras::FilterInfo _what)
    : NETRootInfo(QX11Info::connection(),
                  _what >= KX11Extras::INFO_WINDOWS ? windowsProperties : desktopProperties,
                  _what >= KX11Extras::INFO_WINDOWS ? windowsProperties2 : desktopProperties2,
                  QX11Info::appScreen(),
                  false)
    , QAbstractNativeEventFilter()
    , strutSignalConnected(false)
    , compositingEnabled(false)
    , haveXfixes(false)
    , what(_what)
    , winId(XCB_WINDOW_NONE)
    , m_appRootWindow(QX11Info::appRootWindow())
{
    QCoreApplication::instance()->installNativeEventFilter(this);

    int errorBase;
    if ((haveXfixes = XFixesQueryExtension(QX11Info::display(), &xfixesEventBase, &errorBase))) {
        create_atoms();
        winId = xcb_generate_id(QX11Info::connection());
        uint32_t values[] = {true, XCB_EVENT_MASK_PROPERTY_CHANGE | XCB_EVENT_MASK_STRUCTURE_NOTIFY};
        xcb_create_window(QX11Info::connection(),
                          XCB_COPY_FROM_PARENT,
                          winId,
                          m_appRootWindow,
                          0,
                          0,
                          1,
                          1,
                          0,
                          XCB_WINDOW_CLASS_INPUT_ONLY,
                          XCB_COPY_FROM_PARENT,
                          XCB_CW_OVERRIDE_REDIRECT | XCB_CW_EVENT_MASK,
                          values);
        XFixesSelectSelectionInput(QX11Info::display(),
                                   winId,
                                   net_wm_cm,
                                   XFixesSetSelectionOwnerNotifyMask | XFixesSelectionWindowDestroyNotifyMask | XFixesSelectionClientCloseNotifyMask);
        compositingEnabled = XGetSelectionOwner(QX11Info::display(), net_wm_cm) != None;
    }
}

NETEventFilter::~NETEventFilter()
{
    if (QX11Info::connection() && winId != XCB_WINDOW_NONE) {
        xcb_destroy_window(QX11Info::connection(), winId);
        winId = XCB_WINDOW_NONE;
    }
}

// not virtual, but it's called directly only from init()
void NETEventFilter::activate()
{
    NETRootInfo::activate();
    updateStackingOrder();
}

bool NETEventFilter::nativeEventFilter(const QByteArray &eventType, void *message, qintptr *)
{
    if (eventType != "xcb_generic_event_t") {
        // only interested in XCB events of course
        return false;
    }
    return nativeEventFilter(reinterpret_cast<xcb_generic_event_t *>(message));
}

bool NETEventFilter::nativeEventFilter(xcb_generic_event_t *ev)
{
    KWindowSystem *s_q = KWindowSystem::self();
    const uint8_t eventType = ev->response_type & ~0x80;

    if (eventType == xfixesEventBase + XCB_XFIXES_SELECTION_NOTIFY) {
        xcb_xfixes_selection_notify_event_t *event = reinterpret_cast<xcb_xfixes_selection_notify_event_t *>(ev);
        if (event->window == winId) {
            bool haveOwner = event->owner != XCB_WINDOW_NONE;
            if (compositingEnabled != haveOwner) {
                compositingEnabled = haveOwner;
                Q_EMIT KX11Extras::self()->compositingChanged(compositingEnabled);
            }
            return true;
        }
        // Qt compresses XFixesSelectionNotifyEvents without caring about the actual window
        // gui/kernel/qapplication_x11.cpp
        // until that can be assumed fixed, we also react on events on the root (caused by Qts own compositing tracker)
        if (event->window == m_appRootWindow) {
            if (event->selection == net_wm_cm) {
                bool haveOwner = event->owner != XCB_WINDOW_NONE;
                if (compositingEnabled != haveOwner) {
                    compositingEnabled = haveOwner;
                    Q_EMIT KX11Extras::self()->compositingChanged(compositingEnabled);
                }
                // NOTICE this is not our event, we just randomly captured it from Qt -> pass on
                return false;
            }
        }
        return false;
    }

    xcb_window_t eventWindow = XCB_WINDOW_NONE;
    switch (eventType) {
    case XCB_CLIENT_MESSAGE:
        eventWindow = reinterpret_cast<xcb_client_message_event_t *>(ev)->window;
        break;
    case XCB_PROPERTY_NOTIFY:
        eventWindow = reinterpret_cast<xcb_property_notify_event_t *>(ev)->window;
        break;
    case XCB_CONFIGURE_NOTIFY:
        eventWindow = reinterpret_cast<xcb_configure_notify_event_t *>(ev)->window;
        break;
    }

    if (eventWindow == m_appRootWindow) {
        int old_current_desktop = currentDesktop();
        xcb_window_t old_active_window = activeWindow();
        int old_number_of_desktops = numberOfDesktops();
        bool old_showing_desktop = showingDesktop();
        NET::Properties props;
        NET::Properties2 props2;
        NETRootInfo::event(ev, &props, &props2);

        if ((props & CurrentDesktop) && currentDesktop() != old_current_desktop) {
            Q_EMIT KX11Extras::self()->currentDesktopChanged(currentDesktop());
        }
        if ((props & DesktopViewport) && mapViewport() && currentDesktop() != old_current_desktop) {
            Q_EMIT KX11Extras::self()->currentDesktopChanged(currentDesktop());
        }
        if ((props & ActiveWindow) && activeWindow() != old_active_window) {
            Q_EMIT KX11Extras::self()->activeWindowChanged(activeWindow());
        }
        if (props & DesktopNames) {
            Q_EMIT KX11Extras::self()->desktopNamesChanged();
        }
        if ((props & NumberOfDesktops) && numberOfDesktops() != old_number_of_desktops) {
            Q_EMIT KX11Extras::self()->numberOfDesktopsChanged(numberOfDesktops());
        }
        if ((props & DesktopGeometry) && mapViewport() && numberOfDesktops() != old_number_of_desktops) {
            Q_EMIT KX11Extras::self()->numberOfDesktopsChanged(numberOfDesktops());
        }
        if (props & WorkArea) {
            Q_EMIT KX11Extras::self()->workAreaChanged();
        }
        if (props & ClientListStacking) {
            updateStackingOrder();
            Q_EMIT KX11Extras::self()->stackingOrderChanged();
        }
        if ((props2 & WM2ShowingDesktop) && showingDesktop() != old_showing_desktop) {
            Q_EMIT s_q->showingDesktopChanged(showingDesktop());
        }
    } else if (windows.contains(eventWindow)) {
        NETWinInfo ni(QX11Info::connection(), eventWindow, m_appRootWindow, NET::Properties(), NET::Properties2());
        NET::Properties dirtyProperties;
        NET::Properties2 dirtyProperties2;
        ni.event(ev, &dirtyProperties, &dirtyProperties2);
        if (eventType == XCB_PROPERTY_NOTIFY) {
            xcb_property_notify_event_t *event = reinterpret_cast<xcb_property_notify_event_t *>(ev);
            if (event->atom == XCB_ATOM_WM_HINTS) {
                dirtyProperties |= NET::WMIcon; // support for old icons
            } else if (event->atom == XCB_ATOM_WM_NAME) {
                dirtyProperties |= NET::WMName; // support for old name
            } else if (event->atom == XCB_ATOM_WM_ICON_NAME) {
                dirtyProperties |= NET::WMIconName; // support for old iconic name
            }
        }
        if (mapViewport() && (dirtyProperties & (NET::WMState | NET::WMGeometry))) {
            /* geometry change -> possible viewport change
             * state change -> possible NET::Sticky change
             */
            dirtyProperties |= NET::WMDesktop;
        }
        if ((dirtyProperties & NET::WMStrut) != 0) {
            removeStrutWindow(eventWindow);
            if (!possibleStrutWindows.contains(eventWindow)) {
                possibleStrutWindows.append(eventWindow);
            }
        }
        if (dirtyProperties || dirtyProperties2) {
            Q_EMIT KX11Extras::self()->windowChanged(eventWindow, dirtyProperties, dirtyProperties2);

            if ((dirtyProperties & NET::WMStrut) != 0) {
                Q_EMIT KX11Extras::self()->strutChanged();
            }
        }
    }

    return false;
}

bool NETEventFilter::removeStrutWindow(WId w)
{
    for (QList<StrutData>::Iterator it = strutWindows.begin(); it != strutWindows.end(); ++it) {
        if ((*it).window == w) {
            strutWindows.erase(it);
            return true;
        }
    }
    return false;
}

void NETEventFilter::updateStackingOrder()
{
    stackingOrder.clear();
    for (int i = 0; i < clientListStackingCount(); i++) {
        stackingOrder.append(clientListStacking()[i]);
    }
}

void NETEventFilter::addClient(xcb_window_t w)
{
    if ((what >= KX11Extras::INFO_WINDOWS)) {
        xcb_connection_t *c = QX11Info::connection();
        UniqueCPointer<xcb_get_window_attributes_reply_t> attr(xcb_get_window_attributes_reply(c, xcb_get_window_attributes_unchecked(c, w), nullptr));

        uint32_t events = XCB_EVENT_MASK_PROPERTY_CHANGE | XCB_EVENT_MASK_STRUCTURE_NOTIFY;
        if (attr) {
            events = events | attr->your_event_mask;
        }
        xcb_change_window_attributes(c, w, XCB_CW_EVENT_MASK, &events);
    }

    bool emit_strutChanged = false;

    if (strutSignalConnected) {
        NETWinInfo info(QX11Info::connection(), w, QX11Info::appRootWindow(), NET::WMStrut | NET::WMDesktop, NET::Properties2());
        NETStrut strut = info.strut();
        if (strut.left || strut.top || strut.right || strut.bottom) {
            strutWindows.append(StrutData(w, strut, info.desktop()));
            emit_strutChanged = true;
        }
    } else {
        possibleStrutWindows.append(w);
    }

    windows.append(w);
    Q_EMIT KX11Extras::self()->windowAdded(w);
    if (emit_strutChanged) {
        Q_EMIT KX11Extras::self()->strutChanged();
    }
}

void NETEventFilter::removeClient(xcb_window_t w)
{
    bool emit_strutChanged = removeStrutWindow(w);
    if (strutSignalConnected && possibleStrutWindows.contains(w)) {
        NETWinInfo info(QX11Info::connection(), w, QX11Info::appRootWindow(), NET::WMStrut, NET::Properties2());
        NETStrut strut = info.strut();
        if (strut.left || strut.top || strut.right || strut.bottom) {
            emit_strutChanged = true;
        }
    }

    possibleStrutWindows.removeAll(w);
    windows.removeAll(w);
    Q_EMIT KX11Extras::self()->windowRemoved(w);
    if (emit_strutChanged) {
        Q_EMIT KX11Extras::self()->strutChanged();
    }
}

bool NETEventFilter::mapViewport()
{
    // compiz claims support even though it doesn't use virtual desktops :(
    //    if( isSupported( NET::DesktopViewport ) && !isSupported( NET::NumberOfDesktops ))

    // this test is duplicated in KWindowSystem::mapViewport()
    if (isSupported(NET::DesktopViewport) && numberOfDesktops(true) <= 1
        && (desktopGeometry().width > displayWidth() || desktopGeometry().height > displayHeight())) {
        return true;
    }
    return false;
}

static bool atoms_created = false;

static Atom _wm_protocols;
static Atom _wm_change_state;
static Atom kwm_utf8_string;

static void create_atoms()
{
    if (!atoms_created) {
        const int max = 20;
        Atom *atoms[max];
        const char *names[max];
        Atom atoms_return[max];
        int n = 0;

        atoms[n] = &_wm_protocols;
        names[n++] = "WM_PROTOCOLS";

        atoms[n] = &_wm_change_state;
        names[n++] = "WM_CHANGE_STATE";

        atoms[n] = &kwm_utf8_string;
        names[n++] = "UTF8_STRING";

        char net_wm_cm_name[100];
        sprintf(net_wm_cm_name, "_NET_WM_CM_S%d", QX11Info::appScreen());
        atoms[n] = &net_wm_cm;
        names[n++] = net_wm_cm_name;

        // we need a const_cast for the shitty X API
        XInternAtoms(QX11Info::display(), const_cast<char **>(names), n, false, atoms_return);
        for (int i = 0; i < n; i++) {
            *atoms[i] = atoms_return[i];
        }

        atoms_created = True;
    }
}

#define CHECK_X11                                                                                                                                              \
    if (!KWindowSystem::isPlatformX11()) {                                                                                                                     \
        qCWarning(LOG_KWINDOWSYSTEM) << Q_FUNC_INFO << "may only be used on X11";                                                                              \
        return {};                                                                                                                                             \
    }

#define CHECK_X11_VOID                                                                                                                                         \
    if (!KWindowSystem::isPlatformX11()) {                                                                                                                     \
        qCWarning(LOG_KWINDOWSYSTEM) << Q_FUNC_INFO << "may only be used on X11";                                                                              \
        return;                                                                                                                                                \
    }

// WARNING
// you have to call s_d_func() again after calling this function if you want a valid pointer!
void KX11Extras::init(FilterInfo what)
{
    NETEventFilter *const s_d = s_d_func();

    if (what >= INFO_WINDOWS) {
        what = INFO_WINDOWS;
    } else {
        what = INFO_BASIC;
    }

    if (!s_d || s_d->what < what) {
        const bool wasCompositing = s_d ? s_d->compositingEnabled : false;
        MainThreadInstantiator instantiator(what);
        NETEventFilter *filter;
        if (instantiator.thread() == QCoreApplication::instance()->thread()) {
            filter = instantiator.createNETEventFilter();
        } else {
            // the instantiator is not in the main app thread, which implies
            // we are being called in a thread that is not the main app thread
            // so we move the instantiator to the main app thread and invoke
            // the method with a blocking call
            instantiator.moveToThread(QCoreApplication::instance()->thread());
            QMetaObject::invokeMethod(&instantiator, "createNETEventFilter", Qt::BlockingQueuedConnection, Q_RETURN_ARG(NETEventFilter *, filter));
        }
        d.reset(filter);
        d->activate();
        if (wasCompositing != s_d_func()->compositingEnabled) {
            Q_EMIT KX11Extras::self()->compositingChanged(s_d_func()->compositingEnabled);
        }
    }
}

KX11Extras *KX11Extras::self()
{
    static KX11Extras instance;
    return &instance;
}

QList<WId> KX11Extras::windows()
{
    CHECK_X11
    KX11Extras::self()->init(INFO_BASIC);
    return KX11Extras::self()->s_d_func()->windows;
}

bool KX11Extras::hasWId(WId w)
{
    CHECK_X11
    return windows().contains(w);
}

QList<WId> KX11Extras::stackingOrder()
{
    CHECK_X11
    KX11Extras::self()->init(INFO_BASIC);
    return KX11Extras::self()->s_d_func()->stackingOrder;
}

WId KX11Extras::activeWindow()
{
    CHECK_X11
    NETEventFilter *const s_d = KX11Extras::self()->s_d_func();
    if (s_d) {
        return s_d->activeWindow();
    }
    NETRootInfo info(QX11Info::connection(), NET::ActiveWindow, NET::Properties2(), QX11Info::appScreen());
    return info.activeWindow();
}

void KX11Extras::activateWindow(WId win, long time)
{
    CHECK_X11_VOID
    NETRootInfo info(QX11Info::connection(), NET::Properties(), NET::Properties2(), QX11Info::appScreen());
    if (time == 0) {
        time = QX11Info::appUserTime();
    }
    info.setActiveWindow(win, NET::FromApplication, time, QGuiApplication::focusWindow() ? QGuiApplication::focusWindow()->winId() : 0);
}

void KX11Extras::forceActiveWindow(WId win, long time)
{
    CHECK_X11_VOID
    NETRootInfo info(QX11Info::connection(), NET::Properties(), NET::Properties2(), QX11Info::appScreen());
    if (time == 0) {
        time = QX11Info::appTime();
    }
    info.setActiveWindow(win, NET::FromTool, time, 0);
}

void KX11Extras::forceActiveWindow(QWindow *win, long time)
{
    CHECK_X11_VOID
    forceActiveWindow(win->winId(), time);
}

bool KX11Extras::compositingActive()
{
    CHECK_X11
    KX11Extras::self()->init(INFO_BASIC);
    if (KX11Extras::self()->s_d_func()->haveXfixes) {
        return KX11Extras::self()->s_d_func()->compositingEnabled;
    } else {
        create_atoms();
        return XGetSelectionOwner(QX11Info::display(), net_wm_cm);
    }
}

int KX11Extras::currentDesktop()
{
    CHECK_X11
    if (!QX11Info::connection()) {
        return 1;
    }

    if (mapViewport()) {
        KX11Extras::self()->init(INFO_BASIC);
        NETEventFilter *const s_d = KX11Extras::self()->s_d_func();
        NETPoint p = s_d->desktopViewport(s_d->currentDesktop(true));
        return KX11Extras::self()->viewportToDesktop(QPoint(p.x, p.y) / qApp->devicePixelRatio());
    }

    NETEventFilter *const s_d = KX11Extras::self()->s_d_func();
    if (s_d) {
        return s_d->currentDesktop(true);
    }
    NETRootInfo info(QX11Info::connection(), NET::CurrentDesktop, NET::Properties2(), QX11Info::appScreen());
    return info.currentDesktop(true);
}

int KX11Extras::numberOfDesktops()
{
    CHECK_X11
    if (!QX11Info::connection()) {
        return 1;
    }

    if (mapViewport()) {
        KX11Extras::self()->init(INFO_BASIC);
        NETEventFilter *const s_d = KX11Extras::self()->s_d_func();
        NETSize s = s_d->desktopGeometry();
        return s.width / displayWidth() * s.height / displayHeight();
    }

    NETEventFilter *const s_d = KX11Extras::self()->s_d_func();
    if (s_d) {
        return s_d->numberOfDesktops(true);
    }
    NETRootInfo info(QX11Info::connection(), NET::NumberOfDesktops, NET::Properties2(), QX11Info::appScreen());
    return info.numberOfDesktops(true);
}

void KX11Extras::setCurrentDesktop(int desktop)
{
    CHECK_X11_VOID
    if (mapViewport()) {
        KX11Extras::self()->init(INFO_BASIC);
        NETEventFilter *const s_d = KX11Extras::self()->s_d_func();
        NETRootInfo info(QX11Info::connection(), NET::Properties(), NET::Properties2(), QX11Info::appScreen());
        QPoint pos = KX11Extras::self()->desktopToViewport(desktop, true);
        NETPoint p;
        p.x = pos.x();
        p.y = pos.y();
        info.setDesktopViewport(s_d->currentDesktop(true), p);
        return;
    }
    NETRootInfo info(QX11Info::connection(), NET::Properties(), NET::Properties2(), QX11Info::appScreen());
    info.setCurrentDesktop(desktop, true);
}

void KX11Extras::setOnAllDesktops(WId win, bool b)
{
    CHECK_X11_VOID
    if (mapViewport()) {
        if (b) {
            setState(win, NET::Sticky);
        } else {
            clearState(win, NET::Sticky);
        }
        return;
    }
    NETWinInfo info(QX11Info::connection(), win, QX11Info::appRootWindow(), NET::WMDesktop, NET::Properties2());
    if (b) {
        info.setDesktop(NETWinInfo::OnAllDesktops, true);
    } else if (info.desktop(true) == NETWinInfo::OnAllDesktops) {
        NETRootInfo rinfo(QX11Info::connection(), NET::CurrentDesktop, NET::Properties2(), QX11Info::appScreen());
        info.setDesktop(rinfo.currentDesktop(true), true);
    }
}

void KX11Extras::setOnDesktop(WId win, int desktop)
{
    CHECK_X11_VOID
    if (mapViewport()) {
        if (desktop == NET::OnAllDesktops) {
            return setOnAllDesktops(win, true);
        } else {
            clearState(win, NET::Sticky);
        }
        KX11Extras::self()->init(INFO_BASIC);
        QPoint p = KX11Extras::self()->desktopToViewport(desktop, false);
        Window dummy;
        int x;
        int y;
        unsigned int w;
        unsigned int h;
        unsigned int b;
        unsigned int dp;
        XGetGeometry(QX11Info::display(), win, &dummy, &x, &y, &w, &h, &b, &dp);
        // get global position
        XTranslateCoordinates(QX11Info::display(), win, QX11Info::appRootWindow(), 0, 0, &x, &y, &dummy);
        x += w / 2; // center
        y += h / 2;
        // transform to coordinates on the current "desktop"
        x = x % displayWidth();
        y = y % displayHeight();
        if (x < 0) {
            x = x + displayWidth();
        }
        if (y < 0) {
            y = y + displayHeight();
        }
        x += p.x(); // move to given "desktop"
        y += p.y();
        x -= w / 2; // from center back to topleft
        y -= h / 2;
        p = KX11Extras::self()->constrainViewportRelativePosition(QPoint(x, y));
        int flags = (NET::FromTool << 12) | (0x03 << 8) | 10; // from tool(?), x/y, static gravity
        NETEventFilter *const s_d = KX11Extras::self()->s_d_func();
        s_d->moveResizeWindowRequest(win, flags, p.x(), p.y(), w, h);
        return;
    }
    NETWinInfo info(QX11Info::connection(), win, QX11Info::appRootWindow(), NET::WMDesktop, NET::Properties2());
    info.setDesktop(desktop, true);
}

void KX11Extras::setOnActivities(WId win, const QStringList &activities)
{
    CHECK_X11_VOID
    NETWinInfo info(QX11Info::connection(), win, QX11Info::appRootWindow(), NET::Properties(), NET::WM2Activities);
    info.setActivities(activities.join(QLatin1Char(',')).toLatin1().constData());
}

QPixmap KX11Extras::icon(WId win, int width, int height, bool scale)
{
    CHECK_X11
    return icon(win, width, height, scale, NETWM | WMHints | ClassHint | XApp);
}

QPixmap iconFromNetWinInfo(int width, int height, bool scale, int flags, NETWinInfo *info)
{
    QPixmap result;
    if (!info) {
        return result;
    }
    if (flags & KX11Extras::NETWM) {
        NETIcon ni = info->icon(width, height);
        if (ni.data && ni.size.width > 0 && ni.size.height > 0) {
            QImage img((uchar *)ni.data, (int)ni.size.width, (int)ni.size.height, QImage::Format_ARGB32);
            if (scale && width > 0 && height > 0 && img.size() != QSize(width, height) && !img.isNull()) {
                img = img.scaled(width, height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
            }
            if (!img.isNull()) {
                result = QPixmap::fromImage(img);
            }
            return result;
        }
    }

    if (flags & KX11Extras::WMHints) {
        xcb_pixmap_t p = info->icccmIconPixmap();
        xcb_pixmap_t p_mask = info->icccmIconPixmapMask();

        if (p != XCB_PIXMAP_NONE) {
            QPixmap pm = KXUtils::createPixmapFromHandle(info->xcbConnection(), p, p_mask);
            if (scale && width > 0 && height > 0 && !pm.isNull() //
                && (pm.width() != width || pm.height() != height)) {
                result = QPixmap::fromImage(pm.toImage().scaled(width, height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
            } else {
                result = pm;
            }
        }
    }

    // Since width can be any arbitrary size, but the icons cannot,
    // take the nearest value for best results (ignoring 22 pixel
    // icons as they don't exist for apps):
    int iconWidth;
    if (width < 24) {
        iconWidth = 16;
    } else if (width < 40) {
        iconWidth = 32;
    } else if (width < 56) {
        iconWidth = 48;
    } else if (width < 96) {
        iconWidth = 64;
    } else if (width < 192) {
        iconWidth = 128;
    } else {
        iconWidth = 256;
    }

    if (flags & KX11Extras::ClassHint) {
        // Try to load the icon from the classhint if the app didn't specify
        // its own:
        if (result.isNull()) {
            const QIcon icon = QIcon::fromTheme(QString::fromUtf8(info->windowClassClass()).toLower());
            const QPixmap pm = icon.isNull() ? QPixmap() : icon.pixmap(iconWidth, iconWidth);
            if (scale && !pm.isNull()) {
                result = QPixmap::fromImage(pm.toImage().scaled(width, height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
            } else {
                result = pm;
            }
        }
    }

    if (flags & KX11Extras::XApp) {
        // If the icon is still a null pixmap, load the icon for X applications
        // as a last resort:
        if (result.isNull()) {
            const QIcon icon = QIcon::fromTheme(QStringLiteral("xorg"));
            const QPixmap pm = icon.isNull() ? QPixmap() : icon.pixmap(iconWidth, iconWidth);
            if (scale && !pm.isNull()) {
                result = QPixmap::fromImage(pm.toImage().scaled(width, height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
            } else {
                result = pm;
            }
        }
    }
    return result;
}

QPixmap KX11Extras::icon(WId win, int width, int height, bool scale, int flags)
{
    CHECK_X11
    NETWinInfo info(QX11Info::connection(), win, QX11Info::appRootWindow(), NET::WMIcon, NET::WM2WindowClass | NET::WM2IconPixmap);
    return iconFromNetWinInfo(width, height, scale, flags, &info);
}

QPixmap KX11Extras::icon(WId win, int width, int height, bool scale, int flags, NETWinInfo *info)
{
    // No CHECK_X11 here, kwin_wayland calls this to get the icon for XWayland windows
    width *= qGuiApp->devicePixelRatio();
    height *= qGuiApp->devicePixelRatio();

    if (info) {
        return iconFromNetWinInfo(width, height, scale, flags, info);
    }
    CHECK_X11

    NETWinInfo newInfo(QX11Info::connection(), win, QX11Info::appRootWindow(), NET::WMIcon, NET::WM2WindowClass | NET::WM2IconPixmap);

    return iconFromNetWinInfo(width, height, scale, flags, &newInfo);
}

// enum values for ICCCM 4.1.2.4 and 4.1.4, defined to not depend on xcb-icccm
enum {
    _ICCCM_WM_STATE_WITHDRAWN = 0,
    _ICCCM_WM_STATE_NORMAL = 1,
    _ICCCM_WM_STATE_ICONIC = 3,
};

void KX11Extras::minimizeWindow(WId win)
{
    CHECK_X11_VOID
    create_atoms();
    // as described in ICCCM 4.1.4
    KXcbEvent<xcb_client_message_event_t> ev;
    ev.response_type = XCB_CLIENT_MESSAGE;
    ev.window = win;
    ev.type = _wm_change_state;
    ev.format = 32;
    ev.data.data32[0] = _ICCCM_WM_STATE_ICONIC;
    ev.data.data32[1] = 0;
    ev.data.data32[2] = 0;
    ev.data.data32[3] = 0;
    ev.data.data32[4] = 0;
    xcb_send_event(QX11Info::connection(),
                   false,
                   QX11Info::appRootWindow(),
                   XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT,
                   ev.buffer());
}

void KX11Extras::unminimizeWindow(WId win)
{
    CHECK_X11_VOID
    xcb_map_window(QX11Info::connection(), win);
}

QRect KX11Extras::workArea(int desktop)
{
    CHECK_X11
    KX11Extras::self()->init(INFO_BASIC);
    int desk = (desktop > 0 && desktop <= (int)KX11Extras::self()->s_d_func()->numberOfDesktops()) ? desktop : currentDesktop();
    if (desk <= 0) {
        return displayGeometry() / qApp->devicePixelRatio();
    }

    NETRect r = KX11Extras::self()->s_d_func()->workArea(desk);
    if (r.size.width <= 0 || r.size.height <= 0) { // not set
        return displayGeometry() / qApp->devicePixelRatio();
    }

    return QRect(r.pos.x, r.pos.y, r.size.width, r.size.height) / qApp->devicePixelRatio();
}

QRect KX11Extras::workArea(const QList<WId> &exclude, int desktop)
{
    CHECK_X11
    KX11Extras::self()->init(INFO_WINDOWS); // invalidates s_d_func's return value
    NETEventFilter *const s_d = KX11Extras::self()->s_d_func();

    QRect all = displayGeometry();
    QRect a = all;

    if (desktop == -1) {
        desktop = s_d->currentDesktop();
    }

    QList<WId>::ConstIterator it1;
    for (it1 = s_d->windows.constBegin(); it1 != s_d->windows.constEnd(); ++it1) {
        if (exclude.contains(*it1)) {
            continue;
        }

        // Kicker (very) extensively calls this function, causing hundreds of roundtrips just
        // to repeatedly find out struts of all windows. Therefore strut values for strut
        // windows are cached here.
        NETStrut strut;
        auto it2 = s_d->strutWindows.begin();
        for (; it2 != s_d->strutWindows.end(); ++it2) {
            if ((*it2).window == *it1) {
                break;
            }
        }

        if (it2 != s_d->strutWindows.end()) {
            if (!((*it2).desktop == desktop || (*it2).desktop == NETWinInfo::OnAllDesktops)) {
                continue;
            }

            strut = (*it2).strut;
        } else if (s_d->possibleStrutWindows.contains(*it1)) {
            NETWinInfo info(QX11Info::connection(), (*it1), QX11Info::appRootWindow(), NET::WMStrut | NET::WMDesktop, NET::Properties2());
            strut = info.strut();
            s_d->possibleStrutWindows.removeAll(*it1);
            s_d->strutWindows.append(NETEventFilter::StrutData(*it1, info.strut(), info.desktop()));

            if (!(info.desktop() == desktop || info.desktop() == NETWinInfo::OnAllDesktops)) {
                continue;
            }
        } else {
            continue; // not a strut window
        }

        QRect r = all;
        if (strut.left > 0) {
            r.setLeft(r.left() + (int)strut.left);
        }
        if (strut.top > 0) {
            r.setTop(r.top() + (int)strut.top);
        }
        if (strut.right > 0) {
            r.setRight(r.right() - (int)strut.right);
        }
        if (strut.bottom > 0) {
            r.setBottom(r.bottom() - (int)strut.bottom);
        }

        a = a.intersected(r);
    }
    return a / qApp->devicePixelRatio();
}

QString KX11Extras::desktopName(int desktop)
{
    CHECK_X11
    KX11Extras::self()->init(INFO_BASIC);
    NETEventFilter *const s_d = KX11Extras::self()->s_d_func();

    bool isDesktopSane = (desktop > 0 && desktop <= (int)s_d->numberOfDesktops());
    const char *name = s_d->desktopName(isDesktopSane ? desktop : currentDesktop());

    if (name && name[0]) {
        return QString::fromUtf8(name);
    }

    return KWindowSystem::tr("Desktop %1").arg(desktop);
}

void KX11Extras::setDesktopName(int desktop, const QString &name)
{
    CHECK_X11_VOID
    NETEventFilter *const s_d = KX11Extras::self()->s_d_func();

    if (desktop <= 0 || desktop > (int)numberOfDesktops()) {
        desktop = currentDesktop();
    }

    if (s_d) {
        s_d->setDesktopName(desktop, name.toUtf8().constData());
        return;
    }

    NETRootInfo info(QX11Info::connection(), NET::Properties(), NET::Properties2(), QX11Info::appScreen());
    info.setDesktopName(desktop, name.toUtf8().constData());
}

QString KX11Extras::readNameProperty(WId win, unsigned long atom)
{
    CHECK_X11
    XTextProperty tp;
    char **text = nullptr;
    int count;
    QString result;
    if (XGetTextProperty(QX11Info::display(), win, &tp, atom) != 0 && tp.value != nullptr) {
        create_atoms();

        if (tp.encoding == kwm_utf8_string) {
            result = QString::fromUtf8((const char *)tp.value);
        } else if (XmbTextPropertyToTextList(QX11Info::display(), &tp, &text, &count) == Success && text != nullptr && count > 0) {
            result = QString::fromLocal8Bit(text[0]);
        } else if (tp.encoding == XA_STRING) {
            result = QString::fromLocal8Bit((const char *)tp.value);
        }
        if (text != nullptr) {
            XFreeStringList(text);
        }
        XFree(tp.value);
    }
    return result;
}

bool KX11Extras::mapViewport()
{
    CHECK_X11
    NETEventFilter *const s_d = KX11Extras::self()->s_d_func();
    if (s_d) {
        return s_d->mapViewport();
    }

    // Handle case of not having a QGuiApplication
    if (!QX11Info::connection()) {
        return false;
    }

    // avoid creating KWindowSystemPrivate
    NETRootInfo infos(QX11Info::connection(), NET::Supported, NET::Properties2(), QX11Info::appScreen());
    if (!infos.isSupported(NET::DesktopViewport)) {
        return false;
    }
    NETRootInfo info(QX11Info::connection(), NET::NumberOfDesktops | NET::CurrentDesktop | NET::DesktopGeometry, NET::Properties2(), QX11Info::appScreen());
    if (info.numberOfDesktops(true) <= 1 && (info.desktopGeometry().width > displayWidth() || info.desktopGeometry().height > displayHeight())) {
        return true;
    }
    return false;
}

int KX11Extras::viewportWindowToDesktop(const QRect &rect)
{
    CHECK_X11
    const QRect r = rect / qApp->devicePixelRatio();

    KX11Extras::self()->init(INFO_BASIC);
    NETEventFilter *const s_d = KX11Extras::self()->s_d_func();
    QPoint p = r.center();
    // make absolute
    p = QPoint(p.x() + s_d->desktopViewport(s_d->currentDesktop(true)).x, p.y() + s_d->desktopViewport(s_d->currentDesktop(true)).y);
    NETSize s = s_d->desktopGeometry();
    QSize vs(displayWidth(), displayHeight());
    int xs = s.width / vs.width();
    int x = p.x() < 0 ? 0 : p.x() >= s.width ? xs - 1 : p.x() / vs.width();
    int ys = s.height / vs.height();
    int y = p.y() < 0 ? 0 : p.y() >= s.height ? ys - 1 : p.y() / vs.height();
    return y * xs + x + 1;
}

void KX11Extras::setExtendedStrut(WId win,
                                  qreal left_width,
                                  qreal left_start,
                                  qreal left_end,
                                  qreal right_width,
                                  qreal right_start,
                                  qreal right_end,
                                  qreal top_width,
                                  qreal top_start,
                                  qreal top_end,
                                  qreal bottom_width,
                                  qreal bottom_start,
                                  qreal bottom_end)
{
    CHECK_X11_VOID
    const qreal dpr = qApp->devicePixelRatio();

    NETWinInfo info(QX11Info::connection(), win, QX11Info::appRootWindow(), NET::Properties(), NET::Properties2());
    NETExtendedStrut strut;
    strut.left_width = std::lround(left_width * dpr);
    strut.right_width = std::lround(right_width * dpr);
    strut.top_width = std::lround(top_width * dpr);
    strut.bottom_width = std::lround(bottom_width * dpr);
    strut.left_start = std::lround(left_start * dpr);
    strut.left_end = std::lround(left_end * dpr);
    strut.right_start = std::lround(right_start * dpr);
    strut.right_end = std::lround(right_end * dpr);
    strut.top_start = std::lround(top_start * dpr);
    strut.top_end = std::lround(top_end * dpr);
    strut.bottom_start = std::lround(bottom_start * dpr);
    strut.bottom_end = std::lround(bottom_end * dpr);
    info.setExtendedStrut(strut);
    NETStrut oldstrut;
    oldstrut.left = std::lround(left_width * dpr);
    oldstrut.right = std::lround(right_width * dpr);
    oldstrut.top = std::lround(top_width * dpr);
    oldstrut.bottom = std::lround(bottom_width * dpr);
    info.setStrut(oldstrut);
}

void KX11Extras::setStrut(WId win, qreal left, qreal right, qreal top, qreal bottom)
{
    CHECK_X11_VOID
    const qreal dpr = qApp->devicePixelRatio();

    int w = displayWidth();
    int h = displayHeight();
    setExtendedStrut(win,
                     std::lround(left * dpr),
                     0,
                     std::lround(left * dpr) != 0 ? w : 0,
                     std::lround(right * dpr),
                     0,
                     std::lround(right * dpr) != 0 ? w : 0,
                     std::lround(top * dpr),
                     0,
                     std::lround(top * dpr) != 0 ? h : 0,
                     std::lround(bottom * dpr),
                     0,
                     std::lround(bottom * dpr) != 0 ? h : 0);
}

// optimalization - create private only when needed and only for what is needed
void KX11Extras::connectNotify(const QMetaMethod &signal)
{
    CHECK_X11_VOID
    FilterInfo what = INFO_BASIC;
    if (signal == QMetaMethod::fromSignal(&KX11Extras::workAreaChanged)) {
        what = INFO_WINDOWS;
    } else if (signal == QMetaMethod::fromSignal(&KX11Extras::strutChanged)) {
        what = INFO_WINDOWS;
    } else if (signal == QMetaMethod::fromSignal(&KX11Extras::windowChanged)) {
        what = INFO_WINDOWS;
    }

    init(what);
    NETEventFilter *const s_d = s_d_func();
    if (!s_d->strutSignalConnected && signal == QMetaMethod::fromSignal(&KX11Extras::strutChanged)) {
        s_d->strutSignalConnected = true;
    }
    QObject::connectNotify(signal);
}

void KX11Extras::setType(WId win, NET::WindowType windowType)
{
    CHECK_X11_VOID
    NETWinInfo info(QX11Info::connection(), win, QX11Info::appRootWindow(), NET::Properties(), NET::Properties2());
    info.setWindowType(windowType);
}

void KX11Extras::setState(WId win, NET::States state)
{
    CHECK_X11_VOID
    NETWinInfo info(QX11Info::connection(), win, QX11Info::appRootWindow(), NET::WMState, NET::Properties2());
    info.setState(state, state);
}

void KX11Extras::clearState(WId win, NET::States state)
{
    CHECK_X11_VOID
    NETWinInfo info(QX11Info::connection(), win, QX11Info::appRootWindow(), NET::WMState, NET::Properties2());
    info.setState(NET::States(), state);
}

int KX11Extras::viewportToDesktop(const QPoint &p)
{
    CHECK_X11
    KX11Extras::self()->init(INFO_BASIC);
    NETEventFilter *const s_d = KX11Extras::self()->s_d_func();
    NETSize s = s_d->desktopGeometry();
    QSize vs(displayWidth(), displayHeight());
    int xs = s.width / vs.width();
    int x = p.x() < 0 ? 0 : p.x() >= s.width ? xs - 1 : p.x() / vs.width();
    int ys = s.height / vs.height();
    int y = p.y() < 0 ? 0 : p.y() >= s.height ? ys - 1 : p.y() / vs.height();
    return y * xs + x + 1;
}

QPoint KX11Extras::constrainViewportRelativePosition(const QPoint &pos)
{
    CHECK_X11
    init(INFO_BASIC);
    NETEventFilter *const s_d = s_d_func();
    NETSize s = s_d->desktopGeometry();
    NETPoint c = s_d->desktopViewport(s_d->currentDesktop(true));
    int x = (pos.x() + c.x) % s.width;
    int y = (pos.y() + c.y) % s.height;
    if (x < 0) {
        x += s.width;
    }
    if (y < 0) {
        y += s.height;
    }
    return QPoint(x - c.x, y - c.y);
}

QPoint KX11Extras::desktopToViewport(int desktop, bool absolute)
{
    CHECK_X11
    init(INFO_BASIC);
    NETEventFilter *const s_d = s_d_func();
    NETSize s = s_d->desktopGeometry();
    QSize vs(displayWidth(), displayHeight());
    int xs = s.width / vs.width();
    int ys = s.height / vs.height();
    if (desktop <= 0 || desktop > xs * ys) {
        return QPoint(0, 0);
    }
    --desktop;
    QPoint ret(vs.width() * (desktop % xs), vs.height() * (desktop / xs));
    if (!absolute) {
        ret = QPoint(ret.x() - s_d->desktopViewport(s_d->currentDesktop(true)).x, ret.y() - s_d->desktopViewport(s_d->currentDesktop(true)).y);
        if (ret.x() >= s.width) {
            ret.setX(ret.x() - s.width);
        }
        if (ret.x() < 0) {
            ret.setX(ret.x() + s.width);
        }
        if (ret.y() >= s.height) {
            ret.setY(ret.y() - s.height);
        }
        if (ret.y() < 0) {
            ret.setY(ret.y() + s.height);
        }
    }
    return ret;
}

bool KX11Extras::showingDesktop()
{
    KX11Extras::self()->init(INFO_BASIC);
    return KX11Extras::self()->s_d_func()->showingDesktop();
}

void KX11Extras::setShowingDesktop(bool showing)
{
    NETRootInfo info(QX11Info::connection(), NET::Properties(), NET::WM2ShowingDesktop, QX11Info::appScreen());
    info.setShowingDesktop(showing);
}

#include "kx11extras.moc"
#include "moc_kx11extras.cpp"
