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

    SPDX-FileCopyrightText: 2012 Martin Gräßlin <mgraesslin@kde.org>
    SPDX-FileCopyrightText: 2018 David Edmundson <davidedmundson@kde.org>

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

#include "scriptedeffect.h"
#include "opengl/glshader.h"
#include "opengl/glshadermanager.h"
#include "scripting_logging.h"
#include "workspace_wrapper.h"

#include "core/output.h"
#include "effect/effecthandler.h"
#include "input.h"
#include "screenedge.h"
#include "workspace.h"
// KDE
#include <KConfigGroup>
#include <KGlobalAccel>
#include <KPluginMetaData>
#include <kconfigloader.h>
// Qt
#include <QAction>
#include <QFile>
#include <QJSEngine>
#include <QList>
#include <QStandardPaths>

#include <optional>

Q_DECLARE_METATYPE(KSharedConfigPtr)

namespace KWin
{

struct AnimationSettings
{
    enum {
        Type = 1 << 0,
        Curve = 1 << 1,
        Delay = 1 << 2,
        Duration = 1 << 3,
        FullScreen = 1 << 4,
        KeepAlive = 1 << 5,
        FrozenTime = 1 << 6
    };
    AnimationEffect::Attribute type;
    QEasingCurve::Type curve;
    QJSValue from;
    QJSValue to;
    int delay;
    qint64 frozenTime;
    uint duration;
    uint set;
    uint metaData;
    bool fullScreenEffect;
    bool keepAlive;
    std::optional<uint> shader;
};

AnimationSettings animationSettingsFromObject(const QJSValue &object)
{
    AnimationSettings settings;
    settings.set = 0;
    settings.metaData = 0;

    settings.to = object.property(QStringLiteral("to"));
    settings.from = object.property(QStringLiteral("from"));

    const QJSValue duration = object.property(QStringLiteral("duration"));
    if (duration.isNumber()) {
        settings.duration = duration.toUInt();
        settings.set |= AnimationSettings::Duration;
    } else {
        settings.duration = 0;
    }

    const QJSValue delay = object.property(QStringLiteral("delay"));
    if (delay.isNumber()) {
        settings.delay = delay.toInt();
        settings.set |= AnimationSettings::Delay;
    } else {
        settings.delay = 0;
    }

    const QJSValue curve = object.property(QStringLiteral("curve"));
    if (curve.isNumber()) {
        settings.curve = static_cast<QEasingCurve::Type>(curve.toInt());
        settings.set |= AnimationSettings::Curve;
    } else {
        settings.curve = QEasingCurve::Linear;
    }

    const QJSValue type = object.property(QStringLiteral("type"));
    if (type.isNumber()) {
        settings.type = static_cast<AnimationEffect::Attribute>(type.toInt());
        settings.set |= AnimationSettings::Type;
    } else {
        settings.type = static_cast<AnimationEffect::Attribute>(-1);
    }

    const QJSValue isFullScreen = object.property(QStringLiteral("fullScreen"));
    if (isFullScreen.isBool()) {
        settings.fullScreenEffect = isFullScreen.toBool();
        settings.set |= AnimationSettings::FullScreen;
    } else {
        settings.fullScreenEffect = false;
    }

    const QJSValue keepAlive = object.property(QStringLiteral("keepAlive"));
    if (keepAlive.isBool()) {
        settings.keepAlive = keepAlive.toBool();
        settings.set |= AnimationSettings::KeepAlive;
    } else {
        settings.keepAlive = true;
    }

    const QJSValue frozenTime = object.property(QStringLiteral("frozenTime"));
    if (frozenTime.isNumber()) {
        settings.frozenTime = frozenTime.toInt();
        settings.set |= AnimationSettings::FrozenTime;
    } else {
        settings.frozenTime = -1;
    }

    if (const auto shader = object.property(QStringLiteral("fragmentShader")); shader.isNumber()) {
        settings.shader = shader.toUInt();
    }

    return settings;
}

static KWin::FPx2 fpx2FromScriptValue(const QJSValue &value)
{
    if (value.isNull()) {
        return FPx2();
    }
    if (value.isNumber()) {
        return FPx2(value.toNumber());
    }
    if (value.isObject()) {
        const QJSValue value1 = value.property(QStringLiteral("value1"));
        const QJSValue value2 = value.property(QStringLiteral("value2"));
        if (!value1.isNumber() || !value2.isNumber()) {
            qCDebug(KWIN_SCRIPTING) << "Cannot cast scripted FPx2 to C++";
            return FPx2();
        }
        return FPx2(value1.toNumber(), value2.toNumber());
    }
    return FPx2();
}

ScriptedEffect *ScriptedEffect::create(const KPluginMetaData &effect)
{
    const QString name = effect.pluginId();
    const QString scriptFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation,
                                                      QLatin1String("kwin/effects/") + name + QLatin1String("/contents/code/main.js"));
    if (scriptFile.isEmpty()) {
        qCDebug(KWIN_SCRIPTING) << "Could not locate effect script" << name;
        return nullptr;
    }

    return ScriptedEffect::create(name, scriptFile, effect.value(QStringLiteral("X-KDE-Ordering"), 0), effect.value(QStringLiteral("X-KWin-Exclusive-Category")));
}

ScriptedEffect *ScriptedEffect::create(const QString &effectName, const QString &pathToScript, int chainPosition, const QString &exclusiveCategory)
{
    ScriptedEffect *effect = new ScriptedEffect();
    effect->m_exclusiveCategory = exclusiveCategory;
    if (!effect->init(effectName, pathToScript)) {
        delete effect;
        return nullptr;
    }
    effect->m_chainPosition = chainPosition;

    return effect;
}

bool ScriptedEffect::supported()
{
    return effects->animationsSupported();
}

ScriptedEffect::ScriptedEffect()
    : AnimationEffect()
    , m_engine(new QJSEngine(this))
    , m_scriptFile(QString())
    , m_config(nullptr)
    , m_chainPosition(0)
{
    Q_ASSERT(effects);
    connect(effects, &EffectsHandler::activeFullScreenEffectChanged, this, [this]() {
        Effect *fullScreenEffect = effects->activeFullScreenEffect();
        if (fullScreenEffect == m_activeFullScreenEffect) {
            return;
        }
        if (m_activeFullScreenEffect == this || fullScreenEffect == this) {
            Q_EMIT isActiveFullScreenEffectChanged();
        }
        m_activeFullScreenEffect = fullScreenEffect;
    });
}

ScriptedEffect::~ScriptedEffect() = default;

bool ScriptedEffect::init(const QString &effectName, const QString &pathToScript)
{
    qRegisterMetaType<QJSValueList>();
    qRegisterMetaType<QList<KWin::EffectWindow *>>();

    QFile scriptFile(pathToScript);
    if (!scriptFile.open(QIODevice::ReadOnly)) {
        qCDebug(KWIN_SCRIPTING) << "Could not open script file: " << pathToScript;
        return false;
    }
    m_effectName = effectName;
    m_scriptFile = pathToScript;

    // does the effect contain an KConfigXT file?
    const QString kconfigXTFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kwin/effects/") + m_effectName + QLatin1String("/contents/config/main.xml"));
    if (!kconfigXTFile.isNull()) {
        KConfigGroup cg = QCoreApplication::instance()->property("config").value<KSharedConfigPtr>()->group(QStringLiteral("Effect-%1").arg(m_effectName));
        QFile xmlFile(kconfigXTFile);
        m_config = new KConfigLoader(cg, &xmlFile, this);
        m_config->load();
    }

    m_engine->installExtensions(QJSEngine::ConsoleExtension);

    QJSValue globalObject = m_engine->globalObject();

    QJSValue effectsObject = m_engine->newQObject(effects);
    QJSEngine::setObjectOwnership(effects, QJSEngine::CppOwnership);
    globalObject.setProperty(QStringLiteral("effects"), effectsObject);

    QJSValue selfObject = m_engine->newQObject(this);
    QJSEngine::setObjectOwnership(this, QJSEngine::CppOwnership);
    globalObject.setProperty(QStringLiteral("effect"), selfObject);

    globalObject.setProperty(QStringLiteral("Effect"),
                             m_engine->newQMetaObject(&ScriptedEffect::staticMetaObject));
    globalObject.setProperty(QStringLiteral("KWin"),
                             m_engine->newQMetaObject(&QtScriptWorkspaceWrapper::staticMetaObject));
    globalObject.setProperty(QStringLiteral("Globals"),
                             m_engine->newQMetaObject(&KWin::staticMetaObject));
    globalObject.setProperty(QStringLiteral("QEasingCurve"),
                             m_engine->newQMetaObject(&QEasingCurve::staticMetaObject));

    static const QStringList globalProperties{
        QStringLiteral("animationTime"),
        QStringLiteral("displayWidth"),
        QStringLiteral("displayHeight"),

        QStringLiteral("registerShortcut"),
        QStringLiteral("registerScreenEdge"),
        QStringLiteral("registerRealtimeScreenEdge"),
        QStringLiteral("registerTouchScreenEdge"),
        QStringLiteral("unregisterScreenEdge"),
        QStringLiteral("unregisterTouchScreenEdge"),

        QStringLiteral("animate"),
        QStringLiteral("set"),
        QStringLiteral("retarget"),
        QStringLiteral("freezeInTime"),
        QStringLiteral("redirect"),
        QStringLiteral("complete"),
        QStringLiteral("cancel"),
        QStringLiteral("addShader"),
        QStringLiteral("setUniform"),
    };

    for (const QString &propertyName : globalProperties) {
        globalObject.setProperty(propertyName, selfObject.property(propertyName));
    }

    const QJSValue result = m_engine->evaluate(QString::fromUtf8(scriptFile.readAll()));

    if (result.isError()) {
        qCWarning(KWIN_SCRIPTING, "%s:%d: error: %s", qPrintable(scriptFile.fileName()),
                  result.property(QStringLiteral("lineNumber")).toInt(),
                  qPrintable(result.property(QStringLiteral("message")).toString()));
        return false;
    }

    return true;
}

void ScriptedEffect::animationEnded(KWin::EffectWindow *w, Attribute a, uint meta)
{
    AnimationEffect::animationEnded(w, a, meta);
    Q_EMIT animationEnded(w, 0);
}

QString ScriptedEffect::pluginId() const
{
    return m_effectName;
}

bool ScriptedEffect::isActiveFullScreenEffect() const
{
    return effects->activeFullScreenEffect() == this;
}

QList<int> ScriptedEffect::touchEdgesForAction(const QString &action) const
{
    QList<int> ret;
    if (m_exclusiveCategory == QLatin1StringView("show-desktop") && action == QLatin1StringView("show-desktop")) {
        for (const auto b : {ElectricTop, ElectricRight, ElectricBottom, ElectricLeft}) {
            if (workspace()->screenEdges()->actionForTouchBorder(b) == ElectricActionShowDesktop) {
                ret.append(b);
            }
        }
        return ret;
    } else {
        if (!m_config) {
            return ret;
        }
        return m_config->property(QStringLiteral("TouchBorderActivate") + action).value<QList<int>>();
    }
}

QJSValue ScriptedEffect::animate_helper(const QJSValue &object, AnimationType animationType)
{
    QJSValue windowProperty = object.property(QStringLiteral("window"));
    if (!windowProperty.isObject()) {
        m_engine->throwError(QStringLiteral("Window property missing in animation options"));
        return QJSValue();
    }

    EffectWindow *window = qobject_cast<EffectWindow *>(windowProperty.toQObject());
    if (!window) {
        m_engine->throwError(QStringLiteral("Window property references invalid window"));
        return QJSValue();
    }

    QList<AnimationSettings> settings{animationSettingsFromObject(object)}; // global

    QJSValue animations = object.property(QStringLiteral("animations")); // array
    if (!animations.isUndefined()) {
        if (!animations.isArray()) {
            m_engine->throwError(QStringLiteral("Animations provided but not an array"));
            return QJSValue();
        }

        const int length = static_cast<int>(animations.property(QStringLiteral("length")).toInt());
        for (int i = 0; i < length; ++i) {
            QJSValue value = animations.property(QString::number(i));
            if (value.isObject()) {
                AnimationSettings s = animationSettingsFromObject(value);
                const uint set = s.set | settings.at(0).set;
                // Catch show stoppers (incompletable animation)
                if (!(set & AnimationSettings::Type)) {
                    m_engine->throwError(QStringLiteral("Type property missing in animation options"));
                    return QJSValue();
                }
                if (!(set & AnimationSettings::Duration)) {
                    m_engine->throwError(QStringLiteral("Duration property missing in animation options"));
                    return QJSValue();
                }
                // Complete local animations from global settings
                if (!(s.set & AnimationSettings::Duration)) {
                    s.duration = settings.at(0).duration;
                }
                if (!(s.set & AnimationSettings::Curve)) {
                    s.curve = settings.at(0).curve;
                }
                if (!(s.set & AnimationSettings::Delay)) {
                    s.delay = settings.at(0).delay;
                }
                if (!(s.set & AnimationSettings::FullScreen)) {
                    s.fullScreenEffect = settings.at(0).fullScreenEffect;
                }
                if (!(s.set & AnimationSettings::KeepAlive)) {
                    s.keepAlive = settings.at(0).keepAlive;
                }
                if (!s.shader.has_value()) {
                    s.shader = settings.at(0).shader;
                }

                s.metaData = 0;
                typedef QMap<AnimationEffect::MetaType, QString> MetaTypeMap;
                static MetaTypeMap metaTypes({{AnimationEffect::SourceAnchor, QStringLiteral("sourceAnchor")},
                                              {AnimationEffect::TargetAnchor, QStringLiteral("targetAnchor")},
                                              {AnimationEffect::RelativeSourceX, QStringLiteral("relativeSourceX")},
                                              {AnimationEffect::RelativeSourceY, QStringLiteral("relativeSourceY")},
                                              {AnimationEffect::RelativeTargetX, QStringLiteral("relativeTargetX")},
                                              {AnimationEffect::RelativeTargetY, QStringLiteral("relativeTargetY")},
                                              {AnimationEffect::Axis, QStringLiteral("axis")}});

                for (auto it = metaTypes.constBegin(),
                          end = metaTypes.constEnd();
                     it != end; ++it) {
                    QJSValue metaVal = value.property(*it);
                    if (metaVal.isNumber()) {
                        AnimationEffect::setMetaData(it.key(), metaVal.toInt(), s.metaData);
                    }
                }
                if (s.type == ShaderUniform && s.shader) {
                    auto uniformProperty = value.property(QStringLiteral("uniform")).toString();
                    auto shader = findShader(s.shader.value());
                    if (!shader) {
                        m_engine->throwError(QStringLiteral("Shader for given shaderId not found"));
                        return {};
                    }
                    if (!effects->makeOpenGLContextCurrent()) {
                        m_engine->throwError(QStringLiteral("Failed to make OpenGL context current"));
                        return {};
                    }
                    ShaderBinder binder{shader};
                    s.metaData = shader->uniformLocation(uniformProperty.toUtf8().constData());
                }

                settings << s;
            }
        }
    }

    if (settings.count() == 1) {
        const uint set = settings.at(0).set;
        if (!(set & AnimationSettings::Type)) {
            m_engine->throwError(QStringLiteral("Type property missing in animation options"));
            return QJSValue();
        }
        if (!(set & AnimationSettings::Duration)) {
            m_engine->throwError(QStringLiteral("Duration property missing in animation options"));
            return QJSValue();
        }
    } else if (!(settings.at(0).set & AnimationSettings::Type)) { // invalid global
        settings.removeAt(0); // -> get rid of it, only used to complete the others
    }

    if (settings.isEmpty()) {
        m_engine->throwError(QStringLiteral("No animations provided"));
        return QJSValue();
    }

    QJSValue array = m_engine->newArray(settings.length());
    for (int i = 0; i < settings.count(); i++) {
        const AnimationSettings &setting = settings[i];
        int animationId;
        if (animationType == AnimationType::Set) {
            animationId = set(window,
                              setting.type,
                              setting.duration,
                              setting.to,
                              setting.from,
                              setting.metaData,
                              setting.curve,
                              setting.delay,
                              setting.fullScreenEffect,
                              setting.keepAlive,
                              setting.shader ? setting.shader.value() : 0u);
            if (setting.frozenTime >= 0) {
                freezeInTime(animationId, setting.frozenTime);
            }
        } else {
            animationId = animate(window,
                                  setting.type,
                                  setting.duration,
                                  setting.to,
                                  setting.from,
                                  setting.metaData,
                                  setting.curve,
                                  setting.delay,
                                  setting.fullScreenEffect,
                                  setting.keepAlive,
                                  setting.shader ? setting.shader.value() : 0u);
            if (setting.frozenTime >= 0) {
                freezeInTime(animationId, setting.frozenTime);
            }
        }
        array.setProperty(i, animationId);
    }

    return array;
}

quint64 ScriptedEffect::animate(KWin::EffectWindow *window, KWin::AnimationEffect::Attribute attribute,
                                int ms, const QJSValue &to, const QJSValue &from, uint metaData, int curve,
                                int delay, bool fullScreen, bool keepAlive, uint shaderId)
{
    QEasingCurve qec;
    if (curve < QEasingCurve::Custom) {
        qec.setType(static_cast<QEasingCurve::Type>(curve));
    } else if (curve == GaussianCurve) {
        qec.setCustomType(qecGaussian);
    }
    return AnimationEffect::animate(window, attribute, metaData, ms, fpx2FromScriptValue(to), qec,
                                    delay, fpx2FromScriptValue(from), fullScreen, keepAlive, findShader(shaderId));
}

QJSValue ScriptedEffect::animate(const QJSValue &object)
{
    return animate_helper(object, AnimationType::Animate);
}

quint64 ScriptedEffect::set(KWin::EffectWindow *window, KWin::AnimationEffect::Attribute attribute,
                            int ms, const QJSValue &to, const QJSValue &from, uint metaData, int curve,
                            int delay, bool fullScreen, bool keepAlive, uint shaderId)
{
    QEasingCurve qec;
    if (curve < QEasingCurve::Custom) {
        qec.setType(static_cast<QEasingCurve::Type>(curve));
    } else if (curve == GaussianCurve) {
        qec.setCustomType(qecGaussian);
    }
    return AnimationEffect::set(window, attribute, metaData, ms, fpx2FromScriptValue(to), qec,
                                delay, fpx2FromScriptValue(from), fullScreen, keepAlive, findShader(shaderId));
}

QJSValue ScriptedEffect::set(const QJSValue &object)
{
    return animate_helper(object, AnimationType::Set);
}

bool ScriptedEffect::retarget(quint64 animationId, const QJSValue &newTarget, int newRemainingTime)
{
    return AnimationEffect::retarget(animationId, fpx2FromScriptValue(newTarget), newRemainingTime);
}

bool ScriptedEffect::retarget(const QList<quint64> &animationIds, const QJSValue &newTarget, int newRemainingTime)
{
    return std::all_of(animationIds.begin(), animationIds.end(), [&](quint64 animationId) {
        return retarget(animationId, newTarget, newRemainingTime);
    });
}

bool ScriptedEffect::freezeInTime(quint64 animationId, qint64 frozenTime)
{
    return AnimationEffect::freezeInTime(animationId, frozenTime);
}

bool ScriptedEffect::freezeInTime(const QList<quint64> &animationIds, qint64 frozenTime)
{
    return std::all_of(animationIds.begin(), animationIds.end(), [&](quint64 animationId) {
        return AnimationEffect::freezeInTime(animationId, frozenTime);
    });
}

bool ScriptedEffect::redirect(quint64 animationId, Direction direction, TerminationFlags terminationFlags)
{
    return AnimationEffect::redirect(animationId, direction, terminationFlags);
}

bool ScriptedEffect::redirect(const QList<quint64> &animationIds, Direction direction, TerminationFlags terminationFlags)
{
    return std::all_of(animationIds.begin(), animationIds.end(), [&](quint64 animationId) {
        return redirect(animationId, direction, terminationFlags);
    });
}

bool ScriptedEffect::complete(quint64 animationId)
{
    return AnimationEffect::complete(animationId);
}

bool ScriptedEffect::complete(const QList<quint64> &animationIds)
{
    return std::all_of(animationIds.begin(), animationIds.end(), [&](quint64 animationId) {
        return complete(animationId);
    });
}

bool ScriptedEffect::cancel(quint64 animationId)
{
    return AnimationEffect::cancel(animationId);
}

bool ScriptedEffect::cancel(const QList<quint64> &animationIds)
{
    bool ret = false;
    for (const quint64 &animationId : animationIds) {
        ret |= cancel(animationId);
    }
    return ret;
}

bool ScriptedEffect::isGrabbed(EffectWindow *w, ScriptedEffect::DataRole grabRole)
{
    void *e = w->data(static_cast<KWin::DataRole>(grabRole)).value<void *>();
    if (e) {
        return e != this;
    } else {
        return false;
    }
}

bool ScriptedEffect::grab(EffectWindow *w, DataRole grabRole, bool force)
{
    void *grabber = w->data(grabRole).value<void *>();

    if (grabber == this) {
        return true;
    }

    if (grabber != nullptr && grabber != this && !force) {
        return false;
    }

    w->setData(grabRole, QVariant::fromValue(static_cast<void *>(this)));

    return true;
}

bool ScriptedEffect::ungrab(EffectWindow *w, DataRole grabRole)
{
    void *grabber = w->data(grabRole).value<void *>();

    if (grabber == nullptr) {
        return true;
    }

    if (grabber != this) {
        return false;
    }

    w->setData(grabRole, QVariant());

    return true;
}

void ScriptedEffect::reconfigure(ReconfigureFlags flags)
{
    AnimationEffect::reconfigure(flags);
    if (m_config) {
        m_config->read();
    }
    Q_EMIT configChanged();
}

void ScriptedEffect::registerShortcut(const QString &objectName, const QString &text,
                                      const QString &keySequence, const QJSValue &callback)
{
    if (!callback.isCallable()) {
        m_engine->throwError(QStringLiteral("Shortcut handler must be callable"));
        return;
    }
    QAction *action = new QAction(this);
    action->setObjectName(objectName);
    action->setText(text);
    const QKeySequence shortcut = QKeySequence(keySequence);
    KGlobalAccel::self()->setShortcut(action, QList<QKeySequence>() << shortcut);
    connect(action, &QAction::triggered, this, [this, action, callback]() {
        QJSValue actionObject = m_engine->newQObject(action);
        QJSEngine::setObjectOwnership(action, QJSEngine::CppOwnership);
        QJSValue(callback).call(QJSValueList{actionObject});
    });
}

bool ScriptedEffect::borderActivated(ElectricBorder edge)
{
    auto it = screenEdgeCallbacks().constFind(edge);
    if (it != screenEdgeCallbacks().constEnd()) {
        for (const QJSValue &callback : it.value()) {
            QJSValue(callback).call();
        }
    }
    return true;
}

QJSValue ScriptedEffect::readConfig(const QString &key, const QJSValue &defaultValue)
{
    if (!m_config) {
        return defaultValue;
    }
    return m_engine->toScriptValue(m_config->property(key));
}

int ScriptedEffect::displayWidth() const
{
    return workspace()->geometry().width();
}

int ScriptedEffect::displayHeight() const
{
    return workspace()->geometry().height();
}

int ScriptedEffect::animationTime(int defaultTime) const
{
    return Effect::animationTime(std::chrono::milliseconds(defaultTime));
}

bool ScriptedEffect::registerScreenEdge(int edge, const QJSValue &callback)
{
    if (!callback.isCallable()) {
        m_engine->throwError(QStringLiteral("Screen edge handler must be callable"));
        return false;
    }
    auto it = screenEdgeCallbacks().find(edge);
    if (it == screenEdgeCallbacks().end()) {
        // not yet registered
        workspace()->screenEdges()->reserve(static_cast<KWin::ElectricBorder>(edge), this, "borderActivated");
        screenEdgeCallbacks().insert(edge, QJSValueList{callback});
    } else {
        it->append(callback);
    }
    return true;
}

bool ScriptedEffect::registerRealtimeScreenEdge(int edge, const QJSValue &callback)
{
    if (!callback.isCallable()) {
        m_engine->throwError(QStringLiteral("Screen edge handler must be callable"));
        return false;
    }
    auto it = realtimeScreenEdgeCallbacks().find(edge);
    if (it == realtimeScreenEdgeCallbacks().end()) {
        // not yet registered
        realtimeScreenEdgeCallbacks().insert(edge, QJSValueList{callback});
        auto *triggerAction = new QAction(this);
        connect(triggerAction, &QAction::triggered, this, [this, edge]() {
            auto it = realtimeScreenEdgeCallbacks().constFind(edge);
            if (it != realtimeScreenEdgeCallbacks().constEnd()) {
                for (const QJSValue &callback : it.value()) {
                    QJSValue(callback).call({edge});
                }
            }
        });
        effects->registerRealtimeTouchBorder(static_cast<KWin::ElectricBorder>(edge), triggerAction, [this](ElectricBorder border, const QPointF &deltaProgress, Output *screen) {
            auto it = realtimeScreenEdgeCallbacks().constFind(border);
            if (it != realtimeScreenEdgeCallbacks().constEnd()) {
                for (const QJSValue &callback : it.value()) {
                    QJSValue delta = m_engine->newObject();
                    delta.setProperty("width", deltaProgress.x());
                    delta.setProperty("height", deltaProgress.y());

                    QJSValue(callback).call({border, QJSValue(delta), m_engine->newQObject(screen)});
                }
            }
        });
    } else {
        it->append(callback);
    }
    return true;
}

bool ScriptedEffect::unregisterScreenEdge(int edge)
{
    auto it = screenEdgeCallbacks().find(edge);
    if (it == screenEdgeCallbacks().end()) {
        // not previously registered
        return false;
    }
    workspace()->screenEdges()->unreserve(static_cast<KWin::ElectricBorder>(edge), this);
    screenEdgeCallbacks().erase(it);
    return true;
}

bool ScriptedEffect::registerTouchScreenEdge(int edge, const QJSValue &callback)
{
    if (m_touchScreenEdgeCallbacks.constFind(edge) != m_touchScreenEdgeCallbacks.constEnd()) {
        return false;
    }
    if (!callback.isCallable()) {
        m_engine->throwError(QStringLiteral("Touch screen edge handler must be callable"));
        return false;
    }
    QAction *action = new QAction(this);
    connect(action, &QAction::triggered, this, [callback]() {
        QJSValue(callback).call();
    });
    workspace()->screenEdges()->reserveTouch(KWin::ElectricBorder(edge), action);
    m_touchScreenEdgeCallbacks.insert(edge, action);
    return true;
}

bool ScriptedEffect::unregisterTouchScreenEdge(int edge)
{
    auto it = m_touchScreenEdgeCallbacks.find(edge);
    if (it == m_touchScreenEdgeCallbacks.end()) {
        return false;
    }
    delete it.value();
    m_touchScreenEdgeCallbacks.erase(it);
    return true;
}

QJSEngine *ScriptedEffect::engine() const
{
    return m_engine;
}

uint ScriptedEffect::addFragmentShader(ShaderTrait traits, const QString &fragmentShaderFile)
{
    if (!effects->makeOpenGLContextCurrent()) {
        m_engine->throwError(QStringLiteral("Failed to make OpenGL context current"));
        return 0;
    }
    const QString shaderDir{QLatin1String("kwin/effects/") + m_effectName + QLatin1String("/contents/shaders/")};
    const QString fragment = fragmentShaderFile.isEmpty() ? QString{} : QStandardPaths::locate(QStandardPaths::GenericDataLocation, shaderDir + fragmentShaderFile);

    auto shader = ShaderManager::instance()->generateShaderFromFile(static_cast<KWin::ShaderTraits>(int(traits)), {}, fragment);
    if (!shader->isValid()) {
        m_engine->throwError(QStringLiteral("Shader failed to load"));
        // 0 is never a valid shader identifier, it's ensured the first shader gets id 1
        return 0;
    }

    const uint shaderId{m_nextShaderId};
    m_nextShaderId++;
    m_shaders[shaderId] = std::move(shader);
    return shaderId;
}

GLShader *ScriptedEffect::findShader(uint shaderId) const
{
    if (auto it = m_shaders.find(shaderId); it != m_shaders.end()) {
        return it->second.get();
    }
    return nullptr;
}

void ScriptedEffect::setUniform(uint shaderId, const QString &name, const QJSValue &value)
{
    auto shader = findShader(shaderId);
    if (!shader) {
        m_engine->throwError(QStringLiteral("Shader for given shaderId not found"));
        return;
    }
    if (!effects->makeOpenGLContextCurrent()) {
        m_engine->throwError(QStringLiteral("Failed to make OpenGL context current"));
        return;
    }
    auto setColorUniform = [this, shader, name](const QColor &color) {
        if (!color.isValid()) {
            return;
        }
        if (!shader->setUniform(name.toUtf8().constData(), color)) {
            m_engine->throwError(QStringLiteral("Failed to set uniform ") + name);
        }
    };
    ShaderBinder binder{shader};
    if (value.isString()) {
        setColorUniform(value.toString());
    } else if (value.isNumber()) {
        if (!shader->setUniform(name.toUtf8().constData(), float(value.toNumber()))) {
            m_engine->throwError(QStringLiteral("Failed to set uniform ") + name);
        }
    } else if (value.isArray()) {
        const auto length = value.property(QStringLiteral("length")).toInt();
        if (length == 2) {
            if (!shader->setUniform(name.toUtf8().constData(), QVector2D{float(value.property(0).toNumber()), float(value.property(1).toNumber())})) {
                m_engine->throwError(QStringLiteral("Failed to set uniform ") + name);
            }
        } else if (length == 3) {
            if (!shader->setUniform(name.toUtf8().constData(), QVector3D{float(value.property(0).toNumber()), float(value.property(1).toNumber()), float(value.property(2).toNumber())})) {
                m_engine->throwError(QStringLiteral("Failed to set uniform ") + name);
            }
        } else if (length == 4) {
            if (!shader->setUniform(name.toUtf8().constData(), QVector4D{float(value.property(0).toNumber()), float(value.property(1).toNumber()), float(value.property(2).toNumber()), float(value.property(3).toNumber())})) {
                m_engine->throwError(QStringLiteral("Failed to set uniform ") + name);
            }
        } else {
            m_engine->throwError(QStringLiteral("Invalid number of elements in array"));
        }
    } else if (value.isVariant()) {
        const auto variant = value.toVariant();
        setColorUniform(variant.value<QColor>());
    } else {
        m_engine->throwError(QStringLiteral("Invalid value provided for uniform"));
    }
}

} // namespace

#include "moc_scriptedeffect.cpp"
