/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
/*
 * Copyright (C) 2011 - 2017 Red Hat, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General
 * Public License along with this library; if not, see <http://www.gnu.org/licenses/>.
 *
 * Author: David Zeuthen <davidz@redhat.com>
 */

#include "config.h"

#include <gio/gio.h>
#include <glib/gi18n-lib.h>
#include <string.h>

#define GOA_API_IS_SUBJECT_TO_CHANGE
#include <goa/goa.h>

#include "cc-online-account-provider-row.h"
#include "cc-online-account-row.h"
#include "cc-online-accounts-panel.h"
#include "cc-online-accounts-resources.h"

#define GOA_API_IS_SUBJECT_TO_CHANGE
#define GOA_BACKEND_API_IS_SUBJECT_TO_CHANGE
#include <goabackend/goabackend.h>

struct _CcOnlineAccountsPanel {
    CcPanel parent_instance;

    GtkFrame *accounts_frame;
    GtkListBox *accounts_listbox;
    AdwBanner *offline_banner;
    GtkListBox *providers_listbox;
    GtkWidget *toast_overlay;

    GoaClient *client;
    GVariant *parameters;
    GListStore *providers;
};

CC_PANEL_REGISTER (CcOnlineAccountsPanel, cc_online_accounts_panel);

enum {
    PROP_0,
    PROP_PARAMETERS
};

/* Rows methods */

typedef void (*RowForAccountCallback) (CcOnlineAccountsPanel *self, GtkWidget *row, GList *other_rows);

static void
remove_row_for_account_cb (CcOnlineAccountsPanel *self, GtkWidget *row, GList *other_rows)
{
    gtk_list_box_remove (self->accounts_listbox, row);
    gtk_widget_set_visible (GTK_WIDGET (self->accounts_frame), other_rows != NULL);
}

static void
modify_row_for_account (CcOnlineAccountsPanel *self, GoaObject *object, RowForAccountCallback callback)
{
    GtkWidget *child;
    GList *children = NULL;
    GList *l;

    for (child = gtk_widget_get_first_child (GTK_WIDGET (self->accounts_listbox)); child;
         child = gtk_widget_get_next_sibling (child)) {
        children = g_list_prepend (children, child);
    }

    children = g_list_reverse (children);

    for (l = children; l != NULL; l = l->next) {
        GoaObject *row_object;

        row_object = cc_online_account_row_get_object (CC_ONLINE_ACCOUNT_ROW (l->data));
        if (row_object == object) {
            GtkWidget *row = GTK_WIDGET (l->data);

            children = g_list_remove_link (children, l);
            callback (self, row, children);
            g_list_free (l);
            break;
        }
    }

    g_list_free (children);
}

static void
show_account_cb (GoaProvider *provider, GAsyncResult *result, CcOnlineAccountsPanel *self)
{
    g_autoptr(GError) error = NULL;

    if (!goa_provider_show_account_finish (provider, result, &error)) {
        if (!g_error_matches (error, GOA_ERROR, GOA_ERROR_DIALOG_DISMISSED)
            && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
            g_warning ("Error showing account: %s", error->message);
    }
}

static void
show_account (CcOnlineAccountsPanel *self, GoaObject *object)
{
    g_autoptr(GoaProvider) provider = NULL;
    GtkRoot *root;
    GoaAccount *account;
    const char *provider_type;

    /* Find the provider with a matching type */
    account = goa_object_peek_account (object);
    provider_type = goa_account_get_provider_type (account);
    provider = goa_provider_get_for_provider_type (provider_type);
    if (provider == NULL) {
        g_warning ("Error showing account: Unsupported provider");
        return;
    }

    root = gtk_widget_get_root (GTK_WIDGET (self));
    goa_provider_show_account (provider, self->client, object, GTK_WIDGET (root),
                               cc_panel_get_cancellable (CC_PANEL (self)), (GAsyncReadyCallback) show_account_cb, self);
}

static void
create_account_cb (GoaProvider *provider, GAsyncResult *result, CcOnlineAccountsPanel *self)
{
    g_autoptr(GoaObject) object = NULL;
    g_autoptr(GError) error = NULL;

    object = goa_provider_add_account_finish (provider, result, &error);
    if (error != NULL) {
        if (!g_error_matches (error, GOA_ERROR, GOA_ERROR_DIALOG_DISMISSED)
            && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
            AdwToast *toast = NULL;

            toast = adw_toast_new (error->message);
            adw_toast_set_use_markup (toast, FALSE);
            adw_toast_overlay_add_toast (ADW_TOAST_OVERLAY (self->toast_overlay), toast);
        }

        return;
    }

    g_debug ("Created account for \"%s\"", goa_account_get_identity (goa_object_peek_account (object)));
}

static void
create_account (CcOnlineAccountsPanel *self, GoaProvider *provider)
{
    GtkRoot *parent;

    g_return_if_fail (GOA_IS_PROVIDER (provider));

    parent = gtk_widget_get_root (GTK_WIDGET (self));
    goa_provider_add_account (provider, self->client, GTK_WIDGET (parent), cc_panel_get_cancellable (CC_PANEL (self)),
                              (GAsyncReadyCallback) create_account_cb, self);
}

static void
select_account_by_id (CcOnlineAccountsPanel *self, const gchar *account_id)
{
    GtkWidget *child;

    for (child = gtk_widget_get_first_child (GTK_WIDGET (self->accounts_listbox)); child;
         child = gtk_widget_get_next_sibling (child)) {
        GoaAccount *account;
        GoaObject *row_object;

        row_object = cc_online_account_row_get_object (CC_ONLINE_ACCOUNT_ROW (child));
        account = goa_object_peek_account (row_object);

        if (g_strcmp0 (goa_account_get_id (account), account_id) == 0) {
            show_account (self, row_object);
            break;
        }
    }
}

static void
command_add (CcOnlineAccountsPanel *self, GVariant *parameters)
{
    const gchar *provider_name = NULL;
    g_autoptr(GVariant) v = NULL;

    g_assert (self != NULL);
    g_assert (parameters != NULL);

    switch (g_variant_n_children (parameters)) {
    case 2:
        g_variant_get_child (parameters, 1, "v", &v);
        if (g_variant_is_of_type (v, G_VARIANT_TYPE_STRING))
            provider_name = g_variant_get_string (v, NULL);
        else
            g_warning ("Wrong type for the second argument (provider name) GVariant, expected 's' but got '%s'",
                       (gchar *) g_variant_get_type (v));
        break;

    default:
        g_warning ("Unexpected parameters found, ignore request");
        return;
    }

    if (provider_name != NULL) {
        g_autoptr(GoaProvider) provider = NULL;
        unsigned int n_items = 0;

        n_items = g_list_model_get_n_items (G_LIST_MODEL (self->providers));
        for (unsigned int i = 0; i < n_items; i++) {
            const char *provider_type = NULL;

            provider = g_list_model_get_item (G_LIST_MODEL (self->providers), i);
            provider_type = goa_provider_get_provider_type (provider);

            if (g_strcmp0 (provider_type, provider_name) == 0)
                break;

            g_clear_object (&provider);
        }

        if (provider == NULL) {
            g_warning ("Unable to get a provider for type '%s'", provider_name);
            return;
        }

        create_account (self, provider);
    }
}

/* Callbacks */

static int
goa_provider_priority (const char *provider_type)
{
    static const char *goa_priority[] = {
        "owncloud",  /* Nextcloud */
        "google",    /* Google */
        "ms_graph",  /* Microsoft 365 */
        "exchange",  /* Microsoft Exchange */
        "fedora",    /* Fedora */
        "imap_smtp", /* Email (IMAP and SMTP) */
        "webdav",    /* Calendars, Contacts, Files (WebDAV) */
        "kerberos",  /* Enterprise Login (Kerberos) */
    };

    for (size_t i = 0; i < G_N_ELEMENTS (goa_priority); i++) {
        if (g_str_equal (goa_priority[i], provider_type))
            return i;
    }

    /* New or unknown providers are sorted last */
    return G_N_ELEMENTS (goa_priority) + 1;
}

static GtkWidget *
provider_create_row (gpointer item, gpointer user_data)
{
    return (GtkWidget *) cc_online_account_provider_row_new (GOA_PROVIDER (item));
}

static int
sort_accounts_func (GtkListBoxRow *a, GtkListBoxRow *b, gpointer user_data)
{
    GoaAccount *a_account, *b_account;
    GoaObject *a_object, *b_object;
    const char *a_name, *b_name;

    a_object = cc_online_account_row_get_object (CC_ONLINE_ACCOUNT_ROW (a));
    a_account = goa_object_peek_account (a_object);
    a_name = goa_account_get_provider_type (a_account);

    b_object = cc_online_account_row_get_object (CC_ONLINE_ACCOUNT_ROW (b));
    b_account = goa_object_peek_account (b_object);
    b_name = goa_account_get_provider_type (b_account);

    return goa_provider_priority (a_name) - goa_provider_priority (b_name);
}

static int
sort_providers_func (GoaProvider *a, GoaProvider *b, gpointer user_data)
{
    const char *a_name = goa_provider_get_provider_type (a);
    const char *b_name = goa_provider_get_provider_type (b);

    return goa_provider_priority (a_name) - goa_provider_priority (b_name);
}

static void
add_account (CcOnlineAccountsPanel *self, GoaObject *object)
{
    CcOnlineAccountRow *row;

    row = cc_online_account_row_new (object);
    gtk_list_box_append (self->accounts_listbox, GTK_WIDGET (row));
    gtk_widget_set_visible (GTK_WIDGET (self->accounts_frame), TRUE);
}

static void
add_provider (CcOnlineAccountsPanel *self, GoaProvider *provider)
{
    g_list_store_insert_sorted (self->providers, provider, (GCompareDataFunc) sort_providers_func, self);
}

static void
on_account_added_cb (CcOnlineAccountsPanel *self, GoaObject *object)
{
    add_account (self, object);
}

static void
on_account_removed_cb (CcOnlineAccountsPanel *self, GoaObject *object)
{
    modify_row_for_account (self, object, remove_row_for_account_cb);
}

static void
on_accounts_listbox_row_activated (CcOnlineAccountsPanel *self, GtkListBoxRow *activated_row)
{
    GoaObject *object = cc_online_account_row_get_object (CC_ONLINE_ACCOUNT_ROW (activated_row));

    show_account (self, object);
}

static void
on_provider_row_activated_cb (CcOnlineAccountsPanel *self, GtkListBoxRow *activated_row)
{
    GoaProvider *provider =
        cc_online_account_provider_row_get_provider (CC_ONLINE_ACCOUNT_PROVIDER_ROW (activated_row));

    create_account (self, provider);
}

static void
goa_provider_get_all_cb (GObject *object, GAsyncResult *res, gpointer user_data)
{
    g_autoptr(CcOnlineAccountsPanel) self = CC_ONLINE_ACCOUNTS_PANEL (user_data);
    g_autolist(GoaProvider) providers = NULL;
    g_autolist(GoaAccount) accounts = NULL;
    g_autoptr(GError) error = NULL;

    /* goa_provider_get_all() doesn't have a cancellable argument, so check if
     * the panel cancellable was triggered.
     */
    if (g_cancellable_is_cancelled (cc_panel_get_cancellable (CC_PANEL (self))))
        return;

    if (!goa_provider_get_all_finish (&providers, res, &error)) {
        g_warning ("Error listing providers: %s", error->message);
        return;
    }

    for (const GList *iter = providers; iter != NULL; iter = iter->next)
        add_provider (self, GOA_PROVIDER (iter->data));

    /* Load existing accounts */
    accounts = goa_client_get_accounts (self->client);

    for (const GList *iter = accounts; iter != NULL; iter = iter->next)
        add_account (self, GOA_OBJECT (iter->data));

    g_signal_connect_swapped (self->client, "account-added", G_CALLBACK (on_account_added_cb), self);

    g_signal_connect_swapped (self->client, "account-removed", G_CALLBACK (on_account_removed_cb), self);

    /* With the client ready, check if we have a pending command */
    gtk_widget_set_sensitive (GTK_WIDGET (self), TRUE);

    if (self->parameters != NULL) {
        g_autoptr(GVariant) parameters = NULL;

        parameters = g_steal_pointer (&self->parameters);
        g_object_set (self, "parameters", parameters, NULL);
    }
}

static void
goa_client_new_cb (GObject *object, GAsyncResult *res, gpointer user_data)
{
    g_autoptr(CcOnlineAccountsPanel) self = CC_ONLINE_ACCOUNTS_PANEL (user_data);
    g_autoptr(GError) error = NULL;

    self->client = goa_client_new_finish (res, &error);
    if (self->client == NULL) {
        g_warning ("Error connect to service: %s", error->message);
        gtk_widget_set_sensitive (GTK_WIDGET (self), FALSE);
        return;
    }

    goa_provider_get_all (goa_provider_get_all_cb, g_object_ref (self));
}

/* CcPanel overrides */

static const char *
cc_online_accounts_panel_get_help_uri (CcPanel *panel)
{
    return "help:gnome-help/accounts";
}

/* GObject overrides */

static void
cc_online_accounts_panel_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec)
{
    CcOnlineAccountsPanel *self = CC_ONLINE_ACCOUNTS_PANEL (object);

    switch (property_id) {
    case PROP_PARAMETERS: {
        GVariant *parameters;
        g_autoptr(GVariant) v = NULL;
        const gchar *first_arg = NULL;

        parameters = g_value_get_variant (value);
        if (parameters == NULL)
            return;

        if (g_variant_n_children (parameters) > 0) {
            g_variant_get_child (parameters, 0, "v", &v);
            if (g_variant_is_of_type (v, G_VARIANT_TYPE_STRING))
                first_arg = g_variant_get_string (v, NULL);
            else
                g_warning ("Wrong type for the second argument GVariant, expected 's' but got '%s'",
                           (gchar *) g_variant_get_type (v));
        }

        /* Waiting for the client to load */
        if (self->client == NULL) {
            g_clear_pointer (&self->parameters, g_variant_unref);
            self->parameters = g_value_dup_variant (value);
        } else if (g_strcmp0 (first_arg, "add") == 0) {
            command_add (CC_ONLINE_ACCOUNTS_PANEL (object), parameters);
        } else if (first_arg != NULL) {
            select_account_by_id (CC_ONLINE_ACCOUNTS_PANEL (object), first_arg);
        }

        return;
    }
    }

    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}

static void
cc_online_accounts_panel_finalize (GObject *object)
{
    CcOnlineAccountsPanel *self = CC_ONLINE_ACCOUNTS_PANEL (object);

    g_clear_object (&self->client);
    g_clear_pointer (&self->parameters, g_variant_unref);
    g_clear_object (&self->providers);

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

static void
cc_online_accounts_panel_class_init (CcOnlineAccountsPanelClass *klass)
{
    GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
    GObjectClass *object_class = G_OBJECT_CLASS (klass);
    CcPanelClass *panel_class = CC_PANEL_CLASS (klass);

    panel_class->get_help_uri = cc_online_accounts_panel_get_help_uri;

    object_class->set_property = cc_online_accounts_panel_set_property;
    object_class->finalize = cc_online_accounts_panel_finalize;

    g_object_class_override_property (object_class, PROP_PARAMETERS, "parameters");

    gtk_widget_class_set_template_from_resource (
        widget_class, "/org/gnome/control-center/online-accounts/cc-online-accounts-panel.ui");

    gtk_widget_class_bind_template_child (widget_class, CcOnlineAccountsPanel, accounts_frame);
    gtk_widget_class_bind_template_child (widget_class, CcOnlineAccountsPanel, accounts_listbox);
    gtk_widget_class_bind_template_child (widget_class, CcOnlineAccountsPanel, offline_banner);
    gtk_widget_class_bind_template_child (widget_class, CcOnlineAccountsPanel, providers_listbox);
    gtk_widget_class_bind_template_child (widget_class, CcOnlineAccountsPanel, toast_overlay);

    gtk_widget_class_bind_template_callback (widget_class, on_accounts_listbox_row_activated);
    gtk_widget_class_bind_template_callback (widget_class, on_provider_row_activated_cb);
}

static void
cc_online_accounts_panel_init (CcOnlineAccountsPanel *self)
{
    GNetworkMonitor *monitor;

    g_resources_register (cc_online_accounts_get_resource ());

    gtk_widget_init_template (GTK_WIDGET (self));

    gtk_list_box_set_sort_func (self->accounts_listbox, sort_accounts_func, self, NULL);

    self->providers = g_list_store_new (GOA_TYPE_PROVIDER);
    gtk_list_box_bind_model (self->providers_listbox, G_LIST_MODEL (self->providers), provider_create_row, self, NULL);

    monitor = g_network_monitor_get_default ();
    g_object_bind_property (monitor, "network-available", self->offline_banner, "revealed",
                            G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN);

    g_object_bind_property (monitor, "network-available", self->providers_listbox, "sensitive", G_BINDING_SYNC_CREATE);

    /* Disable the panel while we wait for the client */
    gtk_widget_set_sensitive (GTK_WIDGET (self), FALSE);
    goa_client_new (cc_panel_get_cancellable (CC_PANEL (self)), goa_client_new_cb, g_object_ref (self));
}
