/*
 * Copyright (C) 2018 Igalia S.L.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public License
 * along with this library; see the file COPYING.LIB.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#include "config.h"
#include "JSCClass.h"

#include "APICast.h"
#include "JSAPIWrapperGlobalObject.h"
#include "JSAPIWrapperObject.h"
#include "JSCCallbackFunction.h"
#include "JSCClassPrivate.h"
#include "JSCContextPrivate.h"
#include "JSCExceptionPrivate.h"
#include "JSCInlines.h"
#include "JSCValuePrivate.h"
#include "JSCallbackObject.h"
#include "JSRetainPtr.h"
#include <wtf/glib/GUniquePtr.h>
#include <wtf/glib/WTFGType.h>

/**
 * JSCClass:
 * @short_description: JavaScript custom class
 * @title: JSCClass
 * @see_also: JSCContext
 *
 * A JSSClass represents a custom JavaScript class registered by the user in a #JSCContext.
 * It allows to create new JavaScripts objects whose instances are created by the user using
 * this API.
 * It's possible to add constructors, properties and methods for a JSSClass by providing
 * #GCallback<!-- -->s to implement them.
 */

enum {
    PROP_0,

    PROP_CONTEXT,
    PROP_NAME,
    PROP_PARENT
};

typedef struct _JSCClassPrivate {
    JSGlobalContextRef context;
    CString name;
    JSClassRef jsClass;
    JSCClassVTable* vtable;
    GDestroyNotify destroyFunction;
    JSCClass* parentClass;
    JSC::Weak<JSC::JSObject> prototype;
} JSCClassPrivate;

#if !ENABLE(2022_GLIB_API)
struct _JSCClassClass {
    GObjectClass parent_class;
};
#endif

WEBKIT_DEFINE_FINAL_TYPE(JSCClass, jsc_class, G_TYPE_OBJECT, GObject)

class VTableExceptionHandler {
public:
    VTableExceptionHandler(JSCContext* context, JSValueRef* exception)
        : m_context(context)
        , m_exception(exception)
        , m_savedException(exception ? jsc_context_get_exception(m_context) : nullptr)
    {
    }

    ~VTableExceptionHandler()
    {
        if (!m_exception)
            return;

        auto* exception = jsc_context_get_exception(m_context);
        if (m_savedException.get() == exception)
            return;

        *m_exception = jscExceptionGetJSValue(exception);
        if (m_savedException)
            jsc_context_throw_exception(m_context, m_savedException.get());
        else
            jsc_context_clear_exception(m_context);
    }

private:
    JSCContext* m_context { nullptr };
    JSValueRef* m_exception { nullptr };
    GRefPtr<JSCException> m_savedException;
};

static bool isWrappedObject(JSC::JSObject* jsObject)
{
    if (jsObject->isGlobalObject())
        return jsObject->inherits<JSC::JSCallbackObject<JSC::JSAPIWrapperGlobalObject>>();
    return jsObject->inherits<JSC::JSCallbackObject<JSC::JSAPIWrapperObject>>();
}

static JSClassRef wrappedObjectClass(JSC::JSObject* jsObject)
{
    ASSERT(isWrappedObject(jsObject));
    if (jsObject->isGlobalObject())
        return JSC::jsCast<JSC::JSCallbackObject<JSC::JSAPIWrapperGlobalObject>*>(jsObject)->classRef();
    return JSC::jsCast<JSC::JSCallbackObject<JSC::JSAPIWrapperObject>*>(jsObject)->classRef();
}

static GRefPtr<JSCContext> jscContextForObject(JSC::JSObject* jsObject)
{
    ASSERT(isWrappedObject(jsObject));
    JSC::JSGlobalObject* globalObject = jsObject->globalObject();
    if (jsObject->isGlobalObject()) {
        if (auto* globalScopeExtension = globalObject->globalScopeExtension())
            globalObject = JSC::JSScope::objectAtScope(globalScopeExtension)->globalObject();
    }
    return jscContextGetOrCreate(toGlobalRef(globalObject));
}

static JSValueRef getProperty(JSContextRef callerContext, JSObjectRef object, JSStringRef propertyName, JSValueRef* exception)
{
    JSC::JSLockHolder locker(toJS(callerContext));
    auto* jsObject = toJS(object);
    if (!isWrappedObject(jsObject))
        return nullptr;

    auto context = jscContextForObject(jsObject);
    gpointer instance = jscContextWrappedObject(context.get(), object);
    if (!instance)
        return nullptr;

    VTableExceptionHandler exceptionHandler(context.get(), exception);

    RefPtr jsClass = wrappedObjectClass(jsObject);
    for (auto* jscClass = jscContextGetRegisteredClass(context.get(), jsClass.get()); jscClass; jscClass = jscClass->priv->parentClass) {
        if (!jscClass->priv->vtable)
            continue;

        if (auto* getPropertyFunction = jscClass->priv->vtable->get_property) {
            if (GRefPtr<JSCValue> value = adoptGRef(getPropertyFunction(jscClass, context.get(), instance, propertyName->string().utf8().data())))
                return jscValueGetJSValue(value.get());
        }
    }
    return nullptr;
}

static bool setProperty(JSContextRef callerContext, JSObjectRef object, JSStringRef propertyName, JSValueRef value, JSValueRef* exception)
{
    JSC::JSLockHolder locker(toJS(callerContext));
    auto* jsObject = toJS(object);
    if (!isWrappedObject(jsObject))
        return false;

    auto context = jscContextForObject(jsObject);
    gpointer instance = jscContextWrappedObject(context.get(), object);
    if (!instance)
        return false;

    VTableExceptionHandler exceptionHandler(context.get(), exception);

    GRefPtr<JSCValue> propertyValue;
    RefPtr jsClass = wrappedObjectClass(jsObject);
    for (auto* jscClass = jscContextGetRegisteredClass(context.get(), jsClass.get()); jscClass; jscClass = jscClass->priv->parentClass) {
        if (!jscClass->priv->vtable)
            continue;

        if (auto* setPropertyFunction = jscClass->priv->vtable->set_property) {
            if (!propertyValue)
                propertyValue = jscContextGetOrCreateValue(context.get(), value);
            if (setPropertyFunction(jscClass, context.get(), instance, propertyName->string().utf8().data(), propertyValue.get()))
                return true;
        }
    }
    return false;
}

static bool hasProperty(JSContextRef callerContext, JSObjectRef object, JSStringRef propertyName)
{
    JSC::JSLockHolder locker(toJS(callerContext));
    auto* jsObject = toJS(object);
    if (!isWrappedObject(jsObject))
        return false;

    auto context = jscContextForObject(jsObject);
    gpointer instance = jscContextWrappedObject(context.get(), object);
    if (!instance)
        return false;

    RefPtr jsClass = wrappedObjectClass(jsObject);
    for (auto* jscClass = jscContextGetRegisteredClass(context.get(), jsClass.get()); jscClass; jscClass = jscClass->priv->parentClass) {
        if (!jscClass->priv->vtable)
            continue;

        if (auto* hasPropertyFunction = jscClass->priv->vtable->has_property) {
            if (hasPropertyFunction(jscClass, context.get(), instance, propertyName->string().utf8().data()))
                return true;
        }
    }

    return false;
}

static bool deleteProperty(JSContextRef callerContext, JSObjectRef object, JSStringRef propertyName, JSValueRef* exception)
{
    JSC::JSLockHolder locker(toJS(callerContext));
    auto* jsObject = toJS(object);
    if (!isWrappedObject(jsObject))
        return false;

    auto context = jscContextForObject(jsObject);
    gpointer instance = jscContextWrappedObject(context.get(), object);
    if (!instance)
        return false;

    VTableExceptionHandler exceptionHandler(context.get(), exception);

    RefPtr jsClass = wrappedObjectClass(jsObject);
    for (auto* jscClass = jscContextGetRegisteredClass(context.get(), jsClass.get()); jscClass; jscClass = jscClass->priv->parentClass) {
        if (!jscClass->priv->vtable)
            continue;

        if (auto* deletePropertyFunction = jscClass->priv->vtable->delete_property) {
            if (deletePropertyFunction(jscClass, context.get(), instance, propertyName->string().utf8().data()))
                return true;
        }
    }
    return false;
}

static void getPropertyNames(JSContextRef callerContext, JSObjectRef object, JSPropertyNameAccumulatorRef propertyNames)
{
    JSC::JSLockHolder locker(toJS(callerContext));
    auto* jsObject = toJS(object);
    if (!isWrappedObject(jsObject))
        return;

    auto context = jscContextForObject(jsObject);
    gpointer instance = jscContextWrappedObject(context.get(), object);
    if (!instance)
        return;

    RefPtr jsClass = wrappedObjectClass(jsObject);
    for (auto* jscClass = jscContextGetRegisteredClass(context.get(), jsClass.get()); jscClass; jscClass = jscClass->priv->parentClass) {
        if (!jscClass->priv->vtable)
            continue;

        if (auto* enumeratePropertiesFunction = jscClass->priv->vtable->enumerate_properties) {
            GUniquePtr<char*> properties(enumeratePropertiesFunction(jscClass, context.get(), instance));
            if (properties) {
                unsigned i = 0;
                WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN // GLib port
                while (const auto* name = properties.get()[i++]) {
                    JSRetainPtr<JSStringRef> propertyName(Adopt, JSStringCreateWithUTF8CString(name));
                    JSPropertyNameAccumulatorAddName(propertyNames, propertyName.get());
                }
                WTF_ALLOW_UNSAFE_BUFFER_USAGE_END
            }
        }
    }
}

static void jscClassGetProperty(GObject* object, guint propID, GValue* value, GParamSpec* paramSpec)
{
    JSCClass* jscClass = JSC_CLASS(object);

    switch (propID) {
    case PROP_NAME:
        g_value_set_string(value, jscClass->priv->name.data());
        break;
    case PROP_PARENT:
        g_value_set_object(value, jscClass->priv->parentClass);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propID, paramSpec);
    }
}

static void jscClassSetProperty(GObject* object, guint propID, const GValue* value, GParamSpec* paramSpec)
{
    JSCClass* jscClass = JSC_CLASS(object);

    switch (propID) {
    case PROP_CONTEXT:
        jscClass->priv->context = jscContextGetJSContext(JSC_CONTEXT(g_value_get_object(value)));
        break;
    case PROP_NAME:
        jscClass->priv->name = g_value_get_string(value);
        break;
    case PROP_PARENT:
        if (auto* parent = g_value_get_object(value))
            jscClass->priv->parentClass = JSC_CLASS(parent);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propID, paramSpec);
    }
}

static void jscClassDispose(GObject* object)
{
    JSCClass* jscClass = JSC_CLASS(object);
    if (jscClass->priv->jsClass) {
        JSClassRelease(jscClass->priv->jsClass);
        jscClass->priv->jsClass = nullptr;
    }

    G_OBJECT_CLASS(jsc_class_parent_class)->dispose(object);
}

static void jsc_class_class_init(JSCClassClass* klass)
{
    GObjectClass* objClass = G_OBJECT_CLASS(klass);
    objClass->dispose = jscClassDispose;
    objClass->get_property = jscClassGetProperty;
    objClass->set_property = jscClassSetProperty;

    /**
     * JSCClass:context:
     *
     * The #JSCContext in which the class was registered.
     */
    g_object_class_install_property(objClass,
        PROP_CONTEXT,
        g_param_spec_object(
            "context",
            nullptr, nullptr,
            JSC_TYPE_CONTEXT,
            static_cast<GParamFlags>(WEBKIT_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY)));

    /**
     * JSCClass:name:
     *
     * The name of the class.
     */
    g_object_class_install_property(objClass,
        PROP_NAME,
        g_param_spec_string(
            "name",
            nullptr, nullptr,
            nullptr,
            static_cast<GParamFlags>(WEBKIT_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)));

    /**
     * JSCClass:parent:
     *
     * The parent class or %NULL in case of final classes.
     */
    g_object_class_install_property(objClass,
        PROP_PARENT,
        g_param_spec_object(
            "parent",
            nullptr, nullptr,
            JSC_TYPE_CLASS,
            static_cast<GParamFlags>(WEBKIT_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)));
}

/**
 * JSCClassGetPropertyFunction:
 * @jsc_class: a #JSCClass
 * @context: a #JSCContext
 * @instance: the @jsc_class instance
 * @name: the property name
 *
 * The type of get_property in #JSCClassVTable. This is only required when you need to handle
 * external properties not added to the prototype.
 *
 * Returns: (transfer full) (nullable): a #JSCValue or %NULL to forward the request to
 *    the parent class or prototype chain
 */

/**
 * JSCClassSetPropertyFunction:
 * @jsc_class: a #JSCClass
 * @context: a #JSCContext
 * @instance: the @jsc_class instance
 * @name: the property name
 * @value: the #JSCValue to set
 *
 * The type of set_property in #JSCClassVTable. This is only required when you need to handle
 * external properties not added to the prototype.
 *
 * Returns: %TRUE if handled or %FALSE to forward the request to the parent class or prototype chain.
 */

/**
 * JSCClassHasPropertyFunction:
 * @jsc_class: a #JSCClass
 * @context: a #JSCContext
 * @instance: the @jsc_class instance
 * @name: the property name
 *
 * The type of has_property in #JSCClassVTable. This is only required when you need to handle
 * external properties not added to the prototype.
 *
 * Returns: %TRUE if @instance has a property with @name or %FALSE to forward the request
 *    to the parent class or prototype chain.
 */

/**
 * JSCClassDeletePropertyFunction:
 * @jsc_class: a #JSCClass
 * @context: a #JSCContext
 * @instance: the @jsc_class instance
 * @name: the property name
 *
 * The type of delete_property in #JSCClassVTable. This is only required when you need to handle
 * external properties not added to the prototype.
 *
 * Returns: %TRUE if handled or %FALSE to to forward the request to the parent class or prototype chain.
 */

/**
 * JSCClassEnumeratePropertiesFunction:
 * @jsc_class: a #JSCClass
 * @context: a #JSCContext
 * @instance: the @jsc_class instance
 *
 * The type of enumerate_properties in #JSCClassVTable. This is only required when you need to handle
 * external properties not added to the prototype.
 *
 * Returns: (array zero-terminated=1) (transfer full) (nullable): a %NULL-terminated array of strings
 *    containing the property names, or %NULL if @instance doesn't have enumerable properties.
 */

/**
 * JSCClassVTable:
 * @get_property: a #JSCClassGetPropertyFunction for getting a property.
 * @set_property: a #JSCClassSetPropertyFunction for setting a property.
 * @has_property: a #JSCClassHasPropertyFunction for querying a property.
 * @delete_property: a #JSCClassDeletePropertyFunction for deleting a property.
 * @enumerate_properties: a #JSCClassEnumeratePropertiesFunction for enumerating properties.
 *
 * Virtual table for a JSCClass. This can be optionally used when registering a #JSCClass in a #JSCContext
 * to provide a custom implementation for the class. All virtual functions are optional and can be set to
 * %NULL to fallback to the default implementation.
 */

GRefPtr<JSCClass> jscClassCreate(JSCContext* context, const char* name, JSCClass* parentClass, JSCClassVTable* vtable, GDestroyNotify destroyFunction)
{
    JSC::JSLockHolder locker(toJS(jscContextGetJSContext(context))->vm());
    GRefPtr<JSCClass> jscClass = adoptGRef(JSC_CLASS(g_object_new(JSC_TYPE_CLASS, "context", context, "name", name, "parent", parentClass, nullptr)));

    JSCClassPrivate* priv = jscClass->priv;
    priv->vtable = vtable;
    priv->destroyFunction = destroyFunction;

    JSClassDefinition definition = kJSClassDefinitionEmpty;
    definition.className = priv->name.data();

#define SET_IMPL_IF_NEEDED(definitionFunc, vtableFunc) \
    for (auto* klass = jscClass.get(); klass; klass = klass->priv->parentClass) { \
        if (klass->priv->vtable && klass->priv->vtable->vtableFunc) { \
            definition.definitionFunc = definitionFunc; \
            break; \
        } \
    }

    SET_IMPL_IF_NEEDED(getProperty, get_property);
    SET_IMPL_IF_NEEDED(setProperty, set_property);
    SET_IMPL_IF_NEEDED(hasProperty, has_property);
    SET_IMPL_IF_NEEDED(deleteProperty, delete_property);
    SET_IMPL_IF_NEEDED(getPropertyNames, enumerate_properties);

#undef SET_IMPL_IF_NEEDED

    priv->jsClass = JSClassCreate(&definition);

    GUniquePtr<char> prototypeName(g_strdup_printf("%sPrototype", priv->name.data()));
    JSClassDefinition prototypeDefinition = kJSClassDefinitionEmpty;
    prototypeDefinition.className = prototypeName.get();
    RefPtr prototypeClass = JSClassCreate(&prototypeDefinition);
    priv->prototype = jscContextGetOrCreateJSWrapper(context, prototypeClass.get());
    JSClassRelease(prototypeClass.get());

    if (priv->parentClass)
        JSObjectSetPrototype(jscContextGetJSContext(context), toRef(priv->prototype.get()), toRef(priv->parentClass->priv->prototype.get()));
    return jscClass;
}

JSClassRef jscClassGetJSClass(JSCClass* jscClass)
{
    return jscClass->priv->jsClass;
}

JSC::JSObject* jscClassGetOrCreateJSWrapper(JSCClass* jscClass, JSCContext* context, gpointer wrappedObject)
{
    JSCClassPrivate* priv = jscClass->priv;
    return jscContextGetOrCreateJSWrapper(context, priv->jsClass, toRef(priv->prototype.get()), wrappedObject, priv->destroyFunction);
}

JSGlobalContextRef jscClassCreateContextWithJSWrapper(JSCClass* jscClass, JSCContext* context, gpointer wrappedObject)
{
    JSCClassPrivate* priv = jscClass->priv;
    return jscContextCreateContextWithJSWrapper(context, priv->jsClass, toRef(priv->prototype.get()), wrappedObject, priv->destroyFunction);
}

void jscClassInvalidate(JSCClass* jscClass)
{
    jscClass->priv->context = nullptr;
}

/**
 * jsc_class_get_name:
 * @jsc_class: a @JSCClass
 *
 * Get the class name of @jsc_class
 *
 * Returns: (transfer none): the name of @jsc_class
 */
const char* jsc_class_get_name(JSCClass* jscClass)
{
    g_return_val_if_fail(JSC_IS_CLASS(jscClass), nullptr);

    return jscClass->priv->name.data();
}

/**
 * jsc_class_get_parent:
 * @jsc_class: a @JSCClass
 *
 * Get the parent class of @jsc_class
 *
 * Returns: (transfer none): the parent class of @jsc_class
 */
JSCClass* jsc_class_get_parent(JSCClass* jscClass)
{
    g_return_val_if_fail(JSC_IS_CLASS(jscClass), nullptr);

    return jscClass->priv->parentClass;
}

static GRefPtr<JSCValue> jscClassCreateConstructor(JSCClass* jscClass, const char* name, GCallback callback, gpointer userData, GDestroyNotify destroyNotify, GType returnType, std::optional<Vector<GType>>&& parameters)
{
    // If the constructor doesn't have arguments, we need to swap the fake instance and user data to ensure
    // user data is the first parameter and fake instance ignored.
    GRefPtr<GClosure> closure;
    if (parameters && parameters->isEmpty() && userData)
        closure = adoptGRef(g_cclosure_new_swap(callback, userData, reinterpret_cast<GClosureNotify>(reinterpret_cast<GCallback>(destroyNotify))));
    else
        closure = adoptGRef(g_cclosure_new(callback, userData, reinterpret_cast<GClosureNotify>(reinterpret_cast<GCallback>(destroyNotify))));
    JSCClassPrivate* priv = jscClass->priv;
    JSC::JSGlobalObject* globalObject = toJS(priv->context);
    Ref vm = globalObject->vm();
    JSC::JSLockHolder locker(vm);
    auto* functionObject = JSC::JSCCallbackFunction::create(vm, globalObject, String::fromUTF8(name),
        JSC::JSCCallbackFunction::Type::Constructor, jscClass, WTFMove(closure), returnType, WTFMove(parameters));
    auto context = jscContextGetOrCreate(priv->context);
    auto constructor = jscContextGetOrCreateValue(context.get(), toRef(functionObject));
    GRefPtr<JSCValue> prototype = jscContextGetOrCreateValue(context.get(), toRef(priv->prototype.get()));
    auto nonEnumerable = static_cast<JSCValuePropertyFlags>(JSC_VALUE_PROPERTY_CONFIGURABLE | JSC_VALUE_PROPERTY_WRITABLE);
    jsc_value_object_define_property_data(constructor.get(), "prototype", nonEnumerable, prototype.get());
    jsc_value_object_define_property_data(prototype.get(), "constructor", nonEnumerable, constructor.get());
    return constructor;
}

/**
 * jsc_class_add_constructor: (skip)
 * @jsc_class: a #JSCClass
 * @name: (nullable): the constructor name or %NULL
 * @callback: (scope async): a #GCallback to be called to create an instance of @jsc_class
 * @user_data: user data to pass to @callback
 * @destroy_notify: (nullable): destroy notifier for @user_data
 * @return_type: the #GType of the constructor return value
 * @n_params: the number of parameter types to follow or 0 if constructor doesn't receive parameters.
 * @...: a list of #GType<!-- -->s, one for each parameter.
 *
 * Add a constructor to @jsc_class. If @name is %NULL, the class name will be used. When <function>new</function>
 * is used with the constructor or jsc_value_constructor_call() is called, @callback is invoked receiving the
 * parameters and @user_data as the last parameter. When the constructor object is cleared in the #JSCClass context,
 * @destroy_notify is called with @user_data as parameter.
 *
 * This function creates the constructor, which needs to be added to an object as a property to be able to use it. Use
 * jsc_context_set_value() to make the constructor available in the global object.
 *
 * Note that the value returned by @callback is adopted by @jsc_class, and the #GDestroyNotify passed to
 * jsc_context_register_class() is responsible for disposing of it.
 *
 * Returns: (transfer full): a #JSCValue representing the class constructor.
 */
JSCValue* jsc_class_add_constructor(JSCClass* jscClass, const char* name, GCallback callback, gpointer userData, GDestroyNotify destroyNotify, GType returnType, unsigned paramCount, ...)
{
    g_return_val_if_fail(JSC_IS_CLASS(jscClass), nullptr);
    g_return_val_if_fail(callback, nullptr);

    JSCClassPrivate* priv = jscClass->priv;
    g_return_val_if_fail(priv->context, nullptr);

    if (!name)
        name = priv->name.data();

    va_list args;
    va_start(args, paramCount);
    Vector<GType> parameters(paramCount, [&](size_t) -> GType {
        return va_arg(args, GType);
    });
    va_end(args);

    return jscClassCreateConstructor(jscClass, name ? name : priv->name.data(), callback, userData, destroyNotify, returnType, WTFMove(parameters)).leakRef();

}

/**
 * jsc_class_add_constructorv: (rename-to jsc_class_add_constructor)
 * @jsc_class: a #JSCClass
 * @name: (nullable): the constructor name or %NULL
 * @callback: (scope async): a #GCallback to be called to create an instance of @jsc_class
 * @user_data: user data to pass to @callback
 * @destroy_notify: (nullable): destroy notifier for @user_data
 * @return_type: the #GType of the constructor return value
 * @n_parameters: the number of parameters
 * @parameter_types: (nullable) (array length=n_parameters) (element-type GType): a list of #GType<!-- -->s, one for each parameter, or %NULL
 *
 * Add a constructor to @jsc_class. If @name is %NULL, the class name will be used. When <function>new</function>
 * is used with the constructor or jsc_value_constructor_call() is called, @callback is invoked receiving the
 * parameters and @user_data as the last parameter. When the constructor object is cleared in the #JSCClass context,
 * @destroy_notify is called with @user_data as parameter.
 *
 * This function creates the constructor, which needs to be added to an object as a property to be able to use it. Use
 * jsc_context_set_value() to make the constructor available in the global object.
 *
 * Note that the value returned by @callback is adopted by @jsc_class, and the #GDestroyNotify passed to
 * jsc_context_register_class() is responsible for disposing of it.
 *
 * Returns: (transfer full): a #JSCValue representing the class constructor.
 */
JSCValue* jsc_class_add_constructorv(JSCClass* jscClass, const char* name, GCallback callback, gpointer userData, GDestroyNotify destroyNotify, GType returnType, unsigned parametersCount, GType* parameterTypes)
{
    g_return_val_if_fail(JSC_IS_CLASS(jscClass), nullptr);
    g_return_val_if_fail(callback, nullptr);
    g_return_val_if_fail(!parametersCount || parameterTypes, nullptr);

    JSCClassPrivate* priv = jscClass->priv;
    g_return_val_if_fail(priv->context, nullptr);

    if (!name)
        name = priv->name.data();

    Vector<GType> parameters(parametersCount, [&](size_t i) -> GType {
        WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN // GLib port
        return parameterTypes[i];
        WTF_ALLOW_UNSAFE_BUFFER_USAGE_END
    });

    return jscClassCreateConstructor(jscClass, name ? name : priv->name.data(), callback, userData, destroyNotify, returnType, WTFMove(parameters)).leakRef();
}

/**
 * jsc_class_add_constructor_variadic:
 * @jsc_class: a #JSCClass
 * @name: (nullable): the constructor name or %NULL
 * @callback: (scope async): a #GCallback to be called to create an instance of @jsc_class
 * @user_data: user data to pass to @callback
 * @destroy_notify: (nullable): destroy notifier for @user_data
 * @return_type: the #GType of the constructor return value
 *
 * Add a constructor to @jsc_class. If @name is %NULL, the class name will be used. When <function>new</function>
 * is used with the constructor or jsc_value_constructor_call() is called, @callback is invoked receiving
 * a #GPtrArray of #JSCValue<!-- -->s as arguments and @user_data as the last parameter. When the constructor object
 * is cleared in the #JSCClass context, @destroy_notify is called with @user_data as parameter.
 *
 * This function creates the constructor, which needs to be added to an object as a property to be able to use it. Use
 * jsc_context_set_value() to make the constructor available in the global object.
 *
 * Note that the value returned by @callback is adopted by @jsc_class, and the #GDestroyNotify passed to
 * jsc_context_register_class() is responsible for disposing of it.
 *
 * Returns: (transfer full): a #JSCValue representing the class constructor.
 */
JSCValue* jsc_class_add_constructor_variadic(JSCClass* jscClass, const char* name, GCallback callback, gpointer userData, GDestroyNotify destroyNotify, GType returnType)
{
    g_return_val_if_fail(JSC_IS_CLASS(jscClass), nullptr);
    g_return_val_if_fail(callback, nullptr);

    JSCClassPrivate* priv = jscClass->priv;
    g_return_val_if_fail(jscClass->priv->context, nullptr);

    if (!name)
        name = priv->name.data();

    return jscClassCreateConstructor(jscClass, name ? name : priv->name.data(), callback, userData, destroyNotify, returnType, std::nullopt).leakRef();
}

static void jscClassAddMethod(JSCClass* jscClass, const char* name, GCallback callback, gpointer userData, GDestroyNotify destroyNotify, GType returnType, std::optional<Vector<GType>>&& parameters)
{
    JSCClassPrivate* priv = jscClass->priv;
    GRefPtr<GClosure> closure = adoptGRef(g_cclosure_new(callback, userData, reinterpret_cast<GClosureNotify>(reinterpret_cast<GCallback>(destroyNotify))));
    JSC::JSGlobalObject* globalObject = toJS(priv->context);
    Ref vm = globalObject->vm();
    JSC::JSLockHolder locker(vm);
    auto* functionObject = toRef(JSC::JSCCallbackFunction::create(vm, globalObject, String::fromUTF8(name),
        JSC::JSCCallbackFunction::Type::Method, jscClass, WTFMove(closure), returnType, WTFMove(parameters)));
    auto context = jscContextGetOrCreate(priv->context);
    auto method = jscContextGetOrCreateValue(context.get(), functionObject);
    GRefPtr<JSCValue> prototype = jscContextGetOrCreateValue(context.get(), toRef(priv->prototype.get()));
    auto nonEnumerable = static_cast<JSCValuePropertyFlags>(JSC_VALUE_PROPERTY_CONFIGURABLE | JSC_VALUE_PROPERTY_WRITABLE);
    jsc_value_object_define_property_data(prototype.get(), name, nonEnumerable, method.get());
}

/**
 * jsc_class_add_method: (skip)
 * @jsc_class: a #JSCClass
 * @name: the method name
 * @callback: (scope async): a #GCallback to be called to invoke method @name of @jsc_class
 * @user_data: user data to pass to @callback
 * @destroy_notify: (nullable): destroy notifier for @user_data
 * @return_type: the #GType of the method return value, or %G_TYPE_NONE if the method is void.
 * @n_params: the number of parameter types to follow or 0 if the method doesn't receive parameters.
 * @...: a list of #GType<!-- -->s, one for each parameter.
 *
 * Add method with @name to @jsc_class. When the method is called by JavaScript or jsc_value_object_invoke_method(),
 * @callback is called receiving the class instance as first parameter, followed by the method parameters and then
 * @user_data as last parameter. When the method is cleared in the #JSCClass context, @destroy_notify is called with
 * @user_data as parameter.
 *
 * Note that the value returned by @callback must be transfer full. In case of non-refcounted boxed types, you should use
 * %G_TYPE_POINTER instead of the actual boxed #GType to ensure that the instance owned by #JSCClass is used.
 * If you really want to return a new copy of the boxed type, use #JSC_TYPE_VALUE and return a #JSCValue created
 * with jsc_value_new_object() that receives the copy as the instance parameter.
 */
void jsc_class_add_method(JSCClass* jscClass, const char* name, GCallback callback, gpointer userData, GDestroyNotify destroyNotify, GType returnType, unsigned paramCount, ...)
{
    g_return_if_fail(JSC_IS_CLASS(jscClass));
    g_return_if_fail(name);
    g_return_if_fail(callback);
    g_return_if_fail(jscClass->priv->context);

    va_list args;
    va_start(args, paramCount);
    Vector<GType> parameters(paramCount, [&](size_t) -> GType {
            return va_arg(args, GType);
    });
    va_end(args);

    jscClassAddMethod(jscClass, name, callback, userData, destroyNotify, returnType, WTFMove(parameters));
}

/**
 * jsc_class_add_methodv: (rename-to jsc_class_add_method)
 * @jsc_class: a #JSCClass
 * @name: the method name
 * @callback: (scope async): a #GCallback to be called to invoke method @name of @jsc_class
 * @user_data: user data to pass to @callback
 * @destroy_notify: (nullable): destroy notifier for @user_data
 * @return_type: the #GType of the method return value, or %G_TYPE_NONE if the method is void.
 * @n_parameters: the number of parameter types to follow or 0 if the method doesn't receive parameters.
 * @parameter_types: (nullable) (array length=n_parameters) (element-type GType): a list of #GType<!-- -->s, one for each parameter, or %NULL
 *
 * Add method with @name to @jsc_class. When the method is called by JavaScript or jsc_value_object_invoke_method(),
 * @callback is called receiving the class instance as first parameter, followed by the method parameters and then
 * @user_data as last parameter. When the method is cleared in the #JSCClass context, @destroy_notify is called with
 * @user_data as parameter.
 *
 * Note that the value returned by @callback must be transfer full. In case of non-refcounted boxed types, you should use
 * %G_TYPE_POINTER instead of the actual boxed #GType to ensure that the instance owned by #JSCClass is used.
 * If you really want to return a new copy of the boxed type, use #JSC_TYPE_VALUE and return a #JSCValue created
 * with jsc_value_new_object() that receives the copy as the instance parameter.
 */
void jsc_class_add_methodv(JSCClass* jscClass, const char* name, GCallback callback, gpointer userData, GDestroyNotify destroyNotify, GType returnType, unsigned parametersCount, GType *parameterTypes)
{
    g_return_if_fail(JSC_IS_CLASS(jscClass));
    g_return_if_fail(name);
    g_return_if_fail(callback);
    g_return_if_fail(!parametersCount || parameterTypes);
    g_return_if_fail(jscClass->priv->context);

    Vector<GType> parameters(parametersCount, [&](size_t i) -> GType {
        WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN // GLib port
        return parameterTypes[i];
        WTF_ALLOW_UNSAFE_BUFFER_USAGE_END
    });

    jscClassAddMethod(jscClass, name, callback, userData, destroyNotify, returnType, WTFMove(parameters));
}

/**
 * jsc_class_add_method_variadic:
 * @jsc_class: a #JSCClass
 * @name: the method name
 * @callback: (scope async): a #GCallback to be called to invoke method @name of @jsc_class
 * @user_data: user data to pass to @callback
 * @destroy_notify: (nullable): destroy notifier for @user_data
 * @return_type: the #GType of the method return value, or %G_TYPE_NONE if the method is void.
 *
 * Add method with @name to @jsc_class. When the method is called by JavaScript or jsc_value_object_invoke_method(),
 * @callback is called receiving the class instance as first parameter, followed by a #GPtrArray of #JSCValue<!-- -->s
 * with the method arguments and then @user_data as last parameter. When the method is cleared in the #JSCClass context,
 * @destroy_notify is called with @user_data as parameter.
 *
 * Note that the value returned by @callback must be transfer full. In case of non-refcounted boxed types, you should use
 * %G_TYPE_POINTER instead of the actual boxed #GType to ensure that the instance owned by #JSCClass is used.
 * If you really want to return a new copy of the boxed type, use #JSC_TYPE_VALUE and return a #JSCValue created
 * with jsc_value_new_object() that receives the copy as the instance parameter.
 */
void jsc_class_add_method_variadic(JSCClass* jscClass, const char* name, GCallback callback, gpointer userData, GDestroyNotify destroyNotify, GType returnType)
{
    g_return_if_fail(JSC_IS_CLASS(jscClass));
    g_return_if_fail(name);
    g_return_if_fail(callback);
    g_return_if_fail(jscClass->priv->context);

    jscClassAddMethod(jscClass, name, callback, userData, destroyNotify, returnType, std::nullopt);
}

/**
 * jsc_class_add_property:
 * @jsc_class: a #JSCClass
 * @name: the property name
 * @property_type: the #GType of the property value
 * @getter: (scope async) (nullable): a #GCallback to be called to get the property value
 * @setter: (scope async) (nullable): a #GCallback to be called to set the property value
 * @user_data: user data to pass to @getter and @setter
 * @destroy_notify: (nullable): destroy notifier for @user_data
 *
 * Add a property with @name to @jsc_class. When the property value needs to be getted, @getter is called
 * receiving the the class instance as first parameter and @user_data as last parameter. When the property
 * value needs to be set, @setter is called receiving the the class instance as first parameter, followed
 * by the value to be set and then @user_data as the last parameter. When the property is cleared in the
 * #JSCClass context, @destroy_notify is called with @user_data as parameter.
 *
 * Note that the value returned by @getter must be transfer full. In case of non-refcounted boxed types, you should use
 * %G_TYPE_POINTER instead of the actual boxed #GType to ensure that the instance owned by #JSCClass is used.
 * If you really want to return a new copy of the boxed type, use #JSC_TYPE_VALUE and return a #JSCValue created
 * with jsc_value_new_object() that receives the copy as the instance parameter.
 */
void jsc_class_add_property(JSCClass* jscClass, const char* name, GType propertyType, GCallback getter, GCallback setter, gpointer userData, GDestroyNotify destroyNotify)
{
    g_return_if_fail(JSC_IS_CLASS(jscClass));
    g_return_if_fail(name);
    g_return_if_fail(propertyType != G_TYPE_INVALID && propertyType != G_TYPE_NONE);
    g_return_if_fail(getter || setter);

    JSCClassPrivate* priv = jscClass->priv;
    g_return_if_fail(priv->context);

    auto context = jscContextGetOrCreate(priv->context);
    GRefPtr<JSCValue> prototype = jscContextGetOrCreateValue(context.get(), toRef(priv->prototype.get()));
    jscValueAddPropertyAccessor(prototype.get(), name, propertyType, getter, setter, userData, destroyNotify);
}
