/*
    ksmserver - the KDE session management server

    SPDX-FileCopyrightText: 2000 Matthias Ettrich <ettrich@kde.org>
    SPDX-FileCopyrightText: 2005 Lubos Lunak <l.lunak@kde.org>

    SPDX-FileContributor: Oswald Buddenhagen <ob6@inf.tu-dresden.de>

    some code taken from the dcopserver (part of the KDE libraries), which is
    SPDX-FileCopyrightText: 1999 Matthias Ettrich <ettrich@kde.org>
    SPDX-FileCopyrightText: 1999 Preston Brown <pbrown@kde.org>

    SPDX-License-Identifier: MIT
*/

#include <QDebug>
#include <QElapsedTimer>
#include <QGuiApplication>

#include <config-workspace.h>

#include <ksmserver_debug.h>

#ifdef HAVE_SYS_TIME_H
#include <sys/time.h>
#endif

#include "server.h"

#include <unistd.h>

#include <KSharedConfig>
#include <KX11Extras>
#include <kconfig.h>
#include <kconfiggroup.h>
#include <kshell.h>
#include <kwindowsystem.h>

#include <X11/Xatom.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <xcb/xcb.h>

/*
 * Legacy session management
 */

#ifndef NO_LEGACY_SESSION_MANAGEMENT
static WindowMap *windowMapPtr = nullptr;

static Atom wm_save_yourself = XNone;
static Atom wm_protocols = XNone;
static Atom wm_client_leader = XNone;
static Atom sm_client_id = XNone;

static int winsErrorHandler(Display *, XErrorEvent *ev)
{
    if (windowMapPtr) {
        WindowMap::Iterator it = windowMapPtr->find(ev->resourceid);
        if (it != windowMapPtr->end())
            (*it).type = SM_ERROR;
    }
    return 0;
}

void KSMServer::performLegacySessionSave()
{
    qCDebug(KSMSERVER) << "Saving legacy session apps";

    KSharedConfig::Ptr config = KSharedConfig::openConfig();
    config->reparseConfiguration(); // config may have changed in the KControl module
    KConfigGroup cg(config, QStringLiteral("General"));

    int wmSaveYourselfTimeout = cg.readEntry("legacySaveTimeoutSecs", 4) * 1000;

    // Setup error handler
    legacyWindows.clear();
    windowMapPtr = &legacyWindows;
    XErrorHandler oldHandler = XSetErrorHandler(winsErrorHandler);
    // Compute set of leader windows that need legacy session management
    // and determine which style (WM_COMMAND or WM_SAVE_YOURSELF)
    auto display = qGuiApp->nativeInterface<QNativeInterface::QX11Application>()->display();
    if (wm_save_yourself == (Atom)XNone) {
        Atom atoms[4];
        const char *const names[] = {"WM_SAVE_YOURSELF", "WM_PROTOCOLS", "WM_CLIENT_LEADER", "SM_CLIENT_ID"};
        XInternAtoms(display, const_cast<char **>(names), 4, False, atoms);
        wm_save_yourself = atoms[0];
        wm_protocols = atoms[1];
        wm_client_leader = atoms[2];
        sm_client_id = atoms[3];
    }
    const QList<WId> windows = KX11Extras::windows();
    for (QList<WId>::ConstIterator it = windows.begin(); it != windows.end(); ++it) {
        WId leader = windowWmClientLeader(*it);
        if (!legacyWindows.contains(leader) && windowSessionId(*it, leader).isEmpty()) {
            SMType wtype = SM_WMCOMMAND;
            int nprotocols = 0;
            Atom *protocols = nullptr;
            if (XGetWMProtocols(display, leader, &protocols, &nprotocols)) {
                for (int i = 0; i < nprotocols; i++)
                    if (protocols[i] == wm_save_yourself) {
                        wtype = SM_WMSAVEYOURSELF;
                        break;
                    }
                XFree((void *)protocols);
            }
            SMData data;
            data.type = wtype;
            XClassHint classHint;
            if (XGetClassHint(display, leader, &classHint)) {
                data.wmclass1 = QString::fromLocal8Bit(classHint.res_name);
                data.wmclass2 = QString::fromLocal8Bit(classHint.res_class);
                XFree(classHint.res_name);
                XFree(classHint.res_class);
            }
            legacyWindows.insert(leader, data);
        }
    }
    // Open fresh display for sending WM_SAVE_YOURSELF
    XSync(display, False);
    Display *newdisplay = XOpenDisplay(DisplayString(display));
    if (!newdisplay) {
        windowMapPtr = nullptr;
        XSetErrorHandler(oldHandler);
        return;
    }
    WId root = DefaultRootWindow(newdisplay);
    XGrabKeyboard(newdisplay, root, False, GrabModeAsync, GrabModeAsync, CurrentTime);
    XGrabPointer(newdisplay, root, False, Button1Mask | Button2Mask | Button3Mask, GrabModeAsync, GrabModeAsync, XNone, XNone, CurrentTime);
    // Send WM_SAVE_YOURSELF messages
    XEvent ev;
    int awaiting_replies = 0;
    for (WindowMap::Iterator it = legacyWindows.begin(); it != legacyWindows.end(); ++it) {
        if ((*it).type == SM_WMSAVEYOURSELF) {
            WId w = it.key();
            awaiting_replies += 1;
            memset(&ev, 0, sizeof(ev));
            ev.xclient.type = ClientMessage;
            ev.xclient.window = w;
            ev.xclient.message_type = wm_protocols;
            ev.xclient.format = 32;
            ev.xclient.data.l[0] = wm_save_yourself;
            ev.xclient.data.l[1] = XCB_CURRENT_TIME;
            XSelectInput(newdisplay, w, PropertyChangeMask | StructureNotifyMask);
            XSendEvent(newdisplay, w, False, 0, &ev);
            qCDebug(KSMSERVER) << "sent >save yourself< to legacy app " << (*it).wmclass1 << (*it).wmclass2;
        }
    }
    // Wait for change in WM_COMMAND with timeout
    XFlush(newdisplay);
    QElapsedTimer start;
    while (awaiting_replies > 0) {
        if (XPending(newdisplay)) {
            /* Process pending event */
            XNextEvent(newdisplay, &ev);
            if ((ev.xany.type == UnmapNotify) || (ev.xany.type == PropertyNotify && ev.xproperty.atom == XA_WM_COMMAND)) {
                WindowMap::Iterator it = legacyWindows.find(ev.xany.window);
                if (it != legacyWindows.end() && (*it).type != SM_WMCOMMAND) {
                    awaiting_replies -= 1;
                    if ((*it).type != SM_ERROR)
                        (*it).type = SM_WMCOMMAND;
                }
            }
        } else {
            /* Check timeout */
            int msecs = start.elapsed();
            if (msecs >= wmSaveYourselfTimeout) {
                qCDebug(KSMSERVER) << "legacy timeout expired";
                break;
            }
            /* Wait for more events */
            fd_set fds;
            FD_ZERO(&fds);
            int fd = ConnectionNumber(newdisplay);
            FD_SET(fd, &fds);
            struct timeval tmwait;
            tmwait.tv_sec = (wmSaveYourselfTimeout - msecs) / 1000;
            tmwait.tv_usec = ((wmSaveYourselfTimeout - msecs) % 1000) * 1000;
            ::select(fd + 1, &fds, nullptr, &fds, &tmwait);
        }
    }
    // Terminate work in new display
    XAllowEvents(newdisplay, ReplayPointer, CurrentTime);
    XAllowEvents(newdisplay, ReplayKeyboard, CurrentTime);
    XSync(newdisplay, False);
    XCloseDisplay(newdisplay);
    // Restore old error handler
    XSync(display, False);
    XSetErrorHandler(oldHandler);
    for (WindowMap::Iterator it = legacyWindows.begin(); it != legacyWindows.end(); ++it) {
        if ((*it).type != SM_ERROR) {
            WId w = it.key();
            (*it).wmCommand = windowWmCommand(w);
            (*it).wmClientMachine = windowWmClientMachine(w);
        }
    }
    qCDebug(KSMSERVER) << "Done saving " << legacyWindows.count() << " legacy session apps";
}

/*!
Stores legacy session management data
*/
void KSMServer::storeLegacySession(KConfig *config)
{
    // Write LegacySession data
    config->deleteGroup(QString(u"Legacy" + sessionGroup));
    KConfigGroup group(config, QString(u"Legacy" + sessionGroup));
    int count = 0;
    for (WindowMap::ConstIterator it = legacyWindows.constBegin(); it != legacyWindows.constEnd(); ++it) {
        if ((*it).type != SM_ERROR) {
            if (excludeApps.contains((*it).wmclass1.toLower()) || excludeApps.contains((*it).wmclass2.toLower()))
                continue;
            if (!(*it).wmCommand.isEmpty() && !(*it).wmClientMachine.isEmpty()) {
                count++;
                const QString n = QString::number(count);
                group.writeEntry(QString(u"command" + n), (*it).wmCommand);
                group.writeEntry(QString(u"clientMachine" + n), (*it).wmClientMachine);
            }
        }
    }
    group.writeEntry("count", count);
}

/*!
Restores legacy session management data (i.e. restart applications)
*/
void KSMServer::restoreLegacySession(KConfig *config)
{
    if (config->hasGroup(QString(u"Legacy" + sessionGroup))) {
        KConfigGroup group(config, QString(u"Legacy" + sessionGroup));
        restoreLegacySessionInternal(&group);
    }
}

void KSMServer::restoreLegacySessionInternal(KConfigGroup *config, char sep)
{
    int count = config->readEntry("count", 0);
    for (int i = 1; i <= count; i++) {
        QString n = QString::number(i);
        QStringList wmCommand = (sep == ',') ? // why is this named "wmCommand"?
            config->readEntry(QString(u"command" + n), QStringList())
                                             : KShell::splitArgs(config->readEntry(QString(u"command" + n), QString())); // close enough(?)
        if (wmCommand.isEmpty())
            continue;
        startApplication(wmCommand, config->readEntry(QString(u"clientMachine" + n), QString()), config->readEntry(QString(u"userId" + n), QString()));
    }
}

static QByteArray getQCStringProperty(WId w, Atom prop)
{
    Atom type;
    int format, status;
    unsigned long nitems = 0;
    unsigned long extra = 0;
    unsigned char *data = nullptr;
    auto display = qGuiApp->nativeInterface<QNativeInterface::QX11Application>()->display();
    QByteArray result = "";
    status = XGetWindowProperty(display, w, prop, 0, 10000, false, XA_STRING, &type, &format, &nitems, &extra, &data);
    if (status == Success) {
        if (data)
            result = (char *)data;
        XFree(data);
    }
    return result;
}

static QStringList getQStringListProperty(WId w, Atom prop)
{
    Atom type;
    int format, status;
    unsigned long nitems = 0;
    unsigned long extra = 0;
    unsigned char *data = nullptr;
    auto display = qGuiApp->nativeInterface<QNativeInterface::QX11Application>()->display();
    QStringList result;

    status = XGetWindowProperty(display, w, prop, 0, 10000, false, XA_STRING, &type, &format, &nitems, &extra, &data);
    if (status == Success) {
        if (!data)
            return result;
        for (int i = 0; i < (int)nitems; i++) {
            result << QLatin1String((const char *)data + i);
            while (data[i])
                i++;
        }
        XFree(data);
    }
    return result;
}

QStringList KSMServer::windowWmCommand(WId w)
{
    const QStringList ret = getQStringListProperty(w, XA_WM_COMMAND);
    // hacks here
    if (ret.count() == 1) {
        const QString &command = ret.constFirst();
        // Mozilla is launched using wrapper scripts, so it's launched using "mozilla",
        // but the actual binary is "mozilla-bin" or "<path>/mozilla-bin", and that's what
        // will be also in WM_COMMAND - using this "mozilla-bin" doesn't work at all though
        if (command.endsWith(QLatin1String("mozilla-bin")))
            return QStringList() << QStringLiteral("mozilla");
        if (command.endsWith(QLatin1String("firefox-bin")))
            return QStringList() << QStringLiteral("firefox");
        if (command.endsWith(QLatin1String("thunderbird-bin")))
            return QStringList() << QStringLiteral("thunderbird");
        if (command.endsWith(QLatin1String("sunbird-bin")))
            return QStringList() << QStringLiteral("sunbird");
        if (command.endsWith(QLatin1String("seamonkey-bin")))
            return QStringList() << QStringLiteral("seamonkey");
    }
    return ret;
}

QString KSMServer::windowWmClientMachine(WId w)
{
    QByteArray result = getQCStringProperty(w, XA_WM_CLIENT_MACHINE);
    if (result.isEmpty()) {
        result = "localhost";
    } else {
        // special name for the local machine (localhost)
        char hostnamebuf[80];
        if (gethostname(hostnamebuf, sizeof hostnamebuf) >= 0) {
            hostnamebuf[sizeof(hostnamebuf) - 1] = 0;
            if (result == hostnamebuf)
                result = "localhost";
            if (char *dot = strchr(hostnamebuf, '.')) {
                *dot = '\0';
                if (result == hostnamebuf)
                    result = "localhost";
            }
        }
    }
    return QLatin1String(result);
}

WId KSMServer::windowWmClientLeader(WId w)
{
    Atom type;
    int format, status;
    unsigned long nitems = 0;
    unsigned long extra = 0;
    unsigned char *data = nullptr;
    auto display = qGuiApp->nativeInterface<QNativeInterface::QX11Application>()->display();
    Window result = w;
    status = XGetWindowProperty(display, w, wm_client_leader, 0, 10000, false, XA_WINDOW, &type, &format, &nitems, &extra, &data);
    if (status == Success) {
        if (data && nitems > 0)
            result = *((Window *)data);
        XFree(data);
    }
    return result;
}

/*
Returns sessionId for this client,
taken either from its window or from the leader window.
*/
QByteArray KSMServer::windowSessionId(WId w, WId leader)
{
    QByteArray result = getQCStringProperty(w, sm_client_id);
    if (result.isEmpty() && leader != (WId)None && leader != w)
        result = getQCStringProperty(leader, sm_client_id);
    return result;
}
#endif
