// 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

#include "qqmlnativedebugconnector.h"

#include <private/qhooks_p.h>
#include <private/qversionedpacket_p.h>

#include <QtQml/qjsengine.h>
#include <QtCore/qdebug.h>
#include <QtCore/qjsonarray.h>
#include <QtCore/qjsondocument.h>
#include <QtCore/qjsonobject.h>
#include <QtCore/qjsonvalue.h>
#include <QtCore/qpointer.h>
#include <QtCore/qvector.h>

//#define TRACE_PROTOCOL(s) qDebug() << s
#define TRACE_PROTOCOL(s)

QT_USE_NAMESPACE

static bool expectSyncronousResponse = false;
Q_GLOBAL_STATIC(QByteArray, responseBuffer)

extern "C" {

Q_DECL_EXPORT const char *qt_qmlDebugMessageBuffer;
Q_DECL_EXPORT int qt_qmlDebugMessageLength;
Q_DECL_EXPORT bool qt_qmlDebugConnectionBlocker;

// In blocking mode, this will busy wait until the debugger sets block to false.
Q_DECL_EXPORT void qt_qmlDebugConnectorOpen();

// First thing, set the debug stream version. Please use this function as we might move the version
// member to some other place.
Q_DECL_EXPORT void qt_qmlDebugSetStreamVersion(int version)
{
    QQmlNativeDebugConnector::setDataStreamVersion(version);
}


// Break in this one to process output from an asynchronous message/
Q_DECL_EXPORT void qt_qmlDebugMessageAvailable()
{
}


// Break in this one to get notified about construction and destruction of
// interesting objects, such as QmlEngines.
Q_DECL_EXPORT void qt_qmlDebugObjectAvailable()
{
}

Q_DECL_EXPORT void qt_qmlDebugClearBuffer()
{
    responseBuffer->clear();
    qt_qmlDebugMessageBuffer = nullptr;
    qt_qmlDebugMessageLength = 0;
}

// Send a message to a service.
Q_DECL_EXPORT bool qt_qmlDebugSendDataToService(const char *serviceName, const char *hexData)
{
    QByteArray msg = QByteArray::fromHex(hexData);

    QQmlDebugConnector *instance = QQmlDebugConnector::instance();
    if (!instance)
        return false;

    QQmlDebugService *recipient = instance->service(serviceName);
    if (!recipient)
        return false;

    TRACE_PROTOCOL("Recipient: " << recipient << " got message: " << msg);
    expectSyncronousResponse = true;
    recipient->messageReceived(msg);
    expectSyncronousResponse = false;

    return true;
}

// Enable a service.
Q_DECL_EXPORT bool qt_qmlDebugEnableService(const char *data)
{
    QQmlDebugConnector *instance = QQmlDebugConnector::instance();
    if (!instance)
        return false;

    QString name = QString::fromLatin1(data);
    QQmlDebugService *service = instance->service(name);
    if (!service || service->state() == QQmlDebugService::Enabled)
        return false;

    service->stateAboutToBeChanged(QQmlDebugService::Enabled);
    service->setState(QQmlDebugService::Enabled);
    service->stateChanged(QQmlDebugService::Enabled);
    return true;
}

Q_DECL_EXPORT bool qt_qmlDebugDisableService(const char *data)
{
    QQmlDebugConnector *instance = QQmlDebugConnector::instance();
    if (!instance)
        return false;

    QString name = QString::fromLatin1(data);
    QQmlDebugService *service = instance->service(name);
    if (!service || service->state() == QQmlDebugService::Unavailable)
        return false;

    service->stateAboutToBeChanged(QQmlDebugService::Unavailable);
    service->setState(QQmlDebugService::Unavailable);
    service->stateChanged(QQmlDebugService::Unavailable);
    return true;
}

quintptr qt_qmlDebugTestHooks[] = {
    quintptr(1), // Internal Version
    quintptr(6), // Number of entries following
    quintptr(&qt_qmlDebugMessageBuffer),
    quintptr(&qt_qmlDebugMessageLength),
    quintptr(&qt_qmlDebugSendDataToService),
    quintptr(&qt_qmlDebugEnableService),
    quintptr(&qt_qmlDebugDisableService),
    quintptr(&qt_qmlDebugObjectAvailable),
    quintptr(&qt_qmlDebugClearBuffer)
};

// In blocking mode, this will busy wait until the debugger sets block to false.
Q_DECL_EXPORT void qt_qmlDebugConnectorOpen()
{
    TRACE_PROTOCOL("Opening native debug connector");

    // FIXME: Use a dedicated hook. Startup is a safe workaround, though,
    // as we are already beyond its only use.
    qtHookData[QHooks::Startup] = quintptr(&qt_qmlDebugTestHooks);

    while (qt_qmlDebugConnectionBlocker)
        ;

    TRACE_PROTOCOL("Opened native debug connector");
}

} // extern "C"

QT_BEGIN_NAMESPACE

QQmlNativeDebugConnector::QQmlNativeDebugConnector()
    : m_blockingMode(false)
{
    const QString args = commandLineArguments();
    const auto lstjsDebugArguments = QStringView{args}.split(QLatin1Char(','), Qt::SkipEmptyParts);
    QStringList services;
    for (const QStringView &strArgument : lstjsDebugArguments) {
        if (strArgument == QLatin1String("block")) {
            m_blockingMode = true;
        } else if (strArgument == QLatin1String("native")) {
            // Ignore. This is used to signal that this connector
            // should be loaded and that has already happened.
        } else if (strArgument.startsWith(QLatin1String("services:"))) {
            services.append(strArgument.mid(9).toString());
        } else if (!services.isEmpty()) {
            services.append(strArgument.toString());
        } else if (!strArgument.startsWith(QLatin1String("connector:"))) {
            qWarning("QML Debugger: Invalid argument \"%s\" detected. Ignoring the same.",
                     strArgument.toUtf8().constData());
        }
    }
    setServices(services);
}

QQmlNativeDebugConnector::~QQmlNativeDebugConnector()
{
    for (QQmlDebugService *service : std::as_const(m_services)) {
        service->stateAboutToBeChanged(QQmlDebugService::NotConnected);
        service->setState(QQmlDebugService::NotConnected);
        service->stateChanged(QQmlDebugService::NotConnected);
    }
}

bool QQmlNativeDebugConnector::blockingMode() const
{
    return m_blockingMode;
}

QQmlDebugService *QQmlNativeDebugConnector::service(const QString &name) const
{
    for (QVector<QQmlDebugService *>::ConstIterator i = m_services.begin(); i != m_services.end();
         ++i) {
        if ((*i)->name() == name)
            return *i;
    }
    return nullptr;
}

void QQmlNativeDebugConnector::addEngine(QJSEngine *engine)
{
    Q_ASSERT(!m_engines.contains(engine));

    TRACE_PROTOCOL("Add engine to connector:" << engine);
    for (QQmlDebugService *service : std::as_const(m_services))
        service->engineAboutToBeAdded(engine);

    announceObjectAvailability(QLatin1String("qmlengine"), engine, true);

    for (QQmlDebugService *service : std::as_const(m_services))
        service->engineAdded(engine);

    m_engines.append(engine);
}

void QQmlNativeDebugConnector::removeEngine(QJSEngine *engine)
{
    Q_ASSERT(m_engines.contains(engine));

    TRACE_PROTOCOL("Remove engine from connector:" << engine);
    for (QQmlDebugService *service : std::as_const(m_services))
        service->engineAboutToBeRemoved(engine);

    announceObjectAvailability(QLatin1String("qmlengine"), engine, false);

    for (QQmlDebugService *service : std::as_const(m_services))
        service->engineRemoved(engine);

    m_engines.removeOne(engine);
}

bool QQmlNativeDebugConnector::hasEngine(QJSEngine *engine) const
{
    return m_engines.contains(engine);
}

void QQmlNativeDebugConnector::announceObjectAvailability(const QString &objectType,
                                                          QObject *object, bool available)
{
    QJsonObject ob;
    ob.insert(QLatin1String("objecttype"), objectType);
    ob.insert(QLatin1String("object"), QString::number(quintptr(object)));
    ob.insert(QLatin1String("available"), available);
    QJsonDocument doc;
    doc.setObject(ob);

    QByteArray ba = doc.toJson(QJsonDocument::Compact);
    qt_qmlDebugMessageBuffer = ba.constData();
    qt_qmlDebugMessageLength = ba.size();
    TRACE_PROTOCOL("Reporting engine availabilty");
    qt_qmlDebugObjectAvailable(); // Trigger native breakpoint.
    qt_qmlDebugMessageBuffer = nullptr;
    qt_qmlDebugMessageLength = 0;
}

bool QQmlNativeDebugConnector::addService(const QString &name, QQmlDebugService *service)
{
    TRACE_PROTOCOL("Add service to connector: " << qPrintable(name) << service);
    for (auto it = m_services.cbegin(), end = m_services.cend(); it != end; ++it) {
        if ((*it)->name() == name)
            return false;
    }

    connect(service, &QQmlDebugService::messageToClient,
            this, &QQmlNativeDebugConnector::sendMessage);
    connect(service, &QQmlDebugService::messagesToClient,
            this, &QQmlNativeDebugConnector::sendMessages);

    service->setState(QQmlDebugService::Unavailable);

    m_services << service;
    return true;
}

bool QQmlNativeDebugConnector::removeService(const QString &name)
{
    for (QVector<QQmlDebugService *>::Iterator i = m_services.begin(); i != m_services.end(); ++i) {
        if ((*i)->name() == name) {
            QQmlDebugService *service = *i;
            m_services.erase(i);
            service->setState(QQmlDebugService::NotConnected);

            disconnect(service, &QQmlDebugService::messagesToClient,
                       this, &QQmlNativeDebugConnector::sendMessages);
            disconnect(service, &QQmlDebugService::messageToClient,
                       this, &QQmlNativeDebugConnector::sendMessage);

            return true;
        }
    }
    return false;
}

bool QQmlNativeDebugConnector::open(const QVariantHash &configuration)
{
    m_blockingMode = configuration.value(QStringLiteral("block"), m_blockingMode).toBool();
    qt_qmlDebugConnectionBlocker = m_blockingMode;
    qt_qmlDebugConnectorOpen();
    return true;
}

void QQmlNativeDebugConnector::setDataStreamVersion(int version)
{
    Q_ASSERT(version <= QDataStream::Qt_DefaultCompiledVersion);
    s_dataStreamVersion = version;
}

void QQmlNativeDebugConnector::sendMessage(const QString &name, const QByteArray &message)
{
    (*responseBuffer) += name.toUtf8() + ' ' + QByteArray::number(message.size()) + ' ' + message;
    qt_qmlDebugMessageBuffer = responseBuffer->constData();
    qt_qmlDebugMessageLength = responseBuffer->size();
    // Responses are allowed to accumulate, the buffer will be cleared by
    // separate calls to qt_qmlDebugClearBuffer() once the synchronous
    // function return ('if' branch below) or in the native breakpoint handler
    // ('else' branch below).
    if (expectSyncronousResponse) {
        TRACE_PROTOCOL("Expected synchronous response in " << message);
        // Do not trigger the native breakpoint on qt_qmlDebugMessageFromService.
    } else {
        TRACE_PROTOCOL("Found asynchronous message in " << message);
        // Trigger native breakpoint.
        qt_qmlDebugMessageAvailable();
    }
}

void QQmlNativeDebugConnector::sendMessages(const QString &name, const QList<QByteArray> &messages)
{
    for (int i = 0; i != messages.size(); ++i)
        sendMessage(name, messages.at(i));
}

QQmlDebugConnector *QQmlNativeDebugConnectorFactory::create(const QString &key)
{
    return key == QLatin1String("QQmlNativeDebugConnector") ? new QQmlNativeDebugConnector : nullptr;
}

QT_END_NAMESPACE

#include "moc_qqmlnativedebugconnector.cpp"
