// SPDX-License-Identifier: GPL-2.0+
/* NetworkManager Connection editor -- Connection editor for NetworkManager
 *
 * Rodrigo Moya <rodrigo@gnome-db.org>
 * Dan Williams <dcbw@redhat.com>
 * Lubomir Rintel <lkundrak@v3.sk>
 *
 * Copyright 2007 - 2017 Red Hat, Inc.
 */

#include "nm-default.h"

#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <gdk/gdkx.h>

#include "ce-page.h"
#include "nm-connection-editor.h"
#include "nm-connection-list.h"
#include "ce-polkit.h"
#include "connection-helpers.h"

extern gboolean nm_ce_keep_above;

enum {
	NEW_EDITOR,
	LIST_LAST_SIGNAL
};

static guint list_signals[LIST_LAST_SIGNAL] = { 0 };

struct _NMConnectionListPrivate {
	GtkWidget *connection_add;
	GtkWidget *connection_del;
	GtkWidget *connection_edit;
	GtkTreeView *connection_list;
	GtkSearchBar *search_bar;
	GtkEntry *search_entry;
	GtkTreeModel *model;
	GtkTreeModelFilter *filter;
	GtkTreeSortable *sortable;
	GType displayed_type;

	NMClient *client;

	gboolean populated;
};

#define NM_CONNECTION_LIST_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), \
                                           NM_TYPE_CONNECTION_LIST, \
                                           NMConnectionListPrivate))

G_DEFINE_TYPE_WITH_CODE (NMConnectionList, nm_connection_list, GTK_TYPE_APPLICATION_WINDOW,
                         G_ADD_PRIVATE (NMConnectionList))

#define COL_ID         0
#define COL_LAST_USED  1
#define COL_TIMESTAMP  2
#define COL_CONNECTION 3
#define COL_GTYPE0     4
#define COL_GTYPE1     5
#define COL_GTYPE2     6
#define COL_ORDER      7

static NMRemoteConnection *
get_active_connection (GtkTreeView *treeview)
{
	GtkTreeSelection *selection;
	GList *selected_rows;
	GtkTreeModel *model = NULL;
	GtkTreeIter iter;
	NMRemoteConnection *connection = NULL;

	selection = gtk_tree_view_get_selection (treeview);
	selected_rows = gtk_tree_selection_get_selected_rows (selection, &model);
	if (!selected_rows)
		return NULL;

	if (gtk_tree_model_get_iter (model, &iter, (GtkTreePath *) selected_rows->data))
		gtk_tree_model_get (model, &iter, COL_CONNECTION, &connection, -1);

	g_list_free_full (selected_rows, (GDestroyNotify) gtk_tree_path_free);

	/* gtk_tree_model_get() will have reffed connection, but we don't
	 * need that since we know the model will continue to hold a ref.
	 */
	if (connection)
		g_object_unref (connection);

	return connection;
}

static gboolean
get_iter_for_connection (NMConnectionList *list,
                         NMRemoteConnection *connection,
                         GtkTreeIter *iter)
{
	GtkTreeIter types_iter;
	NMConnectionListPrivate *priv = NM_CONNECTION_LIST_GET_PRIVATE (list);

	if (!gtk_tree_model_get_iter_first (priv->model, &types_iter))
		return FALSE;

	do {
		if (!gtk_tree_model_iter_children (priv->model, iter, &types_iter))
			continue;

		do {
			NMRemoteConnection *candidate = NULL;

			gtk_tree_model_get (priv->model, iter,
			                    COL_CONNECTION, &candidate,
			                    -1);
			if (candidate == connection) {
				g_object_unref (candidate);
				return TRUE;
			}
			g_object_unref (candidate);
		} while (gtk_tree_model_iter_next (priv->model, iter));
	} while (gtk_tree_model_iter_next (priv->model, &types_iter));

	return FALSE;
}

static char *
format_last_used (guint64 timestamp)
{
	GTimeVal now_tv;
	GDate *now, *last;
	char *last_used = NULL;

	if (!timestamp)
		return g_strdup (_("never"));

	g_get_current_time (&now_tv);
	now = g_date_new ();
	g_date_set_time_val (now, &now_tv);

	last = g_date_new ();
	g_date_set_time_t (last, (time_t) timestamp);

	/* timestamp is now or in the future */
	if (now_tv.tv_sec <= timestamp) {
		last_used = g_strdup (_("now"));
		goto out;
	}

	if (g_date_compare (now, last) <= 0) {
		guint minutes, hours;

		/* Same day */

		minutes = (now_tv.tv_sec - timestamp) / 60;
		if (minutes == 0) {
			last_used = g_strdup (_("now"));
			goto out;
		}

		hours = (now_tv.tv_sec - timestamp) / 3600;
		if (hours == 0) {
			/* less than an hour ago */
			last_used = g_strdup_printf (ngettext ("%d minute ago", "%d minutes ago", minutes), minutes);
			goto out;
		}

		last_used = g_strdup_printf (ngettext ("%d hour ago", "%d hours ago", hours), hours);
	} else {
		guint days, months, years;

		days = g_date_get_julian (now) - g_date_get_julian (last);
		if (days == 0) {
			last_used = g_strdup ("today");
			goto out;
		}

		months = days / 30;
		if (months == 0) {
			last_used = g_strdup_printf (ngettext ("%d day ago", "%d days ago", days), days);
			goto out;
		}

		years = days / 365;
		if (years == 0) {
			last_used = g_strdup_printf (ngettext ("%d month ago", "%d months ago", months), months);
			goto out;
		}

		last_used = g_strdup_printf (ngettext ("%d year ago", "%d years ago", years), years);
	}

out:
	g_date_free (now);
	g_date_free (last);
	return last_used;
}

static void
update_connection_row (NMConnectionList *self,
                       GtkTreeIter *iter,
                       NMRemoteConnection *connection)
{
	NMConnectionListPrivate *priv = NM_CONNECTION_LIST_GET_PRIVATE (self);
	NMSettingConnection *s_con;
	char *last_used, *id;

	s_con = nm_connection_get_setting_connection (NM_CONNECTION (connection));
	g_assert (s_con);

	last_used = format_last_used (nm_setting_connection_get_timestamp (s_con));
	id = g_markup_escape_text (nm_setting_connection_get_id (s_con), -1);
	gtk_tree_store_set (GTK_TREE_STORE (priv->model), iter,
	                    COL_ID, id,
	                    COL_LAST_USED, last_used,
	                    COL_TIMESTAMP, nm_setting_connection_get_timestamp (s_con),
	                    COL_CONNECTION, connection,
	                    -1);
	g_free (last_used);
	g_free (id);

	gtk_tree_model_filter_refilter (priv->filter);
}

static void
delete_ports_of_connection (NMConnectionList *list, NMConnection *connection)
{
	NMConnectionListPrivate *priv = NM_CONNECTION_LIST_GET_PRIVATE (list);
	const char *uuid, *iface;
	GtkTreeIter iter, types_iter;

	if (!gtk_tree_model_get_iter_first (priv->model, &types_iter))
		return;

	uuid = nm_connection_get_uuid (connection);
	iface = nm_connection_get_interface_name (connection);

	do {
		if (!gtk_tree_model_iter_children (priv->model, &iter, &types_iter))
			continue;

		do {
			NMRemoteConnection *candidate = NULL;
			NMSettingConnection *s_con;
			const char *controller;

			gtk_tree_model_get (priv->model, &iter,
			                    COL_CONNECTION, &candidate,
			                    -1);
			s_con = nm_connection_get_setting_connection (NM_CONNECTION (candidate));
			controller = nm_setting_connection_get_master (s_con);
			if (controller) {
				if (!g_strcmp0 (controller, uuid) || !g_strcmp0 (controller, iface))
					nm_remote_connection_delete (candidate, NULL, NULL);
			}

			g_object_unref (candidate);
		} while (gtk_tree_model_iter_next (priv->model, &iter));
	} while (gtk_tree_model_iter_next (priv->model, &types_iter));
}


/**********************************************/
/* dialog/UI handling stuff */

static void
add_response_cb (NMConnectionEditor *editor, GtkResponseType response, gpointer user_data)
{
	NMConnectionList *list = user_data;

	if (response == GTK_RESPONSE_CANCEL)
		delete_ports_of_connection (list, nm_connection_editor_get_connection (editor));

	g_object_unref (editor);
}

static void
new_editor_cb (NMConnectionEditor *editor, NMConnectionEditor *new_editor, gpointer user_data)
{
	NMConnectionList *list = user_data;

	g_signal_emit (list, list_signals[NEW_EDITOR], 0, new_editor);
}

typedef struct {
	NMConnectionList *list;
	NMConnectionListCallbackFunc callback;
	gpointer user_data;
} ConnectionResultData;

static void
really_add_connection (FUNC_TAG_NEW_CONNECTION_RESULT_IMPL,
                       NMConnection *connection,
                       gpointer user_data)
{
	ConnectionResultData *data = user_data;
	NMConnectionList *list = data->list;
	NMConnectionListPrivate *priv = NM_CONNECTION_LIST_GET_PRIVATE (list);
	NMConnectionEditor *editor = NULL;

	if (!connection)
		goto out;

	if (connection_supports_proxy (connection) && !nm_connection_get_setting_proxy (connection))
		nm_connection_add_setting (connection, nm_setting_proxy_new ());
	if (connection_supports_ip4 (connection) && !nm_connection_get_setting_ip4_config (connection))
		nm_connection_add_setting (connection, nm_setting_ip4_config_new ());
	if (connection_supports_ip6 (connection) && !nm_connection_get_setting_ip6_config (connection))
		nm_connection_add_setting (connection, nm_setting_ip6_config_new ());

	editor = nm_connection_editor_new (GTK_WINDOW (list), connection, priv->client);
	if (!editor)
		goto out;

	g_signal_connect (editor, NM_CONNECTION_EDITOR_DONE, G_CALLBACK (add_response_cb), list);
	g_signal_connect (editor, NM_CONNECTION_EDITOR_NEW_EDITOR, G_CALLBACK (new_editor_cb), list);

	g_signal_emit (list, list_signals[NEW_EDITOR], 0, editor);

out:
	if (data->callback)
		data->callback (data->list, data->user_data);
	g_slice_free (ConnectionResultData, data);

	if (editor)
		nm_connection_editor_run (editor);
}

static void
add_clicked (GtkButton *button, gpointer user_data)
{
	nm_connection_list_add (user_data, NULL, NULL);
}

void
nm_connection_list_add (NMConnectionList *list,
                        NMConnectionListCallbackFunc callback,
                        gpointer user_data)
{
	NMConnectionListPrivate *priv;
	ConnectionResultData *data;

	g_return_if_fail (NM_IS_CONNECTION_LIST (list));
	priv = NM_CONNECTION_LIST_GET_PRIVATE (list);

	data = g_slice_new0 (ConnectionResultData);
	data->list = list;
	data->callback = callback;
	data->user_data = user_data;

	new_connection_dialog (GTK_WINDOW (list),
	                       priv->client,
	                       NULL,
	                       really_add_connection,
	                       data);
}

static void
edit_done_cb (NMConnectionEditor *editor, GtkResponseType response, gpointer user_data)
{
	NMConnectionList *list = user_data;

	if (response == GTK_RESPONSE_OK) {
		NMRemoteConnection *connection = NM_REMOTE_CONNECTION (nm_connection_editor_get_connection (editor));
		GtkTreeIter iter;

		if (get_iter_for_connection (list, connection, &iter))
			update_connection_row (list, &iter, connection);
	}

	g_object_unref (editor);
}

static void
edit_connection (NMConnectionList *list, NMConnection *connection)
{
	NMConnectionListPrivate *priv = NM_CONNECTION_LIST_GET_PRIVATE (list);
	NMConnectionEditor *editor;

	g_return_if_fail (connection != NULL);

	/* Don't allow two editors for the same connection */
	editor = nm_connection_editor_get (connection);
	if (editor) {
		nm_connection_editor_present (editor);
		return;
	}

	editor = nm_connection_editor_new (GTK_WINDOW (list),
	                                   NM_CONNECTION (connection),
	                                   priv->client);
	if (!editor)
		return;

	g_signal_connect (editor, NM_CONNECTION_EDITOR_DONE, G_CALLBACK (edit_done_cb), list);
	g_signal_connect (editor, NM_CONNECTION_EDITOR_NEW_EDITOR, G_CALLBACK (new_editor_cb), list);

	g_signal_emit (list, list_signals[NEW_EDITOR], 0, editor);
	nm_connection_editor_run (editor);
}

static void
do_edit (NMConnectionList *list)
{
	NMConnectionListPrivate *priv = NM_CONNECTION_LIST_GET_PRIVATE (list);

	if (gtk_widget_get_sensitive (priv->connection_edit))
		edit_connection (list, NM_CONNECTION (get_active_connection (priv->connection_list)));
}

static void
delete_connection_cb (FUNC_TAG_DELETE_CONNECTION_RESULT_IMPL,
                      NMRemoteConnection *connection,
                      gboolean deleted,
                      gpointer user_data)
{
	NMConnectionList *list = user_data;

	if (deleted)
		delete_ports_of_connection (list, NM_CONNECTION (connection));
}

static void
delete_clicked (GtkButton *button, gpointer user_data)
{
	NMConnectionList *list = user_data;
	NMConnectionListPrivate *priv = NM_CONNECTION_LIST_GET_PRIVATE (list);
	NMRemoteConnection *connection;

	connection = get_active_connection (priv->connection_list);
	g_return_if_fail (connection != NULL);

	delete_connection (GTK_WINDOW (list), connection,
	                   delete_connection_cb, list);
}

static void
selection_changed_cb (GtkTreeSelection *selection, gpointer user_data)
{
	NMConnectionList *list = user_data;
	NMConnectionListPrivate *priv = NM_CONNECTION_LIST_GET_PRIVATE (list);
	GtkTreeIter iter;
	GtkTreeModel *model;
	NMRemoteConnection *connection = NULL;
	NMSettingConnection *s_con;
	gboolean sensitive = FALSE;

	if (gtk_tree_selection_get_selected (selection, &model, &iter))
		connection = get_active_connection (priv->connection_list);

	if (connection) {
		s_con = nm_connection_get_setting_connection (NM_CONNECTION (connection));
		g_assert (s_con);

		sensitive = !nm_setting_connection_get_read_only (s_con);

		ce_polkit_set_widget_validation_error (priv->connection_edit,
		                                       sensitive ? NULL : _("Connection cannot be modified"));
		ce_polkit_set_widget_validation_error (priv->connection_del,
		                                       sensitive ? NULL : _("Connection cannot be deleted"));
	} else {
		ce_polkit_set_widget_validation_error (priv->connection_edit,
		                                       _("Select a connection to edit"));
		ce_polkit_set_widget_validation_error (priv->connection_del,
		                                       _("Select a connection to delete"));
	}
}

static gboolean
key_press_cb (GtkWidget *widget, GdkEvent *event, gpointer user_data)
{
	NMConnectionList *list = user_data;
	NMConnectionListPrivate *priv = NM_CONNECTION_LIST_GET_PRIVATE (list);
	GdkEventKey *key_event = (GdkEventKey *) event;

	if (gtk_search_bar_handle_event (priv->search_bar, event) == GDK_EVENT_STOP)
		return GDK_EVENT_STOP;

	if (key_event->keyval == GDK_KEY_Escape) {
		if (gtk_search_bar_get_search_mode (priv->search_bar))
			gtk_search_bar_set_search_mode (priv->search_bar, FALSE);
		else
			gtk_window_close (GTK_WINDOW (user_data));
		return GDK_EVENT_STOP;
	}

	return GDK_EVENT_PROPAGATE;
}

static gboolean
start_search (GtkTreeView *treeview, gpointer user_data)
{
	NMConnectionList *list = user_data;
	NMConnectionListPrivate *priv = NM_CONNECTION_LIST_GET_PRIVATE (list);

	gtk_search_bar_set_search_mode (priv->search_bar, TRUE);

	return TRUE;
}

static void
search_changed (GtkSearchEntry *entry, gpointer user_data)
{
	NMConnectionList *list = user_data;
	NMConnectionListPrivate *priv = NM_CONNECTION_LIST_GET_PRIVATE (list);

	gtk_tree_model_filter_refilter (priv->filter);
	gtk_tree_view_expand_all (priv->connection_list);
}

static void
nm_connection_list_init (NMConnectionList *list)
{
	gtk_widget_init_template (GTK_WIDGET (list));
}

static void
dispose (GObject *object)
{
	NMConnectionList *list = NM_CONNECTION_LIST (object);
	NMConnectionListPrivate *priv = NM_CONNECTION_LIST_GET_PRIVATE (list);

	g_clear_object (&priv->client);

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

static void
nm_connection_list_class_init (NMConnectionListClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

	/* virtual methods */
	object_class->dispose = dispose;

	/* Signals */
	list_signals[NEW_EDITOR] =
		g_signal_new (NM_CONNECTION_LIST_NEW_EDITOR,
		              G_OBJECT_CLASS_TYPE (object_class),
		              G_SIGNAL_RUN_FIRST,
		              0, NULL, NULL, NULL,
		              G_TYPE_NONE, 1, G_TYPE_POINTER);

	/* Initialize the widget template */
        gtk_widget_class_set_template_from_resource (widget_class,
	                                             "/org/gnome/nm_connection_editor/nm-connection-list.ui");

        gtk_widget_class_bind_template_child_private (widget_class, NMConnectionList, connection_list);
        gtk_widget_class_bind_template_child_private (widget_class, NMConnectionList, connection_add);
        gtk_widget_class_bind_template_child_private (widget_class, NMConnectionList, connection_del);
        gtk_widget_class_bind_template_child_private (widget_class, NMConnectionList, connection_edit);
        gtk_widget_class_bind_template_child_private (widget_class, NMConnectionList, search_bar);
        gtk_widget_class_bind_template_child_private (widget_class, NMConnectionList, search_entry);

        gtk_widget_class_bind_template_callback (widget_class, add_clicked);
        gtk_widget_class_bind_template_callback (widget_class, do_edit);
        gtk_widget_class_bind_template_callback (widget_class, delete_clicked);
        gtk_widget_class_bind_template_callback (widget_class, selection_changed_cb);
        gtk_widget_class_bind_template_callback (widget_class, key_press_cb);
        gtk_widget_class_bind_template_callback (widget_class, start_search);
        gtk_widget_class_bind_template_callback (widget_class, search_changed);
}

static void
column_header_clicked_cb (GtkTreeViewColumn *treeviewcolumn, gpointer user_data)
{
	gint sort_col_id = GPOINTER_TO_INT (user_data);

	gtk_tree_view_column_set_sort_column_id (treeviewcolumn, sort_col_id);
}

static gint
sort_connection_types (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer user_data)
{
	GtkTreeSortable *sortable = user_data;
	int order_a, order_b;
	GtkSortType order;

	gtk_tree_model_get (model, a, COL_ORDER, &order_a, -1);
	gtk_tree_model_get (model, b, COL_ORDER, &order_b, -1);

	/* The connection types should stay in the same order regardless of whether
	 * the table is sorted ascending or descending.
	 */
	gtk_tree_sortable_get_sort_column_id (sortable, NULL, &order);
	if (order == GTK_SORT_ASCENDING)
		return order_a - order_b;
	else
		return order_b - order_a;
}

static gint
id_sort_func (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer user_data)
{
	NMConnection *conn_a, *conn_b;
	gint ret;

	gtk_tree_model_get (model, a, COL_CONNECTION, &conn_a, -1);
	gtk_tree_model_get (model, b, COL_CONNECTION, &conn_b, -1);

	if (!conn_a || !conn_b) {
		g_assert (!conn_a && !conn_b);
		return sort_connection_types (model, a, b, user_data);
	}

	ret = strcmp (nm_connection_get_id (conn_a), nm_connection_get_id (conn_b));
	g_object_unref (conn_a);
	g_object_unref (conn_b);

	return ret;
}

static gint
timestamp_sort_func (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer user_data)
{
	NMConnection *conn_a, *conn_b;
	guint64 time_a, time_b;

	gtk_tree_model_get (model, a,
	                    COL_CONNECTION, &conn_a,
	                    COL_TIMESTAMP, &time_a,
	                    -1);
	gtk_tree_model_get (model, b,
	                    COL_CONNECTION, &conn_b,
	                    COL_TIMESTAMP, &time_b,
	                    -1);

	if (!conn_a || !conn_b) {
		g_assert (!conn_a && !conn_b);
		return sort_connection_types (model, a, b, user_data);
	}

	g_object_unref (conn_a);
	g_object_unref (conn_b);

	return time_b - time_a;
}

static gboolean
has_visible_children (NMConnectionList *self, GtkTreeModel *model, GtkTreeIter *parent)
{
	NMConnectionListPrivate *priv = NM_CONNECTION_LIST_GET_PRIVATE (self);
	GtkTreeIter iter;
	const char *search;
	char *id = NULL;

	if (!gtk_search_bar_get_search_mode (priv->search_bar))
		return gtk_tree_model_iter_has_child  (model, parent);

	if (!gtk_tree_model_iter_children (model, &iter, parent))
		return FALSE;

	search = gtk_entry_get_text (GTK_ENTRY (priv->search_entry));
	do {
		gtk_tree_model_get (model, &iter, COL_ID, &id, -1);
		if (!id) {
			/* gtk_tree_store_append() inserts an empty row, ignore
			 * it until it is fully populated. */
			continue;
		}
		if (strcasestr (id, search) != NULL) {
			g_free (id);
			return TRUE;
		}
		g_free (id);
	} while (gtk_tree_model_iter_next (model, &iter));

	return FALSE;
}

static gboolean
tree_model_visible_func (GtkTreeModel *model,
                         GtkTreeIter *iter,
                         gpointer user_data)
{
	NMConnectionList *self = user_data;
	NMConnectionListPrivate *priv = NM_CONNECTION_LIST_GET_PRIVATE (self);
	gs_unref_object NMConnection *connection = NULL;
	NMSettingConnection *s_con;
	const char *controller;
	const char *port_type;
	gs_free char *id = NULL;

	gtk_tree_model_get (model, iter,
	                    COL_ID, &id,
	                    COL_CONNECTION, &connection,
	                    -1);
	if (!connection) {
		/* Top-level type nodes are visible iff they have visible children */
		return has_visible_children (self, model, iter);
	}

	if (   gtk_search_bar_get_search_mode (priv->search_bar)
	    && strcasestr (id, gtk_entry_get_text (GTK_ENTRY (priv->search_entry))) == NULL)
		return FALSE;

	/* A connection node is visible unless it is a slave to a known
	 * bond or team or bridge.
	 */
	s_con = nm_connection_get_setting_connection (connection);
	if (   !s_con
	    || !nm_remote_connection_get_visible (NM_REMOTE_CONNECTION (connection)))
		return FALSE;

	controller = nm_setting_connection_get_master (s_con);
	if (!controller)
		return TRUE;
	port_type = nm_setting_connection_get_slave_type (s_con);
	if (   g_strcmp0 (port_type, NM_SETTING_BOND_SETTING_NAME) != 0
	    && g_strcmp0 (port_type, NM_SETTING_TEAM_SETTING_NAME) != 0
	    && g_strcmp0 (port_type, NM_SETTING_BRIDGE_SETTING_NAME) != 0)
		return TRUE;

	if (nm_client_get_connection_by_uuid (priv->client, controller))
		return FALSE;
	if (nm_connection_editor_get_controller (connection))
		return FALSE;

	/* FIXME: what if controller is an interface name */

	return TRUE;
}

static gboolean
connection_list_equal (GtkTreeModel *model, gint column, const gchar *key,
                       GtkTreeIter *iter, gpointer user_data)
{
	gs_free char *id = NULL;
	gs_unref_object NMConnection *connection = NULL;

	gtk_tree_model_get (model, iter,
	                    COL_ID, &id,
	                    COL_CONNECTION, &connection,
	                    -1);

	if (!connection)
		return TRUE;

	return strcasestr (id, key) == NULL;
}

static void
initialize_treeview (NMConnectionList *self)
{
	NMConnectionListPrivate *priv = NM_CONNECTION_LIST_GET_PRIVATE (self);
	GtkCellRenderer *renderer;
	GtkTreeViewColumn *column;
	GtkTreeSelection *selection;
	ConnectionTypeData *types;
	GtkTreeIter iter;
	char *id, *tmp;
	int i;

	/* Model */
	priv->model = GTK_TREE_MODEL (gtk_tree_store_new (8, G_TYPE_STRING,
	                                                     G_TYPE_STRING,
	                                                     G_TYPE_UINT64,
	                                                     G_TYPE_OBJECT,
	                                                     G_TYPE_GTYPE,
	                                                     G_TYPE_GTYPE,
	                                                     G_TYPE_GTYPE,
	                                                     G_TYPE_INT));

	/* Filter */
	priv->filter = GTK_TREE_MODEL_FILTER (gtk_tree_model_filter_new (priv->model, NULL));
	gtk_tree_model_filter_set_visible_func (priv->filter,
	                                        tree_model_visible_func,
	                                        self, NULL);

	/* Sortable */
	priv->sortable = GTK_TREE_SORTABLE (gtk_tree_model_sort_new_with_model (GTK_TREE_MODEL (priv->filter)));
	gtk_tree_sortable_set_default_sort_func (priv->sortable, NULL, NULL, NULL);
	gtk_tree_sortable_set_sort_func (priv->sortable, COL_TIMESTAMP, timestamp_sort_func,
	                                 priv->sortable, NULL);
	gtk_tree_sortable_set_sort_func (priv->sortable, COL_ID, id_sort_func,
	                                 priv->sortable, NULL);
	gtk_tree_sortable_set_sort_column_id (priv->sortable, COL_TIMESTAMP, GTK_SORT_ASCENDING);

	gtk_tree_view_set_model (priv->connection_list, GTK_TREE_MODEL (priv->sortable));
	gtk_tree_view_set_search_equal_func (priv->connection_list, connection_list_equal, NULL, NULL);
	gtk_tree_view_set_search_entry (priv->connection_list, priv->search_entry);

	/* Name column */
	renderer = gtk_cell_renderer_text_new ();
	column = gtk_tree_view_column_new_with_attributes (_("Name"),
	                                                   renderer,
	                                                   "markup", COL_ID,
	                                                   NULL);
	gtk_tree_view_column_set_expand (column, TRUE);
	gtk_tree_view_column_set_sort_column_id (column, COL_ID);
	g_signal_connect (column, "clicked", G_CALLBACK (column_header_clicked_cb), GINT_TO_POINTER (COL_ID));
	gtk_tree_view_append_column (priv->connection_list, column);

	/* Last Used column */
	renderer = g_object_new (GTK_TYPE_CELL_RENDERER_TEXT,
	                         "foreground", "SlateGray",
	                         NULL);
	column = gtk_tree_view_column_new_with_attributes (_("Last Used"),
	                                                   renderer,
	                                                   "text", COL_LAST_USED,
	                                                   NULL);
	gtk_tree_view_column_set_sort_column_id (column, COL_TIMESTAMP);
	g_signal_connect (column, "clicked", G_CALLBACK (column_header_clicked_cb), GINT_TO_POINTER (COL_TIMESTAMP));
	gtk_tree_view_append_column (priv->connection_list, column);

	/* Selection */
	selection = gtk_tree_view_get_selection (priv->connection_list);
	gtk_tree_selection_set_mode (selection, GTK_SELECTION_SINGLE);

	/* Fill in connection types */
	types = get_connection_type_list ();
	for (i = 0; types[i].name; i++) {

		tmp = g_markup_escape_text (types[i].name, -1);
		id = g_strdup_printf ("<b>%s</b>", tmp);
		g_free (tmp);

		gtk_tree_store_append (GTK_TREE_STORE (priv->model), &iter, NULL);
		gtk_tree_store_set (GTK_TREE_STORE (priv->model), &iter,
		                    COL_ID, id,
		                    COL_GTYPE0, types[i].setting_types[0],
		                    COL_GTYPE1, types[i].setting_types[1],
		                    COL_GTYPE2, types[i].setting_types[2],
		                    COL_ORDER, i,
		                    -1);
		g_free (id);
	}
}

static void
add_connection_buttons (NMConnectionList *self)
{
	NMConnectionListPrivate *priv = NM_CONNECTION_LIST_GET_PRIVATE (self);

	ce_polkit_connect_widget (priv->connection_edit,
	                          _("Edit the selected connection"),
	                          _("Authenticate to edit the selected connection"),
	                          priv->client,
	                          NM_CLIENT_PERMISSION_SETTINGS_MODIFY_SYSTEM);

	ce_polkit_connect_widget (priv->connection_del,
	                          _("Delete the selected connection"),
	                          _("Authenticate to delete the selected connection"),
	                          priv->client,
	                          NM_CLIENT_PERMISSION_SETTINGS_MODIFY_SYSTEM);
}

static void
connection_removed (NMClient *client,
                    NMRemoteConnection *connection,
                    gpointer user_data)
{
	NMConnectionList *self = NM_CONNECTION_LIST (user_data);
	NMConnectionListPrivate *priv = NM_CONNECTION_LIST_GET_PRIVATE (self);
	GtkTreeIter iter, parent_iter;

	if (get_iter_for_connection (self, connection, &iter)) {
		gtk_tree_model_iter_parent (priv->model, &parent_iter, &iter);
		gtk_tree_store_remove (GTK_TREE_STORE (priv->model), &iter);
	}
	gtk_tree_model_filter_refilter (priv->filter);
}

static void
connection_changed (NMRemoteConnection *connection, gpointer user_data)
{
	NMConnectionList *self = NM_CONNECTION_LIST (user_data);
	GtkTreeIter iter;

	if (   !nm_remote_connection_get_visible (connection)
	    || !nm_connection_get_setting_connection (NM_CONNECTION (connection))) {
		return;
	}

	if (get_iter_for_connection (self, connection, &iter))
		update_connection_row (self, &iter, connection);
}

static gboolean
get_parent_iter_for_connection (NMConnectionList *list,
                                NMRemoteConnection *connection,
                                GtkTreeIter *iter)
{
	NMConnectionListPrivate *priv = NM_CONNECTION_LIST_GET_PRIVATE (list);
	NMSettingConnection *s_con;
	const char *str_type;
	GType type, row_type0, row_type1, row_type2;

	s_con = nm_connection_get_setting_connection (NM_CONNECTION (connection));
	g_assert (s_con);
	str_type = nm_setting_connection_get_connection_type (s_con);
	if (!str_type) {
		g_warning ("Ignoring incomplete connection");
		return FALSE;
	}

	type = nm_setting_lookup_type (str_type);

	if (gtk_tree_model_get_iter_first (priv->model, iter)) {
		do {
			gtk_tree_model_get (priv->model, iter,
			                    COL_GTYPE0, &row_type0,
			                    COL_GTYPE1, &row_type1,
			                    COL_GTYPE2, &row_type2,
			                    -1);
			if (row_type0 == type || row_type1 == type || row_type2 == type)
				return TRUE;
		} while (gtk_tree_model_iter_next (priv->model, iter));
	}

	return FALSE;
}

static void
connection_added (NMClient *client,
                  NMRemoteConnection *connection,
                  gpointer user_data)
{
	NMConnectionList *self = NM_CONNECTION_LIST (user_data);
	NMConnectionListPrivate *priv = NM_CONNECTION_LIST_GET_PRIVATE (self);
	GtkTreeIter parent_iter, iter;
	NMSettingConnection *s_con;
	char *last_used, *id;
	gboolean expand = TRUE;

	if (!get_parent_iter_for_connection (self, connection, &parent_iter))
		return;

	s_con = nm_connection_get_setting_connection (NM_CONNECTION (connection));

	last_used = format_last_used (nm_setting_connection_get_timestamp (s_con));

	id = g_markup_escape_text (nm_setting_connection_get_id (s_con), -1);

	gtk_tree_store_append (GTK_TREE_STORE (priv->model), &iter, &parent_iter);
	gtk_tree_store_set (GTK_TREE_STORE (priv->model), &iter,
	                    COL_ID, id,
	                    COL_LAST_USED, last_used,
	                    COL_TIMESTAMP, nm_setting_connection_get_timestamp (s_con),
	                    COL_CONNECTION, connection,
	                    -1);

	g_free (id);
	g_free (last_used);

	if (priv->displayed_type) {
		GType added_type0, added_type1, added_type2;

		gtk_tree_model_get (priv->model, &parent_iter,
		                    COL_GTYPE0, &added_type0,
		                    COL_GTYPE1, &added_type1,
		                    COL_GTYPE2, &added_type2,
		                    -1);
		if (   added_type0 != priv->displayed_type
		    && added_type1 != priv->displayed_type
		    && added_type2 != priv->displayed_type)
			expand = FALSE;
	}

	if (expand) {
		GtkTreePath *path, *filtered_path;

		path = gtk_tree_model_get_path (priv->model, &parent_iter);
		filtered_path = gtk_tree_model_filter_convert_child_path_to_path (priv->filter, path);
		gtk_tree_view_expand_row (priv->connection_list, filtered_path, FALSE);
		gtk_tree_path_free (filtered_path);
		gtk_tree_path_free (path);
	}

	g_signal_connect (connection, NM_CONNECTION_CHANGED, G_CALLBACK (connection_changed), self);
	gtk_tree_model_filter_refilter (priv->filter);
}

NMConnectionList *
nm_connection_list_new (void)
{
	NMConnectionList *list;
	NMConnectionListPrivate *priv;
	GError *error = NULL;

	list = g_object_new (NM_TYPE_CONNECTION_LIST, NULL);
	if (!list)
		return NULL;

	priv = NM_CONNECTION_LIST_GET_PRIVATE (list);

	gtk_window_set_default_icon_name ("preferences-system-network");

	priv->client = nm_client_new (NULL, &error);
	if (!priv->client) {
		g_warning ("Couldn't construct the client instance: %s", error->message);
		g_error_free (error);
		goto error;
	}
	g_signal_connect (priv->client,
	                  NM_CLIENT_CONNECTION_ADDED,
	                  G_CALLBACK (connection_added),
	                  list);
	g_signal_connect (priv->client,
	                  NM_CLIENT_CONNECTION_REMOVED,
	                  G_CALLBACK (connection_removed),
	                  list);

	add_connection_buttons (list);
	initialize_treeview (list);

	if (nm_ce_keep_above)
		gtk_window_set_keep_above (GTK_WINDOW (list), TRUE);

	return list;

error:
	g_object_unref (list);
	return NULL;
}

void
nm_connection_list_set_type (NMConnectionList *self, GType ctype)
{
	NMConnectionListPrivate *priv;

	g_return_if_fail (NM_IS_CONNECTION_LIST (self));
	priv = NM_CONNECTION_LIST_GET_PRIVATE (self);

	priv->displayed_type = ctype;
}

void
nm_connection_list_create (NMConnectionList *list,
                           GType ctype,
                           const char *detail,
                           const char *import_filename,
                           NMConnectionListCallbackFunc callback,
                           gpointer user_data)
{
	NMConnectionListPrivate *priv;
	ConnectionTypeData *types;
	ConnectionResultData *data;
	gs_unref_object NMConnection *connection = NULL;
	gs_free_error GError *error = NULL;
	int i;

	g_return_if_fail (NM_IS_CONNECTION_LIST (list));
	priv = NM_CONNECTION_LIST_GET_PRIVATE (list);

	if (import_filename) {
		if (ctype == G_TYPE_INVALID) {
			connection = connection_import_from_file (import_filename, ctype, NULL, NULL);
			if (!connection) {
				g_set_error (&error, NMA_ERROR, NMA_ERROR_GENERIC,
				             _ ("Unrecognized connection type"));
			}
		} else if (NM_IN_SET (ctype, NM_TYPE_SETTING_VPN, NM_TYPE_SETTING_WIREGUARD)) {
			connection = connection_import_from_file (import_filename, ctype, detail, &error);
		} else {
			g_set_error (&error, NMA_ERROR, NMA_ERROR_GENERIC,
			             _("Don’t know how to import “%s” connections"), g_type_name (ctype));
		}

		if (!connection) {
			nm_connection_editor_error (NULL, _("Error importing connection"), "%s", error->message);
			callback (list, user_data);
			return;
		}

		if (nm_streq0 (nm_connection_get_connection_type (connection),
		               NM_SETTING_WIREGUARD_SETTING_NAME))
			ctype = NM_TYPE_SETTING_WIREGUARD;
		else {
			nm_assert (NM_IN_STRSET (nm_connection_get_connection_type (connection), NULL,
			                         NM_SETTING_VPN_SETTING_NAME));
			ctype = NM_TYPE_SETTING_VPN;
		}
	}

	if (ctype == G_TYPE_INVALID) {
		nm_connection_editor_error (NULL, _("Error creating connection"), _("Connection type not specified."));
		callback (list, user_data);
		return;
	}

	types = get_connection_type_list ();
	for (i = 0; types[i].name; i++) {
		if (   types[i].setting_types[0] == ctype
		    || types[i].setting_types[1] == ctype
		    || types[i].setting_types[2] == ctype)
			break;
	}

	if (!types[i].name) {
		if (ctype == NM_TYPE_SETTING_VPN) {
			nm_connection_editor_error (NULL, _("Error creating connection"),
			                            _("No VPN plugins are installed."));
		} else {
			nm_connection_editor_error (NULL, _("Error creating connection"),
			                            _("Don’t know how to create “%s” connections"), g_type_name (ctype));
		}

		callback (list, user_data);
		return;
	}

	data = g_slice_new0 (ConnectionResultData);
	data->list = list;
	data->callback = callback;
	data->user_data = user_data;

	new_connection_of_type (GTK_WINDOW (list),
	                        detail,
	                        NULL,
	                        connection,
	                        priv->client,
	                        types[i].new_connection_func,
	                        really_add_connection,
	                        data);
}

void
nm_connection_list_edit (NMConnectionList *self, const gchar *uuid)
{
	NMConnectionListPrivate *priv;
	NMConnection *connection;

	g_return_if_fail (NM_IS_CONNECTION_LIST (self));
	priv = NM_CONNECTION_LIST_GET_PRIVATE (self);

	connection = (NMConnection *) nm_client_get_connection_by_uuid (priv->client, uuid);
	if (!connection) {
		nm_connection_editor_error (NULL,
		                            _("Error editing connection"),
		                            _("Did not find a connection with UUID “%s”"), uuid);
		return;
	}

	edit_connection (self, connection);
}

void
nm_connection_list_present (NMConnectionList *list)
{
	NMConnectionListPrivate *priv;
	const GPtrArray *all_cons;
	GtkTreePath *path;
	GtkTreeIter iter;
	int i;

	g_return_if_fail (NM_IS_CONNECTION_LIST (list));
	priv = NM_CONNECTION_LIST_GET_PRIVATE (list);

	if (!priv->populated) {
		/* Fill the treeview initially */
		all_cons = nm_client_get_connections (priv->client);
		for (i = 0; i < all_cons->len; i++)
			connection_added (priv->client, all_cons->pdata[i], list);
		if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (priv->sortable), &iter)) {
			path = gtk_tree_model_get_path (GTK_TREE_MODEL (priv->sortable), &iter);
			gtk_tree_view_scroll_to_cell (priv->connection_list,
			                              path, NULL,
			                              FALSE, 0, 0);
			gtk_tree_path_free (path);
		}

		priv->populated = TRUE;
	}

	gtk_window_present (GTK_WINDOW (list));
}
