// 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 "qv4datacollector.h"
#include "qv4debugger.h"
#include "qv4debugjob.h"

#include <private/qv4script_p.h>
#include <private/qv4string_p.h>
#include <private/qv4objectiterator_p.h>
#include <private/qv4identifierhash_p.h>
#include <private/qv4runtime_p.h>
#include <private/qv4identifiertable_p.h>

#include <private/qqmlcontext_p.h>
#include <private/qqmlengine_p.h>

#include <QtCore/qjsonarray.h>
#include <QtCore/qjsonobject.h>

QT_BEGIN_NAMESPACE

QV4::CppStackFrame *QV4DataCollector::findFrame(int frame)
{
    QV4::CppStackFrame *f = engine()->currentStackFrame;
    while (f && frame) {
        --frame;
        f = f->parentFrame();
    }
    return f;
}

QV4::Heap::ExecutionContext *QV4DataCollector::findContext(int frame)
{
    QV4::CppStackFrame *f = findFrame(frame);

    return f ? f->context()->d() : nullptr;
}

QV4::Heap::ExecutionContext *QV4DataCollector::findScope(QV4::Heap::ExecutionContext *ctx, int scope)
{
    for (; scope > 0 && ctx; --scope)
        ctx = ctx->outer;

    return ctx;
}

QVector<QV4::Heap::ExecutionContext::ContextType> QV4DataCollector::getScopeTypes(int frame)
{
    QVector<QV4::Heap::ExecutionContext::ContextType> types;

    QV4::Heap::ExecutionContext *it = findFrame(frame)->context()->d();

    for (; it; it = it->outer)
        types.append(QV4::Heap::ExecutionContext::ContextType(it->type));

    return types;
}

int QV4DataCollector::encodeScopeType(QV4::Heap::ExecutionContext::ContextType scopeType)
{
    switch (scopeType) {
    case QV4::Heap::ExecutionContext::Type_GlobalContext:
        break;
    case QV4::Heap::ExecutionContext::Type_WithContext:
        return 2;
    case QV4::Heap::ExecutionContext::Type_CallContext:
        return 1;
    case QV4::Heap::ExecutionContext::Type_QmlContext:
        return 3;
    case QV4::Heap::ExecutionContext::Type_BlockContext:
        return 4;
    }
    return 0;
}

QV4DataCollector::QV4DataCollector(QV4::ExecutionEngine *engine)
    : m_engine(engine)
{
    m_values.set(engine, engine->newArrayObject());
}

QV4DataCollector::Ref QV4DataCollector::addValueRef(const QV4::ScopedValue &value)
{
    return addRef(value);
}

const QV4::Object *collectProperty(const QV4::ScopedValue &value, QV4::ExecutionEngine *engine,
                                   QJsonObject &dict)
{
    QV4::Scope scope(engine);
    QV4::ScopedValue typeString(scope, QV4::Runtime::TypeofValue::call(engine, value));
    dict.insert(QStringLiteral("type"), typeString->toQStringNoThrow());

    const QLatin1String valueKey("value");
    switch (value->type()) {
    case QV4::Value::Empty_Type:
        Q_ASSERT(!"empty Value encountered");
        return nullptr;
    case QV4::Value::Undefined_Type:
        dict.insert(valueKey, QJsonValue::Undefined);
        return nullptr;
    case QV4::Value::Null_Type:
        dict.insert(valueKey, QJsonValue::Null);
        return nullptr;
    case QV4::Value::Boolean_Type:
        dict.insert(valueKey, value->booleanValue());
        return nullptr;
    case QV4::Value::Managed_Type:
        if (const QV4::String *s = value->as<QV4::String>()) {
            dict.insert(valueKey, s->toQString());
        } else if (const QV4::ArrayObject *a = value->as<QV4::ArrayObject>()) {
            // size of an array is number of its numerical properties; We don't consider free form
            // object properties here.
            dict.insert(valueKey, qint64(a->getLength()));
            return a;
        } else if (const QV4::Object *o = value->as<QV4::Object>()) {
            int numProperties = 0;
            QV4::ObjectIterator it(scope, o, QV4::ObjectIterator::EnumerableOnly);
            QV4::PropertyAttributes attrs;
            QV4::ScopedPropertyKey name(scope);
            while (true) {
                name = it.next(nullptr, &attrs);
                if (!name->isValid())
                    break;
                ++numProperties;
            }
            dict.insert(valueKey, numProperties);
            return o;
        } else {
            Q_UNREACHABLE();
        }
        return nullptr;
    case QV4::Value::Integer_Type:
        dict.insert(valueKey, value->integerValue());
        return nullptr;
    default: {// double
        const double val = value->doubleValue();
        if (qIsFinite(val))
            dict.insert(valueKey, val);
        else if (qIsNaN(val))
            dict.insert(valueKey, QStringLiteral("NaN"));
        else if (val < 0)
            dict.insert(valueKey, QStringLiteral("-Infinity"));
        else
            dict.insert(valueKey, QStringLiteral("Infinity"));
        return nullptr;
    }
    }
}

QJsonObject QV4DataCollector::lookupRef(Ref ref)
{
    QJsonObject dict;

    dict.insert(QStringLiteral("handle"), qint64(ref));
    QV4::Scope scope(engine());
    QV4::ScopedValue value(scope, getValue(ref));

    const QV4::Object *object = collectProperty(value, engine(), dict);
    if (object)
        dict.insert(QStringLiteral("properties"), collectProperties(object));

    return dict;
}

bool QV4DataCollector::isValidRef(QV4DataCollector::Ref ref) const
{
    QV4::Scope scope(engine());
    QV4::ScopedObject array(scope, m_values.value());
    return ref < array->getLength();
}

bool QV4DataCollector::collectScope(QJsonObject *dict, int frameNr, int scopeNr)
{
    QV4::Scope scope(engine());

    QV4::Scoped<QV4::ExecutionContext> ctxt(scope, findScope(findContext(frameNr), scopeNr));
    if (!ctxt)
        return false;

    QV4::ScopedObject scopeObject(scope, engine()->newObject());
    if (ctxt->d()->type == QV4::Heap::ExecutionContext::Type_CallContext ||
            ctxt->d()->type == QV4::Heap::ExecutionContext::Type_BlockContext) {
        QStringList names;
        Refs collectedRefs;

        QV4::ScopedValue v(scope);
        QV4::Heap::InternalClass *ic = ctxt->internalClass();
        for (uint i = 0; i < ic->size; ++i) {
            QV4::ScopedValue stringOrSymbol(scope, ic->keyAt(i));
            QV4::ScopedString propName(scope, stringOrSymbol->toString(scope.engine));
            names.append(propName->toQString());
            v = ctxt->getProperty(propName);
            collectedRefs.append(addValueRef(v));
        }

        Q_ASSERT(names.size() == collectedRefs.size());
        QV4::ScopedString propName(scope);
        for (int i = 0, ei = collectedRefs.size(); i != ei; ++i) {
            propName = engine()->newString(names.at(i));
            scopeObject->put(propName, (v = getValue(collectedRefs.at(i))));
        }
    }

    *dict = lookupRef(addRef(scopeObject));

    return true;
}

QJsonObject toRef(QV4DataCollector::Ref ref) {
    QJsonObject dict;
    dict.insert(QStringLiteral("ref"), qint64(ref));
    return dict;
}

QJsonObject QV4DataCollector::buildFrame(const QV4::StackFrame &stackFrame, int frameNr)
{
    QJsonObject frame;
    frame[QLatin1String("index")] = frameNr;
    frame[QLatin1String("debuggerFrame")] = false;
    frame[QLatin1String("func")] = stackFrame.function;
    frame[QLatin1String("script")] = stackFrame.source;
    frame[QLatin1String("line")] = qAbs(stackFrame.line) - 1;
    if (stackFrame.column >= 0)
        frame[QLatin1String("column")] = stackFrame.column;

    QJsonArray scopes;
    QV4::Scope scope(engine());
    QV4::ScopedContext ctxt(scope, findContext(frameNr));
    while (ctxt) {
        if (QV4::CallContext *cCtxt = ctxt->asCallContext()) {
            if (cCtxt->d()->activation)
                break;
        }
        ctxt = ctxt->d()->outer;
    }

    if (ctxt) {
        QV4::ScopedValue o(scope, ctxt->d()->activation);
        frame[QLatin1String("receiver")] = toRef(addValueRef(o));
    }

    // Only type and index are used by Qt Creator, so we keep it easy:
    QVector<QV4::Heap::ExecutionContext::ContextType> scopeTypes = getScopeTypes(frameNr);
    for (int i = 0, ei = scopeTypes.size(); i != ei; ++i) {
        int type = encodeScopeType(scopeTypes[i]);
        if (type == -1)
            continue;

        QJsonObject scope;
        scope[QLatin1String("index")] = i;
        scope[QLatin1String("type")] = type;
        scopes.push_back(scope);
    }

    frame[QLatin1String("scopes")] = scopes;

    return frame;
}

void QV4DataCollector::clear()
{
    m_values.set(engine(), engine()->newArrayObject());
}

QV4DataCollector::Ref QV4DataCollector::addRef(QV4::Value value, bool deduplicate)
{
    class ExceptionStateSaver
    {
        quint8 *hasExceptionLoc;
        quint8 hadException;

    public:
        ExceptionStateSaver(QV4::ExecutionEngine *engine)
            : hasExceptionLoc(&engine->hasException)
            , hadException(false)
        { std::swap(*hasExceptionLoc, hadException); }

        ~ExceptionStateSaver()
        { std::swap(*hasExceptionLoc, hadException); }
    };

    // if we wouldn't do this, the put won't work.
    ExceptionStateSaver resetExceptionState(engine());
    QV4::Scope scope(engine());
    QV4::ScopedObject array(scope, m_values.value());
    if (deduplicate) {
        for (Ref i = 0; i < array->getLength(); ++i) {
            if (array->get(i) == value.rawValue())
                return i;
        }
    }
    Ref ref = array->getLength();
    array->put(ref, value);
    Q_ASSERT(array->getLength() - 1 == ref);
    return ref;
}

QV4::ReturnedValue QV4DataCollector::getValue(Ref ref)
{
    QV4::Scope scope(engine());
    QV4::ScopedObject array(scope, m_values.value());
    Q_ASSERT(ref < array->getLength());
    return array->get(ref, nullptr);
}

class CapturePreventer
{
public:
    CapturePreventer(QV4::ExecutionEngine *engine)
    {
        if (QQmlEngine *e = engine->qmlEngine()) {
            m_engine = QQmlEnginePrivate::get(e);
            m_capture = m_engine->propertyCapture;
            m_engine->propertyCapture = nullptr;
        }
    }

    ~CapturePreventer()
    {
        if (m_engine && m_capture) {
            Q_ASSERT(!m_engine->propertyCapture);
            m_engine->propertyCapture = m_capture;
        }
    }

private:
    QQmlEnginePrivate *m_engine = nullptr;
    QQmlPropertyCapture *m_capture = nullptr;
};

QJsonArray QV4DataCollector::collectProperties(const QV4::Object *object)
{
    CapturePreventer capturePreventer(engine());
    Q_UNUSED(capturePreventer);

    QJsonArray res;

    QV4::Scope scope(engine());
    QV4::ObjectIterator it(scope, object, QV4::ObjectIterator::EnumerableOnly);
    QV4::ScopedValue name(scope);
    QV4::ScopedValue value(scope);
    while (true) {
        QV4::Value v;
        name = it.nextPropertyNameAsString(&v);
        if (name->isNull())
            break;
        QString key = name->toQStringNoThrow();
        value = v;
        res.append(collectAsJson(key, value));
    }

    return res;
}

QJsonObject QV4DataCollector::collectAsJson(const QString &name, const QV4::ScopedValue &value)
{
    QJsonObject dict;
    if (!name.isNull())
        dict.insert(QStringLiteral("name"), name);
    if (value->isManaged() && !value->isString()) {
        Ref ref = addRef(value);
        dict.insert(QStringLiteral("ref"), qint64(ref));
    }

    collectProperty(value, engine(), dict);
    return dict;
}

QT_END_NAMESPACE
