// Copyright (C) 2018 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 "qv4debugclient_p.h"
#include "qv4debugclient_p_p.h"
#include "qqmldebugconnection_p.h"

#include <private/qpacket_p.h>

#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QJsonArray>

QT_BEGIN_NAMESPACE

const char *V8REQUEST = "v8request";
const char *V8MESSAGE = "v8message";
const char *SEQ = "seq";
const char *TYPE = "type";
const char *COMMAND = "command";
const char *ARGUMENTS = "arguments";
const char *STEPACTION = "stepaction";
const char *STEPCOUNT = "stepcount";
const char *EXPRESSION = "expression";
const char *FRAME = "frame";
const char *CONTEXT = "context";
const char *GLOBAL = "global";
const char *DISABLEBREAK = "disable_break";
const char *HANDLES = "handles";
const char *INCLUDESOURCE = "includeSource";
const char *FROMFRAME = "fromFrame";
const char *TOFRAME = "toFrame";
const char *BOTTOM = "bottom";
const char *NUMBER = "number";
const char *FRAMENUMBER = "frameNumber";
const char *TYPES = "types";
const char *IDS = "ids";
const char *FILTER = "filter";
const char *FROMLINE = "fromLine";
const char *TOLINE = "toLine";
const char *TARGET = "target";
const char *LINE = "line";
const char *COLUMN = "column";
const char *ENABLED = "enabled";
const char *CONDITION = "condition";
const char *IGNORECOUNT = "ignoreCount";
const char *BREAKPOINT = "breakpoint";
const char *FLAGS = "flags";

const char *CONTINEDEBUGGING = "continue";
const char *EVALUATE = "evaluate";
const char *LOOKUP = "lookup";
const char *BACKTRACE = "backtrace";
const char *SCOPE = "scope";
const char *SCOPES = "scopes";
const char *SCRIPTS = "scripts";
const char *SOURCE = "source";
const char *SETBREAKPOINT = "setbreakpoint";
const char *CLEARBREAKPOINT = "clearbreakpoint";
const char *CHANGEBREAKPOINT = "changebreakpoint";
const char *SETEXCEPTIONBREAK = "setexceptionbreak";
const char *VERSION = "version";
const char *DISCONNECT = "disconnect";
const char *GARBAGECOLLECTOR = "gc";

const char *CONNECT = "connect";
const char *INTERRUPT = "interrupt";

const char *REQUEST = "request";
const char *IN = "in";
const char *NEXT = "next";
const char *OUT = "out";

const char *SCRIPT = "script";
const char *SCRIPTREGEXP = "scriptRegExp";
const char *EVENT = "event";

const char *ALL = "all";
const char *UNCAUGHT = "uncaught";

#define VARIANTMAPINIT \
    Q_D(QV4DebugClient); \
    QJsonObject jsonVal; \
    jsonVal.insert(QLatin1String(SEQ), d->seq++); \
    jsonVal.insert(QLatin1String(TYPE), QLatin1String(REQUEST));

QV4DebugClient::QV4DebugClient(QQmlDebugConnection *connection)
    : QQmlDebugClient(*new QV4DebugClientPrivate(connection))
{
    QObject::connect(this, &QQmlDebugClient::stateChanged,
                     this, [this](State state) { d_func()->onStateChanged(state); });
}

QV4DebugClientPrivate::QV4DebugClientPrivate(QQmlDebugConnection *connection) :
    QQmlDebugClientPrivate(QLatin1String("V8Debugger"), connection)
{
}

void QV4DebugClient::connect()
{
    Q_D(QV4DebugClient);
    d->sendMessage(CONNECT);
}

void QV4DebugClient::interrupt()
{
    Q_D(QV4DebugClient);
    d->sendMessage(INTERRUPT);
}

void QV4DebugClient::continueDebugging(StepAction action)
{
    //    { "seq"       : <number>,
    //      "type"      : "request",
    //      "command"   : "continue",
    //      "arguments" : { "stepaction" : <"in", "next" or "out">,
    //                      "stepcount"  : <number of steps (default 1)>
    //                    }
    //    }
    VARIANTMAPINIT;
    jsonVal.insert(QLatin1String(COMMAND), QLatin1String(CONTINEDEBUGGING));

    if (action != Continue) {
        QJsonObject args;
        switch (action) {
        case In:
            args.insert(QLatin1String(STEPACTION), QLatin1String(IN));
            break;
        case Out:
            args.insert(QLatin1String(STEPACTION), QLatin1String(OUT));
            break;
        case Next:
            args.insert(QLatin1String(STEPACTION), QLatin1String(NEXT));
            break;
        default:
            break;
        }
        jsonVal.insert(QLatin1String(ARGUMENTS), args);
    }

    d->sendMessage(V8REQUEST, jsonVal);
}

void QV4DebugClient::evaluate(const QString &expr, int frame, int context)
{
    //    { "seq"       : <number>,
    //      "type"      : "request",
    //      "command"   : "evaluate",
    //      "arguments" : { "expression"    : <expression to evaluate>,
    //                      "frame"         : <number>,
    //                      "context"       : <object ID>
    //                    }
    //    }
    VARIANTMAPINIT;
    jsonVal.insert(QLatin1String(COMMAND), QLatin1String(EVALUATE));

    QJsonObject args;
    args.insert(QLatin1String(EXPRESSION), expr);

    if (frame != -1)
        args.insert(QLatin1String(FRAME), frame);

    if (context != -1)
        args.insert(QLatin1String(CONTEXT), context);

    jsonVal.insert(QLatin1String(ARGUMENTS), args);

    d->sendMessage(V8REQUEST, jsonVal);
}

void QV4DebugClient::lookup(const QList<int> &handles, bool includeSource)
{
    //    { "seq"       : <number>,
    //      "type"      : "request",
    //      "command"   : "lookup",
    //      "arguments" : { "handles"       : <array of handles>,
    //                      "includeSource" : <boolean indicating whether the source will be included when script objects are returned>,
    //                    }
    //    }
    VARIANTMAPINIT;
    jsonVal.insert(QLatin1String(COMMAND),(QLatin1String(LOOKUP)));

    QJsonObject args;
    QJsonArray array;

    for (int handle : handles)
        array.append(handle);

    args.insert(QLatin1String(HANDLES), array);

    if (includeSource)
        args.insert(QLatin1String(INCLUDESOURCE), includeSource);

    jsonVal.insert(QLatin1String(ARGUMENTS), args);

    d->sendMessage(V8REQUEST, jsonVal);
}

void QV4DebugClient::backtrace(int fromFrame, int toFrame, bool bottom)
{
    //    { "seq"       : <number>,
    //      "type"      : "request",
    //      "command"   : "backtrace",
    //      "arguments" : { "fromFrame" : <number>
    //                      "toFrame" : <number>
    //                      "bottom" : <boolean, set to true if the bottom of the stack is requested>
    //                    }
    //    }
    VARIANTMAPINIT;
    jsonVal.insert(QLatin1String(COMMAND), QLatin1String(BACKTRACE));

    QJsonObject args;

    if (fromFrame != -1)
        args.insert(QLatin1String(FROMFRAME), fromFrame);

    if (toFrame != -1)
        args.insert(QLatin1String(TOFRAME), toFrame);

    if (bottom)
        args.insert(QLatin1String(BOTTOM), bottom);

    jsonVal.insert(QLatin1String(ARGUMENTS), args);
    d->sendMessage(V8REQUEST, jsonVal);
}

void QV4DebugClient::frame(int number)
{
    //    { "seq"       : <number>,
    //      "type"      : "request",
    //      "command"   : "frame",
    //      "arguments" : { "number" : <frame number>
    //                    }
    //    }
    VARIANTMAPINIT;
    jsonVal.insert(QLatin1String(COMMAND), QLatin1String(FRAME));

    if (number != -1) {
        QJsonObject args;
        args.insert(QLatin1String(NUMBER), number);
        jsonVal.insert(QLatin1String(ARGUMENTS), args);
    }

    d->sendMessage(V8REQUEST, jsonVal);
}

void QV4DebugClient::scope(int number, int frameNumber)
{
    //    { "seq"       : <number>,
    //      "type"      : "request",
    //      "command"   : "scope",
    //      "arguments" : { "number" : <scope number>
    //                      "frameNumber" : <frame number, optional uses selected frame if missing>
    //                    }
    //    }
    VARIANTMAPINIT;
    jsonVal.insert(QLatin1String(COMMAND), QLatin1String(SCOPE));

    if (number != -1) {
        QJsonObject args;
        args.insert(QLatin1String(NUMBER), number);

        if (frameNumber != -1)
            args.insert(QLatin1String(FRAMENUMBER), frameNumber);

        jsonVal.insert(QLatin1String(ARGUMENTS), args);
    }

    d->sendMessage(V8REQUEST, jsonVal);
}

void QV4DebugClient::scripts(int types, const QList<int> &ids, bool includeSource)
{
    //    { "seq"       : <number>,
    //      "type"      : "request",
    //      "command"   : "scripts",
    //      "arguments" : { "types"         : <types of scripts to retrieve
    //                                           set bit 0 for native scripts
    //                                           set bit 1 for extension scripts
    //                                           set bit 2 for normal scripts
    //                                         (default is 4 for normal scripts)>
    //                      "ids"           : <array of id's of scripts to return. If this is not specified all scripts are requrned>
    //                      "includeSource" : <boolean indicating whether the source code should be included for the scripts returned>
    //                      "filter"        : <string or number: filter string or script id.
    //                                         If a number is specified, then only the script with the same number as its script id will be retrieved.
    //                                         If a string is specified, then only scripts whose names contain the filter string will be retrieved.>
    //                    }
    //    }
    VARIANTMAPINIT;
    jsonVal.insert(QLatin1String(COMMAND), QLatin1String(SCRIPTS));

    QJsonObject args;
    args.insert(QLatin1String(TYPES), types);

    if (ids.size()) {
        QJsonArray array;
        for (int id : ids)
            array.append(id);

        args.insert(QLatin1String(IDS), array);
    }

    if (includeSource)
        args.insert(QLatin1String(INCLUDESOURCE), includeSource);

    jsonVal.insert(QLatin1String(ARGUMENTS), args);
    d->sendMessage(V8REQUEST, jsonVal);
}

void QV4DebugClient::setBreakpoint(const QString &target, int line, int column, bool enabled,
                                   const QString &condition, int ignoreCount)
{
    //    { "seq"       : <number>,
    //      "type"      : "request",
    //      "command"   : "setbreakpoint",
    //      "arguments" : { "type"        : "scriptRegExp"
    //                      "target"      : <function expression or script identification>
    //                      "line"        : <line in script or function>
    //                      "column"      : <character position within the line>
    //                      "enabled"     : <initial enabled state. True or false, default is true>
    //                      "condition"   : <string with break point condition>
    //                      "ignoreCount" : <number specifying the number of break point hits to ignore, default value is 0>
    //                    }
    //    }

    VARIANTMAPINIT;
    jsonVal.insert(QLatin1String(COMMAND), QLatin1String(SETBREAKPOINT));

    QJsonObject args;

    args.insert(QLatin1String(TYPE), QLatin1String(SCRIPTREGEXP));
    args.insert(QLatin1String(TARGET), target);

    if (line != -1)
        args.insert(QLatin1String(LINE), line);

    if (column != -1)
        args.insert(QLatin1String(COLUMN), column);

    args.insert(QLatin1String(ENABLED), enabled);

    if (!condition.isEmpty())
        args.insert(QLatin1String(CONDITION), condition);

    if (ignoreCount != -1)
        args.insert(QLatin1String(IGNORECOUNT), ignoreCount);

    jsonVal.insert(QLatin1String(ARGUMENTS),args);
    d->sendMessage(V8REQUEST, jsonVal);
}

void QV4DebugClient::clearBreakpoint(int breakpoint)
{
    //    { "seq"       : <number>,
    //      "type"      : "request",
    //      "command"   : "clearbreakpoint",
    //      "arguments" : { "breakpoint" : <number of the break point to clear>
    //                    }
    //    }
    VARIANTMAPINIT;
    jsonVal.insert(QLatin1String(COMMAND), QLatin1String(CLEARBREAKPOINT));

    QJsonObject args;
    args.insert(QLatin1String(BREAKPOINT), breakpoint);
    jsonVal.insert(QLatin1String(ARGUMENTS),args);

    d->sendMessage(V8REQUEST, jsonVal);
}

void QV4DebugClient::changeBreakpoint(int breakpoint, bool enabled)
{
    //    { "seq"       : <number>,
    //      "type"      : "request",
    //      "command"   : "changebreakpoint",
    //      "arguments" : { "breakpoint" : <number of the break point to change>
    //                      "enabled" : <bool: enables the break type if true, disables if false>
    //                    }
    //    }
    VARIANTMAPINIT;
    jsonVal.insert(QLatin1String(COMMAND), QLatin1String(CHANGEBREAKPOINT));

    QJsonObject args;
    args.insert(QLatin1String(BREAKPOINT), breakpoint);
    args.insert(QLatin1String(ENABLED), enabled);

    jsonVal.insert(QLatin1String(ARGUMENTS), args);
    d->sendMessage(V8REQUEST, jsonVal);
}

void QV4DebugClient::setExceptionBreak(Exception type, bool enabled)
{
    //    { "seq"       : <number>,
    //      "type"      : "request",
    //      "command"   : "setexceptionbreak",
    //      "arguments" : { "type"    : <string: "all", or "uncaught">,
    //                      "enabled" : <optional bool: enables the break type if true>
    //                    }
    //    }
    VARIANTMAPINIT;
    jsonVal.insert(QLatin1String(COMMAND), QLatin1String(SETEXCEPTIONBREAK));

    QJsonObject args;

    if (type == All)
        args.insert(QLatin1String(TYPE), QLatin1String(ALL));
    else if (type == Uncaught)
        args.insert(QLatin1String(TYPE), QLatin1String(UNCAUGHT));

    if (enabled)
        args.insert(QLatin1String(ENABLED), enabled);

    jsonVal.insert(QLatin1String(ARGUMENTS), args);
    d->sendMessage(V8REQUEST, jsonVal);
}

void QV4DebugClient::version()
{
    //    { "seq"       : <number>,
    //      "type"      : "request",
    //      "command"   : "version",
    //    }
    VARIANTMAPINIT;
    jsonVal.insert(QLatin1String(COMMAND), QLatin1String(VERSION));
    d->sendMessage(V8REQUEST, jsonVal);
}

QV4DebugClient::Response QV4DebugClient::response() const
{
    Q_D(const QV4DebugClient);
    const QJsonObject value = QJsonDocument::fromJson(d->response).object();
    return {
        value.value(QLatin1String(COMMAND)).toString(),
        value.value(QLatin1String("body"))
    };
}

void QV4DebugClient::disconnect()
{
    //    { "seq"     : <number>,
    //      "type"    : "request",
    //      "command" : "disconnect",
    //    }
    VARIANTMAPINIT;
    jsonVal.insert(QLatin1String(COMMAND), QLatin1String(DISCONNECT));
    d->sendMessage(DISCONNECT, jsonVal);
}

void QV4DebugClientPrivate::onStateChanged(QQmlDebugClient::State state)
{
    if (state == QQmlDebugClient::Enabled)
        flushSendBuffer();
}

void QV4DebugClient::messageReceived(const QByteArray &data)
{
    Q_D(QV4DebugClient);
    QPacket ds(connection()->currentDataStreamVersion(), data);
    QByteArray command;
    ds >> command;

    if (command == "V8DEBUG") {
        QByteArray type;
        ds >> type >> d->response;

        if (type == CONNECT) {
            emit connected();

        } else if (type == INTERRUPT) {
            emit interrupted();

        } else if (type == V8MESSAGE) {
            const QJsonObject value = QJsonDocument::fromJson(d->response).object();
            QString type = value.value(QLatin1String(TYPE)).toString();

            if (type == QLatin1String("response")) {

                if (!value.value(QLatin1String("success")).toBool()) {
                    emit failure();
                    qDebug() << "Received success == false response from application:"
                             << value.value(QLatin1String("message")).toString();
                    return;
                }

                QString debugCommand(value.value(QLatin1String(COMMAND)).toString());
                if (debugCommand == QLatin1String(BACKTRACE) ||
                        debugCommand == QLatin1String(LOOKUP) ||
                        debugCommand == QLatin1String(SETBREAKPOINT) ||
                        debugCommand == QLatin1String(EVALUATE) ||
                        debugCommand == QLatin1String(VERSION) ||
                        debugCommand == QLatin1String(DISCONNECT) ||
                        debugCommand == QLatin1String(GARBAGECOLLECTOR) ||
                        debugCommand == QLatin1String(CHANGEBREAKPOINT) ||
                        debugCommand == QLatin1String(CLEARBREAKPOINT) ||
                        debugCommand == QLatin1String(FRAME) ||
                        debugCommand == QLatin1String(SCOPE) ||
                        debugCommand == QLatin1String(SCOPES) ||
                        debugCommand == QLatin1String(SCRIPTS) ||
                        debugCommand == QLatin1String(SOURCE) ||
                        debugCommand == QLatin1String(SETEXCEPTIONBREAK)) {
                    emit result();
                } else {
                    // DO NOTHING
                }

            } else if (type == QLatin1String(EVENT)) {
                QString event(value.value(QLatin1String(EVENT)).toString());

                if (event == QLatin1String("break") || event == QLatin1String("exception"))
                    emit stopped();
            }
        }
    }
}

void QV4DebugClientPrivate::sendMessage(const QByteArray &command, const QJsonObject &args)
{
    Q_Q(QV4DebugClient);
    const QByteArray msg = packMessage(command, args);
    if (q->state() == QQmlDebugClient::Enabled) {
        q->sendMessage(msg);
    } else {
        sendBuffer.append(msg);
    }
}

void QV4DebugClientPrivate::flushSendBuffer()
{
    for (const QByteArray &msg : std::as_const(sendBuffer))
        sendMessage(msg);
    sendBuffer.clear();
}

QByteArray QV4DebugClientPrivate::packMessage(const QByteArray &type, const QJsonObject &object)
{
    QPacket rs(connection->currentDataStreamVersion());
    QByteArray cmd = "V8DEBUG";
    rs << cmd << type << QJsonDocument(object).toJson(QJsonDocument::Compact);
    return rs.data();
}

QT_END_NAMESPACE

#include "moc_qv4debugclient_p.cpp"
