/*****************************************************************

Copyright 2008 Christian Mollekopf <chrigi_1@hotmail.com>
Copyright (C) 2011 Craig Drummond <craig@kde.org>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

******************************************************************/

// Own
#include "taskitem.h"

#include <kactivities/info.h>
#include <KConfig>
#include <KConfigGroup>
#include <KDebug>
#include <KDesktopFile>
#include <KService>
#include <KServiceTypeTrader>
#include <KStandardDirs>
#include <ksysguard/processcore/processes.h>
#include <ksysguard/processcore/process.h>

#include <QtCore/QFile>
#include <QtCore/QTimer>

#include "groupmanager.h"

namespace TaskManager
{


class TaskItem::Private
{
public:
    Private()
        : checkedForLauncher(false) {
    }

    QWeakPointer<Task> task;
    QWeakPointer<Startup> startupTask;
    KUrl launcherUrl;
    bool checkedForLauncher;
    QString taskName;
};


TaskItem::TaskItem(QObject *parent, Task *task)
    : AbstractGroupableItem(parent),
      d(new Private)
{
    setTaskPointer(task);
}


TaskItem::TaskItem(QObject *parent, Startup *task)
    : AbstractGroupableItem(parent),
      d(new Private)
{
    d->startupTask = task;
    connect(task, SIGNAL(changed(::TaskManager::TaskChanges)), this, SIGNAL(changed(::TaskManager::TaskChanges)));
    connect(task, SIGNAL(destroyed(QObject*)), this, SLOT(taskDestroyed())); //this item isn't useful anymore if the Task was closed
}

TaskItem::~TaskItem()
{
    emit destroyed(this);
    //kDebug();
    /*  if (parentGroup()) {
          parentGroup()->remove(this);
      }*/
    delete d;
}

void TaskItem::taskDestroyed()
{
    d->startupTask.clear();
    d->task.clear();
    // FIXME: due to a bug in Qt 4.x, the event loop reference count is incorrect
    // when going through x11EventFilter .. :/ so we have to singleShot the deleteLater
    QTimer::singleShot(0, this, SLOT(deleteLater()));
}

void TaskItem::setTaskPointer(Task *task)
{
    const bool differentTask = d->task.data() != task;

    if (d->startupTask) {
        disconnect(d->startupTask.data(), 0, this, 0);
        d->startupTask.clear();
    } else if (differentTask) {
        // if we aren't moving from startup -> task and the task pointer is changing on us
        // let's clear the launcher url
        d->launcherUrl.clear();
    }

    if (differentTask) {
        if (d->task) {
            disconnect(d->task.data(), 0, this, 0);
        }

        d->task = task;

        if (task) {
            connect(task, SIGNAL(changed(::TaskManager::TaskChanges)), this, SIGNAL(changed(::TaskManager::TaskChanges)));
            connect(task, SIGNAL(destroyed(QObject*)), this, SLOT(taskDestroyed()));
            emit gotTaskPointer();
        }
    }

    if (!d->task) {
        // FIXME: due to a bug in Qt 4.x, the event loop reference count is incorrect
        // when going through x11EventFilter .. :/ so we have to singleShot the deleteLater
        QTimer::singleShot(0, this, SLOT(deleteLater()));
    }
}

Task *TaskItem::task() const
{
    return d->task.data();
}

Startup *TaskItem::startup() const
{
    /*
    if (d->startupTask.isNull()) {
        kDebug() << "pointer is Null";
    }
    */
    return d->startupTask.data();
}

bool TaskItem::isStartupItem() const
{
    return d->startupTask;
}

WindowList TaskItem::winIds() const
{
    if (!d->task) {
        kDebug() << "no winId: probably startup task";
        return WindowList();
    }
    WindowList list;
    list << d->task.data()->window();
    return list;
}

QIcon TaskItem::icon() const
{
    if (d->task) {
        return d->task.data()->icon();
    }

    if (d->startupTask) {
        return d->startupTask.data()->icon();
    }

    return QIcon();
}

QString TaskItem::name() const
{
    if (d->task) {
        return d->task.data()->visibleName();
    }

    if (d->startupTask) {
        return d->startupTask.data()->text();
    }

    return QString();
}

QString TaskItem::taskName() const
{
    if (d->taskName.isEmpty()) {
        KUrl lUrl = launcherUrl();

        if (!lUrl.isEmpty() && lUrl.isLocalFile() && KDesktopFile::isDesktopFile(lUrl.toLocalFile())) {
            KDesktopFile f(lUrl.toLocalFile());

            if (f.tryExec()) {
                d->taskName = f.readName();
            }
        }
        if (d->taskName.isEmpty() && d->task) {
            d->taskName = d->task.data()->classClass().toLower();
        }
    }

    return d->taskName;
}

QStringList TaskItem::activities() const
{
    if (!task()) {
        return QStringList();
    }

    return task()->activities();
}

QStringList TaskItem::activityNames(bool includeCurrent) const
{
    if (!task()) {
        return QStringList();
    }

    QStringList names;

    QStringList activities = task()->activities();
    if (!includeCurrent) {
        activities.removeOne(TaskManager::self()->currentActivity());
    }

    Q_FOREACH(QString activity, activities) {
        KActivities::Info info(activity);
        if (info.state() != KActivities::Info::Invalid) {
            names << info.name();
        }
    }

    return names;
}

ItemType TaskItem::itemType() const
{
    return TaskItemType;
}

bool TaskItem::isGroupItem() const
{
    return false;
}

void TaskItem::setShaded(bool state)
{
    if (!d->task) {
        return;
    }
    d->task.data()->setShaded(state);
}

void TaskItem::toggleShaded()
{
    if (!d->task) {
        return;
    }
    d->task.data()->toggleShaded();
}

bool TaskItem::isShaded() const
{
    if (!d->task) {
        return false;
    }
    return d->task.data()->isShaded();
}

void TaskItem::toDesktop(int desk)
{
    if (!d->task) {
        return;
    }
    d->task.data()->toDesktop(desk);
}

bool TaskItem::isOnCurrentDesktop() const
{
    return d->task && d->task.data()->isOnCurrentDesktop();
}

bool TaskItem::isOnAllDesktops() const
{
    return d->task && d->task.data()->isOnAllDesktops();
}

int TaskItem::desktop() const
{
    if (!d->task) {
        return 0;
    }
    return d->task.data()->desktop();
}

void TaskItem::setMaximized(bool state)
{
    if (!d->task) {
        return;
    }
    d->task.data()->setMaximized(state);
}

void TaskItem::toggleMaximized()
{
    if (!d->task) {
        return;
    }
    d->task.data()->toggleMaximized();
}

bool TaskItem::isMaximized() const
{
    return d->task && d->task.data()->isMaximized();
}

void TaskItem::setMinimized(bool state)
{
    if (!d->task) {
        return;
    }
    d->task.data()->setIconified(state);
}

void TaskItem::toggleMinimized()
{
    if (!d->task) {
        return;
    }
    d->task.data()->toggleIconified();
}

bool TaskItem::isMinimized() const
{
    if (!d->task) {
        return false;
    }
    return d->task.data()->isMinimized();
}

void TaskItem::setFullScreen(bool state)
{
    if (!d->task) {
        return;
    }
    d->task.data()->setFullScreen(state);
}

void TaskItem::toggleFullScreen()
{
    if (!d->task) {
        return;
    }
    d->task.data()->toggleFullScreen();
}

bool TaskItem::isFullScreen() const
{
    if (!d->task) {
        return false;
    }
    return d->task.data()->isFullScreen();
}

void TaskItem::setKeptBelowOthers(bool state)
{
    if (!d->task) {
        return;
    }
    d->task.data()->setKeptBelowOthers(state);
}

void TaskItem::toggleKeptBelowOthers()
{
    if (!d->task) {
        return;
    }
    d->task.data()->toggleKeptBelowOthers();
}

bool TaskItem::isKeptBelowOthers() const
{
    if (!d->task) {
        return false;
    }
    return d->task.data()->isKeptBelowOthers();
}

void TaskItem::setAlwaysOnTop(bool state)
{
    if (!d->task) {
        return;
    }
    d->task.data()->setAlwaysOnTop(state);
}

void TaskItem::toggleAlwaysOnTop()
{
    if (!d->task) {
        return;
    }
    d->task.data()->toggleAlwaysOnTop();
}

bool TaskItem::isAlwaysOnTop() const
{
    if (!d->task) {
        return false;
    }
    return d->task.data()->isAlwaysOnTop();
}

bool TaskItem::isActionSupported(NET::Action action) const
{
    return d->task && d->task.data()->info().actionSupported(action);
}

void TaskItem::addMimeData(QMimeData *mimeData) const
{
    if (!d->task) {
        return;
    }

    d->task.data()->addMimeData(mimeData);
}

void TaskItem::setLauncherUrl(const KUrl &url)
{
    if (!d->launcherUrl.isEmpty()) {
        return;
    }
    d->launcherUrl = url;
    d->taskName = QString(); // Cause name to be re-generated...

    KConfig cfg("taskmanagerrulesrc");
    KConfigGroup grp(&cfg, "Mapping");
    grp.writeEntry(d->task.data()->classClass() + "::" + d->task.data()->className(), url.url());
    cfg.sync();
}

void TaskItem::setLauncherUrl(const AbstractGroupableItem *item)
{
    if (!d->launcherUrl.isEmpty() || !item) {
        return;
    }
    d->launcherUrl = item->launcherUrl();
    d->taskName = QString(); // Cause name to be re-generated...
}

static KService::List getServicesViaPid(int pid)
{
    // Attempt to find using commandline...
    KService::List services;

    if (pid == 0) {
        return services;
    }

    KSysGuard::Processes procs;
    procs.updateOrAddProcess(pid);

    KSysGuard::Process *proc = procs.getProcess(pid);
    QString cmdline = proc ? proc->command.simplified() : QString(); // proc->command has a trailing space???

    if (cmdline.isEmpty()) {
        return services;
    }

    const int firstSpace = cmdline.indexOf(' ');

    services = KServiceTypeTrader::self()->query("Application", QString("exist Exec and ('%1' =~ Exec)").arg(cmdline));
    if (services.empty()) {
        // Could not find with complete commandline, so strip out path part...
        int slash = cmdline.lastIndexOf('/', firstSpace);
        if (slash > 0) {
            services = KServiceTypeTrader::self()->query("Application", QString("exist Exec and ('%1' =~ Exec)").arg(cmdline.mid(slash + 1)));
        }
    }

    if (services.empty() && firstSpace > 0) {
        // Could not find with arguments, so try without...
        cmdline = cmdline.left(firstSpace);
        services = KServiceTypeTrader::self()->query("Application", QString("exist Exec and ('%1' =~ Exec)").arg(cmdline));

        int slash = cmdline.lastIndexOf('/');
        if (slash > 0) {
            services = KServiceTypeTrader::self()->query("Application", QString("exist Exec and ('%1' =~ Exec)").arg(cmdline.mid(slash + 1)));
        }
    }

    if (services.empty() && !KStandardDirs::findExe(cmdline).isEmpty()) {
        // cmdline now exists without arguments if there were any
        services << KSharedPtr<KService>(new KService(proc->name, cmdline, QString()));
        kDebug() << "adding for" << proc->name << cmdline;
    }
    return services;
}

static KUrl getServiceLauncherUrl(int pid, const QString &type, const QStringList &cmdRemovals = QStringList())
{
    if (pid == 0) {
        return KUrl();
    }

    KSysGuard::Processes procs;
    procs.updateOrAddProcess(pid);

    KSysGuard::Process *proc = procs.getProcess(pid);
    QString cmdline = proc ? proc->command.simplified() : QString(); // proc->command has a trailing space???

    if (cmdline.isEmpty()) {
        return KUrl();
    }

    foreach (const QString & r, cmdRemovals) {
        cmdline.replace(r, "");
    }

    KService::List services = KServiceTypeTrader::self()->query(type, QString("exist Exec and ('%1' =~ Exec)").arg(cmdline));

    if (services.empty()) {
        // Could not find with complete commandline, so strip out path part...
        int slash = cmdline.lastIndexOf('/', cmdline.indexOf(' '));
        if (slash > 0) {
            services = KServiceTypeTrader::self()->query(type, QString("exist Exec and ('%1' =~ Exec)").arg(cmdline.mid(slash + 1)));
        }

        if (services.empty()) {
            return KUrl();
        }
    }

    QString path = services[0]->entryPath();
    if (!path.startsWith("/")) {
        QStringList dirs = KGlobal::dirs()->resourceDirs("services");
        foreach (const QString & d, dirs) {
            if (QFile::exists(d + path)) {
                path = d + path;
                break;
            }
        }
    }

    if (QFile::exists(path)) {
        return KUrl::fromPath(path);
    }

    return KUrl();
}

KUrl TaskItem::launcherUrl() const
{
    if (!d->task && !isStartupItem()) {
        return KUrl();
    }

    if (!d->launcherUrl.isEmpty() || d->checkedForLauncher) {
        return d->launcherUrl;
    }

    // Search for applications which are executable and case-insensitively match the windowclass of the task and
    // See http://techbase.kde.org/Development/Tutorials/Services/Traders#The_KTrader_Query_Language
    KService::List services;
    bool triedPid = false;

    // Set a flag so that we remeber that we have already checked for a launcher. This is becasue if we fail, then
    // we will keep on failing so the isEmpty()  check above is not enough.
    d->checkedForLauncher = true;

    if (d->task && !(d->task.data()->classClass().isEmpty() && d->task.data()->className().isEmpty())) {

        // For KCModules, if we matched on window class, etc, we would end up matching to kcmshell4 - but we are more than likely
        // interested in the actual control module. Therefore we obtain this via the commandline. This commandline may contain
        // "kdeinit4:" or "[kdeinit]", so we remove these first.
        if ("Kcmshell4" == d->task.data()->classClass()) {
            d->launcherUrl = getServiceLauncherUrl(d->task.data()->pid(), "KCModule", QStringList() << "kdeinit4:" << "[kdeinit]");
            if (!d->launcherUrl.isEmpty()) {
                return d->launcherUrl;
            }
        }

        // Check to see if this wmClass matched a saved one...
        KConfig cfg("taskmanagerrulesrc");
        KConfigGroup grp(&cfg, "Mapping");
        KConfigGroup set(&cfg, "Settings");

        // Some apps have different launchers depending upon commandline...
        QStringList matchCommandLineFirst = set.readEntry("MatchCommandLineFirst", QStringList());
        if (!d->task.data()->classClass().isEmpty() && matchCommandLineFirst.contains(d->task.data()->classClass())) {
            triedPid = true;
            services = getServicesViaPid(d->task.data()->pid());
        }
        // Try to match using className also
        if (!d->task.data()->className().isEmpty() && matchCommandLineFirst.contains("::"+d->task.data()->className())) {
            triedPid = true;
            services = getServicesViaPid(d->task.data()->pid());
        }

        // If the user has manualy set a mapping, respect this first...
        QString mapped(grp.readEntry(d->task.data()->classClass() + "::" + d->task.data()->className(), QString()));

        if (mapped.endsWith(".desktop")) {
            d->launcherUrl = mapped;
            return d->launcherUrl;
        }

        if (!d->task.data()->classClass().isEmpty()) {
            if (mapped.isEmpty()) {
                mapped = grp.readEntry(d->task.data()->classClass(), QString());

                if (mapped.endsWith(".desktop")) {
                    d->launcherUrl = mapped;
                    return d->launcherUrl;
                }
            }

            // Some apps, such as Wine, cannot use className to map to launcher name - as Wine itself is not a GUI app
            // So, Settings/ManualOnly lists window classes where the user will always have to manualy set the launcher...
            QStringList manualOnly = set.readEntry("ManualOnly", QStringList());

            if (!d->task.data()->classClass().isEmpty() && manualOnly.contains(d->task.data()->classClass())) {
                return d->launcherUrl;
            }

            if (!mapped.isEmpty()) {
                services = KServiceTypeTrader::self()->query("Application", QString("exist Exec and ('%1' =~ DesktopEntryName)").arg(mapped));
            }

            if (!mapped.isEmpty() && services.empty()) {
                services = KServiceTypeTrader::self()->query("Application", QString("exist Exec and ('%1' =~ Name)").arg(mapped));
            }

            if (services.empty() && qobject_cast<GroupManager *>(parent())) {
                KUrl savedUrl = static_cast<GroupManager *>(parent())->launcherForWmClass(d->task.data()->classClass());
                if (savedUrl.isValid()) {
                    d->launcherUrl = savedUrl;
                    return d->launcherUrl;
                }
            }

            // To match other docks (docky, unity, etc.) attempt to match on DesktopEntryName first...
            if (services.empty()) {
                services = KServiceTypeTrader::self()->query("Application", QString("exist Exec and ('%1' =~ DesktopEntryName)").arg(d->task.data()->classClass()));
            }

            // Try StartupWMClass
            if (services.empty()) {
                services = KServiceTypeTrader::self()->query("Application", QString("exist Exec and ('%1' =~ StartupWMClass)").arg(d->task.data()->classClass()));
            }

            // Try 'Name' - unfortunately this can be translated, so has a good chance of failing! (As it does for KDE's own "System Settings" (even in English!!))
            if (services.empty()) {
                services = KServiceTypeTrader::self()->query("Application", QString("exist Exec and ('%1' =~ Name)").arg(d->task.data()->classClass()));
            }
        }

        // Ok, absolute *last* chance, try matching via pid (but only if we have not already tried this!)...
        if (services.empty() && !triedPid) {
            services = getServicesViaPid(d->task.data()->pid());
        }
    }

    if (services.empty() && isStartupItem()) {
        // Try to match via desktop filename...
        if (!startup()->desktopId().isNull() && startup()->desktopId().endsWith(".desktop")) {
            if (startup()->desktopId().startsWith("/")) {
                d->launcherUrl = KUrl::fromPath(startup()->desktopId());
                return d->launcherUrl;
            } else {
                QString desktopName = startup()->desktopId();

                if (desktopName.endsWith(".desktop")) {
                    desktopName = desktopName.mid(desktopName.length() - 8);
                }

                services = KServiceTypeTrader::self()->query("Application", QString("exist Exec and ('%1' =~ DesktopEntryName)").arg(desktopName));
            }
        }

        // Try StartupWMClass
        if (services.empty() && !startup()->wmClass().isNull()) {
            services = KServiceTypeTrader::self()->query("Application", QString("exist Exec and ('%1' =~ StartupWMClass)").arg(startup()->wmClass()));
        }

        // Try via name...
        if (services.empty() && !startup()->text().isNull()) {
            services = KServiceTypeTrader::self()->query("Application", QString("exist Exec and ('%1' =~ Name)").arg(startup()->text()));
        }
    }

    if (!services.empty()) {
        QString path = services[0]->entryPath();
        if (path.isEmpty()) {
            path = services[0]->exec();
        }

        if (!path.isEmpty()) {
            d->launcherUrl = KUrl::fromPath(path);
        }
    }

    return d->launcherUrl;
}

void TaskItem::resetLauncherCheck()
{
    if (d->launcherUrl.isEmpty()) {
        d->checkedForLauncher = false;
    }
}

void TaskItem::close()
{
    if (!d->task) {
        return;
    }
    d->task.data()->close();
}

bool TaskItem::isActive() const
{
    if (!d->task) {
        return false;
    }
    return d->task.data()->isActive();
}

bool TaskItem::demandsAttention() const
{
    if (!d->task) {
        return false;
    }
    return d->task.data()->demandsAttention();
}

} // TaskManager namespace

#include "taskitem.moc"
