// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#include "qmlprofilertraceclient.h"

#include "qdebugmessageclient.h"
#include "qmlenginecontrolclient.h"
#include "qmltypedevent.h"
#include "qpacketprotocol.h"

#include <utils/qtcassert.h>

#include <QQueue>
#include <QScopedPointer>

namespace QmlDebug {

class QmlProfilerTraceClientPrivate {
public:
    QmlProfilerTraceClientPrivate(
            QmlProfilerTraceClient *q, QmlDebugConnection *connection,
            const QmlProfilerTraceClient::EventTypeLoader &eventTypeLoader,
            const QmlProfilerTraceClient::EventLoader &eventLoader)
        : q(q)
        , eventTypeLoader(eventTypeLoader)
        , eventLoader(eventLoader)
        , engineControl(new QmlEngineControlClient(connection))
        , maximumTime(0)
        , recording(false)
        , requestedFeatures(0)
        , recordedFeatures(0)
        , flushInterval(0)
    {
    }

    void sendRecordingStatus(int engineId);
    bool updateFeatures(quint8 feature);
    int resolveType(const QmlTypedEvent &type);
    int resolveStackTop();
    void forwardEvents(QmlEvent &&last);
    void processCurrentEvent();
    void finalize();

    QmlProfilerTraceClient *q;

    QmlProfilerTraceClient::EventTypeLoader eventTypeLoader;
    QmlProfilerTraceClient::EventLoader eventLoader;

    // Use QScopedPointerDeleteLater here. The connection will call stateChanged() on all clients that are
    // alive when it gets disconnected. One way to notice a disconnection is failing to send the
    // plugin advertisement when a client unregisters. If one of the other clients is
    // half-destructed at that point, we get invalid memory accesses. Therefore, we cannot nest the
    // dtor calls.
    std::unique_ptr<QmlEngineControlClient, QScopedPointerDeleteLater> engineControl;
    std::unique_ptr<QDebugMessageClient, QScopedPointerDeleteLater> messageClient;
    qint64 maximumTime;
    bool recording;
    quint64 requestedFeatures;
    quint64 recordedFeatures;
    quint32 flushInterval;

    // Reuse the same event, so that we don't have to constantly reallocate all the data.
    QmlTypedEvent currentEvent;
    QHash<QmlEventType, int> eventTypeIds;
    QHash<qint64, int> serverTypeIds;
    QStack<QmlTypedEvent> rangesInProgress;
    QQueue<QmlEvent> pendingMessages;
    QQueue<QmlEvent> pendingDebugMessages;

    QList<int> trackedEngines;
};

int QmlProfilerTraceClientPrivate::resolveType(const QmlTypedEvent &event)
{
    int typeIndex = -1;
    if (event.serverTypeId != 0) {
        QHash<qint64, int>::ConstIterator it = serverTypeIds.constFind(event.serverTypeId);

        if (it != serverTypeIds.constEnd()) {
            typeIndex = it.value();
        } else {
            // We can potentially move the type away here, as we don't need to access it anymore,
            // but that requires some more refactoring.
            typeIndex = eventTypeLoader(QmlEventType(event.type));
            serverTypeIds[event.serverTypeId] = typeIndex;
        }
    } else {
        QHash<QmlEventType, int>::ConstIterator it = eventTypeIds.constFind(event.type);

        if (it != eventTypeIds.constEnd()) {
            typeIndex = it.value();
        } else {
            typeIndex = eventTypeLoader(QmlEventType(event.type));
            eventTypeIds[event.type] = typeIndex;
        }
    }
    return typeIndex;
}

int QmlProfilerTraceClientPrivate::resolveStackTop()
{
    if (rangesInProgress.isEmpty())
        return -1;

    QmlTypedEvent &typedEvent = rangesInProgress.top();
    int typeIndex = typedEvent.event.typeIndex();
    if (typeIndex >= 0)
        return typeIndex;

    typeIndex = resolveType(typedEvent);
    typedEvent.event.setTypeIndex(typeIndex);
    while (!pendingMessages.isEmpty()
           && pendingMessages.head().timestamp() < typedEvent.event.timestamp()) {
        forwardEvents(pendingMessages.dequeue());
    }
    forwardEvents(QmlEvent(typedEvent.event));
    return typeIndex;
}

void QmlProfilerTraceClientPrivate::forwardEvents(QmlEvent &&last)
{
    while (!pendingDebugMessages.isEmpty()
           && pendingDebugMessages.front().timestamp() <= last.timestamp()) {
         eventLoader(pendingDebugMessages.dequeue());
    }
    eventLoader(std::move(last));
}

void QmlProfilerTraceClientPrivate::processCurrentEvent()
{
    // RangeData and RangeLocation always apply to the range on the top of the stack. Furthermore,
    // all ranges are perfectly nested. This is why we can defer the type resolution until either
    // the range ends or a child range starts. With only the information in RangeStart we wouldn't
    // be able to uniquely identify the event type.
    Message rangeStage = currentEvent.type.rangeType() == UndefinedRangeType ?
                currentEvent.type.message() : currentEvent.event.rangeStage();
    switch (rangeStage) {
    case RangeStart:
        resolveStackTop();
        rangesInProgress.push(currentEvent);
        break;
    case RangeEnd: {
        int typeIndex = resolveStackTop();
        if (typeIndex == -1)
            break;
        currentEvent.event.setTypeIndex(typeIndex);
        while (!pendingMessages.isEmpty())
            forwardEvents(pendingMessages.dequeue());
        forwardEvents(QmlEvent(currentEvent.event));
        rangesInProgress.pop();
        break;
    }
    case RangeData:
        if (!rangesInProgress.isEmpty())
            rangesInProgress.top().type.setData(currentEvent.type.data());
        break;
    case RangeLocation:
        if (!rangesInProgress.isEmpty())
            rangesInProgress.top().type.setLocation(currentEvent.type.location());
        break;
    case DebugMessage:
        currentEvent.event.setTypeIndex(resolveType(currentEvent));
        pendingDebugMessages.enqueue(currentEvent.event);
        break;
    default: {
        int typeIndex = resolveType(currentEvent);
        currentEvent.event.setTypeIndex(typeIndex);
        if (rangesInProgress.isEmpty())
            forwardEvents(QmlEvent(currentEvent.event));
        else
            pendingMessages.enqueue(currentEvent.event);
        break;
    }
    }
}

void QmlProfilerTraceClientPrivate::finalize()
{
    while (!rangesInProgress.isEmpty()) {
        currentEvent = rangesInProgress.top();
        currentEvent.event.setRangeStage(RangeEnd);
        currentEvent.event.setTimestamp(maximumTime);
        processCurrentEvent();
    }
    QTC_CHECK(pendingMessages.isEmpty());
    while (!pendingDebugMessages.isEmpty())
        eventLoader(pendingDebugMessages.dequeue());
}

void QmlProfilerTraceClientPrivate::sendRecordingStatus(int engineId)
{
    QPacket stream(q->dataStreamVersion());
    stream << recording << engineId; // engineId -1 is OK. It means "all of them"
    if (recording) {
        stream << requestedFeatures << flushInterval;
        stream << true; // yes, we support type IDs
    }
    q->sendMessage(stream.data());
}

QmlProfilerTraceClient::QmlProfilerTraceClient(
        QmlDebugConnection *client, const EventTypeLoader &eventTypeLoader,
        const EventLoader &eventLoader, quint64 features)
    : QmlDebugClient(QLatin1String("CanvasFrameRate"), client)
    , d(new QmlProfilerTraceClientPrivate(this, client, eventTypeLoader, eventLoader))
{
    setRequestedFeatures(features);
    connect(d->engineControl.get(), &QmlEngineControlClient::engineAboutToBeAdded,
            this, &QmlProfilerTraceClient::sendRecordingStatus);
    connect(d->engineControl.get(), &QmlEngineControlClient::engineAboutToBeRemoved,
            this, [this](int engineId) {
        // We may already be done with that engine. Then we don't need to block it.
        if (d->trackedEngines.contains(engineId))
            d->engineControl->blockEngine(engineId);
    });
    connect(this, &QmlProfilerTraceClient::traceFinished,
            d->engineControl.get(), [this](qint64 timestamp, const QList<int> &engineIds) {
        Q_UNUSED(timestamp)
        // The engines might not be blocked because the trace can get finished before engine control
        // sees them.
        for (int blocked : d->engineControl->blockedEngines()) {
            if (engineIds.contains(blocked))
                d->engineControl->releaseEngine(blocked);
        }
    });
}

QmlProfilerTraceClient::~QmlProfilerTraceClient()
{
    //Disable profiling if started by client
    //Profiling data will be lost!!
    if (isRecording())
        setRecording(false);
    delete d;
}

void QmlProfilerTraceClient::clearEvents()
{
    d->rangesInProgress.clear();
    d->pendingMessages.clear();
    d->pendingDebugMessages.clear();
    if (d->recordedFeatures != 0) {
        d->recordedFeatures = 0;
        emit recordedFeaturesChanged(0);
    }
    emit cleared();
}

void QmlProfilerTraceClient::clear()
{
    d->eventTypeIds.clear();
    d->serverTypeIds.clear();
    d->trackedEngines.clear();
    clearEvents();
}

void QmlProfilerTraceClient::sendRecordingStatus(int engineId)
{
    d->sendRecordingStatus(engineId);
}

bool QmlProfilerTraceClient::isRecording() const
{
    return d->recording;
}

void QmlProfilerTraceClient::setRecording(bool v)
{
    if (v == d->recording)
        return;

    d->recording = v;

    if (state() == Enabled)
        sendRecordingStatus();

    emit recordingChanged(v);
}

quint64 QmlProfilerTraceClient::recordedFeatures() const
{
    return d->recordedFeatures;
}

void QmlProfilerTraceClient::setRequestedFeatures(quint64 features)
{
    d->requestedFeatures = features;
    if (features & static_cast<quint64>(1) << ProfileDebugMessages) {
        d->messageClient.reset(new QDebugMessageClient(connection()));
        connect(d->messageClient.get(), &QDebugMessageClient::message, this,
                [this](QtMsgType type, const QString &text,
                       const QDebugContextInfo &context)
        {
            QTC_ASSERT(d->updateFeatures(ProfileDebugMessages), return);
            d->currentEvent.event.setTimestamp(context.timestamp > 0 ? context.timestamp : 0);
            d->currentEvent.event.setTypeIndex(-1);
            d->currentEvent.event.setString(text);
            d->currentEvent.type = QmlEventType(DebugMessage, UndefinedRangeType, type,
                                                QmlEventLocation(context.file, context.line, 1));
            d->currentEvent.serverTypeId = 0;
            d->processCurrentEvent();
        });
    } else {
        d->messageClient.reset();
    }
}

void QmlProfilerTraceClient::setFlushInterval(quint32 flushInterval)
{
    d->flushInterval = flushInterval;
}

bool QmlProfilerTraceClientPrivate::updateFeatures(quint8 feature)
{
    if (feature == UndefinedProfileFeature)
        return true;
    quint64 flag = 1ULL << feature;
    if (!(requestedFeatures & flag))
        return false;
    if (!(recordedFeatures & flag)) {
        recordedFeatures |= flag;
        emit q->recordedFeaturesChanged(recordedFeatures);
    }
    return true;
}

void QmlProfilerTraceClient::stateChanged(State status)
{
    if (status == Enabled)
        sendRecordingStatus(-1);
    else
        d->finalize();
}

void QmlProfilerTraceClient::messageReceived(const QByteArray &data)
{
    QPacket stream(dataStreamVersion(), data);

    stream >> d->currentEvent;

    d->maximumTime = qMax(d->currentEvent.event.timestamp(), d->maximumTime);
    if (d->currentEvent.type.message() == Complete) {
        d->finalize();
        emit complete(d->maximumTime);
    } else if (d->currentEvent.type.message() == Event
               && d->currentEvent.type.detailType() == StartTrace) {
        const QList<int> engineIds = d->currentEvent.event.numbers<QList<int>, qint32>();
        d->trackedEngines.append(engineIds);
        emit traceStarted(d->currentEvent.event.timestamp(), engineIds);
    } else if (d->currentEvent.type.message() == Event
               && d->currentEvent.type.detailType() == EndTrace) {
        const QList<int> engineIds = d->currentEvent.event.numbers<QList<int>, qint32>();
        for (int engineId : engineIds)
            d->trackedEngines.removeAll(engineId);
        emit traceFinished(d->currentEvent.event.timestamp(), engineIds);
    } else if (d->updateFeatures(d->currentEvent.type.feature())) {
        d->processCurrentEvent();
    }
}

} // namespace QmlDebug
