/* NetworkManager
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Copyright 2010 - 2017 Red Hat, Inc.
 */

#include "nm-default.h"

#include "nm-meta-setting-access.h"

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

static const NMMetaSettingInfoEditor *
_get_meta_setting_info_editor_from_msi (const NMMetaSettingInfo *meta_setting_info)
{
	const NMMetaSettingInfoEditor *setting_info;

	if (!meta_setting_info)
		return NULL;

	nm_assert (meta_setting_info->get_setting_gtype);
	nm_assert (meta_setting_info->meta_type < G_N_ELEMENTS (nm_meta_setting_infos_editor));

	setting_info = &nm_meta_setting_infos_editor[meta_setting_info->meta_type];

	nm_assert (setting_info->general == meta_setting_info);
	return setting_info;
}

const NMMetaSettingInfoEditor *
nm_meta_setting_info_editor_find_by_name (const char *setting_name, gboolean use_alias)
{
	const NMMetaSettingInfoEditor *setting_info;
	guint i;

	g_return_val_if_fail (setting_name, NULL);

	setting_info = _get_meta_setting_info_editor_from_msi (nm_meta_setting_infos_by_name (setting_name));
	if (!setting_info && use_alias) {
		for (i = 0; i < _NM_META_SETTING_TYPE_NUM; i++) {
			if (nm_streq0 (nm_meta_setting_infos_editor[i].alias, setting_name)) {
				setting_info = &nm_meta_setting_infos_editor[i];
				break;
			}
		}
	}

	return setting_info;
}

const NMMetaSettingInfoEditor *
nm_meta_setting_info_editor_find_by_gtype (GType gtype)
{
	return _get_meta_setting_info_editor_from_msi (nm_meta_setting_infos_by_gtype (gtype));
}

const NMMetaSettingInfoEditor *
nm_meta_setting_info_editor_find_by_setting (NMSetting *setting)
{
	const NMMetaSettingInfoEditor *setting_info;

	g_return_val_if_fail (NM_IS_SETTING (setting), NULL);

	setting_info = nm_meta_setting_info_editor_find_by_gtype (G_OBJECT_TYPE (setting));

	nm_assert (setting_info);
	nm_assert (G_TYPE_CHECK_INSTANCE_TYPE (setting, setting_info->general->get_setting_gtype ()));
	return setting_info;
}

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

const NMMetaPropertyInfo *
nm_meta_setting_info_editor_get_property_info (const NMMetaSettingInfoEditor *setting_info, const char *property_name)
{
	guint i;

	g_return_val_if_fail (setting_info, NULL);
	g_return_val_if_fail (property_name, NULL);

	for (i = 0; i < setting_info->properties_num; i++) {
		nm_assert (setting_info->properties[i]->property_name);
		nm_assert (setting_info->properties[i]->setting_info == setting_info);
		if (nm_streq (setting_info->properties[i]->property_name, property_name))
			return setting_info->properties[i];
	}

	return NULL;
}

const NMMetaPropertyInfo *
nm_meta_property_info_find_by_name (const char *setting_name, const char *property_name)
{
	const NMMetaSettingInfoEditor *setting_info;
	const NMMetaPropertyInfo *property_info;

	setting_info = nm_meta_setting_info_editor_find_by_name (setting_name, FALSE);
	if (!setting_info)
		return NULL;

	property_info = nm_meta_setting_info_editor_get_property_info (setting_info, property_name);
	if (!property_info)
		return NULL;

	nm_assert (property_info->setting_info == setting_info);

	return property_info;
}

const NMMetaPropertyInfo *
nm_meta_property_info_find_by_setting (NMSetting *setting, const char *property_name)
{
	const NMMetaSettingInfoEditor *setting_info;
	const NMMetaPropertyInfo *property_info;

	setting_info = nm_meta_setting_info_editor_find_by_setting (setting);
	if (!setting_info)
		return NULL;
	property_info = nm_meta_setting_info_editor_get_property_info (setting_info, property_name);
	if (!property_info)
		return NULL;

	nm_assert (property_info->setting_info == setting_info);
	nm_assert (property_info == nm_meta_property_info_find_by_name (nm_setting_get_name (setting), property_name));

	return property_info;
}

NMSetting *
nm_meta_setting_info_editor_new_setting (const NMMetaSettingInfoEditor *setting_info,
                                         NMMetaAccessorSettingInitType init_type)
{
	NMSetting *setting;

	g_return_val_if_fail (setting_info, NULL);

	setting = g_object_new (setting_info->general->get_setting_gtype (), NULL);

	if (   setting_info->setting_init_fcn
	    && init_type != NM_META_ACCESSOR_SETTING_INIT_TYPE_DEFAULT) {
		setting_info->setting_init_fcn (setting_info,
		                                setting,
		                                init_type);
	}

	return setting;
}

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

const NMMetaSettingInfoEditor *const*
nm_meta_setting_infos_editor_p (void)
{
	static const NMMetaSettingInfoEditor *cache[_NM_META_SETTING_TYPE_NUM + 1] = { NULL };
	guint i;

	if (G_UNLIKELY (!cache[0])) {
		for (i = 0; i < _NM_META_SETTING_TYPE_NUM; i++)
			cache[i] = &nm_meta_setting_infos_editor[i];
	}
	return cache;
}

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

const char *
nm_meta_abstract_info_get_name (const NMMetaAbstractInfo *abstract_info, gboolean for_header)
{
	const char *n;

	nm_assert (abstract_info);
	nm_assert (abstract_info->meta_type);
	nm_assert (abstract_info->meta_type->get_name);
	n = abstract_info->meta_type->get_name (abstract_info, for_header);
	nm_assert (n && n[0]);
	return n;
}

const NMMetaAbstractInfo *const*
nm_meta_abstract_info_get_nested (const NMMetaAbstractInfo *abstract_info,
                                  guint *out_len,
                                  gpointer *nested_to_free)
{
	const NMMetaAbstractInfo *const*nested;
	guint l = 0;
	gs_free gpointer f = NULL;

	nm_assert (abstract_info);
	nm_assert (abstract_info->meta_type);
	nm_assert (nested_to_free && !*nested_to_free);

	if (abstract_info->meta_type->get_nested) {
		nested = abstract_info->meta_type->get_nested (abstract_info, &l, &f);
		nm_assert (NM_PTRARRAY_LEN (nested) == l);
		nm_assert (!f || nested == f);
		if (nested && nested[0]) {
			NM_SET_OUT (out_len, l);
			*nested_to_free = g_steal_pointer (&f);
			return nested;
		}
	}
	NM_SET_OUT (out_len, 0);
	return NULL;
}

gconstpointer
nm_meta_abstract_info_get (const NMMetaAbstractInfo *abstract_info,
                           const NMMetaEnvironment *environment,
                           gpointer environment_user_data,
                           gpointer target,
                           gpointer target_data,
                           NMMetaAccessorGetType get_type,
                           NMMetaAccessorGetFlags get_flags,
                           NMMetaAccessorGetOutFlags *out_flags,
                           gboolean *out_is_default,
                           gpointer *out_to_free)
{
	nm_assert (abstract_info);
	nm_assert (abstract_info->meta_type);
	nm_assert (!out_to_free || !*out_to_free);
	nm_assert (out_flags);

	*out_flags = NM_META_ACCESSOR_GET_OUT_FLAGS_NONE;
	NM_SET_OUT (out_is_default, FALSE);

	if (!abstract_info->meta_type->get_fcn)
		g_return_val_if_reached (NULL);

	return abstract_info->meta_type->get_fcn (abstract_info,
	                                          environment,
	                                          environment_user_data,
	                                          target,
	                                          target_data,
	                                          get_type,
	                                          get_flags,
	                                          out_flags,
	                                          out_is_default,
	                                          out_to_free);
}

const char *const*
nm_meta_abstract_info_complete (const NMMetaAbstractInfo *abstract_info,
                                const NMMetaEnvironment *environment,
                                gpointer environment_user_data,
                                const NMMetaOperationContext *operation_context,
                                const char *text,
                                char ***out_to_free)
{
	const char *const*values;
	gsize i, j, text_len;

	nm_assert (abstract_info);
	nm_assert (abstract_info->meta_type);
	nm_assert (out_to_free && !*out_to_free);

	*out_to_free = NULL;

	if (!abstract_info->meta_type->complete_fcn)
		return NULL;

	values = abstract_info->meta_type->complete_fcn (abstract_info,
	                                                 environment,
	                                                 environment_user_data,
	                                                 operation_context,
	                                                 text,
	                                                 out_to_free);

	nm_assert (!*out_to_free || values == (const char *const*) *out_to_free);

	if (!values)
		return NULL;

	if (!values[0]) {
		nm_clear_g_free (out_to_free);
		return NULL;
	}

	if (!text || !text[0])
		return values;

	/* for convenience, we allow the complete_fcn() implementations to
	 * ignore "text". We filter out invalid matches here. */

	text_len = strlen (text);

	if (*out_to_free) {
		char **v = *out_to_free;

		for (i = 0, j = 0; v[i]; i++) {
			if (strncmp (v[i], text, text_len) != 0) {
				g_free (v[i]);
				continue;
			}
			v[j++] = v[i];
		}
		if (j)
			v[j++] = NULL;
		else {
			g_free (v);
			*out_to_free = v = NULL;
		}
		return (const char *const*) v;
	} else {
		const char *const*v = values;
		char **r;

		for (i = 0, j = 0; v[i]; i++) {
			if (strncmp (v[i], text, text_len) != 0)
				continue;
			j++;
		}
		if (j == i)
			return values;
		else if (!j)
			return NULL;

		r = g_new (char *, j + 1);
		v = values;
		for (i = 0, j = 0; v[i]; i++) {
			if (strncmp (v[i], text, text_len) != 0)
				continue;
			r[j++] = g_strdup (v[i]);
		}
		r[j++] = NULL;
		return (const char *const*) (*out_to_free = r);
	}
}

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

char *
nm_meta_abstract_info_get_nested_names_str (const NMMetaAbstractInfo *abstract_info, const char *name_prefix)
{
	gs_free gpointer nested_to_free = NULL;
	const NMMetaAbstractInfo *const*nested;

	nested = nm_meta_abstract_info_get_nested (abstract_info, NULL, &nested_to_free);
	if (!nested)
		return NULL;

	if (!name_prefix)
		name_prefix = nm_meta_abstract_info_get_name (abstract_info, FALSE);

	return nm_meta_abstract_infos_get_names_str (nested, name_prefix);
}

char *
nm_meta_abstract_infos_get_names_str (const NMMetaAbstractInfo *const*fields_array, const char *name_prefix)
{
	GString *str;
	guint i;

	if (!fields_array || !fields_array[0])
		return NULL;

	str = g_string_sized_new (128);
	for (i = 0; fields_array[i]; i++) {
		if (str->len > 0)
			g_string_append_c (str, ',');
		if (name_prefix) {
			g_string_append (str, name_prefix);
			g_string_append_c (str, '.');
		}
		g_string_append (str, nm_meta_abstract_info_get_name (fields_array[i], FALSE));
	}
	return g_string_free (str, FALSE);
}

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

typedef struct {
	guint idx;
	gsize self_offset_plus_1;
	gsize sub_offset_plus_1;
} OutputSelectionItem;

static NMMetaSelectionResultList *
_output_selection_pack (const NMMetaAbstractInfo *const* fields_array,
                        GArray *array,
                        GString *str)
{
	NMMetaSelectionResultList *result;
	guint i;
	guint len;

	len = array ? array->len : 0;

	/* re-organize the collected output data in one buffer that can be freed using
	 * g_free(). This makes allocation more complicated, but saves us from special
	 * handling for free. */
	result = g_malloc0 (sizeof (NMMetaSelectionResultList) + (len * sizeof (NMMetaSelectionItem)) + (str ? str->len : 0));
	*((guint *) &result->num) = len;
	if (len > 0) {
		char *pdata = &((char *) result)[sizeof (NMMetaSelectionResultList) + (len * sizeof (NMMetaSelectionItem))];

		if (str)
			memcpy (pdata, str->str, str->len);
		for (i = 0; i < len; i++) {
			const OutputSelectionItem *a = &g_array_index (array, OutputSelectionItem, i);
			NMMetaSelectionItem *p = (NMMetaSelectionItem *) &result->items[i];

			p->info = fields_array[a->idx];
			p->idx = a->idx;
			if (a->self_offset_plus_1 > 0)
				p->self_selection = &pdata[a->self_offset_plus_1 - 1];
			if (a->sub_offset_plus_1 > 0)
				p->sub_selection = &pdata[a->sub_offset_plus_1 - 1];
		}
	}

	return result;
}

static gboolean
_output_selection_select_one (const NMMetaAbstractInfo *const* fields_array,
                              const char *fields_prefix,
                              const char *fields_str,
                              gboolean validate_nested,
                              GArray **p_array,
                              GString **p_str,
                              GError **error)
{
	guint i, j;
	const char *i_name;
	const char *right;
	gboolean found = FALSE;
	const NMMetaAbstractInfo *fields_array_failure = NULL;
	gs_free char *fields_str_clone = NULL;

	nm_assert (fields_str);
	nm_assert (p_array);
	nm_assert (p_str);
	nm_assert (!error || !*error);

	right = strchr (fields_str, '.');
	if (right) {
		fields_str_clone = g_strdup (fields_str);
		fields_str_clone[right - fields_str] = '\0';
		i_name = fields_str_clone;
		right = &fields_str_clone[right - fields_str + 1];
	} else
		i_name = fields_str;

	if (!fields_array)
		goto not_found;

	for (i = 0; fields_array[i]; i++) {
		const NMMetaAbstractInfo *fi = fields_array[i];
		const NMMetaAbstractInfo *const*nested;
		gs_free gpointer nested_to_free = NULL;

		if (g_ascii_strcasecmp (i_name, nm_meta_abstract_info_get_name (fi, FALSE)) != 0)
			continue;

		if (!right || !validate_nested) {
			found = TRUE;
			break;
		}

		nested = nm_meta_abstract_info_get_nested (fi, NULL, &nested_to_free);
		if (nested) {
			for (j = 0; nested[j]; nested++) {
				if (g_ascii_strcasecmp (right, nm_meta_abstract_info_get_name (nested[j], FALSE)) == 0) {
					found = TRUE;
					break;
				}
			}
		}
		fields_array_failure = fields_array[i];
		break;
	}

	if (!found) {
not_found:
		if (   !right
		    && !fields_prefix
		    && (   !g_ascii_strcasecmp (i_name, "all")
		        || !g_ascii_strcasecmp (i_name, "common")))
			g_set_error (error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, _("field '%s' has to be alone"), i_name);
		else {
			gs_free char *allowed_fields = NULL;

			if (fields_array_failure) {
				gs_free char *p = NULL;

				if (fields_prefix) {
					p = g_strdup_printf ("%s.%s", fields_prefix,
					                     nm_meta_abstract_info_get_name (fields_array_failure, FALSE));
				}
				allowed_fields = nm_meta_abstract_info_get_nested_names_str (fields_array_failure, p);
			} else
				allowed_fields = nm_meta_abstract_infos_get_names_str (fields_array, NULL);

			g_set_error (error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, _("invalid field '%s%s%s%s%s'; %s%s%s"),
			             fields_prefix ?: "", fields_prefix ? "." : "",
			             i_name, right ? "." : "", right ?: "",
			             NM_PRINT_FMT_QUOTED (allowed_fields, "allowed fields: ", allowed_fields, "", "no fields"));
		}
		return FALSE;
	}

	{
		GString *str;
		OutputSelectionItem s = {
			.idx = i,
		};

		if (!*p_str)
			*p_str = g_string_sized_new (64);
		str = *p_str;

		s.self_offset_plus_1 = str->len + 1;
		if (fields_prefix) {
			g_string_append (str, fields_prefix);
			g_string_append_c (str, '.');
		}
		g_string_append_len (str, i_name, strlen (i_name) + 1);

		if (right) {
			s.sub_offset_plus_1 = str->len + 1;
			g_string_append_len (str, right, strlen (right) + 1);
		}

		if (!*p_array)
			*p_array = g_array_new (FALSE, FALSE, sizeof (OutputSelectionItem));
		g_array_append_val (*p_array, s);
	}

	return TRUE;
}

NMMetaSelectionResultList *
nm_meta_selection_create_all (const NMMetaAbstractInfo *const* fields_array)
{
	gs_unref_array GArray *array = NULL;
	guint i;

	if (fields_array) {
		array = g_array_new (FALSE, FALSE, sizeof (OutputSelectionItem));
		for (i = 0; fields_array[i]; i++) {
			OutputSelectionItem s = {
				.idx = i,
			};

			g_array_append_val (array, s);
		}
	}

	return _output_selection_pack (fields_array, array, NULL);
}

NMMetaSelectionResultList *
nm_meta_selection_create_parse_one (const NMMetaAbstractInfo *const* fields_array,
                                    const char *fields_prefix,
                                    const char *fields_str, /* one field selector (contains no commas) and is already stripped of spaces. */
                                    gboolean validate_nested,
                                    GError **error)
{
	gs_unref_array GArray *array = NULL;
	nm_auto_free_gstring GString *str = NULL;

	g_return_val_if_fail (!error || !*error, NULL);
	nm_assert (fields_str && !strchr (fields_str, ','));

	if (!_output_selection_select_one (fields_array,
	                                   fields_prefix,
	                                   fields_str,
	                                   validate_nested,
	                                   &array,
	                                   &str,
	                                   error))
		return NULL;
	return _output_selection_pack (fields_array, array, str);

}

NMMetaSelectionResultList *
nm_meta_selection_create_parse_list (const NMMetaAbstractInfo *const* fields_array,
                                     const char *fields_str, /* a comma separated list of selectors */
                                     gboolean validate_nested,
                                     GError **error)
{
	gs_unref_array GArray *array = NULL;
	nm_auto_free_gstring GString *str = NULL;
	gs_free char *fields_str_clone = NULL;
	char *fields_str_cur;
	char *fields_str_next;

	g_return_val_if_fail (!error || !*error, NULL);

	if (!fields_str)
		return nm_meta_selection_create_all (fields_array);

	fields_str_clone = g_strdup (fields_str);
	for (fields_str_cur = fields_str_clone; fields_str_cur; fields_str_cur = fields_str_next) {
		fields_str_cur = nm_str_skip_leading_spaces (fields_str_cur);
		fields_str_next = strchr (fields_str_cur, ',');
		if (fields_str_next)
			*fields_str_next++ = '\0';

		g_strchomp (fields_str_cur);
		if (!fields_str_cur[0])
			continue;
		if (!_output_selection_select_one (fields_array,
		                                   NULL,
		                                   fields_str_cur,
		                                   validate_nested,
		                                   &array,
		                                   &str,
		                                   error))
			return NULL;
	}

	return _output_selection_pack (fields_array, array, str);
}
