/*
    SPDX-FileCopyrightText: 2008 Aaron Seigo <aseigo@kde.org>
    SPDX-FileCopyrightText: 2013 Sebastian Kügler <sebas@kde.org>
    SPDX-FileCopyrightText: 2013 Ivan Cukic <ivan.cukic@kde.org>
    SPDX-FileCopyrightText: 2013 Marco Martin <mart@kde.org>

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

#pragma once

#include "config-X11.h"
#include "plasma/corona.h"

#include <QDBusArgument>
#include <QDBusContext>
#include <QDBusVariant>
#include <QPointer>
#include <QSet>
#include <QTimer>

#include <KConfigWatcher>
#include <KPackage/Package>

class DesktopView;
class PanelView;
class QMenu;
class QScreen;
class ScreenPool;
class StrutManager;
class ShellContainmentConfig;

namespace KActivities
{
class Controller;
} // namespace KActivities

namespace KScreen
{
class Output;
} // namespace KScreen

namespace Plasma
{
class Applet;
} // namespace Plasma

namespace KWayland::Client
{
class PlasmaWindow;
class PlasmaWindowManagement;
}

class ShellCorona : public Plasma::Corona, QDBusContext
{
    Q_OBJECT
    Q_PROPERTY(QString shell READ shell NOTIFY shellChanged)
    Q_PROPERTY(int numScreens READ numScreens)
    Q_PROPERTY(PanelView *panelBeingConfigured READ panelBeingConfigured NOTIFY panelBeingConfiguredChanged)
    Q_CLASSINFO("D-Bus Interface", "org.kde.PlasmaShell")

public:
    explicit ShellCorona(QObject *parent = nullptr);
    ~ShellCorona() override;

    KPackage::Package lookAndFeelPackage();
    void init();

    /**
     * Where to save global configuration that doesn't have anything to do with the scene (e.g. views)
     */
    KSharedConfig::Ptr applicationConfig();

    int numScreens() const override;
    Q_INVOKABLE QRect screenGeometry(int id) const override;
    Q_INVOKABLE QRegion availableScreenRegion(int id) const override;
    Q_INVOKABLE QRect availableScreenRect(int id) const override;
    QRect strictAvailableScreenRect(int id) const;

    // plasmashellCorona's value
    QRegion _availableScreenRegion(int id) const;
    QRect _availableScreenRect(int id) const;

    Q_INVOKABLE QStringList availableActivities() const;

    void clonePanelTo(PanelView *panel, Plasma::Types::Location location, QScreen *screen);
    bool shouldPanelReserveSpace(PanelView *panel);

    PanelView *panelView(Plasma::Containment *containment) const;
    void savePreviousWindow();
    void restorePreviousWindow();
    void clearPreviousWindow();

    // This one is a bit of an hack but are just for desktop scripting
    void insertActivity(const QString &id, const QString &plugin);

    Plasma::Containment *setContainmentTypeForScreen(int screen, const QString &plugin);

    void removeDesktop(DesktopView *desktopView);

    /**
     * @returns a new containment associated with the specified @p activity and @p screen.
     */
    Plasma::Containment *createContainmentForActivity(const QString &activity, int screenNum);

    ScreenPool *screenPool() const;

    QList<int> screenIds() const;

    QString defaultContainmentPlugin() const;

    static QString defaultShell();

    // Set all Desktop containments (for all activities) associated to a given screen to this new screen.
    // swapping ones that had newScreen to oldScreen if existing. Panels are ignored
    void swapDesktopScreens(int oldScreen, int newScreen);

    // Set a single containment to a new screen.
    // If it is a Desktop contaiment, swap it with the other containment that was associated with same screen and activity if existent
    void setScreenForContainment(Plasma::Containment *containment, int screen);

    // Grab a screenshot of the contaiment if it has a view in an async fashion
    // containmentPreviewReady will be emitted when done
    // If there is no view, this will have no effect
    void grabContainmentPreview(Plasma::Containment *containment);

    // If a containment preview has been grabbed, for this containment, return its path
    QString containmentPreviewPath(Plasma::Containment *containment) const;

    /**
     * Whether "accent color from wallpaper" option is enabled in global settings
     */
    bool accentColorFromWallpaperEnabled() const;

    Q_INVOKABLE QVariantMap wallpaper(uint screenNum = 0);

    Q_INVOKABLE void showAddPanelContextMenu(const QPoint pos);

    PanelView *panelBeingConfigured() const;

    Q_INVOKABLE bool enteredEditModeViaDesktop();

    Q_INVOKABLE bool isScreenUiReady(int screen);

    /*!
        \internal

        Must be called from DBus!

        Grabs an image of the desktop containment of the defined screen.
        \a name is the name of the screen (as per QScreen::name()).
        \a width and \a height define the resulting image size.
        \a targetPath defines where the image should be saved.

        Returns true if the image was saved successfully.

        Note that this function is asynchronous and will send a delayed reply.

        DO NOT WAIT BLOCKINGLY!
    */
    [[nodiscard]] bool grabContainmentImage(const QString &name, int width, int height, const QString &targetPath);

Q_SIGNALS:
    void glInitializationFailed();
    // A preview for this containment has been rendered and saved to disk
    void containmentPreviewReady(Plasma::Containment *containment, const QString &path);
    void accentColorFromWallpaperEnabledChanged();
    void colorChanged(const QColor &color);
    // This API is used only by autotests, do we need something else?
    void screenOrderChanged(QList<QScreen *> screens);

    void wallpaperChanged(uint screenNum);
    void panelBeingConfiguredChanged();
    // Emitted when the shell property is set, but before the new shell is loaded
    void shellAboutToChange(const QString &shell);
    // Emitted after the new shell has loaded
    void shellChanged(const QString &shell);
    void screenUiReadyChanged(int screen, bool ready);

public Q_SLOTS:
    /**
     * Request saving applicationConfig on disk, it's event compressed, not immediate
     */
    void requestApplicationConfigSync();

    /**
     * Cycle through all panels
     */
    void slotCyclePanelFocus();

    /**
     * Sets the shell that the corona should display
     */
    void setShell(const QString &shell);

    /**
     * Gets the currently shown shell
     */
    QString shell() const;

    /// DBUS methods

    /**
     * Set the shell and reload containments while an existing shell is displayed
     */
    void changeShell(const QString &shell);
    void toggleDashboard();
    void setDashboardShown(bool show);
    void toggleActivityManager();
    void toggleWidgetExplorer();
    QString evaluateScript(const QString &string);
    void setWallpaper(const QString &wallpaperPlugin, const QVariantMap &parameters, uint screenNum = 0);
    void activateLauncherMenu();
    QRgb color() const;

    QByteArray dumpCurrentLayoutJS() const;

    /**
     * loads the shell layout from a look and feel package,
     * resetting it to the default layout exported in the
     * look and feel package
     */
    void loadLookAndFeelDefaultLayout(const QString &layout);

    Plasma::Containment *addPanel(const QString &plugin);

    void nextActivity();
    void previousActivity();

    void setTestModeLayout(const QString &layout)
    {
        m_testModeLayout = layout;
    }

    int panelCount() const
    {
        return m_panelViews.count();
    }

    void refreshCurrentShell();

protected Q_SLOTS:
    /**
     * Loads the layout and performs the needed checks
     */
    void load();

    /**
     * Unloads everything
     */
    void unload();

    /**
     * Loads the default (system wide) layout for this user
     **/
    void loadDefaultLayout() override;

    /**
     * Execute any update script
     */
    void processUpdateScripts();

    int screenForContainment(const Plasma::Containment *containment) const override;

    void showAlternativesForApplet(Plasma::Applet *applet);

private Q_SLOTS:
    void createWaitingPanels();
    void handleContainmentAdded(Plasma::Containment *c);
    void syncAppConfig();
    void checkActivities();
    void cleanupOldPanelConfig();
    void currentActivityChanged(const QString &newActivity);
    void activityAdded(const QString &id);
    void activityRemoved(const QString &id);
    void checkAddPanelAction();
    void addPanel();
    void addPanel(QAction *action);
    void populateAddPanelsMenu();

    void addOutput(QScreen *screen);

    void panelContainmentDestroyed(QObject *cont);
    void handleScreenRemoved(QScreen *screen);
    void handleScreenOrderChanged(QList<QScreen *> screens);

    void activateTaskManagerEntry(int index);

private:
    void sanitizeScreenLayout(const QString &configFileName);
    void destroyDesktopsAndPanels();
    void updateStruts();
    void configurationChanged(const QString &path);
    DesktopView *desktopForScreen(QScreen *screen) const;
    void setupWaylandIntegration();
    void executeSetupPlasmoidScript(Plasma::Containment *containment, Plasma::Applet *applet);
    void checkAllDesktopsUiReady();
    void activateLauncherMenu(const QString &screenName);

    void setupLookAndFeel();
    void handleColorRequestedFromDBus(const QDBusMessage &msg);

#ifndef NDEBUG
    void screenInvariants() const;
#endif

    KSharedConfig::Ptr m_config;
    QString m_configPath;

    // Accent color setting
    KConfigWatcher::Ptr m_kdeGlobalsConfigWatcher;
    bool m_accentColorFromWallpaperEnabled = false;
    QMetaObject::Connection m_fakeColorRequestConn;

    ScreenPool *m_screenPool;
    QString m_shell;
    KActivities::Controller *m_activityController;
    QMap<const Plasma::Containment *, PanelView *> m_panelViews;
    // map from QScreen to desktop view
    QHash<int, DesktopView *> m_desktopViewForScreen;
    QHash<const Plasma::Containment *, int> m_pendingScreenChanges;
    KConfigGroup m_desktopDefaultsConfig;
    KConfigGroup m_lnfDefaultsConfig;
    QList<QPointer<Plasma::Containment>> m_waitingPanels;
    QHash<QString, QString> m_activityContainmentPlugins;
    QAction *m_addPanelAction;
    std::unique_ptr<QMenu> m_addPanelsMenu;
    KPackage::Package m_lookAndFeelPackage;

#if HAVE_X11
    WId m_previousWId = 0;
#endif
    bool m_blockRestorePreviousWindow = false;

    QTimer m_waitingPanelsTimer;
    QTimer m_appConfigSyncTimer;
#ifndef NDEBUG
    QTimer m_invariantsTimer;
#endif
    // For getting the active window on Wayland
    KWayland::Client::PlasmaWindowManagement *m_waylandWindowManagement = nullptr;
    QPointer<KWayland::Client::PlasmaWindow> m_previousPlasmaWindow;
    bool m_closingDown : 1;
    bool m_screenReorderInProgress = false;
    QString m_testModeLayout;
    Plasma::Applet *m_showingAlternatives = nullptr;

    StrutManager *m_strutManager;
    QPointer<ShellContainmentConfig> m_shellContainmentConfig;
    // The set of all the screens which have both the desktop and all panels (if any) fully loaded
    QSet<int> m_screensWithUiReady;
    friend class ShellTest;
};

const QDBusArgument &operator>>(const QDBusArgument &argument, QColor &color);
QDBusArgument &operator<<(QDBusArgument &argument, const QColor &color);
