/* -*- 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 <string.h>
#include <glib/gi18n-lib.h>

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

#include "cc-online-accounts-panel.h"
#include "cc-online-account-provider-row.h"
#include "cc-online-account-row.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))
        {
          adw_toast_overlay_add_toast (ADW_TOAST_OVERLAY (self->toast_overlay),
                                       adw_toast_new (error->message));
        }

      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);
    }
}

static void
load_custom_css (void)
{
  g_autoptr (GtkCssProvider) provider = NULL;

  provider = gtk_css_provider_new ();
  gtk_css_provider_load_from_resource (provider, "/org/gnome/control-center/online-accounts/online-accounts.css");
  gtk_style_context_add_provider_for_display (gdk_display_get_default (),
                                              GTK_STYLE_PROVIDER (provider),
                                              GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
}

/* Callbacks */

static int
goa_provider_priority (const char *provider_type)
{
  static const char *goa_priority[] = {
    "owncloud",     /* Nextcloud */
    "google",       /* Google */
    "windows_live", /* Microsoft Personal */
    "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);

  load_custom_css ();

  /* 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));
}
