/****************************************************************************
**
** Copyright (C) 2018 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the Qt Virtual Keyboard module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 or (at your option) any later version
** approved by the KDE Free Qt Foundation. The licenses are as published by
** the Free Software Foundation and appearing in the file LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include <QtVirtualKeyboard/private/qvirtualkeyboardinputcontext_p.h>
#include <QtVirtualKeyboard/private/platforminputcontext_p.h>
#include <QtVirtualKeyboard/private/settings_p.h>
#include <QtVirtualKeyboard/private/shifthandler_p.h>
#include <QtVirtualKeyboard/private/virtualkeyboarddebug_p.h>
#include <QtVirtualKeyboard/private/enterkeyaction_p.h>
#include <QtVirtualKeyboard/qvirtualkeyboardinputengine.h>

#include <QGuiApplication>
#include <QtQuick/qquickitem.h>
#include <QtQuick/qquickwindow.h>
#include <QtGui/qpa/qplatformintegration.h>
#include <QtGui/private/qguiapplication_p.h>

QT_BEGIN_NAMESPACE

bool operator==(const QInputMethodEvent::Attribute &attribute1, const QInputMethodEvent::Attribute &attribute2)
{
    return attribute1.start == attribute2.start &&
           attribute1.length == attribute2.length &&
           attribute1.type == attribute2.type &&
           attribute1.value == attribute2.value;
}

using namespace QtVirtualKeyboard;

const bool QtVirtualKeyboard::QT_VIRTUALKEYBOARD_FORCE_EVENTS_WITHOUT_FOCUS = qEnvironmentVariableIsSet("QT_VIRTUALKEYBOARD_FORCE_EVENTS_WITHOUT_FOCUS");

QVirtualKeyboardInputContextPrivate::QVirtualKeyboardInputContextPrivate(QVirtualKeyboardInputContext *q_ptr) :
    QObject(nullptr),
    q_ptr(q_ptr),
    platformInputContext(nullptr),
    inputEngine(nullptr),
    _shiftHandler(nullptr),
    keyboardRect(),
    previewRect(),
    _previewVisible(false),
    animating(false),
    _focus(false),
    cursorPosition(0),
    anchorPosition(0),
    forceAnchorPosition(-1),
    _forceCursorPosition(-1),
    inputMethodHints(Qt::ImhNone),
    preeditText(),
    preeditTextAttributes(),
    surroundingText(),
    selectedText(),
    anchorRectangle(),
    cursorRectangle(),
    selectionControlVisible(false),
    anchorRectIntersectsClipRect(false),
    cursorRectIntersectsClipRect(false)
#ifdef QT_VIRTUALKEYBOARD_ARROW_KEY_NAVIGATION
    , activeNavigationKeys()
#endif
{
}

void QVirtualKeyboardInputContextPrivate::init()
{
    Q_Q(QVirtualKeyboardInputContext);
    QGuiApplicationPrivate *guiApplicationPrivate = QGuiApplicationPrivate::instance();
    QPlatformIntegration *platformIntegration = guiApplicationPrivate->platformIntegration();
    QPlatformInputContext *unknownPlatformInputContext = platformIntegration->inputContext();
    platformInputContext = qobject_cast<PlatformInputContext *>(unknownPlatformInputContext);
    inputEngine = new QVirtualKeyboardInputEngine(q);
    _shiftHandler = new ShiftHandler(q);
    inputEngine->init();
    _shiftHandler->init();
    _shadow.setInputContext(q);
    if (platformInputContext) {
        platformInputContext->setInputContext(q);
        QObject::connect(platformInputContext, &PlatformInputContext::focusObjectChanged, this, &QVirtualKeyboardInputContextPrivate::onInputItemChanged);
        QObject::connect(platformInputContext, &PlatformInputContext::focusObjectChanged, this, &QVirtualKeyboardInputContextPrivate::inputItemChanged);
    }
}

QVirtualKeyboardInputContextPrivate::~QVirtualKeyboardInputContextPrivate()
{
}

bool QVirtualKeyboardInputContextPrivate::focus() const
{
    return _focus;
}

void QVirtualKeyboardInputContextPrivate::setFocus(bool focus)
{
    if (_focus != focus) {
        VIRTUALKEYBOARD_DEBUG() << "QVirtualKeyboardInputContextPrivate::setFocus():" << focus;
        _focus = focus;
        emit focusChanged();
    }
}

QRectF QVirtualKeyboardInputContextPrivate::keyboardRectangle() const
{
    return keyboardRect;
}

void QVirtualKeyboardInputContextPrivate::setKeyboardRectangle(QRectF rectangle)
{
    if (keyboardRect != rectangle) {
        keyboardRect = rectangle;
        emit keyboardRectangleChanged();
        platformInputContext->emitKeyboardRectChanged();
    }
}

QRectF QVirtualKeyboardInputContextPrivate::previewRectangle() const
{
    return previewRect;
}

void QVirtualKeyboardInputContextPrivate::setPreviewRectangle(QRectF rectangle)
{
    if (previewRect != rectangle) {
        previewRect = rectangle;
        emit previewRectangleChanged();
    }
}

bool QVirtualKeyboardInputContextPrivate::previewVisible() const
{
    return _previewVisible;
}

void QVirtualKeyboardInputContextPrivate::setPreviewVisible(bool visible)
{
    if (_previewVisible != visible) {
        _previewVisible = visible;
        emit previewVisibleChanged();
    }
}

QString QVirtualKeyboardInputContextPrivate::locale() const
{
    return platformInputContext ? platformInputContext->locale().name() : QString();
}

void QVirtualKeyboardInputContextPrivate::setLocale(const QString &locale)
{
    VIRTUALKEYBOARD_DEBUG() << "QVirtualKeyboardInputContextPrivate::setLocale():" << locale;
    QLocale newLocale(locale);
    if (newLocale != platformInputContext->locale()) {
        platformInputContext->setLocale(newLocale);
        platformInputContext->setInputDirection(newLocale.textDirection());
        emit localeChanged();
    }
}

QObject *QVirtualKeyboardInputContextPrivate::inputItem() const
{
    return platformInputContext ? platformInputContext->focusObject() : nullptr;
}

ShiftHandler *QVirtualKeyboardInputContextPrivate::shiftHandler() const
{
    return _shiftHandler;
}

ShadowInputContext *QVirtualKeyboardInputContextPrivate::shadow() const
{
    return const_cast<ShadowInputContext *>(&_shadow);
}

QStringList QVirtualKeyboardInputContextPrivate::inputMethods() const
{
    return platformInputContext ? platformInputContext->inputMethods() : QStringList();
}

bool QVirtualKeyboardInputContextPrivate::fileExists(const QUrl &fileUrl)
{
    QString fileName;
    if (fileUrl.scheme() == QLatin1String("qrc")) {
        fileName = QLatin1Char(':') + fileUrl.path();
    } else {
        fileName = fileUrl.toLocalFile();
    }
    return !fileName.isEmpty() && QFile::exists(fileName);
}

bool QVirtualKeyboardInputContextPrivate::hasEnterKeyAction(QObject *item) const
{
    return item != nullptr && qmlAttachedPropertiesObject<EnterKeyAction>(item, false);
}

void QVirtualKeyboardInputContextPrivate::registerInputPanel(QObject *inputPanel)
{
    VIRTUALKEYBOARD_DEBUG() << "QVirtualKeyboardInputContextPrivate::registerInputPanel():" << inputPanel;
    Q_ASSERT(!this->inputPanel);
    this->inputPanel = inputPanel;
}

void QVirtualKeyboardInputContextPrivate::hideInputPanel()
{
    platformInputContext->hideInputPanel();
}

void QVirtualKeyboardInputContextPrivate::updateAvailableLocales(const QStringList &availableLocales)
{
    Settings *settings = Settings::instance();
    if (settings)
        settings->setAvailableLocales(availableLocales);
}

void QVirtualKeyboardInputContextPrivate::forceCursorPosition(int anchorPosition, int cursorPosition)
{
    if (!_shadow.inputItem())
        return;
    if (!platformInputContext->m_visible)
        return;
    if (testState(State::Reselect))
        return;
    if (testState(State::SyncShadowInput))
        return;

    VIRTUALKEYBOARD_DEBUG() << "QVirtualKeyboardInputContextPrivate::forceCursorPosition():" << cursorPosition << "anchorPosition:" << anchorPosition;
    if (!preeditText.isEmpty()) {
        forceAnchorPosition = -1;
        _forceCursorPosition = cursorPosition;
        if (cursorPosition > this->cursorPosition)
            _forceCursorPosition += preeditText.length();
        commit();
    } else {
        forceAnchorPosition = anchorPosition;
        _forceCursorPosition = cursorPosition;
        Q_Q(QVirtualKeyboardInputContext);
        q->setPreeditText(QString());
        if (!inputMethodHints.testFlag(Qt::ImhNoPredictiveText) &&
                   cursorPosition > 0 && selectedText.isEmpty()) {
            QVirtualKeyboardScopedState reselectState(this, State::Reselect);
            if (inputEngine->reselect(cursorPosition, QVirtualKeyboardInputEngine::ReselectFlag::WordAtCursor))
                setState(State::InputMethodClick);
        }
    }
}

void QVirtualKeyboardInputContextPrivate::onInputItemChanged()
{
    if (QObject *item = inputItem()) {
        if (QQuickItem *vkbPanel = qobject_cast<QQuickItem*>(inputPanel)) {
            if (QQuickItem *quickItem = qobject_cast<QQuickItem*>(item)) {
                const QVariant isDesktopPanel = vkbPanel->property("desktopPanel");
                /*
                    For integrated keyboards, make sure it's a sibling to the overlay. The
                    high z-order will make sure it gets events also during a modal session.
                */
                if (isDesktopPanel.isValid() && !isDesktopPanel.toBool()) {
                    if (QQuickWindow *quickWindow = quickItem->window()) {
                        QQuickItem *overlay = quickWindow->property("_q_QQuickOverlay").value<QQuickItem*>();
                        if (overlay && overlay->isVisible()) {
                            if (vkbPanel->parentItem() != overlay->parentItem()) {
                                inputPanelParentItem = vkbPanel->parentItem();
                                inputPanelZ = vkbPanel->z();
                                vkbPanel->setParentItem(overlay->parentItem());
                                vkbPanel->setZ(overlay->z() + 1);
                            }
                        } else {
                            if (QQuickItem *oldParentItem = qobject_cast<QQuickItem *>(inputPanelParentItem.data())) {
                                vkbPanel->setParentItem(oldParentItem);
                                vkbPanel->setZ(inputPanelZ);
                                inputPanelParentItem = nullptr;
                            }
                        }
                    }
                }
            }
        }
    } else {
        if (!activeKeys.isEmpty()) {
            // After losing keyboard focus it is impossible to track pressed keys
            activeKeys.clear();
            clearState(State::KeyEvent);
        }
    }
    clearState(State::InputMethodClick);
}

void QVirtualKeyboardInputContextPrivate::sendPreedit(const QString &text, const QList<QInputMethodEvent::Attribute> &attributes, int replaceFrom, int replaceLength)
{
    VIRTUALKEYBOARD_DEBUG() << "QVirtualKeyboardInputContextPrivate::sendPreedit()"
#ifdef SENSITIVE_DEBUG
           << text << replaceFrom << replaceLength
#endif
        ;

    bool textChanged = preeditText != text;
    bool attributesChanged = preeditTextAttributes != attributes;

    if (textChanged || attributesChanged) {
        preeditText = text;
        preeditTextAttributes = attributes;

        if (platformInputContext) {
            QInputMethodEvent event(text, attributes);
            const bool replace = replaceFrom != 0 || replaceLength > 0;
            if (replace)
                event.setCommitString(QString(), replaceFrom, replaceLength);

            sendInputMethodEvent(&event);

            // Send also to shadow input if only attributes changed.
            // In this case the update() may not be called, so the shadow
            // input may be out of sync.
            if (_shadow.inputItem() && !replace && !text.isEmpty() &&
                    !textChanged && attributesChanged) {
                VIRTUALKEYBOARD_DEBUG() << "QVirtualKeyboardInputContextPrivate::sendPreedit(shadow)"
#ifdef SENSITIVE_DEBUG
                       << text << replaceFrom << replaceLength
#endif
                    ;
                event.setAccepted(true);
                QGuiApplication::sendEvent(_shadow.inputItem(), &event);
            }
        }

        if (textChanged) {
            Q_Q(QVirtualKeyboardInputContext);
            emit q->preeditTextChanged();
        }
    }

    if (preeditText.isEmpty())
        preeditTextAttributes.clear();
}

void QVirtualKeyboardInputContextPrivate::sendInputMethodEvent(QInputMethodEvent *event)
{
    QVirtualKeyboardScopedState inputMethodEventState(this, State::InputMethodEvent);
    platformInputContext->sendEvent(event);
}

void QVirtualKeyboardInputContextPrivate::reset()
{
    inputEngine->reset();
}

void QVirtualKeyboardInputContextPrivate::commit()
{
    inputEngine->update();
}

void QVirtualKeyboardInputContextPrivate::update(Qt::InputMethodQueries queries)
{
    Q_Q(QVirtualKeyboardInputContext);

    // No need to fetch input clip rectangle during animation
    if (!(queries & ~Qt::ImInputItemClipRectangle) && animating)
        return;

    // fetch
    QInputMethodQueryEvent imQueryEvent(Qt::InputMethodQueries(Qt::ImHints |
                    Qt::ImQueryInput | Qt::ImInputItemClipRectangle));
    platformInputContext->sendEvent(&imQueryEvent);
    Qt::InputMethodHints inputMethodHints = Qt::InputMethodHints(imQueryEvent.value(Qt::ImHints).toInt());
    const int cursorPosition = imQueryEvent.value(Qt::ImCursorPosition).toInt();
    const int anchorPosition = imQueryEvent.value(Qt::ImAnchorPosition).toInt();
    QRectF anchorRectangle;
    QRectF cursorRectangle;
    if (const QGuiApplication *app = qApp) {
        anchorRectangle = app->inputMethod()->anchorRectangle();
        cursorRectangle = app->inputMethod()->cursorRectangle();
    } else {
        anchorRectangle = this->anchorRectangle;
        cursorRectangle = this->cursorRectangle;
    }
    QString surroundingText = imQueryEvent.value(Qt::ImSurroundingText).toString();
    QString selectedText = imQueryEvent.value(Qt::ImCurrentSelection).toString();

    // check against changes
    bool newInputMethodHints = inputMethodHints != this->inputMethodHints;
    bool newSurroundingText = surroundingText != this->surroundingText;
    bool newSelectedText = selectedText != this->selectedText;
    bool newAnchorPosition = anchorPosition != this->anchorPosition;
    bool newCursorPosition = cursorPosition != this->cursorPosition;
    bool newAnchorRectangle = anchorRectangle != this->anchorRectangle;
    bool newCursorRectangle = cursorRectangle != this->cursorRectangle;
    bool selectionControlVisible = platformInputContext->evaluateInputPanelVisible() && (cursorPosition != anchorPosition) && !inputMethodHints.testFlag(Qt::ImhNoTextHandles);
    bool newSelectionControlVisible = selectionControlVisible != this->selectionControlVisible;

    QRectF inputItemClipRect = imQueryEvent.value(Qt::ImInputItemClipRectangle).toRectF();
    QRectF anchorRect = imQueryEvent.value(Qt::ImAnchorRectangle).toRectF();
    QRectF cursorRect = imQueryEvent.value(Qt::ImCursorRectangle).toRectF();

    bool anchorRectIntersectsClipRect = inputItemClipRect.intersects(anchorRect);
    bool newAnchorRectIntersectsClipRect = anchorRectIntersectsClipRect != this->anchorRectIntersectsClipRect;

    bool cursorRectIntersectsClipRect = inputItemClipRect.intersects(cursorRect);
    bool newCursorRectIntersectsClipRect = cursorRectIntersectsClipRect != this->cursorRectIntersectsClipRect;

    // update
    this->inputMethodHints = inputMethodHints;
    this->surroundingText = surroundingText;
    this->selectedText = selectedText;
    this->anchorPosition = anchorPosition;
    this->cursorPosition = cursorPosition;
    this->anchorRectangle = anchorRectangle;
    this->cursorRectangle = cursorRectangle;
    this->selectionControlVisible = selectionControlVisible;
    this->anchorRectIntersectsClipRect = anchorRectIntersectsClipRect;
    this->cursorRectIntersectsClipRect = cursorRectIntersectsClipRect;

    // update input engine
    if ((newSurroundingText || newCursorPosition) &&
            !testState(State::InputMethodEvent)) {
        commit();
    }
    if (newInputMethodHints) {
        reset();
    }

    // notify
    if (newInputMethodHints) {
        emit q->inputMethodHintsChanged();
    }
    if (newSurroundingText) {
        emit q->surroundingTextChanged();
    }
    if (newSelectedText) {
        emit q->selectedTextChanged();
    }
    if (newAnchorPosition) {
        emit q->anchorPositionChanged();
    }
    if (newCursorPosition) {
        emit q->cursorPositionChanged();
    }
    if (newAnchorRectangle) {
        emit q->anchorRectangleChanged();
    }
    if (newCursorRectangle) {
        emit q->cursorRectangleChanged();
    }
    if (newSelectionControlVisible) {
        emit q->selectionControlVisibleChanged();
    }
    if (newAnchorRectIntersectsClipRect) {
        emit q->anchorRectIntersectsClipRectChanged();
    }
    if (newCursorRectIntersectsClipRect) {
        emit q->cursorRectIntersectsClipRectChanged();
    }

    // word reselection
    if (newInputMethodHints || newSurroundingText || newSelectedText)
        clearState(State::InputMethodClick);
    if ((newSurroundingText || newCursorPosition) && !newSelectedText && isEmptyState() &&
            !inputMethodHints.testFlag(Qt::ImhNoPredictiveText) &&
            cursorPosition > 0 && this->selectedText.isEmpty()) {
        QVirtualKeyboardScopedState reselectState(this, State::Reselect);
        if (inputEngine->reselect(cursorPosition, QVirtualKeyboardInputEngine::ReselectFlag::WordAtCursor))
            setState(State::InputMethodClick);
    }

    if (!testState(State::SyncShadowInput)) {
        QVirtualKeyboardScopedState syncShadowInputState(this, State::SyncShadowInput);
        _shadow.update(queries);
    }
}

void QVirtualKeyboardInputContextPrivate::invokeAction(QInputMethod::Action action, int cursorPosition)
{
    switch (action) {
    case QInputMethod::Click:
        if (isEmptyState()) {
            if (inputEngine->clickPreeditText(cursorPosition))
                break;

            bool reselect = !inputMethodHints.testFlag(Qt::ImhNoPredictiveText) && selectedText.isEmpty() && cursorPosition < preeditText.length();
            if (reselect) {
                QVirtualKeyboardScopedState reselectState(this, State::Reselect);
                _forceCursorPosition = this->cursorPosition + cursorPosition;
                commit();
                inputEngine->reselect(this->cursorPosition, QVirtualKeyboardInputEngine::ReselectFlag::WordBeforeCursor);
            } else if (!preeditText.isEmpty() && cursorPosition == preeditText.length()) {
                commit();
            }
        }
        clearState(State::InputMethodClick);
        break;

    case QInputMethod::ContextMenu:
        break;
    }
}

bool QVirtualKeyboardInputContextPrivate::filterEvent(const QEvent *event)
{
    QEvent::Type type = event->type();
    if (type == QEvent::KeyPress || type == QEvent::KeyRelease) {
        const QKeyEvent *keyEvent = static_cast<const QKeyEvent *>(event);
        const int key = keyEvent->key();

        // Keep track of pressed keys update key event state
        if (type == QEvent::KeyPress)
            activeKeys += keyEvent->nativeScanCode();
        else if (type == QEvent::KeyRelease)
            activeKeys -= keyEvent->nativeScanCode();

        if (activeKeys.isEmpty())
            clearState(State::KeyEvent);
        else
            setState(State::KeyEvent);

#ifdef QT_VIRTUALKEYBOARD_ARROW_KEY_NAVIGATION
        if ((key >= Qt::Key_Left && key <= Qt::Key_Down) || key == Qt::Key_Return) {
            if (type == QEvent::KeyPress && platformInputContext->isInputPanelVisible()) {
                activeNavigationKeys += key;
                emit navigationKeyPressed(key, keyEvent->isAutoRepeat());
                return true;
            } else if (type == QEvent::KeyRelease && activeNavigationKeys.contains(key)) {
                activeNavigationKeys -= key;
                emit navigationKeyReleased(key, keyEvent->isAutoRepeat());
                return true;
            }
        }
#endif

        // Break composing text since the virtual keyboard does not support hard keyboard events
        if (!preeditText.isEmpty()) {
            if (type == QEvent::KeyPress && (key == Qt::Key_Delete || key == Qt::Key_Backspace)) {
                reset();
                Q_Q(QVirtualKeyboardInputContext);
                q->clear();
                return true;
            } else {
                commit();
            }
        }
    }
#ifdef QT_VIRTUALKEYBOARD_ARROW_KEY_NAVIGATION
    else if (type == QEvent::ShortcutOverride) {
        const QKeyEvent *keyEvent = static_cast<const QKeyEvent *>(event);
        int key = keyEvent->key();
        if ((key >= Qt::Key_Left && key <= Qt::Key_Down) || key == Qt::Key_Return)
            return true;
    }
#endif

    return false;
}

void QVirtualKeyboardInputContextPrivate::addSelectionAttribute(QList<QInputMethodEvent::Attribute> &attributes)
{
    if (!testAttribute(attributes, QInputMethodEvent::Selection)) {
        // Convert Cursor attribute to Selection attribute.
        // In this case the cursor is set in pre-edit text, but
        // the cursor is not being forced to specific location.
        if (_forceCursorPosition == -1) {
            int cursorAttributeIndex = findAttribute(preeditTextAttributes, QInputMethodEvent::Cursor);
            if (cursorAttributeIndex != -1 && preeditTextAttributes[cursorAttributeIndex].length > 0)
                _forceCursorPosition = cursorPosition + preeditTextAttributes[cursorAttributeIndex].start;
            forceAnchorPosition = -1;
        }

        if (_forceCursorPosition != -1) {
            if (forceAnchorPosition != -1)
                attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection, forceAnchorPosition, _forceCursorPosition - forceAnchorPosition, QVariant()));
            else
                attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection, _forceCursorPosition, 0, QVariant()));
        }
    }
    forceAnchorPosition = -1;
    _forceCursorPosition = -1;
}

bool QVirtualKeyboardInputContextPrivate::testAttribute(const QList<QInputMethodEvent::Attribute> &attributes, QInputMethodEvent::AttributeType attributeType) const
{
    for (const QInputMethodEvent::Attribute &attribute : qAsConst(attributes)) {
        if (attribute.type == attributeType)
            return true;
    }
    return false;
}

int QVirtualKeyboardInputContextPrivate::findAttribute(const QList<QInputMethodEvent::Attribute> &attributes, QInputMethodEvent::AttributeType attributeType) const
{
    const int count = attributes.count();
    for (int i = 0; i < count; ++i) {
        if (attributes.at(i).type == attributeType)
            return i;
    }
    return -1;
}

QT_END_NAMESPACE
