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

#include "globalinspector.h"
#include "highlight.h"
#include "inspecttool.h"

#include <private/qqmldebugserviceinterfaces_p.h>
#include <private/qabstractanimation_p.h>
#include <private/qqmlcomponent_p.h>
#include <private/qqmldebugconnector_p.h>
#include <private/qversionedpacket_p.h>

#include <QtGui/qwindow.h>
#if QT_CONFIG(regularexpression)
#  include <QtCore/qregularexpression.h>
#endif
//INSPECTOR SERVICE PROTOCOL
// <HEADER><COMMAND><DATA>
// <HEADER> : <type{request, response, event}><requestId/eventId>[<response_success_bool>]
// <COMMAND> : {"enable", "disable", "select", "setAnimationSpeed",
//              "showAppOnTop", "createObject", "destroyObject", "moveObject"}
// <DATA> : select: <debugIds_int_list>
//          setAnimationSpeed: <speed_real>
//          showAppOnTop: <set_bool>
//          createObject: <qml_string><parentId_int><imports_string_list><filename_string>
//          destroyObject: <debugId_int>
//          moveObject: <debugId_int><newParentId_int>
// Response for "destroyObject" carries the <debugId_int> of the destroyed object.

QT_BEGIN_NAMESPACE

using QQmlDebugPacket = QVersionedPacket<QQmlDebugConnector>;

const char REQUEST[] = "request";
const char RESPONSE[] = "response";
const char EVENT[] = "event";
const char ENABLE[] = "enable";
const char DISABLE[] = "disable";
const char SELECT[] = "select";
const char SET_ANIMATION_SPEED[] = "setAnimationSpeed";
const char SHOW_APP_ON_TOP[] = "showAppOnTop";
const char CREATE_OBJECT[] = "createObject";
const char DESTROY_OBJECT[] = "destroyObject";
const char MOVE_OBJECT[] = "moveObject";

namespace QmlJSDebugger {

void GlobalInspector::removeFromSelectedItems(QObject *object)
{
    if (QQuickItem *item = qobject_cast<QQuickItem*>(object)) {
        if (m_selectedItems.removeOne(item))
            delete m_highlightItems.take(item);
    }
}

void GlobalInspector::setSelectedItems(const QList<QQuickItem *> &items)
{
    if (!syncSelectedItems(items))
        return;

    QList<QObject*> objectList;
    objectList.reserve(items.size());
    for (QQuickItem *item : items)
        objectList << item;

    sendCurrentObjects(objectList);
}

void GlobalInspector::showSelectedItemName(QQuickItem *item, const QPointF &point)
{
    SelectionHighlight *highlightItem = m_highlightItems.value(item, 0);
    if (highlightItem)
        highlightItem->showName(point);
}

void GlobalInspector::sendCurrentObjects(const QList<QObject*> &objects)
{
    QQmlDebugPacket ds;

    ds << QByteArray(EVENT) << m_eventId++ << QByteArray(SELECT);

    QList<int> debugIds;
    debugIds.reserve(objects.size());
    for (QObject *object : objects)
        debugIds << QQmlDebugService::idForObject(object);
    ds << debugIds;

    emit messageToClient(QQmlInspectorService::s_key, ds.data());
}

static bool reparentQmlObject(QObject *object, QObject *newParent)
{
    if (!newParent)
        return false;

    object->setParent(newParent);
    QQuickItem *newParentItem = qobject_cast<QQuickItem*>(newParent);
    QQuickItem *item = qobject_cast<QQuickItem*>(object);
    if (newParentItem && item)
        item->setParentItem(newParentItem);
    return true;
}

class ObjectCreator : public QObject
{
    Q_OBJECT
public:
    ObjectCreator(int requestId, QQmlEngine *engine, QObject *parent) :
        QObject(parent), m_component(engine), m_requestId(requestId)
    {
        connect(&m_component, &QQmlComponent::statusChanged, this, &ObjectCreator::tryCreateObject);
    }

    void run(const QByteArray &qml, const QUrl &filename)
    {
        m_component.setData(qml, filename);
    }

    void tryCreateObject(QQmlComponent::Status status)
    {
        switch (status) {
        case QQmlComponent::Error:
            emit result(m_requestId, false);
            delete this;
            return;
        case QQmlComponent::Ready: {
            // Stuff might have changed. We have to lookup the parentContext again.
            QQmlContext *parentContext = QQmlEngine::contextForObject(parent());
            if (!parentContext) {
                emit result(m_requestId, false);
            } else {
                QObject *newObject = m_component.create(parentContext);
                if (newObject && reparentQmlObject(newObject, parent()))
                    emit result(m_requestId, true);
                else
                    emit result(m_requestId, false);
            }
            deleteLater(); // The component might send more signals
            return;
        }
        default:
            break;
        }
    }

signals:
    void result(int requestId, bool success);

private:
    QQmlComponent m_component;
    int m_requestId;
};

bool GlobalInspector::createQmlObject(int requestId, const QString &qml, QObject *parent,
                                      const QStringList &importList, const QString &filename)
{
    if (!parent)
        return false;

    QQmlContext *parentContext = QQmlEngine::contextForObject(parent);
    if (!parentContext)
        return false;

    QString imports;
    for (const QString &s : importList)
        imports += s + QLatin1Char('\n');

    ObjectCreator *objectCreator = new ObjectCreator(requestId, parentContext->engine(), parent);
    connect(objectCreator, &ObjectCreator::result, this, &GlobalInspector::sendResult);
    objectCreator->run((imports + qml).toUtf8(), QUrl::fromLocalFile(filename));
    return true;
}

void GlobalInspector::addWindow(QQuickWindow *window)
{
    m_windowInspectors.append(new QQuickWindowInspector(window, this));
}

void GlobalInspector::removeWindow(QQuickWindow *window)
{
    for (QList<QmlJSDebugger::QQuickWindowInspector *>::Iterator i = m_windowInspectors.begin();
         i != m_windowInspectors.end();) {
        if ((*i)->quickWindow() == window) {
            delete *i;
            i = m_windowInspectors.erase(i);
        } else {
            ++i;
        }
    }
}

void GlobalInspector::setParentWindow(QQuickWindow *window, QWindow *parentWindow)
{
    for (QmlJSDebugger::QQuickWindowInspector *inspector : std::as_const(m_windowInspectors)) {
        if (inspector->quickWindow() == window)
            inspector->setParentWindow(parentWindow);
    }
}

bool GlobalInspector::syncSelectedItems(const QList<QQuickItem *> &items)
{
    bool selectionChanged = false;

    // Disconnect and remove items that are no longer selected
    const auto selectedItemsCopy = m_selectedItems;
    for (QQuickItem *item : selectedItemsCopy) {
        if (items.contains(item))
            continue;

        selectionChanged = true;
        item->disconnect(this);
        m_selectedItems.removeOne(item);
        delete m_highlightItems.take(item);
    }

    // Connect and add newly selected items
    for (QQuickItem *item : items) {
        if (m_selectedItems.contains(item))
            continue;

        selectionChanged = true;
        connect(item, &QObject::destroyed, this, &GlobalInspector::removeFromSelectedItems);
        m_selectedItems.append(item);
        for (QQuickWindowInspector *inspector : std::as_const(m_windowInspectors)) {
            if (inspector->isEnabled() && inspector->quickWindow() == item->window()) {
                m_highlightItems.insert(item, new SelectionHighlight(titleForItem(item), item,
                                                                     inspector->overlay()));
                break;
            }
        }
    }

    return selectionChanged;
}

QString GlobalInspector::titleForItem(QQuickItem *item) const
{
    QString className = QLatin1String(item->metaObject()->className());
    QString objectStringId = idStringForObject(item);

#if QT_CONFIG(regularexpression)
    className.remove(QRegularExpression(QLatin1String("_QMLTYPE_\\d+")));
    className.remove(QRegularExpression(QLatin1String("_QML_\\d+")));
#endif
    if (className.startsWith(QLatin1String("QQuick")))
        className = className.mid(6);

    QString constructedName;

    if (!objectStringId.isEmpty()) {
        constructedName = objectStringId + QLatin1String(" (") + className + QLatin1Char(')');
    } else if (!item->objectName().isEmpty()) {
        constructedName = item->objectName() + QLatin1String(" (") + className + QLatin1Char(')');
    } else {
        constructedName = className;
    }

    return constructedName;
}

QString GlobalInspector::idStringForObject(QObject *obj) const
{
    QQmlContext *context = qmlContext(obj);
    if (context) {
        QQmlRefPointer<QQmlContextData> cdata = QQmlContextData::get(context);
        if (cdata)
            return cdata->findObjectId(obj);
    }
    return QString();
}

void GlobalInspector::processMessage(const QByteArray &message)
{
    bool success = true;
    QQmlDebugPacket ds(message);

    QByteArray type;
    ds >> type;

    int requestId = -1;
    if (type == REQUEST) {
        QByteArray command;
        ds >> requestId >> command;

        if (command == ENABLE) {
            for (QQuickWindowInspector *inspector : std::as_const(m_windowInspectors))
                inspector->setEnabled(true);
            success = !m_windowInspectors.isEmpty();
        } else if (command == DISABLE) {
            setSelectedItems(QList<QQuickItem*>());
            for (QQuickWindowInspector *inspector : std::as_const(m_windowInspectors))
                inspector->setEnabled(false);
            success = !m_windowInspectors.isEmpty();
        } else if (command == SELECT) {
            QList<int> debugIds;
            ds >> debugIds;

            QList<QQuickItem *> selectedObjects;
            for (int debugId : std::as_const(debugIds)) {
                if (QQuickItem *obj =
                        qobject_cast<QQuickItem *>(QQmlDebugService::objectForId(debugId)))
                    selectedObjects << obj;
            }
            syncSelectedItems(selectedObjects);
        } else if (command == SET_ANIMATION_SPEED) {
            qreal speed;
            ds >> speed;
            QUnifiedTimer::instance()->setSpeedModifier(1 / speed);
        } else if (command == SHOW_APP_ON_TOP) {
            bool showOnTop;
            ds >> showOnTop;
            for (QmlJSDebugger::QQuickWindowInspector *inspector : std::as_const(m_windowInspectors))
                inspector->setShowAppOnTop(showOnTop);
            success = !m_windowInspectors.isEmpty();
        } else if (command == CREATE_OBJECT) {
            QString qml;
            int parentId;
            QString filename;
            QStringList imports;
            ds >> qml >> parentId >> imports >> filename;
            if (QObject *parent = QQmlDebugService::objectForId(parentId)) {
                if (createQmlObject(requestId, qml, parent, imports, filename))
                    return; // will callback for result
                else {
                    success = false;
                }
            } else {
                success = false;
            }

        } else if (command == DESTROY_OBJECT) {
            int debugId;
            ds >> debugId;
            if (QObject *obj = QQmlDebugService::objectForId(debugId))
                delete obj;
            else
                success = false;

        } else if (command == MOVE_OBJECT) {
            int debugId, newParent;
            ds >> debugId >> newParent;
            success = reparentQmlObject(QQmlDebugService::objectForId(debugId),
                                        QQmlDebugService::objectForId(newParent));
        } else {
            qWarning() << "Warning: Not handling command:" << command;
            success = false;
        }
    } else {
        qWarning() << "Warning: Not handling type:" << type << REQUEST;
        success = false;
    }

    sendResult(requestId, success);
}

void GlobalInspector::sendResult(int requestId, bool success)
{
    QQmlDebugPacket rs;
    rs << QByteArray(RESPONSE) << requestId << success;
    emit messageToClient(QQmlInspectorService::s_key, rs.data());
}

GlobalInspector::~GlobalInspector()
{
    // Everything else is parented
    qDeleteAll(m_highlightItems);
}

}

QT_END_NAMESPACE

#include "moc_globalinspector.cpp"

#include <globalinspector.moc>
