/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
 *
 * Copyright © 2020 Endless Mobile, Inc.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 *
 * 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:
 *  - Philip Withnall <withnall@endlessm.com>
 */

#include <gio/gio.h>
#include <glib.h>
#include <glib-object.h>
#include <glib/gi18n.h>
#include <gtk/gtk.h>
#include <libmalcontent/malcontent.h>

#include "user-image.h"
#include "user-selector.h"


static gint sort_users (gconstpointer a,
                        gconstpointer b,
                        gpointer      user_data);
static void reload_users (MctUserSelector *self);
static void notify_n_items_cb (GObject    *obj,
                               GParamSpec *pspec,
                               gpointer    user_data);
static void user_manager_notify_is_loaded_cb (GObject    *object,
                                              GParamSpec *pspec,
                                              void       *user_data);
static void user_added_cb (MctUserManager *user_manager,
                           MctUser        *user,
                           gpointer        user_data);
static void user_removed_cb (MctUserManager *user_manager,
                             MctUser        *user,
                             void           *user_data);
static void on_user_row_activated (MctUserSelector *self,
                                   AdwActionRow    *row);
static GtkWidget *create_user_row (gpointer item, gpointer user_data);


/**
 * MctUserSelector:
 *
 * A widget which lists available user accounts and allows the user to select
 * one.
 *
 * Since: 0.5.0
 */
struct _MctUserSelector
{
  GtkBox parent_instance;

  GtkListBox *user_list;

  GListStore *model /* (owned) */;

  MctUserManager *user_manager;  /* (owned) */
  MctManager *policy_manager; /* (owned) */
  MctUser *current_user;  /* (owned) (nullable) */
  MctUser *selected_user;  /* (owned) (nullable) */
  gboolean show_parents;
  guint n_users;
  GCancellable *cancellable;  /* (owned) */
};

G_DEFINE_TYPE (MctUserSelector, mct_user_selector, GTK_TYPE_BOX)

typedef enum
{
  PROP_SELECTED_USER = 1,
  PROP_POLICY_MANAGER,
  PROP_USER_MANAGER,
  PROP_CURRENT_USER,
  PROP_N_USERS,
  PROP_SHOW_PARENTS,
} MctUserSelectorProperty;

static GParamSpec *properties[PROP_SHOW_PARENTS + 1];

static void
mct_user_selector_constructed (GObject *obj)
{
  MctUserSelector *self = MCT_USER_SELECTOR (obj);

  /* Chain up. */
  G_OBJECT_CLASS (mct_user_selector_parent_class)->constructed (obj);

  /* The policy manager is mandatory and must have been loaded already. */
  g_assert (self->policy_manager != NULL);

  /* The user manager is mandatory and must have been loaded already. */
  g_assert (self->user_manager != NULL);

  self->model = g_list_store_new (MCT_TYPE_USER);
  gtk_list_box_bind_model (self->user_list,
                           G_LIST_MODEL (self->model),
                           (GtkListBoxCreateWidgetFunc)create_user_row,
                           self,
                           NULL);

  g_signal_connect_object (self->model, "notify::n-items",
                           G_CALLBACK (notify_n_items_cb),
                           self, G_CONNECT_DEFAULT);
  g_signal_connect (self->user_manager, "notify::is-loaded",
                    G_CALLBACK (user_manager_notify_is_loaded_cb), self);
  g_signal_connect (self->user_manager, "user-added",
                    G_CALLBACK (user_added_cb), self);
  g_signal_connect (self->user_manager, "user-removed",
                    G_CALLBACK (user_removed_cb), self);
}

static void
mct_user_selector_get_property (GObject    *object,
                                guint       prop_id,
                                GValue     *value,
                                GParamSpec *pspec)
{
  MctUserSelector *self = MCT_USER_SELECTOR (object);

  switch ((MctUserSelectorProperty) prop_id)
    {
    case PROP_SELECTED_USER:
      g_value_set_object (value, self->selected_user);
      break;

    case PROP_POLICY_MANAGER:
      g_value_set_object (value, self->policy_manager);
      break;

    case PROP_USER_MANAGER:
      g_value_set_object (value, self->user_manager);
      break;

    case PROP_CURRENT_USER:
      g_value_set_object (value, self->current_user);
      break;

    case PROP_N_USERS:
      g_value_set_uint (value, mct_user_selector_get_n_users (self));
      break;

    case PROP_SHOW_PARENTS:
      g_value_set_boolean (value, self->show_parents);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
mct_user_selector_set_property (GObject      *object,
                                guint         prop_id,
                                const GValue *value,
                                GParamSpec   *pspec)
{
  MctUserSelector *self = MCT_USER_SELECTOR (object);

  switch ((MctUserSelectorProperty) prop_id)
    {
    case PROP_SELECTED_USER:
      /* Currently read only */
      g_assert_not_reached ();
      break;

    case PROP_POLICY_MANAGER:
      /* Construct-only. May not be %NULL. */
      g_assert (self->policy_manager == NULL);
      self->policy_manager = g_value_dup_object (value);
      g_assert (self->policy_manager != NULL);
      break;

    case PROP_USER_MANAGER:
      g_assert (self->user_manager == NULL);
      self->user_manager = g_value_dup_object (value);
      break;

    case PROP_CURRENT_USER:
      mct_user_selector_set_current_user (self, g_value_get_object (value));
      break;

    case PROP_N_USERS:
      /* Currently read only */
      g_assert_not_reached ();
      break;

    case PROP_SHOW_PARENTS:
      self->show_parents = g_value_get_boolean (value);
      reload_users (self);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
mct_user_selector_dispose (GObject *object)
{
  MctUserSelector *self = (MctUserSelector *)object;

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

  g_clear_object (&self->model);
  g_clear_object (&self->selected_user);

  if (self->user_manager != NULL)
    {
      g_signal_handlers_disconnect_by_func (self->user_manager, user_removed_cb, self);
      g_signal_handlers_disconnect_by_func (self->user_manager, user_added_cb, self);
      g_signal_handlers_disconnect_by_func (self->user_manager, user_manager_notify_is_loaded_cb, self);

      g_clear_object (&self->user_manager);
    }

  g_clear_object (&self->policy_manager);

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

static void
mct_user_selector_class_init (MctUserSelectorClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

  object_class->constructed = mct_user_selector_constructed;
  object_class->get_property = mct_user_selector_get_property;
  object_class->set_property = mct_user_selector_set_property;
  object_class->dispose = mct_user_selector_dispose;

  /**
   * MctUserSelector:selected-user: (nullable)
   *
   * The currently selected user account.
   *
   * This may be `NULL` if no user is selected.
   *
   * Currently read only but may become writable in future.
   *
   * Since: 0.14.0
   */
  properties[PROP_SELECTED_USER] =
      g_param_spec_object ("selected-user", NULL, NULL,
                           MCT_TYPE_USER,
                           G_PARAM_READABLE |
                           G_PARAM_STATIC_STRINGS |
                           G_PARAM_EXPLICIT_NOTIFY);

  /**
   * MctUserSelector:policy-manager: (not nullable)
   *
   * The policy manager providing the data for the widget.
   *
   * Since: 0.14.0
   */
  properties[PROP_POLICY_MANAGER] =
      g_param_spec_object ("policy-manager", NULL, NULL,
                           MCT_TYPE_MANAGER,
                           G_PARAM_READWRITE |
                           G_PARAM_CONSTRUCT_ONLY |
                           G_PARAM_STATIC_STRINGS);

  /**
   * MctUserSelector:user-manager: (not nullable)
   *
   * The user manager providing the data for the widget.
   *
   * This must have already been asynchronously loaded using
   * [method@Malcontent.UserManager.load_async] before this widget is mapped.
   * This widget has no ‘loading’ view.
   *
   * Since: 0.14.0
   */
  properties[PROP_USER_MANAGER] =
      g_param_spec_object ("user-manager", NULL, NULL,
                           MCT_TYPE_USER_MANAGER,
                           G_PARAM_READWRITE |
                           G_PARAM_CONSTRUCT_ONLY |
                           G_PARAM_STATIC_STRINGS |
                           G_PARAM_EXPLICIT_NOTIFY);

  /**
   * MctUserSelector:current-user: (nullable)
   *
   * The user currently using the widget.
   *
   * This is used to display the correct family group. Typically it is the
   * result of calling:
   * ```c
   * mct_user_manager_get_user_by_uid (user_manager, getuid ())
   * ```
   *
   * It may be `NULL` if the user’s data is not yet known, but it must be set
   * before this widget is mapped. This widget has no ‘loading’ view.
   *
   * Since: 0.14.0
   */
  properties[PROP_CURRENT_USER] =
      g_param_spec_object ("current-user", NULL, NULL,
                           MCT_TYPE_USER,
                           G_PARAM_READWRITE |
                           G_PARAM_STATIC_STRINGS |
                           G_PARAM_EXPLICIT_NOTIFY);

  /**
   * MctUserSelector:show-parents:
   *
   * Whether to show parents in the list, or hide them.
   *
   * Since: 0.14.0
   */
  properties[PROP_SHOW_PARENTS] =
      g_param_spec_boolean ("show-parents", NULL, NULL,
                            TRUE,
                            G_PARAM_READWRITE |
                            G_PARAM_STATIC_STRINGS);

  /**
   * MctUserSelector:n-users:
   *
   * The number of users contained in this selector.
   *
   * Since: 0.14.0
   */
  properties[PROP_N_USERS] =
      g_param_spec_uint ("n-users",
                         NULL,
                         NULL,
                         0,
                         G_MAXUINT,
                         0,
                         G_PARAM_READABLE |
                         G_PARAM_STATIC_STRINGS |
                         G_PARAM_EXPLICIT_NOTIFY);

  g_object_class_install_properties (object_class, G_N_ELEMENTS (properties), properties);

  gtk_widget_class_set_template_from_resource (widget_class, "/org/freedesktop/MalcontentControl/ui/user-selector.ui");

  gtk_widget_class_bind_template_child (widget_class, MctUserSelector, user_list);

  gtk_widget_class_bind_template_callback (widget_class, on_user_row_activated);
}

static void
mct_user_selector_init (MctUserSelector *self)
{
  self->show_parents = TRUE;
  self->cancellable = g_cancellable_new ();

  gtk_widget_init_template (GTK_WIDGET (self));
}

static void
notify_n_items_cb (GObject    *obj,
                   GParamSpec *pspec,
                   gpointer    user_data)
{
  MctUserSelector *self = MCT_USER_SELECTOR (user_data);

  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_N_USERS]);
}

static void
user_manager_notify_is_loaded_cb (GObject    *object,
                                  GParamSpec *pspec,
                                  void       *user_data)
{
  MctUserSelector *self = MCT_USER_SELECTOR (user_data);

  reload_users (self);
}

static void
on_user_row_activated (MctUserSelector *self,
                       AdwActionRow    *row)
{
  MctUser *user;

  g_clear_object (&self->selected_user);

  user = g_object_get_data (G_OBJECT (row), "user");

  if (g_set_object (&self->selected_user, user))
    g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SELECTED_USER]);
}

static void
user_notify_display_name_cb (GObject    *object,
                             GParamSpec *pspec,
                             void       *user_data)
{
  MctUser *user = MCT_USER (object);
  AdwPreferencesRow *row = ADW_PREFERENCES_ROW (user_data);
  MctUserSelector *self;

  adw_preferences_row_set_title (ADW_PREFERENCES_ROW (row),
                                 mct_user_get_display_name (user));

  /* The new name might have changed the row’s position in the list, so re-sort
   * the list. */
  self = MCT_USER_SELECTOR (gtk_widget_get_ancestor (GTK_WIDGET (row), MCT_TYPE_USER_SELECTOR));
  if (self != NULL)
    g_list_store_sort (self->model, sort_users, self);
}

static GtkWidget *
create_user_row (gpointer item, gpointer user_data)
{
  MctUser *user;
  GtkWidget *row, *user_image;

  row = adw_action_row_new ();
  gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (row), TRUE);

  user = item;

  g_object_set_data_full (G_OBJECT (row), "user", g_object_ref (user), g_object_unref);

  adw_preferences_row_set_title (ADW_PREFERENCES_ROW (row),
                                 mct_user_get_display_name (user));
  g_signal_connect_object (user, "notify::display-name",
                           G_CALLBACK (user_notify_display_name_cb), row,
                           G_CONNECT_DEFAULT);

  user_image = mct_user_image_new ();
  mct_user_image_set_user (MCT_USER_IMAGE (user_image), user);
  gtk_widget_set_margin_top (user_image, 12);
  gtk_widget_set_margin_bottom (user_image, 12);
  adw_action_row_add_prefix (ADW_ACTION_ROW (row), user_image);
  adw_action_row_activate (ADW_ACTION_ROW (row));

  GtkWidget *arrow = gtk_image_new_from_icon_name ("go-next-symbolic");
  adw_action_row_add_suffix (ADW_ACTION_ROW (row), arrow);

  return row;
}

static gint
sort_users (gconstpointer a, gconstpointer b, gpointer user_data)
{
  MctUserSelector *self = MCT_USER_SELECTOR (user_data);
  MctUser *ua, *ub;
  gint result;

  ua = MCT_USER ((gpointer) a);
  ub = MCT_USER ((gpointer) b);

  /* Make sure the current user is shown first */
  if (self->current_user != NULL && mct_user_equal (ua, self->current_user))
    {
      result = G_MININT32;
    }
  else if (self->current_user != NULL && mct_user_equal (ub, self->current_user))
    {
      result = G_MAXINT32;
    }
  else
    {
      g_autofree gchar *name1 = NULL, *name2 = NULL;

      name1 = g_utf8_collate_key (mct_user_get_display_name (ua), -1);
      name2 = g_utf8_collate_key (mct_user_get_display_name (ub), -1);

      result = strcmp (name1, name2);
    }

  return result;
}

static void reload_users_cb (GObject      *object,
                             GAsyncResult *result,
                             void         *user_data);

static void
reload_users (MctUserSelector *self)
{
  if (!mct_user_manager_get_is_loaded (self->user_manager) ||
      self->current_user == NULL)
    return;

  mct_user_manager_get_family_members_for_user_async (self->user_manager,
                                                      self->current_user,
                                                      self->cancellable,
                                                      reload_users_cb,
                                                      self);
}

static void
reload_users_cb (GObject      *object,
                 GAsyncResult *result,
                 void         *user_data)
{
  MctUserManager *user_manager = MCT_USER_MANAGER (object);
  MctUserSelector *self = MCT_USER_SELECTOR (user_data);
  g_autoptr(MctUserArray) users = NULL;
  size_t n_users = 0;
  g_autoptr(GError) local_error = NULL;

  users = mct_user_manager_get_family_members_for_user_finish (user_manager, result, &n_users, &local_error);
  if (local_error != NULL)
    g_debug ("Error getting users: %s", local_error->message);
  else
    g_debug ("Got %zu users", n_users);

  g_list_store_remove_all (self->model);

  for (size_t i = 0; i < n_users; i++)
    {
      MctUser *user = users[i];
      user_added_cb (self->user_manager, user, self);
    }
}

static void
user_added_cb (MctUserManager *user_manager,
               MctUser        *user,
               void           *user_data)
{
  MctUserSelector *self = MCT_USER_SELECTOR (user_data);

  if (self->current_user != NULL &&
      !mct_user_is_in_same_family (user, self->current_user))
    {
      g_debug ("Ignoring user %s not in the same family as current user %s",
               mct_user_get_display_name (user), mct_user_get_display_name (self->current_user));
      return;
    }

  if (mct_user_get_user_type (user) == MCT_USER_TYPE_PARENT &&
      !self->show_parents)
    {
      g_debug ("Ignoring parent %s", mct_user_get_display_name (user));
      return;
    }

  g_debug ("User added: %u %s", (guint) mct_user_get_uid (user), mct_user_get_display_name (user));

  g_list_store_insert_sorted (self->model, user, sort_users, self);
}

static void
user_removed_cb (MctUserManager *user_manager,
                 MctUser        *user,
                 void           *user_data)
{
  MctUserSelector *self = MCT_USER_SELECTOR (user_data);

  reload_users (self);
}

/**
 * mct_user_selector_new:
 * @policy_manager: (transfer none): a policy manager to provide the policy data
 * @user_manager: (transfer none): a user manager to provide the user data
 *
 * Create a new [class@Malcontent.UserSelector] widget.
 *
 * Returns: (transfer full): a new user selector
 * Since: 0.14.0
 */
MctUserSelector *
mct_user_selector_new (MctManager     *policy_manager,
                       MctUserManager *user_manager)
{
  g_return_val_if_fail (MCT_IS_MANAGER (policy_manager), NULL);
  g_return_val_if_fail (MCT_IS_USER_MANAGER (user_manager), NULL);

  return g_object_new (MCT_TYPE_USER_SELECTOR,
                       "policy-manager", policy_manager,
                       "user-manager", user_manager,
                       NULL);
}

/**
 * mct_user_selector_get_selected_user:
 * @self: an #MctUserSelector
 *
 * Get the value of [property@Malcontent.UserSelector:selected-user].
 *
 * Returns: (transfer none) (nullable): the currently selected user, or `NULL`
 *   if no user is selected.
 * Since: 0.14.0
 */
MctUser *
mct_user_selector_get_selected_user (MctUserSelector *self)
{
  g_return_val_if_fail (MCT_IS_USER_SELECTOR (self), NULL);

  return self->selected_user;
}

/**
 * mct_user_selector_get_current_user:
 * @self: a user selector
 *
 * Get the value of [property@Malcontent.UserSelector:current-user].
 *
 * Returns: (transfer none) (nullable): the user currently using the widget, or
 *   `NULL` if not known
 * Since: 0.14.0
 */
MctUser *
mct_user_selector_get_current_user (MctUserSelector *self)
{
  g_return_val_if_fail (MCT_IS_USER_SELECTOR (self), NULL);

  return self->current_user;
}

/**
 * mct_user_selector_set_current_user:
 * @self: a user selector
 * @current_user: (transfer none) (nullable): the user currently using the
 *   widget, or `NULL` if not known
 *
 * Set the value of [property@Malcontent.UserSelector:current-user].
 *
 * Since: 0.14.0
 */
void
mct_user_selector_set_current_user (MctUserSelector *self,
                                    MctUser         *current_user)
{
  g_return_if_fail (MCT_IS_USER_SELECTOR (self));
  g_return_if_fail (current_user == NULL || MCT_IS_USER (current_user));

  if (g_set_object (&self->current_user, current_user))
    {
      reload_users (self);
      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_CURRENT_USER]);
    }
}

/**
 * mct_user_selector_select_user_by_username:
 * @self: a user selector
 * @user: the user to select
 *
 * Selects the given @user in the widget.
 *
 * This might fail if @user isn’t a valid user, or if they aren’t listed in the
 * selector due to being a parent (see
 * [property@Malcontent.UserSelector:show-parents]).
 *
 * Returns: true if the user was successfully selected, false otherwise
 * Since: 0.14.0
 */
gboolean
mct_user_selector_select_user (MctUserSelector *self,
                               MctUser         *user)
{
  g_return_val_if_fail (MCT_IS_USER_SELECTOR (self), FALSE);
  g_return_val_if_fail (MCT_IS_USER (user), FALSE);

  if (!g_list_store_find_with_equal_func (self->model, user, (GEqualFunc) mct_user_equal, NULL))
    return FALSE;

  if (g_set_object (&self->selected_user, user))
    g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SELECTED_USER]);

  return TRUE;
}

/**
 * mct_user_selector_get_n_users:
 * @self: a user selector
 *
 * Gets the number of users in @self.
 *
 * Returns: the number of users in @self
 * Since: 0.14.0
 */
size_t
mct_user_selector_get_n_users (MctUserSelector *self)
{
  g_return_val_if_fail (MCT_IS_USER_SELECTOR (self), 0);

  return g_list_model_get_n_items (G_LIST_MODEL (self->model));
}
