/********************************************************************
 KWin - the KDE window manager
 This file is part of the KDE project.

Copyright (C) 1999, 2000 Matthias Ettrich <ettrich@kde.org>
Copyright (C) 2003 Lubos Lunak <l.lunak@kde.org>

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*********************************************************************/

#include "sm.h"

#include <unistd.h>
#include <stdlib.h>
#include <pwd.h>
#include <fixx11h.h>
#include <kconfig.h>
#include <kglobal.h>

#include "workspace.h"
#include "client.h"
#include <QDBusInterface>
#include <QDBusPendingCall>
#include <QSocketNotifier>
#include <QSessionManager>
#include <kdebug.h>
#ifdef KWIN_BUILD_TILING
#include "tiling/tiling.h"
#endif

namespace KWin
{

bool SessionManager::saveState(QSessionManager& sm)
{
    // If the session manager is ksmserver, save stacking
    // order, active window, active desktop etc. in phase 1,
    // as ksmserver assures no interaction will be done
    // before the WM finishes phase 1. Saving in phase 2 is
    // too late, as possible user interaction may change some things.
    // Phase2 is still needed though (ICCCM 5.2)
    char* sm_vendor = SmcVendor(static_cast< SmcConn >(sm.handle()));
    bool ksmserver = qstrcmp(sm_vendor, "KDE") == 0;
    free(sm_vendor);
    if (!sm.isPhase2()) {
        Workspace::self()->sessionSaveStarted();
        if (ksmserver)   // save stacking order etc. before "save file?" etc. dialogs change it
            Workspace::self()->storeSession(kapp->sessionConfig(), SMSavePhase0);
        sm.release(); // Qt doesn't automatically release in this case (bug?)
        sm.requestPhase2();
        return true;
    }
    Workspace::self()->storeSession(kapp->sessionConfig(), ksmserver ? SMSavePhase2 : SMSavePhase2Full);
    kapp->sessionConfig()->sync();
    return true;
}

// I bet this is broken, just like everywhere else in KDE
bool SessionManager::commitData(QSessionManager& sm)
{
    if (!sm.isPhase2())
        Workspace::self()->sessionSaveStarted();
    return true;
}

// Workspace

/*!
  Stores the current session in the config file

  \sa loadSessionInfo()
 */
void Workspace::storeSession(KConfig* config, SMSavePhase phase)
{
    KConfigGroup cg(config, "Session");
    int count =  0;
    int active_client = -1;

    if (phase == SMSavePhase2 || phase == SMSavePhase2Full) {
#ifdef KWIN_BUILD_TILING
        cg.writeEntry("tiling", m_tiling->isEnabled());
        if (m_tiling->isEnabled()) {
            kDebug(1212) << "Tiling was ON";
            m_tiling->setEnabled(false);
        }
#endif
    }

    for (ClientList::Iterator it = clients.begin(); it != clients.end(); ++it) {
        Client* c = (*it);
        QByteArray sessionId = c->sessionId();
        QByteArray wmCommand = c->wmCommand();
        if (sessionId.isEmpty())
            // remember also applications that are not XSMP capable
            // and use the obsolete WM_COMMAND / WM_SAVE_YOURSELF
            if (wmCommand.isEmpty())
                continue;
        count++;
        if (c->isActive())
            active_client = count;
        if (phase == SMSavePhase2 || phase == SMSavePhase2Full)
            storeClient(cg, count, c);
    }
    if (phase == SMSavePhase0) {
        // it would be much simpler to save these values to the config file,
        // but both Qt and KDE treat phase1 and phase2 separately,
        // which results in different sessionkey and different config file :(
        session_active_client = active_client;
        session_desktop = currentDesktop();
    } else if (phase == SMSavePhase2) {
        cg.writeEntry("count", count);
        cg.writeEntry("active", session_active_client);
        cg.writeEntry("desktop", session_desktop);
    } else { // SMSavePhase2Full
        cg.writeEntry("count", count);
        cg.writeEntry("active", session_active_client);
        cg.writeEntry("desktop", currentDesktop());
    }
}

void Workspace::storeClient(KConfigGroup &cg, int num, Client *c)
{
    c->setSessionInteract(false); //make sure we get the real values
    QString n = QString::number(num);
    cg.writeEntry(QString("sessionId") + n, c->sessionId().constData());
    cg.writeEntry(QString("windowRole") + n, c->windowRole().constData());
    cg.writeEntry(QString("wmCommand") + n, c->wmCommand().constData());
    cg.writeEntry(QString("wmClientMachine") + n, c->wmClientMachine(true).constData());
    cg.writeEntry(QString("resourceName") + n, c->resourceName().constData());
    cg.writeEntry(QString("resourceClass") + n, c->resourceClass().constData());
    cg.writeEntry(QString("geometry") + n, QRect(c->calculateGravitation(true), c->clientSize()));   // FRAME
    cg.writeEntry(QString("restore") + n, c->geometryRestore());
    cg.writeEntry(QString("fsrestore") + n, c->geometryFSRestore());
    cg.writeEntry(QString("maximize") + n, (int) c->maximizeMode());
    cg.writeEntry(QString("fullscreen") + n, (int) c->fullScreenMode());
    cg.writeEntry(QString("desktop") + n, c->desktop());
    // the config entry is called "iconified" for back. comp. reasons
    // (kconf_update script for updating session files would be too complicated)
    cg.writeEntry(QString("iconified") + n, c->isMinimized());
    cg.writeEntry(QString("opacity") + n, c->opacity());
    // the config entry is called "sticky" for back. comp. reasons
    cg.writeEntry(QString("sticky") + n, c->isOnAllDesktops());
    cg.writeEntry(QString("shaded") + n, c->isShade());
    // the config entry is called "staysOnTop" for back. comp. reasons
    cg.writeEntry(QString("staysOnTop") + n, c->keepAbove());
    cg.writeEntry(QString("keepBelow") + n, c->keepBelow());
    cg.writeEntry(QString("skipTaskbar") + n, c->skipTaskbar(true));
    cg.writeEntry(QString("skipPager") + n, c->skipPager());
    cg.writeEntry(QString("skipSwitcher") + n, c->skipSwitcher());
    // not really just set by user, but name kept for back. comp. reasons
    cg.writeEntry(QString("userNoBorder") + n, c->noBorder());
    cg.writeEntry(QString("windowType") + n, windowTypeToTxt(c->windowType()));
    cg.writeEntry(QString("shortcut") + n, c->shortcut().toString());
    cg.writeEntry(QString("stackingOrder") + n, unconstrained_stacking_order.indexOf(c));
    // KConfig doesn't support long so we need to live with less precision on 64-bit systems
    cg.writeEntry(QString("tabGroup") + n, static_cast<int>(reinterpret_cast<long>(c->tabGroup())));
    cg.writeEntry(QString("activities") + n, c->activities());
}

void Workspace::storeSubSession(const QString &name, QSet<QByteArray> sessionIds)
{
    //TODO clear it first
    KConfigGroup cg(KGlobal::config(), QString("SubSession: ") + name);
    int count =  0;
    int active_client = -1;
    for (ClientList::Iterator it = clients.begin(); it != clients.end(); ++it) {
        Client* c = (*it);
        QByteArray sessionId = c->sessionId();
        QByteArray wmCommand = c->wmCommand();
        if (sessionId.isEmpty())
            // remember also applications that are not XSMP capable
            // and use the obsolete WM_COMMAND / WM_SAVE_YOURSELF
            if (wmCommand.isEmpty())
                continue;
        if (!sessionIds.contains(sessionId))
            continue;

        kDebug() << "storing" << sessionId;
        count++;
        if (c->isActive())
            active_client = count;
        storeClient(cg, count, c);
    }
    cg.writeEntry("count", count);
    cg.writeEntry("active", active_client);
    //cg.writeEntry( "desktop", currentDesktop());
}

bool Workspace::stopActivity(const QString &id)
{
    if (sessionSaving()) {
        return false; //ksmserver doesn't queue requests (yet)
        //FIXME what about session *loading*?
    }

    //ugly hack to avoid dbus deadlocks
#ifdef KWIN_BUILD_ACTIVITIES
    updateActivityList(true, false);
#endif
    QMetaObject::invokeMethod(this, "reallyStopActivity", Qt::QueuedConnection, Q_ARG(QString, id));
    //then lie and assume it worked.
    return true;
}

void Workspace::reallyStopActivity(const QString &id)
{
    if (sessionSaving())
        return; //ksmserver doesn't queue requests (yet)

    kDebug() << id;

    QSet<QByteArray> saveSessionIds;
    QSet<QByteArray> dontCloseSessionIds;
    for (ClientList::const_iterator it = clients.constBegin(); it != clients.constEnd(); ++it) {
        const Client* c = (*it);
        const QByteArray sessionId = c->sessionId();
        if (sessionId.isEmpty()) {
            continue; //TODO support old wm_command apps too?
        }

        //kDebug() << sessionId;

        //if it's on the activity that's closing, it needs saving
        //but if a process is on some other open activity, I don't wanna close it yet
        //this is, of course, complicated by a process having many windows.
        if (c->isOnAllActivities()) {
            dontCloseSessionIds << sessionId;
            continue;
        }

        const QStringList activities = c->activities();
        foreach (const QString & activityId, activities) {
            if (activityId == id) {
                saveSessionIds << sessionId;
            } else if (openActivities_.contains(activityId)) {
                dontCloseSessionIds << sessionId;
            }
        }
    }

    storeSubSession(id, saveSessionIds);

    QStringList saveAndClose;
    QStringList saveOnly;
    foreach (const QByteArray & sessionId, saveSessionIds) {
        if (dontCloseSessionIds.contains(sessionId)) {
            saveOnly << sessionId;
        } else {
            saveAndClose << sessionId;
        }
    }

    kDebug() << "saveActivity" << id << saveAndClose << saveOnly;

    //pass off to ksmserver
    QDBusInterface ksmserver("org.kde.ksmserver", "/KSMServer", "org.kde.KSMServerInterface");
    if (ksmserver.isValid()) {
        ksmserver.asyncCall("saveSubSession", id, saveAndClose, saveOnly);
    } else {
        kDebug() << "couldn't get ksmserver interface";
    }
}

/*!
  Loads the session information from the config file.

  \sa storeSession()
 */
void Workspace::loadSessionInfo()
{
    session.clear();
    KConfigGroup cg(kapp->sessionConfig(), "Session");

#ifdef KWIN_BUILD_TILING
    m_tiling->setEnabled(cg.readEntry("tiling", false));
#endif

    addSessionInfo(cg);
}

void Workspace::addSessionInfo(KConfigGroup &cg)
{
    int count =  cg.readEntry("count", 0);
    int active_client = cg.readEntry("active", 0);
    for (int i = 1; i <= count; i++) {
        QString n = QString::number(i);
        SessionInfo* info = new SessionInfo;
        session.append(info);
        info->sessionId = cg.readEntry(QString("sessionId") + n, QString()).toLatin1();
        info->windowRole = cg.readEntry(QString("windowRole") + n, QString()).toLatin1();
        info->wmCommand = cg.readEntry(QString("wmCommand") + n, QString()).toLatin1();
        info->wmClientMachine = cg.readEntry(QString("wmClientMachine") + n, QString()).toLatin1();
        info->resourceName = cg.readEntry(QString("resourceName") + n, QString()).toLatin1();
        info->resourceClass = cg.readEntry(QString("resourceClass") + n, QString()).toLower().toLatin1();
        info->geometry = cg.readEntry(QString("geometry") + n, QRect());
        info->restore = cg.readEntry(QString("restore") + n, QRect());
        info->fsrestore = cg.readEntry(QString("fsrestore") + n, QRect());
        info->maximized = cg.readEntry(QString("maximize") + n, 0);
        info->fullscreen = cg.readEntry(QString("fullscreen") + n, 0);
        info->desktop = cg.readEntry(QString("desktop") + n, 0);
        info->minimized = cg.readEntry(QString("iconified") + n, false);
        info->opacity = cg.readEntry(QString("opacity") + n, 1.0);
        info->onAllDesktops = cg.readEntry(QString("sticky") + n, false);
        info->shaded = cg.readEntry(QString("shaded") + n, false);
        info->keepAbove = cg.readEntry(QString("staysOnTop") + n, false);
        info->keepBelow = cg.readEntry(QString("keepBelow") + n, false);
        info->skipTaskbar = cg.readEntry(QString("skipTaskbar") + n, false);
        info->skipPager = cg.readEntry(QString("skipPager") + n, false);
        info->skipSwitcher = cg.readEntry(QString("skipSwitcher") + n, false);
        info->noBorder = cg.readEntry(QString("userNoBorder") + n, false);
        info->windowType = txtToWindowType(cg.readEntry(QString("windowType") + n, QString()).toLatin1());
        info->shortcut = cg.readEntry(QString("shortcut") + n, QString());
        info->active = (active_client == i);
        info->stackingOrder = cg.readEntry(QString("stackingOrder") + n, -1);
        info->tabGroup = cg.readEntry(QString("tabGroup") + n, 0);
        info->tabGroupClient = NULL;
        info->activities = cg.readEntry(QString("activities") + n, QStringList());
    }
}

void Workspace::loadSubSessionInfo(const QString &name)
{
    KConfigGroup cg(KGlobal::config(), QString("SubSession: ") + name);
    addSessionInfo(cg);
}

bool Workspace::startActivity(const QString &id)
{
    if (sessionSaving()) {
        return false; //ksmserver doesn't queue requests (yet)
    }

    if (!allActivities_.contains(id)) {
        return false; //bogus id
    }

    loadSubSessionInfo(id);

    QDBusInterface ksmserver("org.kde.ksmserver", "/KSMServer", "org.kde.KSMServerInterface");
    if (ksmserver.isValid()) {
        ksmserver.asyncCall("restoreSubSession", id);
    } else {
        kDebug() << "couldn't get ksmserver interface";
        return false;
    }
    return true;
}

/*!
  Returns a SessionInfo for client \a c. The returned session
  info is removed from the storage. It's up to the caller to delete it.

  This function is called when a new window is mapped and must be managed.
  We try to find a matching entry in the session.

  May return 0 if there's no session info for the client.
 */
SessionInfo* Workspace::takeSessionInfo(Client* c)
{
    SessionInfo *realInfo = 0;
    QByteArray sessionId = c->sessionId();
    QByteArray windowRole = c->windowRole();
    QByteArray wmCommand = c->wmCommand();
    QByteArray wmClientMachine = c->wmClientMachine(true);
    QByteArray resourceName = c->resourceName();
    QByteArray resourceClass = c->resourceClass();

    // First search ``session''
    if (! sessionId.isEmpty()) {
        // look for a real session managed client (algorithm suggested by ICCCM)
        foreach (SessionInfo * info, session) {
            if (realInfo)
                break;
            if (info->sessionId == sessionId && sessionInfoWindowTypeMatch(c, info)) {
                if (! windowRole.isEmpty()) {
                    if (info->windowRole == windowRole) {
                        realInfo = info;
                        session.removeAll(info);
                    }
                } else {
                    if (info->windowRole.isEmpty()
                            && info->resourceName == resourceName
                            && info->resourceClass == resourceClass) {
                        realInfo = info;
                        session.removeAll(info);
                    }
                }
            }
        }
    } else {
        // look for a sessioninfo with matching features.
        foreach (SessionInfo * info, session) {
            if (realInfo)
                break;
            if (info->resourceName == resourceName
                    && info->resourceClass == resourceClass
                    && info->wmClientMachine == wmClientMachine
                    && sessionInfoWindowTypeMatch(c, info)) {
                if (wmCommand.isEmpty() || info->wmCommand == wmCommand) {
                    realInfo = info;
                    session.removeAll(info);
                }
            }
        }
    }

    // Set tabGroupClient for other clients in the same group
    if (realInfo && realInfo->tabGroup) {
        foreach (SessionInfo * info, session) {
            if (!info->tabGroupClient && info->tabGroup == realInfo->tabGroup)
                info->tabGroupClient = c;
        }
    }

    return realInfo;
}

bool Workspace::sessionInfoWindowTypeMatch(Client* c, SessionInfo* info)
{
    if (info->windowType == -2) {
        // undefined (not really part of NET::WindowType)
        return !c->isSpecialWindow();
    }
    return info->windowType == c->windowType();
}

static const char* const window_type_names[] = {
    "Unknown", "Normal" , "Desktop", "Dock", "Toolbar", "Menu", "Dialog",
    "Override", "TopMenu", "Utility", "Splash"
};
// change also the two functions below when adding new entries

const char* Workspace::windowTypeToTxt(NET::WindowType type)
{
    if (type >= NET::Unknown && type <= NET::Splash)
        return window_type_names[ type + 1 ]; // +1 (unknown==-1)
    if (type == -2)   // undefined (not really part of NET::WindowType)
        return "Undefined";
    kFatal(1212) << "Unknown Window Type" ;
    return NULL;
}

NET::WindowType Workspace::txtToWindowType(const char* txt)
{
    for (int i = NET::Unknown;
            i <= NET::Splash;
            ++i)
        if (qstrcmp(txt, window_type_names[ i + 1 ]) == 0)     // +1
            return static_cast< NET::WindowType >(i);
    return static_cast< NET::WindowType >(-2);   // undefined
}




// KWin's focus stealing prevention causes problems with user interaction
// during session save, as it prevents possible dialogs from getting focus.
// Therefore it's temporarily disabled during session saving. Start of
// session saving can be detected in SessionManager::saveState() above,
// but Qt doesn't have API for saying when session saved finished (either
// successfully, or was canceled). Therefore, create another connection
// to session manager, that will provide this information.
// Similarly the remember feature of window-specific settings should be disabled
// during KDE shutdown when windows may move e.g. because of Kicker going away
// (struts changing). When session saving starts, it can be cancelled, in which
// case the shutdown_cancelled callback is invoked, or it's a checkpoint that
// is immediatelly followed by save_complete, or finally it's a shutdown that
// is immediatelly followed by die callback. So getting save_yourself with shutdown
// set disables window-specific settings remembering, getting shutdown_cancelled
// re-enables, otherwise KWin will go away after die.
static void save_yourself(SmcConn conn_P, SmPointer ptr, int, Bool shutdown, int, Bool)
{
    SessionSaveDoneHelper* session = reinterpret_cast< SessionSaveDoneHelper* >(ptr);
    if (conn_P != session->connection())
        return;
    if (shutdown)
        Workspace::self()->disableRulesUpdates(true);
    SmcSaveYourselfDone(conn_P, True);
}

static void die(SmcConn conn_P, SmPointer ptr)
{
    SessionSaveDoneHelper* session = reinterpret_cast< SessionSaveDoneHelper* >(ptr);
    if (conn_P != session->connection())
        return;
    // session->saveDone(); we will quit anyway
    session->close();
}

static void save_complete(SmcConn conn_P, SmPointer ptr)
{
    SessionSaveDoneHelper* session = reinterpret_cast< SessionSaveDoneHelper* >(ptr);
    if (conn_P != session->connection())
        return;
    session->saveDone();
}

static void shutdown_cancelled(SmcConn conn_P, SmPointer ptr)
{
    SessionSaveDoneHelper* session = reinterpret_cast< SessionSaveDoneHelper* >(ptr);
    if (conn_P != session->connection())
        return;
    Workspace::self()->disableRulesUpdates(false);   // re-enable
    // no need to differentiate between successful finish and cancel
    session->saveDone();
}

void SessionSaveDoneHelper::saveDone()
{
    Workspace::self()->sessionSaveDone();
}

SessionSaveDoneHelper::SessionSaveDoneHelper()
{
    SmcCallbacks calls;
    calls.save_yourself.callback = save_yourself;
    calls.save_yourself.client_data = reinterpret_cast< SmPointer >(this);
    calls.die.callback = die;
    calls.die.client_data = reinterpret_cast< SmPointer >(this);
    calls.save_complete.callback = save_complete;
    calls.save_complete.client_data = reinterpret_cast< SmPointer >(this);
    calls.shutdown_cancelled.callback = shutdown_cancelled;
    calls.shutdown_cancelled.client_data = reinterpret_cast< SmPointer >(this);
    char* id = NULL;
    char err[ 11 ];
    conn = SmcOpenConnection(NULL, 0, 1, 0,
                             SmcSaveYourselfProcMask | SmcDieProcMask | SmcSaveCompleteProcMask
                             | SmcShutdownCancelledProcMask, &calls, NULL, &id, 10, err);
    if (id != NULL)
        free(id);
    if (conn == NULL)
        return; // no SM
    // set the required properties, mostly dummy values
    SmPropValue propvalue[ 5 ];
    SmProp props[ 5 ];
    propvalue[ 0 ].length = sizeof(unsigned char);
    unsigned char value0 = SmRestartNever; // so that this extra SM connection doesn't interfere
    propvalue[ 0 ].value = &value0;
    props[ 0 ].name = const_cast< char* >(SmRestartStyleHint);
    props[ 0 ].type = const_cast< char* >(SmCARD8);
    props[ 0 ].num_vals = 1;
    props[ 0 ].vals = &propvalue[ 0 ];
    struct passwd* entry = getpwuid(geteuid());
    propvalue[ 1 ].length = entry != NULL ? strlen(entry->pw_name) : 0;
    propvalue[ 1 ].value = (SmPointer)(entry != NULL ? entry->pw_name : "");
    props[ 1 ].name = const_cast< char* >(SmUserID);
    props[ 1 ].type = const_cast< char* >(SmARRAY8);
    props[ 1 ].num_vals = 1;
    props[ 1 ].vals = &propvalue[ 1 ];
    propvalue[ 2 ].length = 0;
    propvalue[ 2 ].value = (SmPointer)("");
    props[ 2 ].name = const_cast< char* >(SmRestartCommand);
    props[ 2 ].type = const_cast< char* >(SmLISTofARRAY8);
    props[ 2 ].num_vals = 1;
    props[ 2 ].vals = &propvalue[ 2 ];
    propvalue[ 3 ].length = strlen("kwinsmhelper");
    propvalue[ 3 ].value = (SmPointer)"kwinsmhelper";
    props[ 3 ].name = const_cast< char* >(SmProgram);
    props[ 3 ].type = const_cast< char* >(SmARRAY8);
    props[ 3 ].num_vals = 1;
    props[ 3 ].vals = &propvalue[ 3 ];
    propvalue[ 4 ].length = 0;
    propvalue[ 4 ].value = (SmPointer)("");
    props[ 4 ].name = const_cast< char* >(SmCloneCommand);
    props[ 4 ].type = const_cast< char* >(SmLISTofARRAY8);
    props[ 4 ].num_vals = 1;
    props[ 4 ].vals = &propvalue[ 4 ];
    SmProp* p[ 5 ] = { &props[ 0 ], &props[ 1 ], &props[ 2 ], &props[ 3 ], &props[ 4 ] };
    SmcSetProperties(conn, 5, p);
    notifier = new QSocketNotifier(IceConnectionNumber(SmcGetIceConnection(conn)),
                                   QSocketNotifier::Read, this);
    connect(notifier, SIGNAL(activated(int)), SLOT(processData()));
}

SessionSaveDoneHelper::~SessionSaveDoneHelper()
{
    close();
}

void SessionSaveDoneHelper::close()
{
    if (conn != NULL) {
        delete notifier;
        SmcCloseConnection(conn, 0, NULL);
    }
    conn = NULL;
}

void SessionSaveDoneHelper::processData()
{
    if (conn != NULL)
        IceProcessMessages(SmcGetIceConnection(conn), 0, 0);
}

void Workspace::sessionSaveDone()
{
    session_saving = false;
    //remove sessionInteract flag from all clients
    foreach (Client * c, clients) {
        c->setSessionInteract(false);
    }
}

} // namespace

#include "sm.moc"