#include <QCoreApplication>
#include <QDBusConnection>
#include <QDBusError>
#include <QDBusInterface>
#include <QDBusReply>
#include <QDBusUnixFileDescriptor>
#include <QDir>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QTemporaryFile>
#include <QTextStream>
#include <QTimer>
#include <QVariantMap>

#include <unistd.h>

class WindowReceiver : public QObject
{
    Q_OBJECT
    Q_CLASSINFO("D-Bus Interface", "org.codex.ReforgerQueueKWinProbe")

public:
    QString json;
    QString error;
    bool done = false;

public slots:
    void WindowsJson(const QString &payload)
    {
        json = payload;
        done = true;
        QCoreApplication::quit();
    }

    void Error(const QString &message)
    {
        error = message;
        done = true;
        QCoreApplication::quit();
    }
};

QDBusConnection sessionBus()
{
    QDBusConnection bus = QDBusConnection::sessionBus();
    if (bus.isConnected()) {
        return bus;
    }

    QString runtimeDir = qEnvironmentVariable("XDG_RUNTIME_DIR");
    if (runtimeDir.isEmpty()) {
        runtimeDir = QStringLiteral("/run/user/%1").arg(getuid());
    }

    return QDBusConnection::connectToBus(
        QStringLiteral("unix:path=%1/bus").arg(runtimeDir),
        QStringLiteral("reforger-queue-session"));
}

QString jsString(const QString &value)
{
    QJsonArray array;
    array.append(value);
    QByteArray encoded = QJsonDocument(array).toJson(QJsonDocument::Compact);
    encoded.chop(1);
    encoded.remove(0, 1);
    return QString::fromUtf8(encoded);
}

int capture(const QString &method, const QString &target, const QString &outputPath, QTextStream &out, QTextStream &err)
{
    QFile file(outputPath);
    if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
        err << "failed to open output file: " << file.errorString() << "\n";
        return 1;
    }

    QDBusConnection bus = sessionBus();
    if (!bus.isConnected()) {
        err << "failed to connect to the session D-Bus: " << bus.lastError().message() << "\n";
        return 1;
    }

    QDBusInterface iface(
        QStringLiteral("org.kde.KWin"),
        QStringLiteral("/org/kde/KWin/ScreenShot2"),
        QStringLiteral("org.kde.KWin.ScreenShot2"),
        bus);

    if (!iface.isValid()) {
        err << "failed to connect to KWin ScreenShot2: "
            << iface.lastError().message() << "\n";
        return 1;
    }

    QVariantMap options;
    QDBusUnixFileDescriptor fd(file.handle());
    QDBusReply<QVariantMap> reply = iface.call(method, target, options, QVariant::fromValue(fd));

    file.close();

    if (!reply.isValid()) {
        err << method << " failed for " << target << ": "
            << reply.error().name() << ": " << reply.error().message() << "\n";
        return 1;
    }

    QJsonObject resultJson;
    const QVariantMap resultMap = reply.value();
    for (auto it = resultMap.constBegin(); it != resultMap.constEnd(); ++it) {
        resultJson.insert(it.key(), QJsonValue::fromVariant(it.value()));
    }
    out << QJsonDocument(resultJson).toJson(QJsonDocument::Compact) << "\n";

    return 0;
}

QString windowProbeScript(const QString &service, const QString &path, const QString &interface)
{
    return QStringLiteral(R"JS(
function safeString(value) {
    if (value === undefined || value === null) {
        return "";
    }
    return String(value);
}

function safeNumber(value) {
    var number = Number(value);
    return isNaN(number) ? 0 : number;
}

function safeBool(value) {
    return Boolean(value);
}

function prop(object, name, fallback) {
    try {
        if (object[name] === undefined || object[name] === null) {
            return fallback;
        }
        return object[name];
    } catch (error) {
        return fallback;
    }
}

function rect(value) {
    if (!value) {
        return {"x": 0, "y": 0, "width": 0, "height": 0};
    }
    return {
        "x": Math.round(safeNumber(prop(value, "x", 0))),
        "y": Math.round(safeNumber(prop(value, "y", 0))),
        "width": Math.round(safeNumber(prop(value, "width", 0))),
        "height": Math.round(safeNumber(prop(value, "height", 0)))
    };
}

function outputName(window) {
    try {
        if (window.output && window.output.name) {
            return String(window.output.name);
        }
    } catch (error) {
    }
    return "";
}

function windowInfo(window) {
    var geometry = rect(prop(window, "frameGeometry", prop(window, "bufferGeometry", null)));
    return {
        "internalId": safeString(prop(window, "internalId", "")),
        "pid": safeNumber(prop(window, "pid", 0)),
        "caption": safeString(prop(window, "caption", "")),
        "resourceClass": safeString(prop(window, "resourceClass", "")),
        "resourceName": safeString(prop(window, "resourceName", "")),
        "windowRole": safeString(prop(window, "windowRole", "")),
        "desktopFileName": safeString(prop(window, "desktopFileName", "")),
        "output": outputName(window),
        "frameGeometry": geometry,
        "normalWindow": safeBool(prop(window, "normalWindow", false)),
        "fullScreen": safeBool(prop(window, "fullScreen", false)),
        "minimized": safeBool(prop(window, "minimized", false)),
        "hidden": safeBool(prop(window, "hidden", false)),
        "keepAbove": safeBool(prop(window, "keepAbove", false)),
        "skipTaskbar": safeBool(prop(window, "skipTaskbar", false))
    };
}

try {
    var windows = workspace.windowList().map(windowInfo);
    callDBus(%1, %2, %3, "WindowsJson", JSON.stringify(windows));
} catch (error) {
    callDBus(%1, %2, %3, "Error", safeString(error && error.stack ? error.stack : error));
}
)JS")
        .arg(jsString(service), jsString(path), jsString(interface));
}

int listWindows(QCoreApplication &app, QTextStream &out, QTextStream &err)
{
    QDBusConnection bus = sessionBus();
    if (!bus.isConnected()) {
        err << "failed to connect to the session D-Bus: " << bus.lastError().message() << "\n";
        return 1;
    }

    const QString service = QStringLiteral("org.codex.ReforgerQueueKWinProbe%1").arg(QCoreApplication::applicationPid());
    const QString path = QStringLiteral("/Probe");
    const QString interface = QStringLiteral("org.codex.ReforgerQueueKWinProbe");

    WindowReceiver receiver;
    if (!bus.registerService(service)) {
        err << "failed to register D-Bus service " << service << ": " << bus.lastError().message() << "\n";
        return 1;
    }
    if (!bus.registerObject(path, &receiver, QDBusConnection::ExportAllSlots)) {
        err << "failed to register D-Bus object " << path << ": " << bus.lastError().message() << "\n";
        bus.unregisterService(service);
        return 1;
    }

    QTemporaryFile scriptFile;
    scriptFile.setFileTemplate(QDir::tempPath() + QStringLiteral("/reforger-kwin-window-probe-XXXXXX.js"));
    if (!scriptFile.open()) {
        err << "failed to create temporary KWin script: " << scriptFile.errorString() << "\n";
        bus.unregisterObject(path);
        bus.unregisterService(service);
        return 1;
    }
    scriptFile.write(windowProbeScript(service, path, interface).toUtf8());
    scriptFile.close();

    const QString pluginName = QStringLiteral("reforger-queue-window-probe-%1").arg(QCoreApplication::applicationPid());
    QDBusInterface scripting(
        QStringLiteral("org.kde.KWin"),
        QStringLiteral("/Scripting"),
        QStringLiteral("org.kde.kwin.Scripting"),
        bus);

    if (!scripting.isValid()) {
        err << "failed to connect to KWin scripting: " << scripting.lastError().message() << "\n";
        bus.unregisterObject(path);
        bus.unregisterService(service);
        return 1;
    }

    QDBusReply<int> loadReply = scripting.call(QStringLiteral("loadScript"), scriptFile.fileName(), pluginName);
    if (!loadReply.isValid() || loadReply.value() < 0) {
        err << "failed to load KWin window probe script: "
            << loadReply.error().name() << ": " << loadReply.error().message() << "\n";
        bus.unregisterObject(path);
        bus.unregisterService(service);
        return 1;
    }

    const int scriptId = loadReply.value();
    QDBusInterface script(
        QStringLiteral("org.kde.KWin"),
        QStringLiteral("/Scripting/Script%1").arg(scriptId),
        QStringLiteral("org.kde.kwin.Script"),
        bus);
    QDBusMessage runReply = script.call(QStringLiteral("run"));
    if (runReply.type() == QDBusMessage::ErrorMessage) {
        err << "failed to run KWin window probe script: "
            << runReply.errorName() << ": " << runReply.errorMessage() << "\n";
        scripting.call(QStringLiteral("unloadScript"), pluginName);
        bus.unregisterObject(path);
        bus.unregisterService(service);
        return 1;
    }

    QTimer::singleShot(5000, &app, [&receiver]() {
        if (!receiver.done) {
            receiver.error = QStringLiteral("timed out waiting for KWin window probe results");
            receiver.done = true;
            QCoreApplication::quit();
        }
    });
    app.exec();

    scripting.call(QStringLiteral("unloadScript"), pluginName);
    bus.unregisterObject(path);
    bus.unregisterService(service);

    if (!receiver.error.isEmpty()) {
        err << receiver.error << "\n";
        return 1;
    }
    if (receiver.json.isEmpty()) {
        err << "KWin window probe returned no data\n";
        return 1;
    }

    out << receiver.json << "\n";
    return 0;
}

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);
    QTextStream out(stdout);
    QTextStream err(stderr);

    const QStringList args = QCoreApplication::arguments();

    if (args.size() == 3 && !args.at(1).startsWith(QLatin1String("--"))) {
        return capture(QStringLiteral("CaptureScreen"), args.at(1), args.at(2), out, err);
    }

    if (args.size() == 2 && args.at(1) == QLatin1String("--list-windows")) {
        return listWindows(app, out, err);
    }

    if (args.size() == 4 && args.at(1) == QLatin1String("--capture-screen")) {
        return capture(QStringLiteral("CaptureScreen"), args.at(2), args.at(3), out, err);
    }

    if (args.size() == 4 && args.at(1) == QLatin1String("--capture-window")) {
        return capture(QStringLiteral("CaptureWindow"), args.at(2), args.at(3), out, err);
    }

    err << "usage:\n"
        << "  " << args.value(0) << " OUTPUT_NAME OUTPUT_FILE\n"
        << "  " << args.value(0) << " --capture-screen OUTPUT_NAME OUTPUT_FILE\n"
        << "  " << args.value(0) << " --capture-window INTERNAL_ID OUTPUT_FILE\n"
        << "  " << args.value(0) << " --list-windows\n";
    return 64;
}

#include "kwin_capture_screen.moc"
