// 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 "qv4serialize_p.h"

#include <private/qv4dateobject_p.h>
#include <private/qv4objectproto_p.h>
#include <private/qv4qobjectwrapper_p.h>
#include <private/qv4regexp_p.h>
#include <private/qv4regexpobject_p.h>
#include <private/qv4sequenceobject_p.h>
#include <private/qv4value_p.h>

QT_BEGIN_NAMESPACE

using namespace QV4;

// We allow the following JavaScript types to be passed between the main and
// the secondary thread:
//    + undefined
//    + null
//    + Boolean
//    + String
//    + Function
//    + Array
//    + "Simple" Objects
//    + Number
//    + Date
//    + RegExp
// <quint8 type><quint24 size><data>

enum Type {
    WorkerUndefined,
    WorkerNull,
    WorkerTrue,
    WorkerFalse,
    WorkerString,
    WorkerFunction,
    WorkerArray,
    WorkerObject,
    WorkerInt32,
    WorkerUint32,
    WorkerNumber,
    WorkerDate,
    WorkerRegexp,
    WorkerListModel,
    WorkerUrl,
    WorkerSequence
};

static inline quint32 valueheader(Type type, quint32 size = 0)
{
    return quint8(type) << 24 | (size & 0xFFFFFF);
}

static inline Type headertype(quint32 header)
{
    return (Type)(header >> 24);
}

static inline quint32 headersize(quint32 header)
{
    return header & 0xFFFFFF;
}

static inline void push(QByteArray &data, quint32 value)
{
    data.append((const char *)&value, sizeof(quint32));
}

static inline void push(QByteArray &data, double value)
{
    data.append((const char *)&value, sizeof(double));
}

static inline void push(QByteArray &data, void *ptr)
{
    data.append((const char *)&ptr, sizeof(void *));
}

static inline void reserve(QByteArray &data, int extra)
{
    data.reserve(data.size() + extra);
}

static inline quint32 popUint32(const char *&data)
{
    quint32 rv = *((const quint32 *)data);
    data += sizeof(quint32);
    return rv;
}

static inline double popDouble(const char *&data)
{
    double rv = *((const double *)data);
    data += sizeof(double);
    return rv;
}

static inline void *popPtr(const char *&data)
{
    void *rv = *((void *const *)data);
    data += sizeof(void *);
    return rv;
}

#define ALIGN(size) (((size) + 3) & ~3)
static inline void serializeString(QByteArray &data, const QString &str, Type type)
{
    int length = str.size();
    if (length > 0xFFFFFF) {
        push(data, valueheader(WorkerUndefined));
        return;
    }
    int utf16size = ALIGN(length * sizeof(quint16));

    reserve(data, utf16size + sizeof(quint32));
    push(data, valueheader(type, length));

    int offset = data.size();
    data.resize(data.size() + utf16size);
    char *buffer = data.data() + offset;

    memcpy(buffer, str.constData(), length*sizeof(QChar));
}

// XXX TODO: Check that worker script is exception safe in the case of
// serialization/deserialization failures

void Serialize::serialize(QByteArray &data, const QV4::Value &v, ExecutionEngine *engine)
{
    QV4::Scope scope(engine);

    if (v.isEmpty()) {
        Q_ASSERT(!"Serialize: got empty value");
    } else if (v.isUndefined()) {
        push(data, valueheader(WorkerUndefined));
    } else if (v.isNull()) {
        push(data, valueheader(WorkerNull));
    } else if (v.isBoolean()) {
        push(data, valueheader(v.booleanValue() == true ? WorkerTrue : WorkerFalse));
    } else if (v.isString()) {
        serializeString(data, v.toQString(), WorkerString);
    } else if (v.as<FunctionObject>()) {
        // XXX TODO: Implement passing function objects between the main and
        // worker scripts
        push(data, valueheader(WorkerUndefined));
    } else if (const QV4::ArrayObject *array = v.as<ArrayObject>()) {
        uint length = array->getLength();
        if (length > 0xFFFFFF) {
            push(data, valueheader(WorkerUndefined));
            return;
        }
        reserve(data, sizeof(quint32) + length * sizeof(quint32));
        push(data, valueheader(WorkerArray, length));
        ScopedValue val(scope);
        for (uint ii = 0; ii < length; ++ii)
            serialize(data, (val = array->get(ii)), engine);
    } else if (v.isInteger()) {
        reserve(data, 2 * sizeof(quint32));
        push(data, valueheader(WorkerInt32));
        push(data, (quint32)v.integerValue());
//    } else if (v.IsUint32()) {
//        reserve(data, 2 * sizeof(quint32));
//        push(data, valueheader(WorkerUint32));
//        push(data, v.Uint32Value());
    } else if (v.isNumber()) {
        reserve(data, sizeof(quint32) + sizeof(double));
        push(data, valueheader(WorkerNumber));
        push(data, v.asDouble());
    } else if (const QV4::DateObject *d = v.as<DateObject>()) {
        reserve(data, sizeof(quint32) + sizeof(double));
        push(data, valueheader(WorkerDate));
        push(data, d->date());
    } else if (const RegExpObject *re = v.as<RegExpObject>()) {
        quint32 flags = re->flags();
        QString pattern = re->source();
        int length = pattern.size() + 1;
        if (length > 0xFFFFFF) {
            push(data, valueheader(WorkerUndefined));
            return;
        }
        int utf16size = ALIGN(length * sizeof(quint16));

        reserve(data, sizeof(quint32) + utf16size);
        push(data, valueheader(WorkerRegexp, flags));
        push(data, (quint32)length);

        int offset = data.size();
        data.resize(data.size() + utf16size);
        char *buffer = data.data() + offset;

        memcpy(buffer, pattern.constData(), length*sizeof(QChar));
    } else if (const QObjectWrapper *qobjectWrapper = v.as<QV4::QObjectWrapper>()) {
        // XXX TODO: Generalize passing objects between the main thread and worker scripts so
        // that others can trivially plug in their elements.
        if (QObject *lm = qobjectWrapper->object()) {
            if (QObject *agent = qvariant_cast<QObject *>(lm->property("agent"))) {
                if (QMetaObject::invokeMethod(agent, "addref")) {
                    push(data, valueheader(WorkerListModel));
                    push(data, (void *)agent);
                    return;
                }
            }
        }
        // No other QObject's are allowed to be sent
        push(data, valueheader(WorkerUndefined));
    } else if (const Sequence *s = v.as<Sequence>()) {
        // valid sequence.  we generate a length (sequence length + 1 for the sequence type)
        uint seqLength = ScopedValue(scope, s->get(engine->id_length()))->toUInt32();
        uint length = seqLength + 1;
        if (length > 0xFFFFFF) {
            push(data, valueheader(WorkerUndefined));
            return;
        }
        reserve(data, sizeof(quint32) + length * sizeof(quint32));
        push(data, valueheader(WorkerSequence, length));

        // sequence type
        serialize(data, QV4::Value::fromInt32(
                                QV4::SequencePrototype::metaTypeForSequence(s).id()), engine);

        ScopedValue val(scope);
        for (uint ii = 0; ii < seqLength; ++ii)
            serialize(data, (val = s->get(ii)), engine); // sequence elements

        return;
    } else if (const Object *o = v.as<Object>()) {
        const QVariant variant = QV4::ExecutionEngine::toVariant(
                    v, QMetaType::fromType<QUrl>(), false);
        if (variant.userType() == QMetaType::QUrl) {
            serializeString(data, variant.value<QUrl>().toString(), WorkerUrl);
            return;
        }

        // regular object
        QV4::ScopedValue val(scope, v);
        QV4::ScopedArrayObject properties(scope, QV4::ObjectPrototype::getOwnPropertyNames(engine, val));
        quint32 length = properties->getLength();
        if (length > 0xFFFFFF) {
            push(data, valueheader(WorkerUndefined));
            return;
        }
        push(data, valueheader(WorkerObject, length));

        QV4::ScopedValue s(scope);
        for (quint32 ii = 0; ii < length; ++ii) {
            s = properties->get(ii);
            serialize(data, s, engine);

            QV4::String *str = s->as<String>();
            val = o->get(str);
            if (scope.hasException())
                scope.engine->catchException();

            serialize(data, val, engine);
        }
        return;
    } else {
        push(data, valueheader(WorkerUndefined));
    }
}

struct VariantRef
{
    VariantRef() : obj(nullptr) {}
    VariantRef(const VariantRef &r) : obj(r.obj) { addref(); }
    VariantRef(QObject *a) : obj(a) { addref(); }
    ~VariantRef() { release(); }

    VariantRef &operator=(const VariantRef &o) {
        o.addref();
        release();
        obj = o.obj;
        return *this;
    }

    void addref() const
    {
        if (obj)
            QMetaObject::invokeMethod(obj, "addref");
    }

    void release() const
    {
        if (obj)
            QMetaObject::invokeMethod(obj, "release");

    }

    QObject *obj;
};

QT_END_NAMESPACE
Q_DECLARE_METATYPE(VariantRef)
Q_DECLARE_METATYPE(QV4::ExecutionEngine *)
QT_BEGIN_NAMESPACE

ReturnedValue Serialize::deserialize(const char *&data, ExecutionEngine *engine)
{
    quint32 header = popUint32(data);
    Type type = headertype(header);

    Scope scope(engine);

    switch (type) {
    case WorkerUndefined:
        return QV4::Encode::undefined();
    case WorkerNull:
        return QV4::Encode::null();
    case WorkerTrue:
        return QV4::Encode(true);
    case WorkerFalse:
        return QV4::Encode(false);
    case WorkerString:
    case WorkerUrl:
    {
        quint32 size = headersize(header);
        QString qstr((const QChar *)data, size);
        data += ALIGN(size * sizeof(quint16));
        return  (type == WorkerUrl)
                ? engine->fromVariant(QVariant::fromValue(QUrl(qstr)))
                : Encode(engine->newString(qstr));
    }
    case WorkerFunction:
        Q_ASSERT(!"Unreachable");
        break;
    case WorkerArray:
    {
        quint32 size = headersize(header);
        ScopedArrayObject a(scope, engine->newArrayObject());
        ScopedValue v(scope);
        for (quint32 ii = 0; ii < size; ++ii) {
            v = deserialize(data, engine);
            a->put(ii, v);
        }
        return a.asReturnedValue();
    }
    case WorkerObject:
    {
        quint32 size = headersize(header);
        ScopedObject o(scope, engine->newObject());
        ScopedValue name(scope);
        ScopedString n(scope);
        ScopedValue value(scope);
        for (quint32 ii = 0; ii < size; ++ii) {
            name = deserialize(data, engine);
            value = deserialize(data, engine);
            n = name->asReturnedValue();
            o->put(n, value);
        }
        return o.asReturnedValue();
    }
    case WorkerInt32:
        return QV4::Encode((qint32)popUint32(data));
    case WorkerUint32:
        return QV4::Encode(popUint32(data));
    case WorkerNumber:
        return QV4::Encode(popDouble(data));
    case WorkerDate:
        return QV4::Encode(engine->newDateObject(popDouble(data)));
    case WorkerRegexp:
    {
        quint32 flags = headersize(header);
        quint32 length = popUint32(data);
        QString pattern = QString((const QChar *)data, length - 1);
        data += ALIGN(length * sizeof(quint16));
        return Encode(engine->newRegExpObject(pattern, flags));
    }
    case WorkerListModel:
    {
        QObject *agent = reinterpret_cast<QObject *>(popPtr(data));
        QV4::ScopedValue rv(scope, QV4::QObjectWrapper::wrap(engine, agent));
        // ### Find a better solution then the ugly property
        VariantRef ref(agent);
        QVariant var = QVariant::fromValue(ref);
        QV4::ScopedValue v(scope, scope.engine->fromVariant(var));
        QV4::ScopedString s(scope, engine->newString(QStringLiteral("__qml:hidden:ref")));
        rv->as<Object>()->defineReadonlyProperty(s, v);

        QMetaObject::invokeMethod(agent, "release");
        agent->setProperty("engine", QVariant::fromValue(engine));
        return rv->asReturnedValue();
    }
    case WorkerSequence:
    {
        ScopedValue value(scope);
        quint32 length = headersize(header);
        quint32 seqLength = length - 1;
        value = deserialize(data, engine);
        int sequenceType = value->integerValue();
        ScopedArrayObject array(scope, engine->newArrayObject());
        array->arrayReserve(seqLength);
        for (quint32 ii = 0; ii < seqLength; ++ii) {
            value = deserialize(data, engine);
            array->arrayPut(ii, value);
        }
        array->setArrayLengthUnchecked(seqLength);
        QVariant seqVariant = QV4::SequencePrototype::toVariant(array, QMetaType(sequenceType));
        return QV4::SequencePrototype::fromVariant(engine, seqVariant);
    }
    }
    Q_ASSERT(!"Unreachable");
    return QV4::Encode::undefined();
}

QByteArray Serialize::serialize(const QV4::Value &value, ExecutionEngine *engine)
{
    QByteArray rv;
    serialize(rv, value, engine);
    return rv;
}

ReturnedValue Serialize::deserialize(const QByteArray &data, ExecutionEngine *engine)
{
    const char *stream = data.constData();
    return deserialize(stream, engine);
}

QT_END_NAMESPACE
