// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Qt-Security score:significant reason:default

#include <QtVirtualKeyboard/private/desktopinputselectioncontrol_p.h>
#include <QtVirtualKeyboard/qvirtualkeyboardinputcontext.h>
#include <QtVirtualKeyboard/private/qvirtualkeyboardinputcontext_p.h>
#include <QtVirtualKeyboard/private/inputselectionhandle_p.h>
#include <QtVirtualKeyboard/private/settings_p.h>
#include <QtVirtualKeyboard/private/platforminputcontext_p.h>

#include <QtCore/qpropertyanimation.h>
#include <QtGui/qguiapplication.h>
#include <QtGui/qstylehints.h>
#include <QtGui/qimagereader.h>

QT_BEGIN_NAMESPACE
namespace QtVirtualKeyboard {

DesktopInputSelectionControl::DesktopInputSelectionControl(QObject *parent, QVirtualKeyboardInputContext *inputContext)
    : QObject(parent),
      m_inputContext(inputContext),
      m_anchorSelectionHandle(),
      m_cursorSelectionHandle(),
      m_handleState(HandleIsReleased),
      m_enabled(false),
      m_anchorHandleVisible(false),
      m_cursorHandleVisible(false),
      m_eventFilterEnabled(true),
      m_handleWindowSize(40, 40*1.12)   // because a finger patch is slightly taller than its width
{
    QWindow *focusWindow = QGuiApplication::focusWindow();
    Q_ASSERT(focusWindow);
    connect(m_inputContext, &QVirtualKeyboardInputContext::selectionControlVisibleChanged, this, &DesktopInputSelectionControl::updateVisibility);
}

/*
 * Includes the hit area surrounding the visual handle
 */
QRect DesktopInputSelectionControl::handleRectForCursorRect(const QRectF &cursorRect) const
{
    const int topMargin = (m_handleWindowSize.height() - m_handleImage.size().height())/2;
    const QPoint pos(int(cursorRect.x() + (cursorRect.width() - m_handleWindowSize.width())/2),
                     int(cursorRect.bottom()) - topMargin);
    return QRect(pos, m_handleWindowSize);
}

/*
 * Includes the hit area surrounding the visual handle
 */
QRect DesktopInputSelectionControl::anchorHandleRect() const
{
    return handleRectForCursorRect(m_inputContext->anchorRectangle());
}

/*
 * Includes the hit area surrounding the visual handle
 */
QRect DesktopInputSelectionControl::cursorHandleRect() const
{
    return handleRectForCursorRect(m_inputContext->cursorRectangle());
}

void DesktopInputSelectionControl::updateAnchorHandlePosition()
{
    if (QWindow *focusWindow = QGuiApplication::focusWindow()) {
        const QPoint pos = focusWindow->mapToGlobal(anchorHandleRect().topLeft());
        m_anchorSelectionHandle->setPosition(pos);
    }
}

void DesktopInputSelectionControl::updateCursorHandlePosition()
{
    if (QWindow *focusWindow = QGuiApplication::focusWindow()) {
        const QPoint pos = focusWindow->mapToGlobal(cursorHandleRect().topLeft());
        m_cursorSelectionHandle->setPosition(pos);
    }
}

void DesktopInputSelectionControl::updateVisibility()
{
    if (!m_enabled) {
        // if VKB is hidden, we must hide the selection handles immediately,
        // because it might mean that the application is shutting down.
        m_anchorSelectionHandle->hide();
        m_cursorSelectionHandle->hide();
        m_anchorHandleVisible = false;
        m_cursorHandleVisible = false;
        return;
    }
    const bool wasAnchorVisible = m_anchorHandleVisible;
    const bool wasCursorVisible = m_cursorHandleVisible;
    const bool makeVisible = (m_inputContext->isSelectionControlVisible() || m_handleState == HandleIsMoving) && m_enabled;

    m_anchorHandleVisible = makeVisible;
    if (QWindow *focusWindow = QGuiApplication::focusWindow()) {
        QRectF globalAnchorRectangle = m_inputContext->anchorRectangle();
        QPoint tl = focusWindow->mapToGlobal(globalAnchorRectangle.toRect().topLeft());
        globalAnchorRectangle.moveTopLeft(tl);
        m_anchorHandleVisible = m_anchorHandleVisible
                && m_inputContext->anchorRectIntersectsClipRect()
                && !(m_inputContext->priv()->keyboardRectangle().intersects(globalAnchorRectangle));
    }

    if (wasAnchorVisible != m_anchorHandleVisible) {
        const qreal end = m_anchorHandleVisible ? 1 : 0;
        if (m_anchorHandleVisible)
            m_anchorSelectionHandle->show();
        QPropertyAnimation *anim = new QPropertyAnimation(m_anchorSelectionHandle.data(), "opacity");
        anim->setEndValue(end);
        anim->start(QAbstractAnimation::DeleteWhenStopped);
    }

    m_cursorHandleVisible = makeVisible;
    if (QWindow *focusWindow = QGuiApplication::focusWindow()) {
        QRectF globalCursorRectangle = m_inputContext->cursorRectangle();
        QPoint tl = focusWindow->mapToGlobal(globalCursorRectangle.toRect().topLeft());
        globalCursorRectangle.moveTopLeft(tl);
        m_cursorHandleVisible = m_cursorHandleVisible
                && m_inputContext->cursorRectIntersectsClipRect()
                && !(m_inputContext->priv()->keyboardRectangle().intersects(globalCursorRectangle));

    }

    if (wasCursorVisible != m_cursorHandleVisible) {
        const qreal end = m_cursorHandleVisible ? 1 : 0;
        if (m_cursorHandleVisible)
            m_cursorSelectionHandle->show();
        QPropertyAnimation *anim = new QPropertyAnimation(m_cursorSelectionHandle.data(), "opacity");
        anim->setEndValue(end);
        anim->start(QAbstractAnimation::DeleteWhenStopped);
    }
}

void DesktopInputSelectionControl::reloadGraphics()
{
    Settings *settings = Settings::instance();
    const QString stylePath = QString::fromLatin1(":/qt-project.org/imports/QtQuick/VirtualKeyboard/Styles/Builtin/%1/images/selectionhandle-bottom.svg")
                                .arg(settings->styleName());
    QImageReader imageReader(stylePath);
    QSize sz = imageReader.size(); // SVG handler will return default size
    sz.scale(20, 20, Qt::KeepAspectRatioByExpanding);
    imageReader.setScaledSize(sz);
    m_handleImage = imageReader.read();

    m_anchorSelectionHandle->applyImage(m_handleWindowSize); // applies m_handleImage for both selection handles
    m_cursorSelectionHandle->applyImage(m_handleWindowSize);
}

void DesktopInputSelectionControl::createHandles()
{
    if (QWindow *focusWindow = QGuiApplication::focusWindow()) {
        Settings *settings = Settings::instance();
        connect(settings, &Settings::styleChanged, this, &DesktopInputSelectionControl::reloadGraphics);

        m_anchorSelectionHandle = QSharedPointer<InputSelectionHandle>::create(this, focusWindow);
        m_cursorSelectionHandle = QSharedPointer<InputSelectionHandle>::create(this, focusWindow);

        reloadGraphics();
        if (QCoreApplication *app = QCoreApplication::instance()) {
            connect(app, &QCoreApplication::aboutToQuit,
                    this, &DesktopInputSelectionControl::destroyHandles);
        }
    }
}

void DesktopInputSelectionControl::destroyHandles()
{
    m_anchorSelectionHandle.reset();
    m_cursorSelectionHandle.reset();
}

void DesktopInputSelectionControl::setEnabled(bool enable)
{
    // setEnabled(true) just means that the handles _can_ be made visible
    // This will typically be set when a input field gets focus (and having selection).
    m_enabled = enable;
    QWindow *focusWindow = QGuiApplication::focusWindow();
    if (enable) {
        connect(m_inputContext, &QVirtualKeyboardInputContext::anchorRectangleChanged, this, &DesktopInputSelectionControl::updateAnchorHandlePosition);
        connect(m_inputContext, &QVirtualKeyboardInputContext::cursorRectangleChanged, this, &DesktopInputSelectionControl::updateCursorHandlePosition);
        connect(m_inputContext, &QVirtualKeyboardInputContext::anchorRectIntersectsClipRectChanged, this, &DesktopInputSelectionControl::updateVisibility);
        connect(m_inputContext, &QVirtualKeyboardInputContext::cursorRectIntersectsClipRectChanged, this, &DesktopInputSelectionControl::updateVisibility);
        updateAnchorHandlePosition();
        updateCursorHandlePosition();
        if (focusWindow)
            focusWindow->installEventFilter(this);
    } else {
        if (focusWindow)
            focusWindow->removeEventFilter(this);
        disconnect(m_inputContext, &QVirtualKeyboardInputContext::cursorRectIntersectsClipRectChanged, this, &DesktopInputSelectionControl::updateVisibility);
        disconnect(m_inputContext, &QVirtualKeyboardInputContext::anchorRectIntersectsClipRectChanged, this, &DesktopInputSelectionControl::updateVisibility);
        disconnect(m_inputContext, &QVirtualKeyboardInputContext::anchorRectangleChanged, this, &DesktopInputSelectionControl::updateAnchorHandlePosition);
        disconnect(m_inputContext, &QVirtualKeyboardInputContext::cursorRectangleChanged, this, &DesktopInputSelectionControl::updateCursorHandlePosition);
    }
    updateVisibility();
}

QImage *DesktopInputSelectionControl::handleImage()
{
    return &m_handleImage;
}

bool DesktopInputSelectionControl::eventFilter(QObject *object, QEvent *event)
{
    QWindow *focusWindow = QGuiApplication::focusWindow();
    if (!m_cursorSelectionHandle || !m_eventFilterEnabled || object != focusWindow)
        return false;
    const bool windowMoved = event->type() == QEvent::Move;
    const bool windowResized = event->type() == QEvent::Resize;
    if (windowMoved || windowResized) {
        if (m_enabled) {
            if (windowMoved) {
                updateAnchorHandlePosition();
                updateCursorHandlePosition();
            }
            updateVisibility();
        }
    } else if (event->type() == QEvent::MouseButtonPress) {
        QMouseEvent *me = static_cast<QMouseEvent*>(event);
        const QPoint mousePos = me->globalPosition().toPoint();

        // calculate distances from mouse pos to each handle,
        // then choose to interact with the nearest handle
        struct SelectionHandleInfo {
            qreal squaredDistance;
            QPoint delta;
            QRect rect;
        };
        SelectionHandleInfo handles[2];
        handles[AnchorHandle].rect = anchorHandleRect();
        handles[CursorHandle].rect = cursorHandleRect();

        for (int i = 0; i <= CursorHandle; ++i) {
            SelectionHandleInfo &h = handles[i];
            QPoint curHandleTopCenter = focusWindow->mapToGlobal(QPoint(h.rect.x() + qRound((qreal)h.rect.width() / 2), h.rect.top()));  // ### map to desktoppanel
            const QPoint delta = mousePos - curHandleTopCenter;
            h.delta = delta;
            h.squaredDistance = QPoint::dotProduct(delta, delta);
        }

        // (squared) distances calculated, pick the closest handle
        HandleType closestHandle = (handles[AnchorHandle].squaredDistance < handles[CursorHandle].squaredDistance ? AnchorHandle : CursorHandle);

        // Can not be replaced with me->scenePosition(); because the event might be forwarded from the window of the handle
        const QPoint windowPos = focusWindow->mapFromGlobal(mousePos);
        if (m_anchorHandleVisible && handles[closestHandle].rect.contains(windowPos)) {
            m_currentDragHandle = closestHandle;
            m_distanceBetweenMouseAndCursor = handles[closestHandle].delta;
            m_handleState = HandleIsHeld;
            m_handleDragStartedPosition = mousePos;
            const QRect otherRect = handles[1 - closestHandle].rect;
            m_otherSelectionPoint = QPoint(otherRect.x() + otherRect.width()/2, otherRect.top() - 4);

            QMouseEvent *mouseEvent = new QMouseEvent(me->type(), me->position(), me->scenePosition(), me->globalPosition(),
                                                      me->button(), me->buttons(), me->modifiers(), me->source());
            m_eventQueue.append(mouseEvent);
            return true;
        }
    } else if (event->type() == QEvent::MouseMove) {
        QMouseEvent *me = static_cast<QMouseEvent*>(event);
        QPoint mousePos = me->globalPosition().toPoint();
        if (m_handleState == HandleIsHeld) {
            QPoint delta = m_handleDragStartedPosition - mousePos;
            const int startDragDistance = QGuiApplication::styleHints()->startDragDistance();
            if (QPoint::dotProduct(delta, delta) > startDragDistance * startDragDistance)
                m_handleState = HandleIsMoving;
        }
        if (m_handleState == HandleIsMoving) {
            QPoint cursorPos = mousePos - m_distanceBetweenMouseAndCursor;
            cursorPos = focusWindow->mapFromGlobal(cursorPos);
            if (m_currentDragHandle == CursorHandle)
                m_inputContext->setSelectionOnFocusObject(m_otherSelectionPoint, cursorPos);
            else
                m_inputContext->setSelectionOnFocusObject(cursorPos, m_otherSelectionPoint);
            qDeleteAll(m_eventQueue);
            m_eventQueue.clear();
            return true;
        }
    } else if (event->type() == QEvent::MouseButtonRelease) {
        if (m_handleState == HandleIsMoving) {
            m_handleState = HandleIsReleased;
            qDeleteAll(m_eventQueue);
            m_eventQueue.clear();
            return true;
        } else {
            if (QWindow *focusWindow = QGuiApplication::focusWindow()) {
                // playback event queue. These are events that were not designated
                // for the handles in hindsight.
                // This is typically MousePress and MouseRelease (not interleaved with MouseMove)
                // that should instead go through to the underlying input editor
                m_eventFilterEnabled = false;
                while (!m_eventQueue.isEmpty()) {
                    QMouseEvent *e = m_eventQueue.takeFirst();
                    QCoreApplication::sendEvent(focusWindow, e);
                    delete e;
                }
                m_eventFilterEnabled = true;
            }
            m_handleState = HandleIsReleased;
        }
    }
    return false;
}

} // namespace QtVirtualKeyboard
QT_END_NAMESPACE
