/* Copyright © 2018 Red Hat, Inc
 *
 * 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, see <http://www.gnu.org/licenses/>.
 *
 * Authors: Christian J. Kellner <ckellner@redhat.com>
 *
 */

#include <config.h>

#include <glib/gi18n.h>
#include <polkit/polkit.h>

#include "cc-bolt-device-dialog.h"
#include "cc-bolt-device-entry.h"
#include "shell/cc-application.h"

#include "bolt-client.h"
#include "bolt-names.h"
#include "bolt-str.h"

#include "cc-bolt-page.h"

struct _CcBoltPage {
    AdwNavigationPage parent;

    BoltClient *client;

    /* headerbar menu */
    GtkBox *headerbar_box;
    GtkLockButton *lock_button;

    /* main ui */
    GtkStack *container;

    /* empty state */
    AdwStatusPage *notb_page;

    /* notifications */
    AdwToastOverlay *toast_overlay;

    /* authmode */
    GtkSwitch *authmode_switch;
    AdwActionRow *direct_access_row;

    /* device list */
    GHashTable *devices;

    GtkStack *devices_stack;
    GtkBox *devices_box;
    GtkBox *pending_box;

    GtkListBox *devices_list;
    GtkListBox *pending_list;

    /* polkit integration */
    GPermission *permission;

    GCancellable *cancellable;
};

/* initialization */
static void bolt_client_ready (GObject *source, GAsyncResult *res, gpointer user_data);

/* panel functions */
static void cc_bolt_page_set_no_thunderbolt (CcBoltPage *self, const char *custom_msg);

static void cc_bolt_page_name_owner_changed (CcBoltPage *self);

static CcBoltDeviceEntry *cc_bolt_page_add_device (CcBoltPage *self, BoltDevice *dev);

static void cc_bolt_page_del_device_entry (CcBoltPage *self, CcBoltDeviceEntry *entry);

static void cc_bolt_page_authmode_sync (CcBoltPage *self);

static void cc_panel_list_box_migrate (CcBoltPage *self, GtkListBox *from, GtkListBox *to, CcBoltDeviceEntry *entry);

/* bolt client signals */
static void on_bolt_name_owner_changed_cb (GObject *object, GParamSpec *pspec, gpointer user_data);

static void on_bolt_device_added_cb (BoltClient *cli, const char *path, CcBoltPage *self);

static void on_bolt_device_removed_cb (BoltClient *cli, const char *opath, CcBoltPage *self);

static void on_bolt_notify_authmode_cb (GObject *gobject, GParamSpec *pspec, gpointer user_data);

/* panel signals */
static gboolean on_authmode_state_set_cb (CcBoltPage *self, gboolean state, GtkSwitch *toggle);

static void on_device_entry_row_activated_cb (CcBoltPage *self, GtkListBoxRow *row);

static void on_device_entry_status_changed_cb (CcBoltDeviceEntry *entry, BoltStatus new_status, CcBoltPage *self);
/* polkit */
static void on_permission_ready (GObject *source_object, GAsyncResult *res, gpointer user_data);

static void on_permission_notify_cb (GPermission *permission, GParamSpec *pspec, CcBoltPage *self);

G_DEFINE_FINAL_TYPE (CcBoltPage, cc_bolt_page, ADW_TYPE_NAVIGATION_PAGE)

static void
update_visibility (BoltClient *client, const char *path, gpointer user_data)
{
    g_autoptr(GPtrArray) devices = NULL;
    gboolean visible = FALSE;
    CcBoltPage *self;

    self = CC_BOLT_PAGE (user_data);

    if (client) {
        devices = bolt_client_list_devices (client, self->cancellable, NULL);
        if (devices)
            visible = devices->len > 0;
    }

    gtk_widget_set_visible (GTK_WIDGET (self), visible);
}

static void
on_visibility_client_ready (GObject *source, GAsyncResult *res, gpointer user_data)
{
    BoltClient *client;
    CcBoltPage *self;

    self = CC_BOLT_PAGE (user_data);

    client = bolt_client_new_finish (res, NULL);
    if (client == NULL) {
        gtk_widget_set_visible (GTK_WIDGET (self), FALSE);
        return;
    }

    g_signal_connect_object (client, "device-added", G_CALLBACK (update_visibility), self, 0);
    g_signal_connect_object (client, "device-removed", G_CALLBACK (update_visibility), self, 0);
    update_visibility (client, NULL, self);
}

static void
bolt_client_ready (GObject *source, GAsyncResult *res, gpointer user_data)
{
    g_autoptr(GError) err = NULL;
    g_autoptr(CcBoltPage) self = NULL;
    BoltClient *client;

    self = CC_BOLT_PAGE (user_data);
    client = bolt_client_new_finish (res, &err);

    if (client == NULL) {
        const char *text;

        /* operation got cancelled because the panel got destroyed */
        if (g_error_matches (err, G_IO_ERROR, G_IO_ERROR_CANCELLED)
            || g_error_matches (err, G_IO_ERROR, G_IO_ERROR_FAILED_HANDLED))
            return;

        g_warning ("Could not create client: %s", err->message);
        text = _("The Thunderbolt subsystem (boltd) is not installed or "
               "not set up properly.");

        adw_status_page_set_description (self->notb_page, text);
        gtk_stack_set_visible_child_name (self->container, "no-thunderbolt");

        return;
    }

    g_signal_connect_object (client, "notify::g-name-owner", G_CALLBACK (on_bolt_name_owner_changed_cb), self, 0);

    g_signal_connect_object (client, "device-added", G_CALLBACK (on_bolt_device_added_cb), self, 0);

    g_signal_connect_object (client, "device-removed", G_CALLBACK (on_bolt_device_removed_cb), self, 0);

    g_signal_connect_object (client, "notify::auth-mode", G_CALLBACK (on_bolt_notify_authmode_cb), self, 0);

    /* Treat security-level changes, which should rarely happen, as
     * if the name owner changed, i.e. as if boltd got restarted */
    g_signal_connect_object (client, "notify::security-level", G_CALLBACK (on_bolt_name_owner_changed_cb), self, 0);

    self->client = client;

    cc_bolt_page_authmode_sync (self);

    g_object_bind_property (self->authmode_switch, "active", self->devices_box, "sensitive",
                            G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE);

    g_object_bind_property (self->authmode_switch, "active", self->pending_box, "sensitive",
                            G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE);

    gtk_stack_set_visible_child_name (self->devices_stack, "no-devices");
    cc_bolt_page_name_owner_changed (self);
}

static gboolean
devices_table_transfer_entry (GHashTable *from, GHashTable *to, gconstpointer key)
{
    gpointer k, v;
    gboolean found;

    found = g_hash_table_lookup_extended (from, key, &k, &v);

    if (found) {
        g_hash_table_steal (from, key);
        g_hash_table_insert (to, k, v);
    }

    return found;
}

static void
devices_table_clear_entries (GHashTable *table, CcBoltPage *self)
{
    GHashTableIter iter;
    gpointer key, value;

    g_hash_table_iter_init (&iter, table);
    while (g_hash_table_iter_next (&iter, &key, &value)) {
        CcBoltDeviceEntry *entry = value;

        cc_bolt_page_del_device_entry (self, entry);
        g_hash_table_iter_remove (&iter);
    }
}

static void
devices_table_synchronize (CcBoltPage *self)
{
    g_autoptr(GHashTable) old = NULL;
    g_autoptr(GPtrArray) devices = NULL;
    g_autoptr(GError) err = NULL;
    guint i;

    devices = bolt_client_list_devices (self->client, self->cancellable, &err);

    if (!devices) {
        g_warning ("Could not list devices: %s", err->message);
        devices = g_ptr_array_new_with_free_func (g_object_unref);
    }

    old = self->devices;
    self->devices = g_hash_table_new (g_str_hash, g_str_equal);

    for (i = 0; i < devices->len; i++) {
        BoltDevice *dev = g_ptr_array_index (devices, i);
        const char *path;
        gboolean found;

        path = g_dbus_proxy_get_object_path (G_DBUS_PROXY (dev));
        found = devices_table_transfer_entry (old, self->devices, path);

        if (found)
            continue;

        cc_bolt_page_add_device (self, dev);
    }

    devices_table_clear_entries (old, self);
    gtk_stack_set_visible_child_name (self->container, "devices-listing");
}

static gboolean
list_box_sync_visible (GtkListBox *listbox)
{
    GtkWidget *child;
    gboolean show;

    child = gtk_widget_get_first_child (GTK_WIDGET (listbox));
    show = child != NULL;

    gtk_widget_set_visible (GTK_WIDGET (listbox), show);

    return show;
}

static GtkWidget *
cc_bolt_page_box_for_listbox (CcBoltPage *self, GtkListBox *lstbox)
{
    if ((gpointer) lstbox == self->devices_list)
        return GTK_WIDGET (self->devices_box);
    else if ((gpointer) lstbox == self->pending_list)
        return GTK_WIDGET (self->pending_box);

    g_return_val_if_reached (NULL);
}

static CcBoltDeviceEntry *
cc_bolt_page_add_device (CcBoltPage *self, BoltDevice *dev)
{
    CcBoltDeviceEntry *entry;
    BoltDeviceType type;
    BoltStatus status;
    const char *path;

    type = bolt_device_get_device_type (dev);

    if (type != BOLT_DEVICE_PERIPHERAL)
        return FALSE;

    entry = cc_bolt_device_entry_new (dev, FALSE);
    path = g_dbus_proxy_get_object_path (G_DBUS_PROXY (dev));

    /* add to the list box */
    status = bolt_device_get_status (dev);

    if (bolt_status_is_pending (status)) {
        gtk_list_box_append (self->pending_list, GTK_WIDGET (entry));
        gtk_widget_set_visible (GTK_WIDGET (self->pending_list), TRUE);
        gtk_widget_set_visible (GTK_WIDGET (self->pending_box), TRUE);
    } else {
        gtk_list_box_append (self->devices_list, GTK_WIDGET (entry));
        gtk_widget_set_visible (GTK_WIDGET (self->devices_list), TRUE);
        gtk_widget_set_visible (GTK_WIDGET (self->devices_box), TRUE);
    }

    g_signal_connect_object (entry, "status-changed", G_CALLBACK (on_device_entry_status_changed_cb), self, 0);

    gtk_stack_set_visible_child_name (self->devices_stack, "have-devices");
    g_hash_table_insert (self->devices, (gpointer) path, entry);

    return entry;
}

static void
cc_bolt_page_del_device_entry (CcBoltPage *self, CcBoltDeviceEntry *entry)
{
    GtkWidget *box;
    GtkWidget *p;
    gboolean show;

    p = gtk_widget_get_parent (GTK_WIDGET (entry));
    gtk_list_box_remove (GTK_LIST_BOX (p), GTK_WIDGET (entry));

    box = cc_bolt_page_box_for_listbox (self, GTK_LIST_BOX (p));
    show = list_box_sync_visible (GTK_LIST_BOX (p));
    gtk_widget_set_visible (box, show);

    if (!gtk_widget_is_visible (GTK_WIDGET (self->pending_list))
        && !gtk_widget_is_visible (GTK_WIDGET (self->devices_list))) {
        gtk_stack_set_visible_child_name (self->devices_stack, "no-devices");
    }
}

static void
cc_bolt_page_authmode_sync (CcBoltPage *self)
{
    BoltClient *client = self->client;
    BoltAuthMode mode;
    gboolean enabled;

    mode = bolt_client_get_authmode (client);
    enabled = (mode & BOLT_AUTH_ENABLED) != 0;

    g_signal_handlers_block_by_func (self->authmode_switch, on_authmode_state_set_cb, self);

    gtk_switch_set_active (self->authmode_switch, enabled);

    g_signal_handlers_unblock_by_func (self->authmode_switch, on_authmode_state_set_cb, self);

    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self->direct_access_row),
                                   enabled ? _("Allow direct access to devices such as docks and external GPUs.")
                                           : _("Only USB and Display Port devices can attach."));
}

static void
cc_panel_list_box_migrate (CcBoltPage *self, GtkListBox *from, GtkListBox *to, CcBoltDeviceEntry *entry)
{
    GtkWidget *from_box;
    GtkWidget *to_box;
    gboolean show;
    GtkWidget *target;

    target = GTK_WIDGET (entry);

    gtk_list_box_remove (from, target);
    gtk_list_box_append (to, target);
    gtk_widget_set_visible (GTK_WIDGET (to), TRUE);

    from_box = cc_bolt_page_box_for_listbox (self, from);
    to_box = cc_bolt_page_box_for_listbox (self, to);

    show = list_box_sync_visible (from);
    gtk_widget_set_visible (from_box, show);
    gtk_widget_set_visible (to_box, TRUE);
}

/* bolt client signals */
static void
cc_bolt_page_set_no_thunderbolt (CcBoltPage *self, const char *msg)
{
    if (!msg) {
        msg = _("Thunderbolt could not be detected.\n"
              "Either the system lacks Thunderbolt support, "
              "it has been disabled in the BIOS or is set to "
              "an unsupported security level in the BIOS.");
    }

    adw_status_page_set_description (self->notb_page, msg);
    gtk_stack_set_visible_child_name (self->container, "no-thunderbolt");
}

static void
cc_bolt_page_name_owner_changed (CcBoltPage *self)
{
    g_autofree char *name_owner = NULL;
    BoltClient *client = self->client;
    BoltSecurity sl;
    gboolean notb = TRUE;
    const char *text = NULL;

    name_owner = g_dbus_proxy_get_name_owner (G_DBUS_PROXY (self->client));

    if (name_owner == NULL) {
        cc_bolt_page_set_no_thunderbolt (self, NULL);
        devices_table_clear_entries (self->devices, self);
        gtk_widget_set_visible (GTK_WIDGET (self->headerbar_box), FALSE);
        return;
    }

    gtk_stack_set_visible_child_name (self->container, "loading");

    sl = bolt_client_get_security (client);

    switch (sl) {
    case BOLT_SECURITY_NONE:
    case BOLT_SECURITY_SECURE:
    case BOLT_SECURITY_USER:
        /* we fetch the device list and show them here */
        notb = FALSE;
        break;

    case BOLT_SECURITY_DPONLY:
    case BOLT_SECURITY_USBONLY:
        text = _("Thunderbolt support has been disabled in the BIOS.");
        break;

    case BOLT_SECURITY_UNKNOWN:
        text = _("Thunderbolt security level could not be determined.");
        break;
    }

    if (notb) {
        /* security level is unknown or un-handled */
        cc_bolt_page_set_no_thunderbolt (self, text);
        return;
    }

    if (self->permission) {
        gtk_widget_set_visible (GTK_WIDGET (self->headerbar_box), TRUE);
    } else {
        polkit_permission_new ("org.freedesktop.bolt.manage", NULL, self->cancellable, on_permission_ready,
                               g_object_ref (self));
    }

    devices_table_synchronize (self);
}

/* bolt client signals */
static void
on_bolt_name_owner_changed_cb (GObject *object, GParamSpec *pspec, gpointer user_data)
{
    cc_bolt_page_name_owner_changed (CC_BOLT_PAGE (user_data));
}

static void
on_bolt_device_added_cb (BoltClient *cli, const char *path, CcBoltPage *self)
{
    g_autoptr(GError) err = NULL;
    GDBusConnection *bus;
    BoltDevice *dev;
    gboolean found;

    found = g_hash_table_contains (self->devices, path);

    if (found)
        return;

    bus = g_dbus_proxy_get_connection (G_DBUS_PROXY (self->client));
    dev = bolt_device_new_for_object_path (bus, path, self->cancellable, &err);

    if (!dev) {
        g_warning ("Could not create proxy for %s", path);
        return;
    }

    cc_bolt_page_add_device (self, dev);
}

static void
on_bolt_device_removed_cb (BoltClient *cli, const char *path, CcBoltPage *self)
{
    CcBoltDeviceEntry *entry;

    entry = g_hash_table_lookup (self->devices, path);

    if (!entry)
        return;

    cc_bolt_page_del_device_entry (self, entry);
    g_hash_table_remove (self->devices, path);
}

static void
on_bolt_notify_authmode_cb (GObject *gobject, GParamSpec *pspec, gpointer user_data)
{
    cc_bolt_page_authmode_sync (CC_BOLT_PAGE (user_data));
}

/* panel signals */

static void
on_authmode_ready (GObject *source_object, GAsyncResult *res, gpointer user_data)
{
    g_autoptr(GError) error = NULL;
    BoltClient *client = BOLT_CLIENT (source_object);
    CcBoltPage *self;
    gboolean ok;

    ok = bolt_client_set_authmode_finish (client, res, &error);
    if (ok) {
        BoltAuthMode mode;
        gboolean enabled;

        self = CC_BOLT_PAGE (user_data);
        mode = bolt_client_get_authmode (client);
        enabled = (mode & BOLT_AUTH_ENABLED) != 0;
        gtk_switch_set_state (self->authmode_switch, enabled);
    } else {
        AdwToast *toast;

        g_warning ("Could not set authmode: %s", error->message);

        if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
            return;

        self = CC_BOLT_PAGE (user_data);
        toast = adw_toast_new_format (_("Error switching direct mode: %s"), error->message);
        adw_toast_overlay_add_toast (self->toast_overlay, toast);

        /* make sure we are reflecting the correct state */
        cc_bolt_page_authmode_sync (self);
    }

    gtk_widget_set_sensitive (GTK_WIDGET (self->authmode_switch), TRUE);
}

static gboolean
on_authmode_state_set_cb (CcBoltPage *self, gboolean enable, GtkSwitch *toggle)
{
    BoltClient *client = self->client;
    BoltAuthMode mode;

    gtk_widget_set_sensitive (GTK_WIDGET (self->authmode_switch), FALSE);

    mode = bolt_client_get_authmode (client);

    if (enable)
        mode = mode | BOLT_AUTH_ENABLED;
    else
        mode = mode & ~BOLT_AUTH_ENABLED;

    bolt_client_set_authmode_async (client, mode, NULL, on_authmode_ready, self);

    return TRUE;
}

static void
on_device_entry_row_activated_cb (CcBoltPage *self, GtkListBoxRow *row)
{
    g_autoptr(GPtrArray) parents = NULL;
    CcBoltDeviceEntry *entry;
    BoltDevice *device;
    BoltDevice *iter;
    const char *parent;
    CcBoltDeviceDialog *dialog;

    if (!CC_IS_BOLT_DEVICE_ENTRY (row))
        return;

    entry = CC_BOLT_DEVICE_ENTRY (row);
    device = cc_bolt_device_entry_get_device (entry);

    /* walk up the chain and collect all parents */
    parents = g_ptr_array_new_with_free_func (g_object_unref);
    iter = device;

    parent = bolt_device_get_parent (iter);
    while (parent != NULL) {
        g_autofree char *path = NULL;
        CcBoltDeviceEntry *child;
        BoltDevice *dev;

        path = bolt_gen_object_path (BOLT_DBUS_PATH_DEVICES, parent);

        /* NB: the host device is not a peripheral and thus not
         * in the hash table; therefore when get a NULL back, we
         * should have reached the end of the chain */
        child = g_hash_table_lookup (self->devices, path);
        if (!child)
            break;

        dev = cc_bolt_device_entry_get_device (child);
        g_ptr_array_add (parents, g_object_ref (dev));
        iter = dev;

        parent = bolt_device_get_parent (iter);
    }

    dialog = cc_bolt_device_dialog_new ();
    cc_bolt_device_dialog_set_client (dialog, self->client);
    cc_bolt_device_dialog_set_device (dialog, device, parents);

    adw_dialog_present (ADW_DIALOG (dialog), GTK_WIDGET (self));
}

static void
on_device_entry_status_changed_cb (CcBoltDeviceEntry *entry, BoltStatus new_status, CcBoltPage *self)
{
    GtkListBox *from = NULL;
    GtkListBox *to = NULL;
    GtkWidget *p;
    gboolean is_pending;
    gboolean parent_pending;

    /* if we are doing some active work, then lets not change
     * the list the entry is in; otherwise we might just hop
     * from one box to the other and back again.
     */
    if (new_status == BOLT_STATUS_CONNECTING || new_status == BOLT_STATUS_AUTHORIZING)
        return;

    is_pending = bolt_status_is_pending (new_status);

    p = gtk_widget_get_parent (GTK_WIDGET (entry));
    parent_pending = (gpointer) p == self->pending_list;

    /*  */
    if (is_pending && !parent_pending) {
        from = self->devices_list;
        to = self->pending_list;
    } else if (!is_pending && parent_pending) {
        from = self->pending_list;
        to = self->devices_list;
    }

    if (from && to)
        cc_panel_list_box_migrate (self, from, to, entry);
}

/* polkit */

static void
on_permission_ready (GObject *source_object, GAsyncResult *res, gpointer user_data)
{
    g_autoptr(CcBoltPage) self = user_data;
    g_autoptr(GError) err = NULL;
    GPermission *permission;
    gboolean is_allowed;
    const char *name;

    permission = polkit_permission_new_finish (res, &err);
    self->permission = permission;

    if (!self->permission) {
        g_warning ("Could not get polkit permissions: %s", err->message);
        return;
    }

    g_signal_connect_object (permission, "notify", G_CALLBACK (on_permission_notify_cb), self, G_CONNECT_AFTER);

    is_allowed = g_permission_get_allowed (permission);
    gtk_widget_set_sensitive (GTK_WIDGET (self->authmode_switch), is_allowed);
    gtk_lock_button_set_permission (self->lock_button, permission);

    name = gtk_stack_get_visible_child_name (self->container);

    gtk_widget_set_visible (GTK_WIDGET (self->headerbar_box), bolt_streq (name, "devices-listing"));
}

static void
on_permission_notify_cb (GPermission *permission, GParamSpec *pspec, CcBoltPage *self)
{
    gboolean is_allowed = g_permission_get_allowed (permission);

    gtk_widget_set_sensitive (GTK_WIDGET (self->authmode_switch), is_allowed);
}

static gint
device_entries_sort_by_recency_cb (GtkListBoxRow *a_row, GtkListBoxRow *b_row, gpointer user_data)
{
    CcBoltDeviceEntry *a_entry = CC_BOLT_DEVICE_ENTRY (a_row);
    CcBoltDeviceEntry *b_entry = CC_BOLT_DEVICE_ENTRY (b_row);
    BoltDevice *a = cc_bolt_device_entry_get_device (a_entry);
    BoltDevice *b = cc_bolt_device_entry_get_device (b_entry);
    BoltStatus status;
    gint64 a_ts, b_ts;
    gint64 score;

    a_ts = (gint64) bolt_device_get_timestamp (a);
    b_ts = (gint64) bolt_device_get_timestamp (b);

    score = b_ts - a_ts;

    if (score != 0)
        return score;

    status = bolt_device_get_status (a);

    if (bolt_status_is_connected (status)) {
        const char *a_path;
        const char *b_path;

        a_path = bolt_device_get_syspath (a);
        b_path = bolt_device_get_syspath (b);

        return g_strcmp0 (a_path, b_path);
    } else {
        const char *a_name;
        const char *b_name;

        a_name = bolt_device_get_name (a);
        b_name = bolt_device_get_name (b);

        return g_strcmp0 (a_name, b_name);
    }

    return 0;
}

static gint
device_entries_sort_by_syspath_cb (GtkListBoxRow *a_row, GtkListBoxRow *b_row, gpointer user_data)
{
    CcBoltDeviceEntry *a_entry = CC_BOLT_DEVICE_ENTRY (a_row);
    CcBoltDeviceEntry *b_entry = CC_BOLT_DEVICE_ENTRY (b_row);
    BoltDevice *a = cc_bolt_device_entry_get_device (a_entry);
    BoltDevice *b = cc_bolt_device_entry_get_device (b_entry);

    const char *a_path;
    const char *b_path;

    a_path = bolt_device_get_syspath (a);
    b_path = bolt_device_get_syspath (b);

    return g_strcmp0 (a_path, b_path);
}

/* GObject overrides */

static void
cc_bolt_page_finalize (GObject *object)
{
    CcBoltPage *self = CC_BOLT_PAGE (object);

    g_clear_object (&self->client);
    g_clear_pointer (&self->devices, g_hash_table_unref);
    g_clear_object (&self->permission);

    g_cancellable_cancel (self->cancellable);
    g_clear_object (&self->cancellable);

    G_OBJECT_CLASS (cc_bolt_page_parent_class)->finalize (object);
}

static void
cc_bolt_page_class_init (CcBoltPageClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS (klass);
    GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

    object_class->finalize = cc_bolt_page_finalize;

    gtk_widget_class_set_template_from_resource (widget_class,
                                                 "/org/gnome/control-center/privacy/bolt/cc-bolt-page.ui");

    gtk_widget_class_bind_template_child (widget_class, CcBoltPage, authmode_switch);
    gtk_widget_class_bind_template_child (widget_class, CcBoltPage, container);
    gtk_widget_class_bind_template_child (widget_class, CcBoltPage, devices_list);
    gtk_widget_class_bind_template_child (widget_class, CcBoltPage, devices_box);
    gtk_widget_class_bind_template_child (widget_class, CcBoltPage, devices_stack);
    gtk_widget_class_bind_template_child (widget_class, CcBoltPage, direct_access_row);
    gtk_widget_class_bind_template_child (widget_class, CcBoltPage, headerbar_box);
    gtk_widget_class_bind_template_child (widget_class, CcBoltPage, lock_button);
    gtk_widget_class_bind_template_child (widget_class, CcBoltPage, notb_page);
    gtk_widget_class_bind_template_child (widget_class, CcBoltPage, pending_box);
    gtk_widget_class_bind_template_child (widget_class, CcBoltPage, pending_list);
    gtk_widget_class_bind_template_child (widget_class, CcBoltPage, toast_overlay);

    gtk_widget_class_bind_template_callback (widget_class, on_authmode_state_set_cb);
    gtk_widget_class_bind_template_callback (widget_class, on_device_entry_row_activated_cb);
}

static void
cc_bolt_page_init (CcBoltPage *self)
{
    gtk_widget_init_template (GTK_WIDGET (self));

    /* Startup invisible as the visibility will be set as a result of BoltClient
     * succeeding its connection to boltd. */
    gtk_widget_set_visible (GTK_WIDGET (self), FALSE);

    self->cancellable = g_cancellable_new ();
    bolt_client_new_async (self->cancellable, on_visibility_client_ready, self);

    gtk_stack_set_visible_child_name (self->container, "loading");

    gtk_list_box_set_sort_func (self->devices_list, device_entries_sort_by_recency_cb, self, NULL);

    gtk_list_box_set_sort_func (self->pending_list, device_entries_sort_by_syspath_cb, self, NULL);

    self->devices = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, NULL);

    bolt_client_new_async (self->cancellable, bolt_client_ready, g_object_ref (self));
}

CcBoltPage *
cc_bolt_page_new (void)
{
    return g_object_new (CC_TYPE_BOLT_PAGE, NULL);
}
