/*
 *   Copyright 2008 Aaron Seigo <aseigo@kde.org>
 *   Copyright 2013 Sebastian Kügler <sebas@kde.org>
 *   Copyright 2013 Ivan Cukic <ivan.cukic@kde.org>
 *   Copyright 2013 Marco Martin <mart@kde.org>
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU Library General Public License as
 *   published by the Free Software Foundation; either version 2, 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 Library General Public
 *   License along with this program; if not, write to the
 *   Free Software Foundation, Inc.,
 *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#include "shellcorona.h"

#include <QApplication>
#include <QDebug>
#include <QMenu>
#include <QQmlContext>
#include <QTimer>
#include <QDBusConnection>

#include <kactioncollection.h>
#include <klocalizedstring.h>
#include <Plasma/Package>
#include <Plasma/PluginLoader>
#include <kactivities/controller.h>
#include <kactivities/consumer.h>
#include <ksycoca.h>
#include <kservicetypetrader.h>
#include <KGlobalAccel>
#include <KAuthorized>
#include <KWindowSystem>
#include <kdeclarative/kdeclarative.h>
#include <KMessageBox>

#include <KScreen/Config>
#include <kscreen/configmonitor.h>

#include "config-ktexteditor.h" // HAVE_KTEXTEDITOR


#include "activity.h"
#include "desktopview.h"
#include "panelview.h"
#include "scripting/desktopscriptengine.h"
#include "widgetexplorer/widgetexplorer.h"
#include "configview.h"
#include "shellpluginloader.h"
#include "shellmanager.h"
#include "osd.h"
#if HAVE_KTEXTEDITOR
#include "interactiveconsole.h"
#endif

#include "plasmashelladaptor.h"

static const int s_configSyncDelay = 10000; // 10 seconds

class ShellCorona::Private {
public:
    Private(ShellCorona *corona)
        : q(corona),
          activityController(new KActivities::Controller(q)),
          activityConsumer(new KActivities::Consumer(q)),
          addPanelAction(nullptr),
          addPanelsMenu(nullptr),
          screenConfiguration(nullptr)
    {
        appConfigSyncTimer.setSingleShot(true);
        appConfigSyncTimer.setInterval(s_configSyncDelay);
        connect(&appConfigSyncTimer, &QTimer::timeout, q, &ShellCorona::syncAppConfig);

        waitingPanelsTimer.setSingleShot(true);
        waitingPanelsTimer.setInterval(250);
        connect(&waitingPanelsTimer, &QTimer::timeout, q, &ShellCorona::createWaitingPanels);
    }

    ShellCorona *q;
    QString shell;
    QList<DesktopView *> views;
    KActivities::Controller *activityController;
    KActivities::Consumer *activityConsumer;
    QHash<const Plasma::Containment *, PanelView *> panelViews;
    KConfigGroup desktopDefaultsConfig;
    QList<Plasma::Containment *> waitingPanels;
    QHash<QString, Activity *> activities;
    QHash<QString, QHash<int, Plasma::Containment *> > desktopContainments;
    QAction *addPanelAction;
    QMenu *addPanelsMenu;
    Plasma::Package lookNFeelPackage;
    QSet<KScreen::Output*> redundantOutputs;
#if HAVE_KTEXTEDITOR
    QWeakPointer<InteractiveConsole> console;
#endif

    KScreen::Config *screenConfiguration;
    QTimer waitingPanelsTimer;
    QTimer appConfigSyncTimer;
};

static QScreen *outputToScreen(KScreen::Output *output)
{
    if (!output)
        return 0;
    foreach(QScreen *screen, QGuiApplication::screens()) {
        if (screen->name() == output->name()) {
            return screen;
        }
    }
    return 0;
}

static KScreen::Output *screenToOutput(QScreen* screen, KScreen::Config* config)
{
    foreach(KScreen::Output* output, config->connectedOutputs()) {
        if (screen->name() == output->name()) {
            return output;
        }
    }
    return 0;
}

ShellCorona::ShellCorona(QObject *parent)
    : Plasma::Corona(parent),
      d(new Private(this))
{
    d->desktopDefaultsConfig = KConfigGroup(KSharedConfig::openConfig(package().filePath("defaults")), "Desktop");

    new PlasmaShellAdaptor(this);

    QDBusConnection dbus = QDBusConnection::sessionBus();
    dbus.registerObject(QStringLiteral("/PlasmaShell"), this);

    connect(this, &Plasma::Corona::startupCompleted, this,
            []() {
                QDBusMessage ksplashProgressMessage = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KSplash"),
                                               QStringLiteral("/KSplash"),
                                               QStringLiteral("org.kde.KSplash"),
                                               QStringLiteral("setStage"));
                ksplashProgressMessage.setArguments(QList<QVariant>() << QStringLiteral("desktop"));
                QDBusConnection::sessionBus().asyncCall(ksplashProgressMessage);
            });

    // Look for theme config in plasmarc, if it isn't configured, take the theme from the
    // LookAndFeel package, if either is set, change the default theme

    connect(qApp, &QCoreApplication::aboutToQuit, [=]() {
        //saveLayout is a slot but arguments not compatible
        saveLayout();
    });

    const QString themeGroupKey = QStringLiteral("Theme");
    const QString themeNameKey = QStringLiteral("name");

    QString themeName;

    KConfigGroup plasmarc(KSharedConfig::openConfig("plasmarc"), themeGroupKey);
    themeName = plasmarc.readEntry(themeNameKey, themeName);

    if (themeName.isEmpty()) {
        const KConfigGroup lnfCfg = KConfigGroup(KSharedConfig::openConfig(
                                                lookAndFeelPackage().filePath("defaults")),
                                                themeGroupKey
                                           );
        themeName = lnfCfg.readEntry(themeNameKey, themeName);
    }

    if (!themeName.isEmpty()) {
        Plasma::Theme *t = new Plasma::Theme(this);
        t->setThemeName(themeName);
    }

    connect(this, &ShellCorona::containmentAdded,
            this, &ShellCorona::handleContainmentAdded);

    QAction *dashboardAction = actions()->add<QAction>("show dashboard");
    QObject::connect(dashboardAction, &QAction::triggered,
                     this, &ShellCorona::setDashboardShown);
    dashboardAction->setText(i18n("Show Dashboard"));

    dashboardAction->setAutoRepeat(true);
    dashboardAction->setCheckable(true);
    dashboardAction->setIcon(QIcon::fromTheme("dashboard-show"));
    dashboardAction->setData(Plasma::Types::ControlAction);
    KGlobalAccel::self()->setDefaultShortcut(dashboardAction, QList<QKeySequence>() << QKeySequence(Qt::CTRL + Qt::Key_F12));
    KGlobalAccel::self()->setShortcut(dashboardAction, QList<QKeySequence>() << QKeySequence(Qt::CTRL + Qt::Key_F12));


    checkAddPanelAction();
    connect(KSycoca::self(), SIGNAL(databaseChanged(QStringList)), this, SLOT(checkAddPanelAction(QStringList)));


    //Activity stuff
    QAction *activityAction = actions()->addAction("manage activities");
    connect(activityAction, &QAction::triggered,
            this, &ShellCorona::toggleActivityManager);
    activityAction->setText(i18n("Activities..."));
    activityAction->setIcon(QIcon::fromTheme("preferences-activities"));
    activityAction->setData(Plasma::Types::ConfigureAction);
    activityAction->setShortcut(QKeySequence("alt+d, alt+a"));
    activityAction->setShortcutContext(Qt::ApplicationShortcut);

    connect(d->activityConsumer, SIGNAL(currentActivityChanged(QString)), this, SLOT(currentActivityChanged(QString)));
    connect(d->activityConsumer, SIGNAL(activityAdded(QString)), this, SLOT(activityAdded(QString)));
    connect(d->activityConsumer, SIGNAL(activityRemoved(QString)), this, SLOT(activityRemoved(QString)));

    new Osd(this);
    d->screenConfiguration = KScreen::Config::current();
}

ShellCorona::~ShellCorona()
{
    qDeleteAll(d->views);
    qDeleteAll(d->panelViews);
    delete d;
}

void ShellCorona::setShell(const QString &shell)
{
    if (d->shell == shell) {
        return;
    }

    d->shell = shell;
    Plasma::Package package = Plasma::PluginLoader::self()->loadPackage("Plasma/Shell");
    package.setPath(shell);
    package.setAllowExternalPaths(true);
    setPackage(package);
    d->desktopDefaultsConfig = KConfigGroup(KSharedConfig::openConfig(package.filePath("defaults")), "Desktop");

    //FIXME: this would change the runtime platform to a fixed one if available
    // but a different way to load platform specific components is needed beforehand
    // because if we import and use two different components plugin, the second time
    // the import is called it will fail
   /* KConfigGroup cg(KSharedConfig::openConfig(package.filePath("defaults")), "General");
    KDeclarative::KDeclarative::setRuntimePlatform(cg.readEntry("DefaultRuntimePlatform", QStringList()));*/

    unload();

    if (d->activityConsumer->serviceStatus() == KActivities::Consumer::Unknown) {
        connect(d->activityConsumer, SIGNAL(serviceStatusChanged(Consumer::ServiceStatus)), SLOT(load()), Qt::UniqueConnection);
    } else {
        load();
    }
}

QString ShellCorona::shell() const
{
    return d->shell;
}

bool outputLess(KScreen::Output *a, KScreen::Output *b)
{
    return ((a->isEnabled() && !b->isEnabled())
         || (a->isEnabled() == b->isEnabled() && (a->isPrimary() && !b->isPrimary()))
         || (a->isPrimary() == b->isPrimary() && (a->pos().x() < b->pos().x()
         || (a->pos().x() == b->pos().x() && a->pos().y() < b->pos().y()))));
}

static QList<KScreen::Output*> sortOutputs(const QHash<int, KScreen::Output*> &outputs)
{
    QList<KScreen::Output*> ret = outputs.values();
    std::sort(ret.begin(), ret.end(), outputLess);
    return ret;
}

void ShellCorona::load()
{
    if (d->shell.isEmpty() ||
        d->activityConsumer->serviceStatus() == KActivities::Consumer::Unknown) {
        return;
    }

    disconnect(d->activityConsumer, SIGNAL(serviceStatusChanged(Consumer::ServiceStatus)), this, SLOT(load()));

    loadLayout("plasma-" + d->shell + "-appletsrc");

    checkActivities();
    if (containments().isEmpty()) {
        loadDefaultLayout();
        foreach(Plasma::Containment *containment, containments()) {
            containment->setActivity(d->activityConsumer->currentActivity());
        }
    }

    processUpdateScripts();
    foreach(Plasma::Containment *containment, containments()) {
        if (containment->formFactor() == Plasma::Types::Horizontal ||
            containment->formFactor() == Plasma::Types::Vertical) {
            if (!d->waitingPanels.contains(containment)) {
                d->waitingPanels << containment;
            }
        } else {
            //FIXME ideally fix this, or at least document the crap out of it
            int screen = containment->lastScreen();
            if (screen < 0) {
                screen = d->desktopContainments[containment->activity()].count();
            }
            insertContainment(containment->activity(), screen, containment);
        }
    }

    KScreen::ConfigMonitor::instance()->addConfig(d->screenConfiguration);
    //we're not going through the connectedOutputs because we need to connect to all outputs
    for (KScreen::Output *output : sortOutputs(d->screenConfiguration->outputs())) {
        addOutput(output);
    }
    connect(d->screenConfiguration, &KScreen::Config::outputAdded, this, &ShellCorona::addOutput);
    connect(d->screenConfiguration, &KScreen::Config::primaryOutputChanged,
            this, &ShellCorona::primaryOutputChanged);

    if (!d->waitingPanels.isEmpty()) {
        d->waitingPanelsTimer.start();
    }
}

void ShellCorona::primaryOutputChanged()
{
    KScreen::Config *current = d->screenConfiguration;
    if (!current->primaryOutput() || !current->primaryOutput()->isEnabled())
        return;
    QScreen *newPrimary = outputToScreen(current->primaryOutput());
    int i=0;
    foreach(DesktopView *view, d->views) {
        if (view->screen() == newPrimary)
            break;
        i++;
    }
    QScreen *oldPrimary = d->views.first()->screen();

    //if it was not found, it means that addOutput hasn't been called yet
    if (i>=d->views.count() || i==0)
        return;
    qDebug() << "primary changed!" << oldPrimary->name() << newPrimary->name() << i;

    Q_ASSERT(oldPrimary != newPrimary);
    Q_ASSERT(d->views[0]->screen() != d->views[i]->screen());
    Q_ASSERT(d->views[0]->screen() == oldPrimary);
    Q_ASSERT(d->views[0]->screen() != newPrimary);
    Q_ASSERT(d->views[0]->geometry() == oldPrimary->geometry());
    qDebug() << "adapting" << newPrimary->geometry() << d->views[0]->containment()->wallpaper()
                           << oldPrimary->geometry() << d->views[i]->containment()->wallpaper() << i;

    d->views[0]->setScreen(newPrimary);
    d->views[i]->setScreen(oldPrimary);
    screenInvariants();

    QList<Plasma::Containment*> panels;
    foreach(PanelView *panel, d->panelViews) {
        if (panel->screen() == oldPrimary)
            panel->setScreen(newPrimary);
        else if (panel->screen() == newPrimary)
            panel->setScreen(oldPrimary);
    }
}

void ShellCorona::screenInvariants() const
{
    Q_ASSERT(d->views.count() <= QGuiApplication::screens().count());
    Q_ASSERT(!outputToScreen(d->screenConfiguration->primaryOutput()) || d->views.isEmpty() || outputToScreen(d->screenConfiguration->primaryOutput()) == d->views.first()->screen() || !d->screenConfiguration->primaryOutput()->isEnabled());
    QScreen *s = d->views.isEmpty() ? nullptr : d->views[0]->screen();
    if (!s) {
        qWarning() << "error: couldn't find primary output" << d->screenConfiguration->primaryOutput();
        return;
    }

    Q_ASSERT(d->views[0]->screen()->name() == s->name());
    Q_ASSERT(d->views[0]->geometry() == s->geometry() || ShellManager::s_forceWindowed);

    QSet<QScreen*> screens;
    int i = 0;
    foreach(DesktopView *view, d->views) {
        Q_ASSERT(!screens.contains(view->screen()));
        Q_ASSERT(view->isVisible());
        Q_ASSERT(view->fillScreen() || ShellManager::s_forceWindowed);
        Q_ASSERT(view->containment()->screen() == i);

        screens.insert(view->screen());
        ++i;
    }
}


void ShellCorona::unload()
{
    if (d->shell.isEmpty()) {
        return;
    }

    qDeleteAll(containments());
}

KSharedConfig::Ptr ShellCorona::applicationConfig()
{
    return KSharedConfig::openConfig();
}

void ShellCorona::requestApplicationConfigSync()
{
    d->appConfigSyncTimer.start();
}

void ShellCorona::loadDefaultLayout()
{
    const QString script = package().filePath("defaultlayout");
    QFile file(script);
    if (file.open(QIODevice::ReadOnly | QIODevice::Text) ) {
        QString code = file.readAll();
        qDebug() << "evaluating startup script:" << script;

        WorkspaceScripting::DesktopScriptEngine scriptEngine(this, true);

        connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::printError, this,
                [](const QString &msg) {
                    qWarning() << msg;
                });
        connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::print, this,
                [](const QString &msg) {
                    qDebug() << msg;
                });
        scriptEngine.evaluateScript(code);
    }
}

void ShellCorona::processUpdateScripts()
{
    WorkspaceScripting::DesktopScriptEngine scriptEngine(this, true);

    connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::printError, this,
            [](const QString &msg) {
                qWarning() << msg;
            });
    connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::print, this,
            [](const QString &msg) {
                qDebug() << msg;
            });
    foreach (const QString &script, WorkspaceScripting::ScriptEngine::pendingUpdateScripts(this)) {
        scriptEngine.evaluateScript(script);
    }
}

KActivities::Controller *ShellCorona::activityController()
{
    return d->activityController;
}

int ShellCorona::numScreens() const
{
    return d->views.count();
}

QRect ShellCorona::screenGeometry(int id) const
{
    if (id>=d->views.count() || id<0) {
        qWarning() << "requesting unexisting screen" << id;
        QScreen *s = outputToScreen(d->screenConfiguration->primaryOutput());
        return s ? s->geometry() : QRect();
    }
    return d->views[id]->geometry();
}

QRegion ShellCorona::availableScreenRegion(int id) const
{
    if (id>=d->views.count() || id<0) {
        //each screen should have a view
        qWarning() << "requesting unexisting screen" << id;
        QScreen *s = outputToScreen(d->screenConfiguration->primaryOutput());
        return s ? s->availableGeometry() : QRegion();
    }
    DesktopView *view = d->views[id];
    const QRect screenGeo(view->geometry());

    QRegion r = view->geometry();
    foreach (PanelView *v, d->panelViews) {
        if (v->screen() == v->screen() && v->visibilityMode() != PanelView::AutoHide) {
            //if the panel is being moved around, we still want to calculate it from the edge
            r -= v->geometryByDistance(0);
        }
    }
    return r;
}

int rectArea(const QRect& r) { return r.width()*r.height(); }

QRect ShellCorona::availableScreenRect(int id) const
{
    if (id >= d->views.count() || id < 0) {
        //each screen should have a view
        qWarning() << "requesting unexisting screen" << id;
        QScreen *s = outputToScreen(d->screenConfiguration->primaryOutput());
        return s ? s->availableGeometry() : QRect();
    }

    DesktopView *view = d->views[id];
    const QRect screenGeo(view->geometry());

    QRect r = view->geometry();
    foreach (PanelView *v, d->panelViews) {
        if (v->screen() == v->screen() && v->visibilityMode() != PanelView::AutoHide) {
            switch (v->location()) {
            case Plasma::Types::LeftEdge:
                r.setLeft(r.left() + v->thickness());
                break;
            case Plasma::Types::RightEdge:
                r.setRight(r.right() - v->thickness());
                break;
            case Plasma::Types::TopEdge:
                r.setTop(r.top() + v->thickness());
                break;
            case Plasma::Types::BottomEdge:
                r.setBottom(r.bottom() - v->thickness());
            default:
                break;
            }
        }
    }
    return r;
}

QScreen* ShellCorona::screenForId(int screenId) const
{
    DesktopView* v = d->views.value(screenId);
    return v ? v->screen() : Q_NULLPTR;
}

PanelView *ShellCorona::panelView(Plasma::Containment *containment) const
{
    return d->panelViews.value(containment);
}

///// SLOTS

QList<PanelView*> ShellCorona::panelsForScreen(QScreen* screen)
{
    QList<PanelView*> ret;
    foreach(PanelView* v, d->panelViews) {
        if(v->screen() == screen)
            ret += v;
    }
    return ret;
}

void ShellCorona::removeView(int idx)
{
    if (idx == d->views.count()-1) {
        QScreen* screen = d->views[idx]->screen();
        QList<PanelView*> panels = panelsForScreen(screen);

        foreach(PanelView* p, panels) {
            delete d->panelViews.take(p->containment());
        }
        delete d->views.takeLast();
    } else if (idx < d->views.count()-1) {
        QScreen* screen = d->views[idx+1]->screen();
        QList<PanelView*> panels = panelsForScreen(screen);

        d->views[idx]->setScreen(screen);
        foreach(PanelView* p, panels) {
            p->setScreen(screen);
        }
        removeView(idx+1);
    }
}

void ShellCorona::outputEnabledChanged()
{
    addOutput(qobject_cast<KScreen::Output*>(sender()));
}

bool ShellCorona::isOutputRedundant(KScreen::Output* screen) const
{
    QRect geometry = screen->geometry();

    //FIXME: QScreen doesn't have any idea of "this qscreen is clone of this other one
    //so this ultra inefficient heuristic has to stay until we have a slightly better api
    foreach (KScreen::Output *s, d->screenConfiguration->connectedOutputs()) {
        if (!s->isEnabled())
            continue;
        QRect sGeometry = s->geometry();
        if (sGeometry.contains(geometry, false) &&
            sGeometry.width() > geometry.width() &&
            sGeometry.height() > geometry.height()) {
            return true;
        }
    }
    return false;
}

void ShellCorona::reconsiderOutputs()
{
    KScreen::Output* output = qobject_cast<KScreen::Output*>(sender());
    reconsiderOutput(output);
    foreach(KScreen::Output* out, d->redundantOutputs) {
        reconsiderOutput(out);
    }
}

void ShellCorona::reconsiderOutput(KScreen::Output* output)
{
    QScreen *screen = outputToScreen(output);
    foreach(DesktopView* view, d->views) {
        if (view->screen() == screen) {
            if (isOutputRedundant(output)) { //if redundant, then remove the screen
                removeScreen(view);
            }
            return; //if it's already in, let's not add it again
        }
    }
    addOutput(output);
}

void ShellCorona::addOutput(KScreen::Output *output)
{
    connect(output, &KScreen::Output::isEnabledChanged, this, &ShellCorona::outputEnabledChanged, Qt::UniqueConnection);
    connect(output, &KScreen::Output::posChanged, this, &ShellCorona::reconsiderOutputs, Qt::UniqueConnection);
    connect(output, &KScreen::Output::currentModeIdChanged, this, &ShellCorona::reconsiderOutputs, Qt::UniqueConnection);
    if (!output->isEnabled()) {
        d->redundantOutputs.remove(output);
        return;
    }
    QScreen *screen = outputToScreen(output);
    Q_ASSERT(screen);

    if (isOutputRedundant(output)) {
        d->redundantOutputs.insert(output);
        return;
    } else
        d->redundantOutputs.remove(output);

    int insertPosition = 0;
    foreach (DesktopView *view, d->views) {
        KScreen::Output *out = screenToOutput(view->screen(), d->screenConfiguration);
        if (outputLess(output, out))
            break;

        insertPosition++;
    }

    QScreen* newScreen = insertScreen(screen, insertPosition);

    DesktopView *view = new DesktopView(this, newScreen);
    d->views.append(view);

    Plasma::Containment *containment = createContainmentForActivity(d->activityController->currentActivity(), d->views.count()-1);
    Q_ASSERT(containment);

    QAction *removeAction = containment->actions()->action("remove");
    if (removeAction) {
        removeAction->deleteLater();
    }

    view->setContainment(containment);
    view->show();

    //were there any panels for this screen before it popped up?
    if (!d->waitingPanels.isEmpty()) {
        d->waitingPanelsTimer.start();
    }

    emit availableScreenRectChanged();
    emit availableScreenRegionChanged();

    screenInvariants();
}

QScreen* ShellCorona::insertScreen(QScreen* screen, int idx)
{
    if(idx==d->views.count())
        return screen;

    DesktopView* v = d->views[idx];
    QScreen *oldScreen = v->screen();
    v->setScreen(screen);
    v->adaptToScreen();
    foreach(PanelView* panel, d->panelViews) {
        if(panel->screen() == screen) {
            panel->setScreen(screen);
        }
    }

    return insertScreen(oldScreen, idx+1);
}

void ShellCorona::removeScreen(DesktopView *view)
{
    foreach(PanelView* panel, d->panelViews) {
        if (panel->screen() == view->screen())
            removePanel(panel);
    }

    int idx = d->views.indexOf(view);

    removeView(idx);

    screenInvariants();
}

void ShellCorona::removePanel(PanelView* panelView)
{
    Plasma::Containment* cont = panelView->containment();
    d->waitingPanels << cont;
    d->panelViews.remove(cont);
    delete panelView;

    emit availableScreenRectChanged();
    emit availableScreenRegionChanged();
}

Plasma::Containment *ShellCorona::createContainmentForActivity(const QString& activity, int screenNum)
{
    if (d->desktopContainments.contains(activity) &&
        d->desktopContainments[activity].contains(screenNum) &&
        d->desktopContainments.value(activity).value(screenNum)) {
        return d->desktopContainments[activity][screenNum];
    }

    QString plugin = "org.kde.desktopcontainment";
    if (d->activities.contains(activity)) {
      //  plugin = d->activities.value(activity)->defaultPlugin();
    }

    Plasma::Containment *containment = createContainment(d->desktopDefaultsConfig.readEntry("Containment", plugin));

    if (containment) {
        containment->setActivity(activity);
        insertContainment(activity, screenNum, containment);
    }

    return containment;
}

void ShellCorona::createWaitingPanels()
{
    QList<Plasma::Containment *> stillWaitingPanels;

    foreach (Plasma::Containment *cont, d->waitingPanels) {
        //ignore non existing (yet?) screens
        int requestedScreen = cont->lastScreen();
        if (requestedScreen < 0)
            ++requestedScreen;

        if (requestedScreen > (d->views.size() - 1)) {
            stillWaitingPanels << cont;
            continue;
        }

        PanelView* panel = new PanelView(this);
        d->panelViews[cont] = panel;

        Q_ASSERT(qBound(0, requestedScreen, d->views.size() -1) == requestedScreen);
        QScreen *screen = d->views[requestedScreen]->screen();

        panel->setScreen(screen);
        panel->setContainment(cont);
        panel->show();

        connect(cont, SIGNAL(destroyed(QObject*)), this, SLOT(containmentDeleted(QObject*)));
    }
    d->waitingPanels = stillWaitingPanels;
    emit availableScreenRectChanged();
    emit availableScreenRegionChanged();
}

void ShellCorona::containmentDeleted(QObject *cont)
{
    d->panelViews.remove(static_cast<Plasma::Containment*>(cont));
    emit availableScreenRectChanged();
    emit availableScreenRegionChanged();
}

void ShellCorona::handleContainmentAdded(Plasma::Containment *c)
{
    connect(c, &Plasma::Containment::showAddWidgetsInterface,
            this, &ShellCorona::toggleWidgetExplorer);
}

void ShellCorona::toggleWidgetExplorer()
{
    const QPoint cursorPos = QCursor::pos();
    foreach (DesktopView *view, d->views) {
        if (view->screen()->geometry().contains(cursorPos)) {
            //The view QML has to provide something to display the activity explorer
            view->rootObject()->metaObject()->invokeMethod(view->rootObject(), "toggleWidgetExplorer", Q_ARG(QVariant, QVariant::fromValue(sender())));
            return;
        }
    }
}

void ShellCorona::toggleActivityManager()
{
    const QPoint cursorPos = QCursor::pos();
    foreach (DesktopView *view, d->views) {
        if (view->screen()->geometry().contains(cursorPos)) {
            //The view QML has to provide something to display the activity explorer
            view->rootObject()->metaObject()->invokeMethod(view->rootObject(), "toggleActivityManager");
            return;
        }
    }
}

void ShellCorona::syncAppConfig()
{
    applicationConfig()->sync();
}

void ShellCorona::setDashboardShown(bool show)
{
    qDebug() << "TODO: Toggling dashboard view";

    QAction *dashboardAction = actions()->action("show dashboard");

    if (dashboardAction) {
        dashboardAction->setText(show ? i18n("Hide Dashboard") : i18n("Show Dashboard"));
    }

    foreach (DesktopView *view, d->views) {
        view->setDashboardShown(show);
    }
}

void ShellCorona::toggleDashboard()
{
    foreach (DesktopView *view, d->views) {
        view->setDashboardShown(!view->isDashboardShown());
    }
}

void ShellCorona::showInteractiveConsole()
{
    if (KSharedConfig::openConfig()->isImmutable() || !KAuthorized::authorize("plasma-desktop/scripting_console")) {
        return;
    }

#if HAVE_KTEXTEDITOR
    InteractiveConsole *console = d->console.data();
    if (!console) {
        d->console = console = new InteractiveConsole(this);
    }
    d->console.data()->setMode(InteractiveConsole::PlasmaConsole);

    KWindowSystem::setOnDesktop(console->winId(), KWindowSystem::currentDesktop());
    console->show();
    console->raise();
    KWindowSystem::forceActiveWindow(console->winId());
#endif
}

void ShellCorona::loadScriptInInteractiveConsole(const QString &script)
{
#if HAVE_KTEXTEDITOR
    showInteractiveConsole();
    if (d->console) {
        d->console.data()->loadScript(script);
    }
#endif
}

void ShellCorona::checkActivities()
{
    KActivities::Consumer::ServiceStatus status = d->activityController->serviceStatus();
    //qDebug() << "$%$%$#%$%$%Status:" << status;
    if (status != KActivities::Consumer::Running) {
        //panic and give up - better than causing a mess
        qDebug() << "ShellCorona::checkActivities is called whilst activity daemon is still connecting";
        return;
    }

    QStringList existingActivities = d->activityConsumer->activities();
    foreach (const QString &id, existingActivities) {
        activityAdded(id);
    }

    // Checking whether the result we got is valid. Just in case.
    Q_ASSERT_X(!existingActivities.isEmpty(), "isEmpty", "There are no activities, and the service is running");
    Q_ASSERT_X(existingActivities[0] != QStringLiteral("00000000-0000-0000-0000-000000000000"),
            "null uuid", "There is a nulluuid activity present");

    // Killing the unassigned containments
    foreach (Plasma::Containment *cont, containments()) {
        if ((cont->containmentType() == Plasma::Types::DesktopContainment ||
             cont->containmentType() == Plasma::Types::CustomContainment) &&
            !existingActivities.contains(cont->activity())) {
            cont->destroy();
        }
    }
}

void ShellCorona::currentActivityChanged(const QString &newActivity)
{
    qDebug() << "Activity changed:" << newActivity;

    for (int i = 0; i < d->views.count(); ++i) {
        Plasma::Containment *c = d->desktopContainments[newActivity].value(i);
        if (!c) {
            c = createContainmentForActivity(newActivity, i);
        }
        QAction *removeAction = c->actions()->action("remove");
        if (removeAction) {
            removeAction->deleteLater();
        }
        d->views[i]->setContainment(c);
    }
}

void ShellCorona::activityAdded(const QString &id)
{
    //TODO more sanity checks
    if (d->activities.contains(id)) {
        qDebug() << "you're late." << id;
        return;
    }

    Activity *a = new Activity(id, this);
    d->activities.insert(id, a);
}

void ShellCorona::activityRemoved(const QString &id)
{
    Activity *a = d->activities.take(id);
    a->deleteLater();
}

Activity *ShellCorona::activity(const QString &id)
{
    return d->activities.value(id);
}

void ShellCorona::insertActivity(const QString &id, Activity *activity)
{
    d->activities.insert(id, activity);
    Plasma::Containment *c = createContainmentForActivity(id, -1);
    c->config().writeEntry("lastScreen", 0);
}

Plasma::Containment *ShellCorona::setContainmentTypeForScreen(int screen, const QString &plugin)
{
    Plasma::Containment *oldContainment = containmentForScreen(screen);

    //no valid containment in given screen, giving up
    if (!oldContainment) {
        return 0;
    }
    if (plugin.isEmpty()) {
        return oldContainment;
    }

    DesktopView *view = 0;
    foreach (DesktopView *v, d->views) {
        if (v->containment() == oldContainment) {
            view = v;
            break;
        }
    }

    //no view? give up
    if (!view) {
        return oldContainment;
    }

    //create a new containment
    Plasma::Containment *newContainment = createContainmentDelayed(plugin);

    //if creation failed or invalid plugin, give up
    if (!newContainment) {
        return oldContainment;
    } else if (!newContainment->pluginInfo().isValid()) {
        newContainment->deleteLater();
        return oldContainment;
    }

    newContainment->setWallpaper(oldContainment->wallpaper());

    //At this point we have a valid new containment from plugin and a view
    //copy all configuration groups (excluded applets)
    KConfigGroup oldCg = oldContainment->config();

    //newCg *HAS* to be from a KSharedConfig, because some KConfigSkeleton will need to be synced
    //this makes the configscheme work
    KConfigGroup newCg(KSharedConfig::openConfig(oldCg.config()->name()), "Containments");
    newCg = KConfigGroup(&newCg, QString::number(newContainment->id()));

    //this makes containment->config() work, is a separate thing from its configscheme
    KConfigGroup newCg2 = newContainment->config();

    foreach (const QString &group, oldCg.groupList()) {
        if (group != "Applets") {
            KConfigGroup subGroup(&oldCg, group);
            KConfigGroup newSubGroup(&newCg, group);
            subGroup.copyTo(&newSubGroup);

            KConfigGroup newSubGroup2(&newCg2, group);
            subGroup.copyTo(&newSubGroup2);
        }
    }

    newContainment->init();
    newContainment->restore(newCg);
    newContainment->updateConstraints(Plasma::Types::StartupCompletedConstraint);
    newContainment->save(newCg);
    requestConfigSync();
    newContainment->flushPendingConstraintsEvents();
    emit containmentAdded(newContainment);

    //Move the applets
    foreach (Plasma::Applet *applet, oldContainment->applets()) {
        newContainment->addApplet(applet);
    }

    //remove the "remove" action
    QAction *removeAction = newContainment->actions()->action("remove");
    if (removeAction) {
        removeAction->deleteLater();
    }
    view->setContainment(newContainment);
    newContainment->setActivity(oldContainment->activity());
    d->desktopContainments.remove(oldContainment->activity());
    insertContainment(oldContainment->activity(), screen, newContainment);

    //removing the focus from the item that is going to be destroyed
    //fixes a crash
    //delayout the destruction of the old containment fixes another crash
    view->rootObject()->setFocus(true, Qt::MouseFocusReason);
    QTimer::singleShot(2500, oldContainment, SLOT(destroy()));

    return newContainment;
}

void ShellCorona::checkAddPanelAction(const QStringList &sycocaChanges)
{
    if (!sycocaChanges.isEmpty() && !sycocaChanges.contains("services")) {
        return;
    }

    delete d->addPanelAction;
    d->addPanelAction = 0;

    delete d->addPanelsMenu;
    d->addPanelsMenu = 0;

    KPluginInfo::List panelContainmentPlugins = Plasma::PluginLoader::listContainmentsOfType("Panel");
    const QString constraint = QString("[X-Plasma-Shell] == '%1' and 'panel' ~in [X-Plasma-ContainmentCategories]")
                                      .arg(qApp->applicationName());
    KService::List templates = KServiceTypeTrader::self()->query("Plasma/LayoutTemplate", constraint);

    if (panelContainmentPlugins.count() + templates.count() == 1) {
        d->addPanelAction = new QAction(i18n("Add Panel"), this);
        d->addPanelAction->setData(Plasma::Types::AddAction);
        connect(d->addPanelAction, SIGNAL(triggered(bool)), this, SLOT(addPanel()));
    } else if (!panelContainmentPlugins.isEmpty()) {
        d->addPanelsMenu = new QMenu;
        d->addPanelAction = d->addPanelsMenu->menuAction();
        d->addPanelAction->setText(i18n("Add Panel"));
        d->addPanelAction->setData(Plasma::Types::AddAction);
        qDebug() << "populateAddPanelsMenu" << panelContainmentPlugins.count();
        connect(d->addPanelsMenu, SIGNAL(aboutToShow()), this, SLOT(populateAddPanelsMenu()));
        connect(d->addPanelsMenu, SIGNAL(triggered(QAction*)), this, SLOT(addPanel(QAction*)));
    }

    if (d->addPanelAction) {
        d->addPanelAction->setIcon(QIcon::fromTheme("list-add"));
        actions()->addAction("add panel", d->addPanelAction);
    }
}

void ShellCorona::addPanel()
{
    KPluginInfo::List panelPlugins = Plasma::PluginLoader::listContainmentsOfType("Panel");

    if (!panelPlugins.isEmpty()) {
        addPanel(panelPlugins.first().pluginName());
    }
}

void ShellCorona::addPanel(QAction *action)
{
    const QString plugin = action->data().toString();
    if (plugin.startsWith("plasma-desktop-template:")) {
        WorkspaceScripting::DesktopScriptEngine scriptEngine(this, true);

        connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::printError, this,
                [](const QString &msg) {
                    qWarning() << msg;
                });
        connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::print, this,
                [](const QString &msg) {
                    qDebug() << msg;
                });
        scriptEngine.evaluateScript(plugin.right(plugin.length() - qstrlen("plasma-desktop-template:")));
    } else if (!plugin.isEmpty()) {
        addPanel(plugin);
    }
}

Plasma::Containment *ShellCorona::addPanel(const QString &plugin)
{
    Plasma::Containment *panel = createContainment(plugin);
    if (!panel) {
        return 0;
    }

    QList<Plasma::Types::Location> availableLocations;
    availableLocations << Plasma::Types::LeftEdge << Plasma::Types::TopEdge << Plasma::Types::RightEdge << Plasma::Types::BottomEdge;

    foreach (const Plasma::Containment *cont, d->panelViews.keys()) {
        availableLocations.removeAll(cont->location());
    }

    Plasma::Types::Location loc;
    if (availableLocations.isEmpty()) {
        loc = Plasma::Types::TopEdge;
    } else {
        loc = availableLocations.first();
    }

    panel->setLocation(loc);
    switch (loc) {
    case Plasma::Types::LeftEdge:
    case Plasma::Types::RightEdge:
        panel->setFormFactor(Plasma::Types::Vertical);
        break;
    default:
        panel->setFormFactor(Plasma::Types::Horizontal);
        break;
    }

    Q_ASSERT(panel);
    d->waitingPanels << panel;
    createWaitingPanels();

    const QPoint cursorPos(QCursor::pos());
    foreach (QScreen *screen, QGuiApplication::screens()) {
        //d->panelViews.contains(panel) == false iff addPanel is executed in a startup script
        if (screen->geometry().contains(cursorPos) && d->panelViews.contains(panel)) {
            d->panelViews[panel]->setScreen(screen);
            break;
        }
    }
    return panel;
}

int ShellCorona::screenForContainment(const Plasma::Containment *containment) const
{
    //case in which this containment is child of an applet, hello systray :)
    if (Plasma::Applet *parentApplet = qobject_cast<Plasma::Applet *>(containment->parent())) {
        if (Plasma::Containment* cont = parentApplet->containment()) {
            return screenForContainment(cont);
        } else {
            return -1;
        }
    }

    for (int i = 0; i < d->views.size(); i++) {
        if (d->views[i]->containment() == containment) {
            return i;
        }
    }

    PanelView *view = d->panelViews.value(containment);
    if (view) {
        QScreen *screen = view->screen();
        for (int i = 0; i < d->views.size(); i++) {
            if (d->views[i]->screen() == screen) {
                return i;
            }
        }
    }
    return -1;
}

void ShellCorona::activityOpened()
{
    Activity *activity = qobject_cast<Activity *>(sender());
    if (activity) {
        QList<Plasma::Containment*> cs = importLayout(activity->config());
        for (Plasma::Containment *containment : cs) {
            insertContainment(activity->name(), containment->lastScreen(), containment);
        }
    }
}

void ShellCorona::activityClosed()
{
    Activity *activity = qobject_cast<Activity *>(sender());
    if (activity) {
        KConfigGroup cg = activity->config();
        exportLayout(cg, d->desktopContainments.value(activity->name()).values());
    }
}

void ShellCorona::activityRemoved()
{
    //when an activity is removed delete all associated desktop containments
    Activity *activity = qobject_cast<Activity *>(sender());
    if (activity) {
        QHash< int, Plasma::Containment* > containmentHash = d->desktopContainments.take(activity->name());
        for (auto a : containmentHash) {
            a->destroy();
        }
    }
}

void ShellCorona::insertContainment(const QString &activity, int screenNum, Plasma::Containment *containment)
{
    if (d->desktopContainments.contains(activity) &&
        d->desktopContainments[activity].contains(screenNum)) {
        Plasma::Containment *containment = d->desktopContainments[activity][screenNum];

        //containment should always be valid, it's been known to get in a mess
        //so guard anyway
        Q_ASSERT(containment);
        if (containment) {
            disconnect(containment, SIGNAL(destroyed(QObject*)),
                   this, SLOT(desktopContainmentDestroyed(QObject*)));
            containment->destroy();
        }
    }
    d->desktopContainments[activity][screenNum] = containment;

    //when a containment gets deleted update our map of containments
    connect(containment, SIGNAL(destroyed(QObject*)), this, SLOT(desktopContainmentDestroyed(QObject*)));
}

void ShellCorona::desktopContainmentDestroyed(QObject *obj)
{
    // when QObject::destroyed arrives, ~Plasma::Containment has run,
    // members of Containment are not accessible anymore,
    // so keep ugly bookeeping by hand
    auto containment = static_cast<Plasma::Containment*>(obj);
    for (auto a : d->desktopContainments) {
        QMutableHashIterator<int, Plasma::Containment *> it(a);
        while (it.hasNext()) {
            it.next();
            if (it.value() == containment) {
                it.remove();
                return;
            }
        }
    }
}

Plasma::Package ShellCorona::lookAndFeelPackage() const
{
    if (!d->lookNFeelPackage.isValid()) {
        d->lookNFeelPackage = ShellPluginLoader::self()->loadPackage("Plasma/LookAndFeel");
        //TODO: make loading from config once we have some UI for setting the package
        d->lookNFeelPackage.setPath("org.kde.lookandfeel");
    }

    return d->lookNFeelPackage;
}

KScreen::Config* ShellCorona::screensConfiguration() const
{
    return d->screenConfiguration;
}

void ShellCorona::showOpenGLNotCompatibleWarning()
{
    static bool s_multipleInvokations = false;
    if (s_multipleInvokations) {
        return;
    }
    s_multipleInvokations = true;
    KMessageBox::error(nullptr,
                       i18n("Your graphics hardware does not support OpenGL (ES) 2. Plasma will abort now."),
                       i18n("Incompatible OpenGL version detected")
                      );
    // this doesn't work and I have no idea why.
    QCoreApplication::exit(1);
}

// Desktop corona handler


#include "moc_shellcorona.cpp"

