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

Copyright (C) 2016 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 "virtualkeyboard.h"
#include "virtualkeyboard_dbus.h"
#include "input.h"
#include "keyboard_input.h"
#include "utils.h"
#include "screens.h"
#include "wayland_server.h"
#include "workspace.h"
#include "xkb.h"
#include "screenlockerwatcher.h"

#include <KWaylandServer/display.h>
#include <KWaylandServer/seat_interface.h>
#include <KWaylandServer/textinput_interface.h>
#include <KWaylandServer/surface_interface.h>

#include <KStatusNotifierItem>
#include <KLocalizedString>

#include <QDBusConnection>
#include <QDBusPendingCall>
#include <QDBusMessage>
#include <QGuiApplication>
#include <QQmlComponent>
#include <QQmlContext>
#include <QQmlEngine>
#include <QQuickItem>
#include <QQuickView>
#include <QQuickWindow>
#include <QTimer>
// xkbcommon
#include <xkbcommon/xkbcommon.h>

using namespace KWaylandServer;

namespace KWin
{

KWIN_SINGLETON_FACTORY(VirtualKeyboard)

VirtualKeyboard::VirtualKeyboard(QObject *parent)
    : QObject(parent)
{
    m_floodTimer = new QTimer(this);
    m_floodTimer->setSingleShot(true);
    m_floodTimer->setInterval(250);
    // this is actually too late. Other processes are started before init,
    // so might miss the availability of text input
    // but without Workspace we don't have the window listed at all
    connect(kwinApp(), &Application::workspaceCreated, this, &VirtualKeyboard::init);
}

VirtualKeyboard::~VirtualKeyboard() = default;

void VirtualKeyboard::init()
{
    // TODO: need a shared Qml engine
    qCDebug(KWIN_VIRTUALKEYBOARD) << "Initializing window";
    m_inputWindow.reset(new QQuickView(nullptr));
    m_inputWindow->setFlags(Qt::FramelessWindowHint);
    m_inputWindow->setGeometry(screens()->geometry(screens()->current()));
    m_inputWindow->setResizeMode(QQuickView::SizeRootObjectToView);
    m_inputWindow->setSource(QUrl::fromLocalFile(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral(KWIN_NAME "/virtualkeyboard/main.qml"))));
    if (m_inputWindow->status() != QQuickView::Status::Ready) {
        qCWarning(KWIN_VIRTUALKEYBOARD) << "window not ready yet";
        m_inputWindow.reset();
        return;
    }
    m_inputWindow->setProperty("__kwin_input_method", true);

    connect(ScreenLockerWatcher::self(), &ScreenLockerWatcher::aboutToLock, this, &VirtualKeyboard::hide);

    if (waylandServer()) {
        m_enabled = !input()->hasAlphaNumericKeyboard();
        qCDebug(KWIN_VIRTUALKEYBOARD) << "enabled by default: " << m_enabled;
        connect(input(), &InputRedirection::hasAlphaNumericKeyboardChanged, this,
            [this] (bool set) {
                qCDebug(KWIN_VIRTUALKEYBOARD) << "AlphaNumeric Keyboard changed:" << set << "toggling VirtualKeyboard.";
                setEnabled(!set);
            }
        );
    }

    qCDebug(KWIN_VIRTUALKEYBOARD) << "Registering the SNI";
    m_sni = new KStatusNotifierItem(QStringLiteral("kwin-virtual-keyboard"), this);
    m_sni->setStandardActionsEnabled(false);
    m_sni->setCategory(KStatusNotifierItem::Hardware);
    m_sni->setStatus(KStatusNotifierItem::Passive);
    m_sni->setTitle(i18n("Virtual Keyboard"));
    updateSni();
    connect(m_sni, &KStatusNotifierItem::activateRequested, this,
        [this] {
            setEnabled(!m_enabled);
        }
    );
    connect(this, &VirtualKeyboard::enabledChanged, this, &VirtualKeyboard::updateSni);

    auto dbus = new VirtualKeyboardDBus(this);
    qCDebug(KWIN_VIRTUALKEYBOARD) << "Registering the DBus interface";
    dbus->setEnabled(m_enabled);
    connect(dbus, &VirtualKeyboardDBus::activateRequested, this, &VirtualKeyboard::setEnabled);
    connect(this, &VirtualKeyboard::enabledChanged, dbus, &VirtualKeyboardDBus::setEnabled);

    if (waylandServer()) {
        // we can announce support for the text input interface
        auto t = waylandServer()->display()->createTextInputManager(TextInputInterfaceVersion::UnstableV0, waylandServer()->display());
        t->create();
        auto t2 = waylandServer()->display()->createTextInputManager(TextInputInterfaceVersion::UnstableV2, waylandServer()->display());
        t2->create();
        connect(waylandServer()->seat(), &SeatInterface::focusedTextInputChanged, this,
            [this] {
                disconnect(m_waylandShowConnection);
                disconnect(m_waylandHideConnection);
                disconnect(m_waylandHintsConnection);
                disconnect(m_waylandSurroundingTextConnection);
                disconnect(m_waylandResetConnection);
                disconnect(m_waylandEnabledConnection);
                qApp->inputMethod()->reset();
                if (auto t = waylandServer()->seat()->focusedTextInput()) {
                    m_waylandShowConnection = connect(t, &TextInputInterface::requestShowInputPanel, this, &VirtualKeyboard::show);
                    m_waylandHideConnection = connect(t, &TextInputInterface::requestHideInputPanel, this, &VirtualKeyboard::hide);
                    m_waylandSurroundingTextConnection = connect(t, &TextInputInterface::surroundingTextChanged, this,
                        [] {
                            qApp->inputMethod()->update(Qt::ImSurroundingText | Qt::ImCursorPosition | Qt::ImAnchorPosition);
                        }
                    );
                    m_waylandHintsConnection = connect(t, &TextInputInterface::contentTypeChanged, this,
                        [] {
                            qApp->inputMethod()->update(Qt::ImHints);
                        }
                    );
                    m_waylandResetConnection = connect(t, &TextInputInterface::requestReset, qApp->inputMethod(), &QInputMethod::reset);
                    m_waylandEnabledConnection = connect(t, &TextInputInterface::enabledChanged, this,
                        [] {
                            qApp->inputMethod()->update(Qt::ImQueryAll);
                        }
                    );

                    auto newClient = waylandServer()->findClient(waylandServer()->seat()->focusedTextInputSurface());
                    // Reset the old client virtual keybaord geom if necessary
                    // Old and new clients could be the same if focus moves between subsurfaces
                    if (newClient != m_trackedClient) {
                        if (m_trackedClient) {
                            m_trackedClient->setVirtualKeyboardGeometry(QRect());
                        }
                        m_trackedClient = newClient;
                    }

                    m_trackedClient = waylandServer()->findClient(waylandServer()->seat()->focusedTextInputSurface());

                    updateInputPanelState();
                } else {
                    m_waylandShowConnection = QMetaObject::Connection();
                    m_waylandHideConnection = QMetaObject::Connection();
                    m_waylandHintsConnection = QMetaObject::Connection();
                    m_waylandSurroundingTextConnection = QMetaObject::Connection();
                    m_waylandResetConnection = QMetaObject::Connection();
                    m_waylandEnabledConnection = QMetaObject::Connection();
                }
                qApp->inputMethod()->update(Qt::ImQueryAll);
            }
        );
    }
    m_inputWindow->installEventFilter(this);
    connect(Workspace::self(), &Workspace::destroyed, this,
        [this] {
            m_inputWindow.reset();
        }
    );
    m_inputWindow->setColor(Qt::transparent);
    m_inputWindow->setMask(m_inputWindow->rootObject()->childrenRect().toRect());
    connect(m_inputWindow->rootObject(), &QQuickItem::childrenRectChanged, m_inputWindow.data(),
        [this] {
            if (!m_inputWindow) {
                return;
            }
            m_inputWindow->setMask(m_inputWindow->rootObject()->childrenRect().toRect());
        }
    );

    connect(qApp->inputMethod(), &QInputMethod::visibleChanged, this, &VirtualKeyboard::updateInputPanelState);

    connect(m_inputWindow->rootObject(), &QQuickItem::childrenRectChanged, this, &VirtualKeyboard::updateInputPanelState);
}

void VirtualKeyboard::setEnabled(bool enabled)
{
    if (m_enabled == enabled) {
        return;
    }
    m_enabled = enabled;
    qApp->inputMethod()->update(Qt::ImQueryAll);
    emit enabledChanged(m_enabled);

    // send OSD message
    QDBusMessage msg = QDBusMessage::createMethodCall(
        QStringLiteral("org.kde.plasmashell"),
        QStringLiteral("/org/kde/osdService"),
        QStringLiteral("org.kde.osdService"),
        QStringLiteral("virtualKeyboardEnabledChanged")
    );
    msg.setArguments({enabled});
    QDBusConnection::sessionBus().asyncCall(msg);
}

void VirtualKeyboard::updateSni()
{
    if (!m_sni) {
        return;
    }
    if (m_enabled) {
        m_sni->setIconByName(QStringLiteral("input-keyboard-virtual-on"));
        m_sni->setTitle(i18n("Virtual Keyboard: enabled"));
    } else {
        m_sni->setIconByName(QStringLiteral("input-keyboard-virtual-off"));
        m_sni->setTitle(i18n("Virtual Keyboard: disabled"));
    }
    m_sni->setToolTipTitle(i18n("Whether to show the virtual keyboard on demand."));
}

void VirtualKeyboard::updateInputPanelState()
{
    if (!waylandServer()) {
        return;
    }

    auto t = waylandServer()->seat()->focusedTextInput();

    if (!t || !m_inputWindow) {
        return;
    }

    const bool inputPanelHasBeenClosed = m_inputWindow->isVisible() && !qApp->inputMethod()->isVisible();
    if (inputPanelHasBeenClosed && m_floodTimer->isActive()) {
        return;
    }
    m_floodTimer->start();

    m_inputWindow->setVisible(qApp->inputMethod()->isVisible());

    if (qApp->inputMethod()->isVisible()) {
        m_inputWindow->setMask(m_inputWindow->rootObject()->childrenRect().toRect());
    }

    if (m_inputWindow->isVisible() && m_trackedClient && m_inputWindow->rootObject()) {
        const QRect inputPanelGeom = m_inputWindow->rootObject()->childrenRect().toRect().translated(m_inputWindow->geometry().topLeft());

        m_trackedClient->setVirtualKeyboardGeometry(inputPanelGeom);

        t->setInputPanelState(true, QRect(0, 0, 0, 0));

    } else {
        if (inputPanelHasBeenClosed && m_trackedClient) {
            m_trackedClient->setVirtualKeyboardGeometry(QRect());
        }

        t->setInputPanelState(false, QRect(0, 0, 0, 0));
    }
}

void VirtualKeyboard::show()
{
    if (m_inputWindow.isNull() || !m_enabled) {
        return;
    }
    m_inputWindow->setGeometry(screens()->geometry(screens()->current()));
    qApp->inputMethod()->show();
}

void VirtualKeyboard::hide()
{
    if (m_inputWindow.isNull()) {
        return;
    }
    m_inputWindow->hide();
    qApp->inputMethod()->hide();
}

bool VirtualKeyboard::event(QEvent *e)
{
    if (e->type() == QEvent::InputMethod) {
        QInputMethodEvent *event = static_cast<QInputMethodEvent*>(e);
        if (m_enabled && waylandServer()) {
            bool isPreedit = false;
            for (auto attribute : event->attributes()) {
                switch (attribute.type) {
                case QInputMethodEvent::TextFormat:
                case QInputMethodEvent::Cursor:
                case QInputMethodEvent::Language:
                case QInputMethodEvent::Ruby:
                    isPreedit = true;
                    break;
                default:
                    break;
                }
            }
            TextInputInterface *ti = waylandServer()->seat()->focusedTextInput();
            if (ti && ti->isEnabled()) {
                if (!isPreedit && event->preeditString().isEmpty() && !event->commitString().isEmpty()) {
                    ti->commit(event->commitString().toUtf8());
                } else {
                    ti->preEdit(event->preeditString().toUtf8(), event->commitString().toUtf8());
                }
            }
        }
    }
    if (e->type() == QEvent::InputMethodQuery) {
        auto event = static_cast<QInputMethodQueryEvent*>(e);
        TextInputInterface *ti = nullptr;
        if (waylandServer() && m_enabled) {
            ti = waylandServer()->seat()->focusedTextInput();
        }
        if (event->queries().testFlag(Qt::ImEnabled)) {
            event->setValue(Qt::ImEnabled, QVariant(ti != nullptr && ti->isEnabled()));
        }
        if (event->queries().testFlag(Qt::ImCursorRectangle)) {
            // not used by virtual keyboard
        }
        if (event->queries().testFlag(Qt::ImFont)) {
            // not used by virtual keyboard
        }
        if (event->queries().testFlag(Qt::ImCursorPosition)) {
            // the virtual keyboard doesn't send us the cursor position in the preedit
            // this would break text input, thus we ignore it
            // see https://bugreports.qt.io/browse/QTBUG-53517
#if 0
            event->setValue(Qt::ImCursorPosition, QString::fromUtf8(ti->surroundingText().left(ti->surroundingTextCursorPosition())).size());
#else
            event->setValue(Qt::ImCursorPosition, 0);
#endif
        }
        if (event->queries().testFlag(Qt::ImSurroundingText)) {
            // the virtual keyboard doesn't send us the cursor position in the preedit
            // this would break text input, thus we ignore it
            // see https://bugreports.qt.io/browse/QTBUG-53517
#if 0
            event->setValue(Qt::ImSurroundingText, QString::fromUtf8(ti->surroundingText()));
#else
            event->setValue(Qt::ImSurroundingText, QString());
#endif
        }
        if (event->queries().testFlag(Qt::ImCurrentSelection)) {
            // TODO: should be text between cursor and anchor, but might be dangerous
        }
        if (event->queries().testFlag(Qt::ImMaximumTextLength)) {
            // not used by virtual keyboard
        }
        if (event->queries().testFlag(Qt::ImAnchorPosition)) {
            // not used by virtual keyboard
        }
        if (event->queries().testFlag(Qt::ImHints)) {
            if (ti && ti->isEnabled()) {
                Qt::InputMethodHints hints;
                const auto contentHints = ti->contentHints();
                if (!contentHints.testFlag(TextInputInterface::ContentHint::AutoCompletion)) {
                    hints |= Qt::ImhNoPredictiveText;
                }
                if (contentHints.testFlag(TextInputInterface::ContentHint::AutoCorrection)) {
                    // no mapping in Qt
                }
                if (!contentHints.testFlag(TextInputInterface::ContentHint::AutoCapitalization)) {
                    hints |= Qt::ImhNoAutoUppercase;
                }
                if (contentHints.testFlag(TextInputInterface::ContentHint::LowerCase)) {
                    hints |= Qt::ImhPreferLowercase;
                }
                if (contentHints.testFlag(TextInputInterface::ContentHint::UpperCase)) {
                    hints |= Qt::ImhPreferUppercase;
                }
                if (contentHints.testFlag(TextInputInterface::ContentHint::TitleCase)) {
                    // no mapping in Qt
                }
                if (contentHints.testFlag(TextInputInterface::ContentHint::HiddenText)) {
                    hints |= Qt::ImhHiddenText;
                }
                if (contentHints.testFlag(TextInputInterface::ContentHint::SensitiveData)) {
                    hints |= Qt::ImhSensitiveData;
                }
                if (contentHints.testFlag(TextInputInterface::ContentHint::Latin)) {
                    hints |= Qt::ImhPreferLatin;
                }
                if (contentHints.testFlag(TextInputInterface::ContentHint::MultiLine)) {
                    hints |= Qt::ImhMultiLine;
                }
                switch (ti->contentPurpose()) {
                case TextInputInterface::ContentPurpose::Digits:
                    hints |= Qt::ImhDigitsOnly;
                    break;
                case TextInputInterface::ContentPurpose::Number:
                    hints |= Qt::ImhFormattedNumbersOnly;
                    break;
                case TextInputInterface::ContentPurpose::Phone:
                    hints |= Qt::ImhDialableCharactersOnly;
                    break;
                case TextInputInterface::ContentPurpose::Url:
                    hints |= Qt::ImhUrlCharactersOnly;
                    break;
                case TextInputInterface::ContentPurpose::Email:
                    hints |= Qt::ImhEmailCharactersOnly;
                    break;
                case TextInputInterface::ContentPurpose::Date:
                    hints |= Qt::ImhDate;
                    break;
                case TextInputInterface::ContentPurpose::Time:
                    hints |= Qt::ImhTime;
                    break;
                case TextInputInterface::ContentPurpose::DateTime:
                    hints |= Qt::ImhDate;
                    hints |= Qt::ImhTime;
                    break;
                case TextInputInterface::ContentPurpose::Name:
                    // no mapping in Qt
                case TextInputInterface::ContentPurpose::Password:
                    // no mapping in Qt
                case TextInputInterface::ContentPurpose::Terminal:
                    // no mapping in Qt
                case TextInputInterface::ContentPurpose::Normal:
                    // that's the default
                case TextInputInterface::ContentPurpose::Alpha:
                    // no mapping in Qt
                    break;
                }
                event->setValue(Qt::ImHints, QVariant(int(hints)));
            } else {
                event->setValue(Qt::ImHints, Qt::ImhNone);
            }
        }
        if (event->queries().testFlag(Qt::ImPreferredLanguage)) {
            // not used by virtual keyboard
        }
        if (event->queries().testFlag(Qt::ImPlatformData)) {
            // not used by virtual keyboard
        }
        if (event->queries().testFlag(Qt::ImAbsolutePosition)) {
            // not used by virtual keyboard
        }
        if (event->queries().testFlag(Qt::ImTextBeforeCursor)) {
            // not used by virtual keyboard
        }
        if (event->queries().testFlag(Qt::ImTextAfterCursor)) {
            // not used by virtual keyboard
        }
        event->accept();
        return true;
    }
    return QObject::event(e);
}

bool VirtualKeyboard::eventFilter(QObject *o, QEvent *e)
{
    if (o != m_inputWindow.data() || !m_inputWindow->isVisible()) {
        return false;
    }
    if (e->type() == QEvent::KeyPress || e->type() == QEvent::KeyRelease) {
        QKeyEvent *event = static_cast<QKeyEvent*>(e);
        if (event->nativeScanCode() == 0) {
            // this is a key composed by the virtual keyboard - we need to send it to the client
            const auto sym = input()->keyboard()->xkb()->fromKeyEvent(event);
            if (sym != 0) {
                if (waylandServer()) {
                    auto t = waylandServer()->seat()->focusedTextInput();
                    if (t && t->isEnabled()) {
                        if (e->type() == QEvent::KeyPress) {
                            t->keysymPressed(sym);
                        } else if (e->type() == QEvent::KeyRelease) {
                            t->keysymReleased(sym);
                        }
                    }
                }
            }
            return true;
        }
    }
    return false;
}

QWindow *VirtualKeyboard::inputPanel() const
{
    return m_inputWindow.data();
}

}
