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

Copyright (C) 2017 Martin Gräßlin <mgraesslin@kde.org>

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, 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 General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*********************************************************************/
#include "kwin_wayland_test.h"
#include "keyboard_input.h"
#include "keyboard_layout.h"
#include "platform.h"
#include "xdgshellclient.h"
#include "virtualdesktops.h"
#include "wayland_server.h"
#include "workspace.h"

#include <KConfigGroup>
#include <KGlobalAccel>

#include <KWayland/Client/surface.h>

#include <QAction>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusMessage>
#include <QDBusPendingCall>

#include <linux/input.h>

using namespace KWin;
using namespace KWayland::Client;

static const QString s_socketName = QStringLiteral("wayland_test_kwin_keyboard_laout-0");

class KeyboardLayoutTest : public QObject
{
    Q_OBJECT
private Q_SLOTS:
    void initTestCase();
    void init();
    void cleanup();

    void testReconfigure();
    void testChangeLayoutThroughDBus();
    void testPerLayoutShortcut();
    void testDBusServiceExport();
    void testVirtualDesktopPolicy();
    void testWindowPolicy();
    void testApplicationPolicy();
    void testNumLock();

private:
    void reconfigureLayouts();
};

void KeyboardLayoutTest::reconfigureLayouts()
{
    // create DBus signal to reload
    QDBusMessage message = QDBusMessage::createSignal(QStringLiteral("/Layouts"), QStringLiteral("org.kde.keyboard"), QStringLiteral("reloadConfig"));
    QDBusConnection::sessionBus().send(message);
}

void KeyboardLayoutTest::initTestCase()
{
    qRegisterMetaType<KWin::XdgShellClient *>();
    qRegisterMetaType<KWin::AbstractClient*>();
    QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated);
    QVERIFY(workspaceCreatedSpy.isValid());
    kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024));
    QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit()));

    kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig));
    kwinApp()->setKxkbConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig));
    kwinApp()->setInputConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig));

    kwinApp()->start();
    QVERIFY(workspaceCreatedSpy.wait());
    waylandServer()->initWorkspace();
}

void KeyboardLayoutTest::init()
{
    QVERIFY(Test::setupWaylandConnection());
}

void KeyboardLayoutTest::cleanup()
{
    Test::destroyWaylandConnection();
}

class LayoutChangedSignalWrapper : public QObject
{
    Q_OBJECT
public:
    LayoutChangedSignalWrapper()
        : QObject()
    {
        QDBusConnection::sessionBus().connect(QStringLiteral("org.kde.keyboard"), QStringLiteral("/Layouts"), QStringLiteral("org.kde.KeyboardLayouts"), QStringLiteral("currentLayoutChanged"), this, SIGNAL(layoutChanged(QString)));
    }

Q_SIGNALS:
    void layoutChanged(const QString &name);
};

void KeyboardLayoutTest::testReconfigure()
{
    // verifies that we can change the keymap

    // default should be a keymap with only us layout
    auto xkb = input()->keyboard()->xkb();
    QCOMPARE(xkb->numberOfLayouts(), 1u);
    QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)"));
    auto layouts = xkb->layoutNames();
    QCOMPARE(layouts.size(), 1);
    QVERIFY(layouts.contains(0));
    QCOMPARE(layouts[0], QStringLiteral("English (US)"));

    // create a new keymap
    KConfigGroup layoutGroup = kwinApp()->kxkbConfig()->group("Layout");
    layoutGroup.writeEntry("LayoutList", QStringLiteral("de,us"));
    layoutGroup.sync();

    reconfigureLayouts();
    // now we should have two layouts
    QTRY_COMPARE(xkb->numberOfLayouts(), 2u);
    // default layout is German
    QCOMPARE(xkb->layoutName(), QStringLiteral("German"));
    layouts = xkb->layoutNames();
    QCOMPARE(layouts.size(), 2);
    QVERIFY(layouts.contains(0));
    QVERIFY(layouts.contains(1));
    QCOMPARE(layouts[0], QStringLiteral("German"));
    QCOMPARE(layouts[1], QStringLiteral("English (US)"));
}

void KeyboardLayoutTest::testChangeLayoutThroughDBus()
{
    // this test verifies that the layout can be changed through DBus
    // first configure layouts
    KConfigGroup layoutGroup = kwinApp()->kxkbConfig()->group("Layout");
    layoutGroup.writeEntry("LayoutList", QStringLiteral("de,us,de(neo)"));
    layoutGroup.sync();
    reconfigureLayouts();
    // now we should have two layouts
    auto xkb = input()->keyboard()->xkb();
    QTRY_COMPARE(xkb->numberOfLayouts(), 3u);
    // default layout is German
    xkb->switchToLayout(0);
    QCOMPARE(xkb->layoutName(), QStringLiteral("German"));

    LayoutChangedSignalWrapper wrapper;
    QSignalSpy layoutChangedSpy(&wrapper, &LayoutChangedSignalWrapper::layoutChanged);
    QVERIFY(layoutChangedSpy.isValid());

    // now change through DBus to english
    auto changeLayout = [] (const QString &layoutName) {
        QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.keyboard"), QStringLiteral("/Layouts"), QStringLiteral("org.kde.KeyboardLayouts"), QStringLiteral("setLayout"));
        msg << layoutName;
        return QDBusConnection::sessionBus().asyncCall(msg);
    };
    auto reply = changeLayout(QStringLiteral("English (US)"));
    reply.waitForFinished();
    QVERIFY(!reply.isError());
    QCOMPARE(reply.reply().arguments().first().toBool(), true);
    QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)"));
    QVERIFY(layoutChangedSpy.wait());
    QCOMPARE(layoutChangedSpy.count(), 1);
    layoutChangedSpy.clear();

    // switch to a layout which does not exist
    reply = changeLayout(QStringLiteral("French"));
    QVERIFY(!reply.isError());
    QCOMPARE(reply.reply().arguments().first().toBool(), false);
    QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)"));
    QVERIFY(!layoutChangedSpy.wait());
    QVERIFY(layoutChangedSpy.isEmpty());

    // switch to another layout should work
    reply = changeLayout(QStringLiteral("German"));
    QVERIFY(!reply.isError());
    QCOMPARE(reply.reply().arguments().first().toBool(), true);
    QCOMPARE(xkb->layoutName(), QStringLiteral("German"));
    QVERIFY(layoutChangedSpy.wait());
    QCOMPARE(layoutChangedSpy.count(), 1);
    layoutChangedSpy.clear();

    // switching to same layout should also work
    reply = changeLayout(QStringLiteral("German"));
    QVERIFY(!reply.isError());
    QCOMPARE(reply.reply().arguments().first().toBool(), true);
    QCOMPARE(xkb->layoutName(), QStringLiteral("German"));
    QVERIFY(!layoutChangedSpy.wait());
    QVERIFY(layoutChangedSpy.isEmpty());
}

void KeyboardLayoutTest::testPerLayoutShortcut()
{
    // this test verifies that per-layout global shortcuts are working correctly.
    // first configure layouts
    KConfigGroup layoutGroup = kwinApp()->kxkbConfig()->group("Layout");
    layoutGroup.writeEntry("LayoutList", QStringLiteral("us,de,de(neo)"));
    layoutGroup.sync();

    // and create the global shortcuts
    const QString componentName = QStringLiteral("KDE Keyboard Layout Switcher");
    QAction *a = new QAction(this);
    a->setObjectName(QStringLiteral("Switch keyboard layout to English (US)"));
    a->setProperty("componentName", componentName);
    KGlobalAccel::self()->setShortcut(a, QList<QKeySequence>{Qt::CTRL+Qt::ALT+Qt::Key_1}, KGlobalAccel::NoAutoloading);
    delete a;
    a = new QAction(this);
    a->setObjectName(QStringLiteral("Switch keyboard layout to German"));
    a->setProperty("componentName", componentName);
    KGlobalAccel::self()->setShortcut(a, QList<QKeySequence>{Qt::CTRL+Qt::ALT+Qt::Key_2}, KGlobalAccel::NoAutoloading);
    delete a;

    reconfigureLayouts();
    // now we should have three layouts
    auto xkb = input()->keyboard()->xkb();
    QTRY_COMPARE(xkb->numberOfLayouts(), 3u);
    // default layout is English
    xkb->switchToLayout(0);
    QTRY_COMPARE(xkb->layoutName(), QStringLiteral("English (US)"));

    LayoutChangedSignalWrapper wrapper;
    QSignalSpy layoutChangedSpy(&wrapper, &LayoutChangedSignalWrapper::layoutChanged);
    QVERIFY(layoutChangedSpy.isValid());

    // now switch to English through the global shortcut
    quint32 timestamp = 1;
    kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++);
    kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++);
    kwinApp()->platform()->keyboardKeyPressed(KEY_2, timestamp++);
    QVERIFY(layoutChangedSpy.wait());
    // now layout should be German
    QCOMPARE(xkb->layoutName(), QStringLiteral("German"));
    // release keys again
    kwinApp()->platform()->keyboardKeyReleased(KEY_2, timestamp++);
    // switch back to English
    kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++);
    QVERIFY(layoutChangedSpy.wait());
    QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)"));
    // release keys again
    kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++);
    kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++);
    kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++);
}

void KeyboardLayoutTest::testDBusServiceExport()
{
    // verifies that the dbus service is only exported if there are at least two layouts

    // first configure layouts, with just one layout
    KConfigGroup layoutGroup = kwinApp()->kxkbConfig()->group("Layout");
    layoutGroup.writeEntry("LayoutList", QStringLiteral("us"));
    layoutGroup.sync();
    reconfigureLayouts();
    auto xkb = input()->keyboard()->xkb();
    QTRY_COMPARE(xkb->numberOfLayouts(), 1u);
    // default layout is English
    QTRY_COMPARE(xkb->layoutName(), QStringLiteral("English (US)"));
    // with one layout we should not have the dbus interface
    QTRY_VERIFY(!QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.keyboard")).value());

    // reconfigure to two layouts
    layoutGroup.writeEntry("LayoutList", QStringLiteral("us,de"));
    layoutGroup.sync();
    reconfigureLayouts();
    QTRY_COMPARE(xkb->numberOfLayouts(), 2u);
    QTRY_VERIFY(QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.keyboard")).value());

    // and back to one layout
    layoutGroup.writeEntry("LayoutList", QStringLiteral("us"));
    layoutGroup.sync();
    reconfigureLayouts();
    QTRY_COMPARE(xkb->numberOfLayouts(), 1u);
    QTRY_VERIFY(!QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.keyboard")).value());
}

void KeyboardLayoutTest::testVirtualDesktopPolicy()
{
    KConfigGroup layoutGroup = kwinApp()->kxkbConfig()->group("Layout");
    layoutGroup.writeEntry("LayoutList", QStringLiteral("us,de,de(neo)"));
    layoutGroup.writeEntry("SwitchMode", QStringLiteral("Desktop"));
    layoutGroup.sync();
    reconfigureLayouts();
    auto xkb = input()->keyboard()->xkb();
    QTRY_COMPARE(xkb->numberOfLayouts(), 3u);
    QTRY_COMPARE(xkb->layoutName(), QStringLiteral("English (US)"));

    VirtualDesktopManager::self()->setCount(4);
    QCOMPARE(VirtualDesktopManager::self()->count(), 4u);

    auto changeLayout = [] (const QString &layoutName) {
        QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.keyboard"), QStringLiteral("/Layouts"), QStringLiteral("org.kde.KeyboardLayouts"), QStringLiteral("setLayout"));
        msg << layoutName;
        return QDBusConnection::sessionBus().asyncCall(msg);
    };
    auto reply = changeLayout(QStringLiteral("German"));
    reply.waitForFinished();
    QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German"));

    // switch to another virtual desktop
    auto desktops = VirtualDesktopManager::self()->desktops();
    QCOMPARE(desktops.count(), 4);
    QCOMPARE(desktops.first(), VirtualDesktopManager::self()->currentDesktop());
    VirtualDesktopManager::self()->setCurrent(desktops.at(1));
    QCOMPARE(desktops.at(1), VirtualDesktopManager::self()->currentDesktop());
    // should be reset to English
    QTRY_COMPARE(xkb->layoutName(), QStringLiteral("English (US)"));reply = changeLayout(QStringLiteral("German (Neo 2)"));
    reply.waitForFinished();
    QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)"));

    // back to desktop 0 -> German
    VirtualDesktopManager::self()->setCurrent(desktops.at(0));
    QCOMPARE(desktops.first(), VirtualDesktopManager::self()->currentDesktop());
    QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German"));
    // desktop 2 -> English
    VirtualDesktopManager::self()->setCurrent(desktops.at(2));
    QTRY_COMPARE(xkb->layoutName(), QStringLiteral("English (US)"));
    // desktop 1 -> Neo
    VirtualDesktopManager::self()->setCurrent(desktops.at(1));
    QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)"));

    // remove virtual desktops
    VirtualDesktopManager::self()->setCount(1);
    QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German"));

    // add another desktop
    VirtualDesktopManager::self()->setCount(2);
    // switching to it should result in going to default
    desktops = VirtualDesktopManager::self()->desktops();
    QCOMPARE(desktops.count(), 2);
    QCOMPARE(desktops.first(), VirtualDesktopManager::self()->currentDesktop());
    VirtualDesktopManager::self()->setCurrent(desktops.last());
    QTRY_COMPARE(xkb->layoutName(), QStringLiteral("English (US)"));

}

void KeyboardLayoutTest::testWindowPolicy()
{
    KConfigGroup layoutGroup = kwinApp()->kxkbConfig()->group("Layout");
    layoutGroup.writeEntry("LayoutList", QStringLiteral("us,de,de(neo)"));
    layoutGroup.writeEntry("SwitchMode", QStringLiteral("Window"));
    layoutGroup.sync();
    reconfigureLayouts();
    auto xkb = input()->keyboard()->xkb();
    QTRY_COMPARE(xkb->numberOfLayouts(), 3u);
    QTRY_COMPARE(xkb->layoutName(), QStringLiteral("English (US)"));

    // create a window
    using namespace KWayland::Client;
    QScopedPointer<Surface> surface(Test::createSurface());
    QScopedPointer<XdgShellSurface> shellSurface(Test::createXdgShellStableSurface(surface.data()));
    auto c1 = Test::renderAndWaitForShown(surface.data(), QSize(100, 100), Qt::blue);
    QVERIFY(c1);

    // now switch layout
    auto changeLayout = [] (const QString &layoutName) {
        QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.keyboard"), QStringLiteral("/Layouts"), QStringLiteral("org.kde.KeyboardLayouts"), QStringLiteral("setLayout"));
        msg << layoutName;
        return QDBusConnection::sessionBus().asyncCall(msg);
    };
    auto reply = changeLayout(QStringLiteral("German"));
    reply.waitForFinished();
    QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German"));

    // create a second window
    QScopedPointer<Surface> surface2(Test::createSurface());
    QScopedPointer<XdgShellSurface> shellSurface2(Test::createXdgShellStableSurface(surface2.data()));
    auto c2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 100), Qt::red);
    QVERIFY(c2);
    // this should have switched back to English
    QTRY_COMPARE(xkb->layoutName(), QStringLiteral("English (US)"));
    // now change to another layout
    reply = changeLayout(QStringLiteral("German (Neo 2)"));
    reply.waitForFinished();
    QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)"));

    // activate other window
    workspace()->activateClient(c1);
    QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German"));
    workspace()->activateClient(c2);
    QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)"));
}

void KeyboardLayoutTest::testApplicationPolicy()
{
    KConfigGroup layoutGroup = kwinApp()->kxkbConfig()->group("Layout");
    layoutGroup.writeEntry("LayoutList", QStringLiteral("us,de,de(neo)"));
    layoutGroup.writeEntry("SwitchMode", QStringLiteral("WinClass"));
    layoutGroup.sync();
    reconfigureLayouts();
    auto xkb = input()->keyboard()->xkb();
    QTRY_COMPARE(xkb->numberOfLayouts(), 3u);
    QTRY_COMPARE(xkb->layoutName(), QStringLiteral("English (US)"));

    // create a window
    using namespace KWayland::Client;
    QScopedPointer<Surface> surface(Test::createSurface());
    QScopedPointer<XdgShellSurface> shellSurface(Test::createXdgShellStableSurface(surface.data()));
    auto c1 = Test::renderAndWaitForShown(surface.data(), QSize(100, 100), Qt::blue);
    QVERIFY(c1);

    // now switch layout
    auto changeLayout = [] (const QString &layoutName) {
        QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.keyboard"), QStringLiteral("/Layouts"), QStringLiteral("org.kde.KeyboardLayouts"), QStringLiteral("setLayout"));
        msg << layoutName;
        return QDBusConnection::sessionBus().asyncCall(msg);
    };
    LayoutChangedSignalWrapper wrapper;
    QSignalSpy layoutChangedSpy(&wrapper, &LayoutChangedSignalWrapper::layoutChanged);
    QVERIFY(layoutChangedSpy.isValid());
    auto reply = changeLayout(QStringLiteral("German"));
    QVERIFY(layoutChangedSpy.wait());
    QCOMPARE(layoutChangedSpy.count(), 1);
    reply.waitForFinished();
    QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German"));

    // create a second window
    QScopedPointer<Surface> surface2(Test::createSurface());
    QScopedPointer<XdgShellSurface> shellSurface2(Test::createXdgShellStableSurface(surface2.data()));
    auto c2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 100), Qt::red);
    QVERIFY(c2);
    // it is the same application and should not switch the layout
    QVERIFY(!layoutChangedSpy.wait());
    QCOMPARE(layoutChangedSpy.count(), 1);
    // now change to another layout
    reply = changeLayout(QStringLiteral("German (Neo 2)"));
    QVERIFY(layoutChangedSpy.wait());
    QCOMPARE(layoutChangedSpy.count(), 2);
    QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)"));

    // activate other window
    workspace()->activateClient(c1);
    QVERIFY(!layoutChangedSpy.wait());
    QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)"));
    workspace()->activateClient(c2);
    QVERIFY(!layoutChangedSpy.wait());
    QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)"));

    shellSurface2.reset();
    surface2.reset();
    QVERIFY(Test::waitForWindowDestroyed(c2));
    QVERIFY(!layoutChangedSpy.wait());
    QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)"));
}

void KeyboardLayoutTest::testNumLock()
{
    qputenv("KWIN_FORCE_NUM_LOCK_EVALUATION", "1");
    KConfigGroup layoutGroup = kwinApp()->kxkbConfig()->group("Layout");
    layoutGroup.writeEntry("LayoutList", QStringLiteral("us"));
    layoutGroup.sync();
    reconfigureLayouts();

    auto xkb = input()->keyboard()->xkb();
    QTRY_COMPARE(xkb->numberOfLayouts(), 1u);
    QTRY_COMPARE(xkb->layoutName(), QStringLiteral("English (US)"));

    // by default not set
    QVERIFY(!xkb->modifiers().testFlag(Qt::KeypadModifier));
    quint32 timestamp = 0;
    kwinApp()->platform()->keyboardKeyPressed(KEY_NUMLOCK, timestamp++);
    kwinApp()->platform()->keyboardKeyReleased(KEY_NUMLOCK, timestamp++);
    // now it should be on
    QVERIFY(xkb->modifiers().testFlag(Qt::KeypadModifier));
    // and back to off
    kwinApp()->platform()->keyboardKeyPressed(KEY_NUMLOCK, timestamp++);
    kwinApp()->platform()->keyboardKeyReleased(KEY_NUMLOCK, timestamp++);
    QVERIFY(!xkb->modifiers().testFlag(Qt::KeypadModifier));

    // let's reconfigure to enable through config
    auto group = kwinApp()->inputConfig()->group("Keyboard");
    group.writeEntry("NumLock", 0);
    group.sync();
    xkb->reconfigure();
    // now it should be on
    QVERIFY(xkb->modifiers().testFlag(Qt::KeypadModifier));
    // pressing should result in it being off
    kwinApp()->platform()->keyboardKeyPressed(KEY_NUMLOCK, timestamp++);
    kwinApp()->platform()->keyboardKeyReleased(KEY_NUMLOCK, timestamp++);
    QVERIFY(!xkb->modifiers().testFlag(Qt::KeypadModifier));

    // pressing again should enable it
    kwinApp()->platform()->keyboardKeyPressed(KEY_NUMLOCK, timestamp++);
    kwinApp()->platform()->keyboardKeyReleased(KEY_NUMLOCK, timestamp++);
    QVERIFY(xkb->modifiers().testFlag(Qt::KeypadModifier));

    // now reconfigure to disable on load
    group.writeEntry("NumLock", 1);
    group.sync();
    xkb->reconfigure();
    QVERIFY(!xkb->modifiers().testFlag(Qt::KeypadModifier));
}


WAYLANDTEST_MAIN(KeyboardLayoutTest)
#include "keyboard_layout_test.moc"
