/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
 * Copyright (C) 2018 Red Hat, Inc.
 */

#include "libnm-core-impl/nm-default-libnm-core.h"

#include "nm-setting-ethtool.h"

#include "nm-setting-private.h"
#include "libnm-base/nm-ethtool-base.h"
#include "libnm-base/nm-ethtool-utils-base.h"
#include "libnm-glib-aux/nm-enum-utils.h"

/*****************************************************************************/

/**
 * SECTION:nm-setting-ethtool
 * @short_description: Describes connection properties for ethtool related options
 *
 * The #NMSettingEthtool object is a #NMSetting subclass that describes properties
 * to control network driver and hardware settings.
 **/

/*****************************************************************************/

/**
 * nm_ethtool_optname_is_feature:
 * @optname: (nullable): the option name to check
 *
 * Checks whether @optname is a valid option name for an offload feature.
 *
 * Returns: %TRUE, if @optname is valid
 *
 * Note that nm_ethtool_optname_is_feature() was first added to the libnm header files
 * in 1.14.0 but forgot to actually add to the library. This happened belatedly in 1.20.0 and
 * the stable versions 1.18.2, 1.16.4 and 1.14.8 (with linker version "libnm_1_14_8").
 *
 * Since: 1.20
 */
gboolean
nm_ethtool_optname_is_feature(const char *optname)
{
    return optname && nm_ethtool_id_is_feature(nm_ethtool_id_get_by_name(optname));
}

/**
 * nm_ethtool_optname_is_coalesce:
 * @optname: (nullable): the option name to check
 *
 * Checks whether @optname is a valid option name for a coalesce setting.
 *
 * Returns: %TRUE, if @optname is valid
 *
 * Since: 1.26
 */
gboolean
nm_ethtool_optname_is_coalesce(const char *optname)
{
    return optname && nm_ethtool_id_is_coalesce(nm_ethtool_id_get_by_name(optname));
}

/**
 * nm_ethtool_optname_is_ring:
 * @optname: (nullable): the option name to check
 *
 * Checks whether @optname is a valid option name for a ring setting.
 *
 * Returns: %TRUE, if @optname is valid
 *
 * Since: 1.26
 */
gboolean
nm_ethtool_optname_is_ring(const char *optname)
{
    return optname && nm_ethtool_id_is_ring(nm_ethtool_id_get_by_name(optname));
}

/**
 * nm_ethtool_optname_is_channels:
 * @optname: (nullable): the option name to check
 *
 * Checks whether @optname is a valid option name for a channels setting.
 *
 * Returns: %TRUE, if @optname is valid
 *
 * Since: 1.46
 */
gboolean
nm_ethtool_optname_is_channels(const char *optname)
{
    return optname && nm_ethtool_id_is_channels(nm_ethtool_id_get_by_name(optname));
}

/**
 * nm_ethtool_optname_is_eee:
 * @optname: (nullable): the option name to check
 *
 * Checks whether @optname is a valid option name for an eee setting.
 *
 * Returns: %TRUE, if @optname is valid
 *
 * Since: 1.46
 */
gboolean
nm_ethtool_optname_is_eee(const char *optname)
{
    return optname && nm_ethtool_id_is_eee(nm_ethtool_id_get_by_name(optname));
}
/**
 * nm_ethtool_optname_is_pause:
 * @optname: (nullable): the option name to check
 *
 * Checks whether @optname is a valid option name for a pause setting.
 *
 * Returns: %TRUE, if @optname is valid
 *
 * Since: 1.32
 */
gboolean
nm_ethtool_optname_is_pause(const char *optname)
{
    return optname && nm_ethtool_id_is_pause(nm_ethtool_id_get_by_name(optname));
}
/**
 * nm_ethtool_optname_is_fec:
 * @optname: (nullable): the option name to check
 *
 * Checks whether @optname is a valid option name for a fec setting.
 *
 * Returns: %TRUE, if @optname is valid
 *
 * Since: 1.52
 */
gboolean
nm_ethtool_optname_is_fec(const char *optname)
{
    return optname && nm_ethtool_id_is_fec(nm_ethtool_id_get_by_name(optname));
}

/*****************************************************************************/

/**
 * NMSettingEthtool:
 *
 * Ethtool Ethernet Settings
 *
 * Since: 1.14
 */
struct _NMSettingEthtool {
    NMSetting parent;
};

struct _NMSettingEthtoolClass {
    NMSettingClass parent;
};

G_DEFINE_TYPE(NMSettingEthtool, nm_setting_ethtool, NM_TYPE_SETTING)

#define NM_SETTING_ETHTOOL_GET_PRIVATE(self) \
    _NM_GET_PRIVATE(self, NMSettingEthtool, NM_IS_SETTING_ETHTOOL, NMSetting)

/*****************************************************************************/

/**
 * nm_setting_ethtool_get_feature:
 * @setting: the #NMSettingEthtool
 * @optname: option name of the offload feature to get
 *
 * Gets and offload feature setting. Returns %NM_TERNARY_DEFAULT if the
 * feature is not set.
 *
 * Note that @optname must be a valid name for a feature, according to
 * nm_ethtool_optname_is_feature().
 *
 * Returns: a #NMTernary value indicating whether the offload feature
 *   is enabled, disabled, or left untouched.
 *
 * Since: 1.14
 *
 * Deprecated: 1.26: use nm_setting_option_get_boolean() instead.
 */
NMTernary
nm_setting_ethtool_get_feature(NMSettingEthtool *setting, const char *optname)
{
    gboolean v;

    g_return_val_if_fail(NM_IS_SETTING_ETHTOOL(setting), NM_TERNARY_DEFAULT);
    g_return_val_if_fail(optname && nm_ethtool_optname_is_feature(optname), NM_TERNARY_DEFAULT);

    if (!nm_setting_option_get_boolean(NM_SETTING(setting), optname, &v))
        return NM_TERNARY_DEFAULT;
    return v ? NM_TERNARY_TRUE : NM_TERNARY_FALSE;
}

/**
 * nm_setting_ethtool_set_feature:
 * @setting: the #NMSettingEthtool
 * @optname: option name of the offload feature to get
 * @value: the new value to set. The special value %NM_TERNARY_DEFAULT
 *   means to clear the offload feature setting.
 *
 * Sets and offload feature setting.
 *
 * Note that @optname must be a valid name for a feature, according to
 * nm_ethtool_optname_is_feature().
 *
 * Since: 1.14
 *
 * Deprecated: 1.26: use nm_setting_option_set() or nm_setting_option_set_boolean() instead.
 */
void
nm_setting_ethtool_set_feature(NMSettingEthtool *setting, const char *optname, NMTernary value)
{
    g_return_if_fail(NM_IS_SETTING_ETHTOOL(setting));
    g_return_if_fail(optname && nm_ethtool_optname_is_feature(optname));
    g_return_if_fail(NM_IN_SET(value, NM_TERNARY_DEFAULT, NM_TERNARY_FALSE, NM_TERNARY_TRUE));

    if (value == NM_TERNARY_DEFAULT)
        nm_setting_option_set(NM_SETTING(setting), optname, NULL);
    else
        nm_setting_option_set_boolean(NM_SETTING(setting), optname, (value != NM_TERNARY_FALSE));
}

/**
 * nm_setting_ethtool_clear_features:
 * @setting: the #NMSettingEthtool
 *
 * Clears all offload features settings
 *
 * Since: 1.14
 *
 * Deprecated: 1.26: use nm_setting_option_clear_by_name() with nm_ethtool_optname_is_feature() predicate instead.
 */
void
nm_setting_ethtool_clear_features(NMSettingEthtool *setting)
{
    g_return_if_fail(NM_IS_SETTING_ETHTOOL(setting));

    nm_setting_option_clear_by_name(NM_SETTING(setting), nm_ethtool_optname_is_feature);
}

/*****************************************************************************/

guint
nm_setting_ethtool_init_features(
    NMSettingEthtool *setting,
    NMOptionBool     *requested /* indexed by NMEthtoolID - _NM_ETHTOOL_ID_FEATURE_FIRST */)
{
    GHashTable    *hash;
    GHashTableIter iter;
    guint          i;
    guint          n_req = 0;
    const char    *name;
    GVariant      *variant;

    nm_assert(NM_IS_SETTING_ETHTOOL(setting));
    nm_assert(requested);

    for (i = 0; i < _NM_ETHTOOL_ID_FEATURE_NUM; i++)
        requested[i] = NM_OPTION_BOOL_DEFAULT;

    hash = _nm_setting_option_hash(NM_SETTING(setting), FALSE);
    if (!hash)
        return 0;

    g_hash_table_iter_init(&iter, hash);
    while (g_hash_table_iter_next(&iter, (gpointer *) &name, (gpointer *) &variant)) {
        NMEthtoolID ethtool_id = nm_ethtool_id_get_by_name(name);

        if (!nm_ethtool_id_is_feature(ethtool_id))
            continue;
        if (!g_variant_is_of_type(variant, G_VARIANT_TYPE_BOOLEAN))
            continue;

        requested[_NM_ETHTOOL_ID_FEATURE_AS_IDX(ethtool_id)] =
            g_variant_get_boolean(variant) ? NM_OPTION_BOOL_TRUE : NM_OPTION_BOOL_FALSE;
        n_req++;
    }

    return n_req;
}

/*****************************************************************************/

/**
 * nm_setting_ethtool_get_optnames:
 * @setting: the #NMSettingEthtool instance.
 * @out_length: (out) (optional): return location for the number of keys returned, or %NULL
 *
 * This returns all options names that are set. This includes the feature names
 * like %NM_ETHTOOL_OPTNAME_FEATURE_GRO. See nm_ethtool_optname_is_feature() to
 * check whether the option name is valid for offload features.
 *
 * Returns: (array zero-terminated=1) (transfer container): list of set option
 *   names or %NULL if no options are set. The option names are still owned by
 *   @setting and may get invalidated when @setting gets modified.
 *
 * Since: 1.20
 *
 * Deprecated: 1.26: use nm_setting_option_get_all_names() instead.
 */
const char **
nm_setting_ethtool_get_optnames(NMSettingEthtool *setting, guint *out_length)
{
    const char *const *names;
    guint              len = 0;

    g_return_val_if_fail(NM_IS_SETTING_ETHTOOL(setting), NULL);

    names = nm_setting_option_get_all_names(NM_SETTING(setting), &len);
    NM_SET_OUT(out_length, len);
    return len > 0 ? nm_memdup(names, sizeof(names[0]) * (((gsize) len) + 1u)) : NULL;
}

/*****************************************************************************/

static gboolean
verify(NMSetting *setting, NMConnection *connection, GError **error)
{
    const char *const *optnames;
    GVariant *const   *variants;
    guint              len;
    guint              i;
    NMTernary          pause_autoneg = NM_TERNARY_DEFAULT;
    NMTernary          pause_tx      = NM_TERNARY_DEFAULT;
    NMTernary          pause_rx      = NM_TERNARY_DEFAULT;
    guint32            fec_mode      = 0;

    len = _nm_setting_option_get_all(setting, &optnames, &variants);

    for (i = 0; i < len; i++) {
        const char         *optname = optnames[i];
        GVariant           *variant = variants[i];
        const GVariantType *variant_type;
        NMEthtoolID         ethtool_id;

        ethtool_id   = nm_ethtool_id_get_by_name(optname);
        variant_type = nm_ethtool_id_get_variant_type(ethtool_id);

        if (!variant_type) {
            g_set_error_literal(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
                                _("unsupported ethtool setting"));
            g_prefix_error(error, "%s.%s: ", NM_SETTING_ETHTOOL_SETTING_NAME, optname);
            return FALSE;
        }

        if (!g_variant_is_of_type(variant, variant_type)) {
            g_set_error_literal(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
                                _("setting has invalid variant type"));
            g_prefix_error(error, "%s.%s: ", NM_SETTING_ETHTOOL_SETTING_NAME, optname);
            return FALSE;
        }

        if (NM_IN_SET(ethtool_id,
                      NM_ETHTOOL_ID_COALESCE_ADAPTIVE_RX,
                      NM_ETHTOOL_ID_COALESCE_ADAPTIVE_TX)) {
            if (!NM_IN_SET(g_variant_get_uint32(variant), 0, 1)) {
                g_set_error_literal(error,
                                    NM_CONNECTION_ERROR,
                                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
                                    _("coalesce option must be either 0 or 1"));
                g_prefix_error(error, "%s.%s: ", NM_SETTING_ETHTOOL_SETTING_NAME, optname);
                return FALSE;
            }
        } else if (NM_IN_SET(ethtool_id, NM_ETHTOOL_ID_PAUSE_AUTONEG))
            pause_autoneg = g_variant_get_boolean(variant);
        else if (NM_IN_SET(ethtool_id, NM_ETHTOOL_ID_PAUSE_RX))
            pause_rx = g_variant_get_boolean(variant);
        else if (NM_IN_SET(ethtool_id, NM_ETHTOOL_ID_PAUSE_TX))
            pause_tx = g_variant_get_boolean(variant);
        else if (NM_IN_SET(ethtool_id, NM_ETHTOOL_ID_FEC_MODE))
            fec_mode = g_variant_get_uint32(variant);
    }

    if (pause_rx != NM_TERNARY_DEFAULT || pause_tx != NM_TERNARY_DEFAULT) {
        if (pause_autoneg == NM_TERNARY_TRUE) {
            g_set_error_literal(error,
                                NM_CONNECTION_ERROR,
                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
                                _("pause-autoneg cannot be enabled when setting rx/tx options"));
            g_prefix_error(error,
                           "%s.%s: ",
                           NM_SETTING_ETHTOOL_SETTING_NAME,
                           NM_ETHTOOL_OPTNAME_PAUSE_AUTONEG);
            return FALSE;
        }
    }

    if (fec_mode == NM_SETTING_ETHTOOL_FEC_MODE_NONE
        || fec_mode >= (_NM_SETTING_ETHTOOL_FEC_MODE_LAST << 1)) {
        gs_free const char  *cur_fec_mode = NULL;
        gs_free const char **valid_all    = NULL;
        gs_free const char  *valid_str    = NULL;

        cur_fec_mode = _nm_utils_enum_to_str_full(nm_setting_ethtool_fec_mode_get_type(),
                                                  (int) (fec_mode & INT_MAX),
                                                  ", ",
                                                  NULL);
        valid_all = nm_utils_enum_get_values(nm_setting_ethtool_fec_mode_get_type(), 0, G_MAXUINT);
        valid_str = g_strjoinv(",", (char **) valid_all);

        g_set_error(error,
                    NM_CONNECTION_ERROR,
                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
                    _("'%s' is not valid FEC modes, valid modes are combinations of %s"),
                    cur_fec_mode,
                    valid_str);
        g_prefix_error(error,
                       "%s.%s: ",
                       NM_SETTING_ETHTOOL_SETTING_NAME,
                       NM_ETHTOOL_OPTNAME_FEC_MODE);
        return FALSE;
    }

    return TRUE;
}

/*****************************************************************************/

static const GVariantType *
get_variant_type(const NMSettInfoSetting *sett_info, const char *name, GError **error)
{
    const GVariantType *variant_type;

    variant_type = nm_ethtool_id_get_variant_type(nm_ethtool_id_get_by_name(name));

    if (!variant_type) {
        g_set_error(error,
                    NM_CONNECTION_ERROR,
                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
                    _("unknown ethtool option '%s'"),
                    name);
        return NULL;
    }

    return variant_type;
}

/*****************************************************************************/

static void
nm_setting_ethtool_init(NMSettingEthtool *setting)
{}

/**
 * nm_setting_ethtool_new:
 *
 * Creates a new #NMSettingEthtool object with default values.
 *
 * Returns: (transfer full): the new empty #NMSettingEthtool object
 *
 * Since: 1.14
 **/
NMSetting *
nm_setting_ethtool_new(void)
{
    return g_object_new(NM_TYPE_SETTING_ETHTOOL, NULL);
}

static void
nm_setting_ethtool_class_init(NMSettingEthtoolClass *klass)
{
    NMSettingClass *setting_class = NM_SETTING_CLASS(klass);

    setting_class->verify = verify;

    _nm_setting_class_commit(
        setting_class,
        NM_META_SETTING_TYPE_ETHTOOL,
        NM_SETT_INFO_SETT_DETAIL(.gendata_info =
                                     NM_SETT_INFO_SETT_GENDATA(.get_variant_type =
                                                                   get_variant_type, ), ),
        NULL,
        0);
}
