/*
 * Copyright (C) 2019 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 "JSCOptions.h"

#include "Options.h"
#include <glib/gi18n-lib.h>
#include <wtf/Vector.h>
#include <wtf/glib/GUniquePtr.h>

/**
 * SECTION: JSCOptions
 * @short_description: JavaScript options
 * @title: JSCOptions
 *
 * JavaScript options allow changing the behavior of the JavaScript engine.
 * They affect the way the engine works, so the options must be set
 * at the very beginning of the program execution, before any other JavaScript
 * API call. Most of the options are only useful for testing and debugging.
 * Only a few of them are documented; you can use the undocumented options at
 * your own risk. (You can find the list of options in the WebKit source code).
 *
 * The API allows to set and get any option using the types defined in #JSCOptionType.
 * You can also iterate all the available options using jsc_options_foreach() and
 * passing a #JSCOptionsFunc callback. If your application uses #GOptionContext to handle
 * command line arguments, you can easily integrate the JSCOptions by adding the
 * #GOptionGroup returned by jsc_options_get_option_group().
 *
 * Since: 2.24
 */

using namespace JSC;

using int32 = int32_t;
using size = size_t;

static bool valueFromGValue(const GValue* gValue, bool& value)
{
    value = g_value_get_boolean(gValue);
    return true;
}

static void valueToGValue(bool value, GValue* gValue)
{
    g_value_set_boolean(gValue, value);
}

static bool valueFromGValue(const GValue* gValue, int32_t& value)
{
    value = g_value_get_int(gValue);
    return true;
}

static void valueToGValue(int32_t value, GValue* gValue)
{
    g_value_set_int(gValue, value);
}

#if CPU(ADDRESS64)
static bool valueFromGValue(const GValue* gValue, unsigned& value)
{
    value = g_value_get_uint(gValue);
    return true;
}

static void valueToGValue(unsigned value, GValue* gValue)
{
    g_value_set_uint(gValue, value);
}
#endif

static bool valueFromGValue(const GValue* gValue, size_t& value)
{
    value = GPOINTER_TO_SIZE(g_value_get_pointer(gValue));
    return true;
}

static void valueToGValue(size_t value, GValue* gValue)
{
    g_value_set_pointer(gValue, GSIZE_TO_POINTER(value));
}

static bool valueFromGValue(const GValue* gValue, const char*& value)
{
    value = g_value_dup_string(gValue);
    return true;
}

static void valueToGValue(const char* value, GValue* gValue)
{
    g_value_set_string(gValue, value);
}

static bool valueFromGValue(const GValue* gValue, double& value)
{
    value = g_value_get_double(gValue);
    return true;
}

static void valueToGValue(double value, GValue* gValue)
{
    g_value_set_double(gValue, value);
}

static bool valueFromGValue(const GValue* gValue, OptionRange& value)
{
    return value.init(g_value_get_string(gValue) ? g_value_get_string(gValue) : "<null>");
}

static void valueToGValue(const OptionRange& value, GValue* gValue)
{
    const char* rangeString = value.rangeString();
    g_value_set_string(gValue, !g_strcmp0(rangeString, "<null>") ? nullptr : rangeString);
}

static bool valueFromGValue(const GValue* gValue, GCLogging::Level& value)
{
    switch (g_value_get_uint(gValue)) {
    case 0:
        value = GCLogging::Level::None;
        return true;
    case 1:
        value = GCLogging::Level::Basic;
        return true;
    case 2:
        value = GCLogging::Level::Verbose;
        return true;
    default:
        break;
    }

    return false;
}

static void valueToGValue(GCLogging::Level value, GValue* gValue)
{
    switch (value) {
    case GCLogging::Level::None:
        g_value_set_uint(gValue, 0);
        break;
    case GCLogging::Level::Basic:
        g_value_set_uint(gValue, 1);
        break;
    case GCLogging::Level::Verbose:
        g_value_set_uint(gValue, 2);
        break;
    }
}

static bool valueFromGValue(const GValue* gValue, OSLogType& value)
{
    unsigned optionValue = g_value_get_uint(gValue);
    if (optionValue > static_cast<unsigned>(OSLogType::Fault))
        return false;
    value = static_cast<OSLogType>(optionValue);
    return true;
}

static void valueToGValue(OSLogType value, GValue* gValue)
{
    g_value_set_uint(gValue, static_cast<unsigned>(value));
}

static gboolean jscOptionsSetValue(const char* option, const GValue* value)
{
    Options::AllowUnfinalizedAccessScope scope;

#define SET_OPTION_VALUE(type_, name_, defaultValue_, availability_, description_) \
    if (!g_strcmp0(#name_, option)) {                                   \
        OptionsStorage::type_ valueToSet;                                  \
        if (!valueFromGValue(value, valueToSet))                        \
            return FALSE;                                               \
        Options::name_() = valueToSet;                                  \
        return TRUE;                                                    \
    }

    Options::initialize();
    FOR_EACH_JSC_OPTION(SET_OPTION_VALUE)
#undef SET_OPTION_VALUE

    return FALSE;
}

static gboolean jscOptionsGetValue(const char* option, GValue* value)
{
    Options::AllowUnfinalizedAccessScope scope;

#define GET_OPTION_VALUE(type_, name_, defaultValue_, availability_, description_) \
    if (!g_strcmp0(#name_, option)) {                                   \
        OptionsStorage::type_ valueToGet = Options::name_();               \
        valueToGValue(valueToGet, value);                               \
        return TRUE;                                                    \
    }

    Options::initialize();
    FOR_EACH_JSC_OPTION(GET_OPTION_VALUE)
#undef GET_OPTION_VALUE

    return FALSE;
}

/**
 * jsc_options_set_boolean:
 * @option: the option identifier
 * @value: the value to set
 *
 * Set @option as a #gboolean value.
 *
 * Returns: %TRUE if option was correctly set or %FALSE otherwise.
 *
 * Since: 2.24
 */
gboolean jsc_options_set_boolean(const char* option, gboolean value)
{
    g_return_val_if_fail(option, FALSE);

    GValue gValue = G_VALUE_INIT;
    g_value_init(&gValue, G_TYPE_BOOLEAN);
    g_value_set_boolean(&gValue, value);
    return jscOptionsSetValue(option, &gValue);
}

/**
 * jsc_options_get_boolean:
 * @option: the option identifier
 * @value: (out): return location for the option value
 *
 * Get @option as a #gboolean value.
 *
 * Returns: %TRUE if @value has been set or %FALSE if the option doesn't exist
 *
 * Since: 2.24
 */
gboolean jsc_options_get_boolean(const char* option, gboolean* value)
{
    g_return_val_if_fail(option, FALSE);
    g_return_val_if_fail(value, FALSE);

    GValue gValue = G_VALUE_INIT;
    g_value_init(&gValue, G_TYPE_BOOLEAN);
    if (!jscOptionsGetValue(option, &gValue))
        return FALSE;

    *value =  g_value_get_boolean(&gValue);
    return TRUE;
}

/**
 * jsc_options_set_int:
 * @option: the option identifier
 * @value: the value to set
 *
 * Set @option as a #gint value.
 *
 * Returns: %TRUE if option was correctly set or %FALSE otherwise.
 *
 * Since: 2.24
 */
gboolean jsc_options_set_int(const char* option, gint value)
{
    g_return_val_if_fail(option, FALSE);

    GValue gValue = G_VALUE_INIT;
    g_value_init(&gValue, G_TYPE_INT);
    g_value_set_int(&gValue, value);
    return jscOptionsSetValue(option, &gValue);
}

/**
 * jsc_options_get_int:
 * @option: the option identifier
 * @value: (out): return location for the option value
 *
 * Get @option as a #gint value.
 *
 * Returns: %TRUE if @value has been set or %FALSE if the option doesn't exist
 *
 * Since: 2.24
 */
gboolean jsc_options_get_int(const char* option, gint* value)
{
    g_return_val_if_fail(option, FALSE);
    g_return_val_if_fail(value, FALSE);

    GValue gValue = G_VALUE_INIT;
    g_value_init(&gValue, G_TYPE_INT);
    if (!jscOptionsGetValue(option, &gValue))
        return FALSE;

    *value = g_value_get_int(&gValue);
    return TRUE;
}

/**
 * jsc_options_set_uint:
 * @option: the option identifier
 * @value: the value to set
 *
 * Set @option as a #guint value.
 *
 * Returns: %TRUE if option was correctly set or %FALSE otherwise.
 *
 * Since: 2.24
 */
gboolean jsc_options_set_uint(const char* option, guint value)
{
    g_return_val_if_fail(option, FALSE);

    GValue gValue = G_VALUE_INIT;
    g_value_init(&gValue, G_TYPE_UINT);
    g_value_set_uint(&gValue, value);
    return jscOptionsSetValue(option, &gValue);
}

/**
 * jsc_options_get_uint:
 * @option: the option identifier
 * @value: (out): return location for the option value
 *
 * Get @option as a #guint value.
 *
 * Returns: %TRUE if @value has been set or %FALSE if the option doesn't exist
 *
 * Since: 2.24
 */
gboolean jsc_options_get_uint(const char* option, guint* value)
{
    g_return_val_if_fail(option, FALSE);
    g_return_val_if_fail(value, FALSE);

    GValue gValue = G_VALUE_INIT;
    g_value_init(&gValue, G_TYPE_UINT);
    if (!jscOptionsGetValue(option, &gValue))
        return FALSE;

    *value = g_value_get_uint(&gValue);
    return TRUE;
}

/**
 * jsc_options_set_size:
 * @option: the option identifier
 * @value: the value to set
 *
 * Set @option as a #gsize value.
 *
 * Returns: %TRUE if option was correctly set or %FALSE otherwise.
 *
 * Since: 2.24
 */
gboolean jsc_options_set_size(const char* option, gsize value)
{
    g_return_val_if_fail(option, FALSE);

    GValue gValue = G_VALUE_INIT;
    g_value_init(&gValue, G_TYPE_POINTER);
    g_value_set_pointer(&gValue, GSIZE_TO_POINTER(value));
    return jscOptionsSetValue(option, &gValue);
}

/**
 * jsc_options_get_size:
 * @option: the option identifier
 * @value: (out): return location for the option value
 *
 * Get @option as a #gsize value.
 *
 * Returns: %TRUE if @value has been set or %FALSE if the option doesn't exist
 *
 * Since: 2.24
 */
gboolean jsc_options_get_size(const char* option, gsize* value)
{
    g_return_val_if_fail(option, FALSE);
    g_return_val_if_fail(value, FALSE);

    GValue gValue = G_VALUE_INIT;
    g_value_init(&gValue, G_TYPE_POINTER);
    if (!jscOptionsGetValue(option, &gValue))
        return FALSE;

    *value = GPOINTER_TO_SIZE(g_value_get_pointer(&gValue));
    return TRUE;
}

/**
 * jsc_options_set_double:
 * @option: the option identifier
 * @value: the value to set
 *
 * Set @option as a #gdouble value.
 *
 * Returns: %TRUE if option was correctly set or %FALSE otherwise.
 *
 * Since: 2.24
 */
gboolean jsc_options_set_double(const char* option, gdouble value)
{
    g_return_val_if_fail(option, FALSE);

    GValue gValue = G_VALUE_INIT;
    g_value_init(&gValue, G_TYPE_DOUBLE);
    g_value_set_double(&gValue, value);
    return jscOptionsSetValue(option, &gValue);
}

/**
 * jsc_options_get_double:
 * @option: the option identifier
 * @value: (out): return location for the option value
 *
 * Get @option as a #gdouble value.
 *
 * Returns: %TRUE if @value has been set or %FALSE if the option doesn't exist
 *
 * Since: 2.24
 */
gboolean jsc_options_get_double(const char* option, gdouble* value)
{
    g_return_val_if_fail(option, FALSE);
    g_return_val_if_fail(value, FALSE);

    GValue gValue = G_VALUE_INIT;
    g_value_init(&gValue, G_TYPE_DOUBLE);
    if (!jscOptionsGetValue(option, &gValue))
        return FALSE;

    *value = g_value_get_double(&gValue);
    return TRUE;
}

/**
 * jsc_options_set_string:
 * @option: the option identifier
 * @value: the value to set
 *
 * Set @option as a string.
 *
 * Returns: %TRUE if option was correctly set or %FALSE otherwise.
 *
 * Since: 2.24
 */
gboolean jsc_options_set_string(const char* option, const char* value)
{
    g_return_val_if_fail(option, FALSE);

    GValue gValue = G_VALUE_INIT;
    g_value_init(&gValue, G_TYPE_STRING);
    g_value_set_string(&gValue, value);
    bool success = jscOptionsSetValue(option, &gValue);
    g_value_unset(&gValue);
    return success;
}

/**
 * jsc_options_get_string:
 * @option: the option identifier
 * @value: (out): return location for the option value
 *
 * Get @option as a string.
 *
 * Returns: %TRUE if @value has been set or %FALSE if the option doesn't exist
 *
 * Since: 2.24
 */
gboolean jsc_options_get_string(const char* option, char** value)
{
    g_return_val_if_fail(option, FALSE);
    g_return_val_if_fail(value, FALSE);

    GValue gValue = G_VALUE_INIT;
    g_value_init(&gValue, G_TYPE_STRING);
    if (!jscOptionsGetValue(option, &gValue))
        return FALSE;

    *value = g_value_dup_string(&gValue);
    g_value_unset(&gValue);
    return TRUE;
}

/**
 * jsc_options_set_range_string:
 * @option: the option identifier
 * @value: the value to set
 *
 * Set @option as a range string. The string must be in the
 * format <emphasis>[!]&lt;low&gt;[:&lt;high&gt;]</emphasis> where low and high are #guint values.
 * Values between low and high (both included) will be considered in
 * the range, unless <emphasis>!</emphasis> is used to invert the range.
 *
 * Returns: %TRUE if option was correctly set or %FALSE otherwise.
 *
 * Since: 2.24
 */
gboolean jsc_options_set_range_string(const char* option, const char* value)
{
    g_return_val_if_fail(option, FALSE);

    GValue gValue = G_VALUE_INIT;
    g_value_init(&gValue, G_TYPE_STRING);
    g_value_set_string(&gValue, value);
    bool success = jscOptionsSetValue(option, &gValue);
    g_value_unset(&gValue);
    return success;
}

/**
 * jsc_options_get_range_string:
 * @option: the option identifier
 * @value: (out): return location for the option value
 *
 * Get @option as a range string. The string must be in the
 * format <emphasis>[!]&lt;low&gt;[:&lt;high&gt;]</emphasis> where low and high are #guint values.
 * Values between low and high (both included) will be considered in
 * the range, unless <emphasis>!</emphasis> is used to invert the range.
 *
 * Returns: %TRUE if @value has been set or %FALSE if the option doesn't exist
 *
 * Since: 2.24
 */
gboolean jsc_options_get_range_string(const char* option, char** value)
{
    g_return_val_if_fail(option, FALSE);
    g_return_val_if_fail(value, FALSE);

    GValue gValue = G_VALUE_INIT;
    g_value_init(&gValue, G_TYPE_STRING);
    if (!jscOptionsGetValue(option, &gValue))
        return FALSE;

    *value = g_value_dup_string(&gValue);
    g_value_unset(&gValue);
    return TRUE;
}

static JSCOptionType jscOptionsType(bool)
{
    return JSC_OPTION_BOOLEAN;
}

static JSCOptionType jscOptionsType(int)
{
    return JSC_OPTION_INT;
}

#if CPU(ADDRESS64)
static JSCOptionType jscOptionsType(unsigned)
{
    return JSC_OPTION_UINT;
}
#endif

static JSCOptionType jscOptionsType(size_t)
{
    return JSC_OPTION_SIZE;
}

static JSCOptionType jscOptionsType(double)
{
    return JSC_OPTION_DOUBLE;
}

static JSCOptionType jscOptionsType(const char*)
{
    return JSC_OPTION_STRING;
}

static JSCOptionType jscOptionsType(const OptionRange&)
{
    return JSC_OPTION_RANGE_STRING;
}

static JSCOptionType jscOptionsType(const OSLogType&)
{
    return JSC_OPTION_UINT;
}

/**
 * JSCOptionType:
 * @JSC_OPTION_BOOLEAN: A #gboolean option type.
 * @JSC_OPTION_INT: A #gint option type.
 * @JSC_OPTION_UINT: A #guint option type.
 * @JSC_OPTION_SIZE: A #gsize options type.
 * @JSC_OPTION_DOUBLE: A #gdouble options type.
 * @JSC_OPTION_STRING: A string option type.
 * @JSC_OPTION_RANGE_STRING: A range string option type.
 *
 * Enum values for options types.
 *
 * Since: 2.24
 */

/**
 * JSCOptionsFunc:
 * @option: the option name
 * @type: the option #JSCOptionType
 * @description: (nullable): the option description, or %NULL
 * @user_data: user data
 *
 * Function used to iterate options.
 *
 * Not that @description string is not localized.
 *
 * Returns: %TRUE to stop the iteration, or %FALSE otherwise
 *
 * Since: 2.24
 */

/**
 * jsc_options_foreach:
 * @function: (scope call): a #JSCOptionsFunc callback
 * @user_data: callback user data
 *
 * Iterates all available options calling @function for each one. Iteration can
 * stop early if @function returns %FALSE.
 *
 * Since: 2.24
 */
void jsc_options_foreach(JSCOptionsFunc function, gpointer userData)
{
    g_return_if_fail(function);

#define VISIT_OPTION(type_, name_, defaultValue_, availability_, description_) \
    if (Options::Availability::availability_ == Options::Availability::Normal \
        || Options::isAvailable(Options::name_##ID, Options::Availability::availability_)) { \
        OptionsStorage::type_ defaultValue { };                            \
        auto optionType = jscOptionsType(defaultValue);                 \
        if (function (#name_, optionType, description_, userData))      \
            return;                                                     \
    }

    Options::initialize();
    FOR_EACH_JSC_OPTION(VISIT_OPTION)
#undef VISIT_OPTION
}

static gboolean setOptionEntry(const char* optionNameFull, const char* value, gpointer, GError** error)
{
    const char* optionName = optionNameFull + 6; // Remove the --jsc- prefix.
    GUniquePtr<char> option(g_strdup_printf("%s=%s", optionName, value));
    if (!Options::setOption(option.get())) {
        g_set_error(error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, "Failed parse value '%s' for %s", value, optionNameFull);
        return FALSE;
    }
    return TRUE;
}

/**
 * jsc_options_get_option_group:
 *
 * Create a #GOptionGroup to handle JSCOptions as command line arguments.
 * The options will be exposed as command line arguments with the form
 * <emphasis>--jsc-&lt;option&gt;=&lt;value&gt;</emphasis>.
 * Each entry in the returned #GOptionGroup is configured to apply the
 * corresponding option during command line parsing. Applications only need to
 * pass the returned group to g_option_context_add_group(), and the rest will
 * be taken care for automatically.
 *
 * Returns: (transfer full): a #GOptionGroup for the JSCOptions
 *
 * Since: 2.24
 */
GOptionGroup* jsc_options_get_option_group(void)
{
    // GOptionEntry works with const strings, so we need to keep the option names around.
    auto* names = new Vector<GUniquePtr<char>>;
    GOptionGroup* group = g_option_group_new("jsc", _("JSC Options"), _("Show JSC Options"), names, [] (gpointer data) {
        delete static_cast<Vector<GUniquePtr<char>>*>(data);
    });
    g_option_group_set_translation_domain(group, GETTEXT_PACKAGE);

    GArray* entries = g_array_new(TRUE, TRUE, sizeof(GOptionEntry));
#define REGISTER_OPTION(type_, name_, defaultValue_, availability_, description_) \
    if (Options::Availability::availability_ == Options::Availability::Normal \
        || Options::isAvailable(Options::name_##ID, Options::Availability::availability_)) { \
        GUniquePtr<char> name(g_strdup_printf("jsc-%s", #name_));       \
        entries = g_array_set_size(entries, entries->len + 1); \
        GOptionEntry* entry = &g_array_index(entries, GOptionEntry, entries->len - 1); \
        entry->long_name = name.get();                                  \
        entry->arg = G_OPTION_ARG_CALLBACK;                             \
        entry->arg_data = reinterpret_cast<gpointer>(setOptionEntry);   \
        entry->description = description_;                              \
        names->append(WTFMove(name));                                   \
    }

    Options::initialize();
    FOR_EACH_JSC_OPTION(REGISTER_OPTION)
#undef REGISTER_OPTION

    g_option_group_add_entries(group, reinterpret_cast<GOptionEntry*>(entries->data));
    return group;
}

/**
 * JSC_OPTIONS_USE_JIT:
 *
 * Allows the executable pages to be allocated for JIT and thunks if %TRUE.
 * Option type: %JSC_OPTION_BOOLEAN
 * Default value: %TRUE.
 *
 * Since: 2.24
 */

/**
 * JSC_OPTIONS_USE_DFG:
 *
 * Allows the DFG JIT to be used if %TRUE.
 * Option type: %JSC_OPTION_BOOLEAN
 * Default value: %TRUE.
 *
 * Since: 2.24
 */

/**
 * JSC_OPTIONS_USE_FTL:
 *
 * Allows the FTL JIT to be used if %TRUE.
 * Option type: %JSC_OPTION_BOOLEAN
 * Default value: %TRUE.
 *
 * Since: 2.24
 */

/**
 * JSC_OPTIONS_USE_LLINT:
 *
 * Allows the LLINT to be used if %TRUE.
 * Option type: %JSC_OPTION_BOOLEAN
 * Default value: %TRUE.
 *
 * Since: 2.24
 */
