/*
 * Copyright 2013 Canonical Ltd.
 * Copyright 2021 Robert Tari
 *
 * Authors:
 *   Charles Kerr <charles.kerr@canonical.com>
 *   Robert Tari <robert@tari.in>
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 3, as published
 * by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranties of
 * MERCHANTABILITY, SATISFACTORY QUALITY, 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/>.
 */

#include "dbus-user.h"

#include "users.h"

struct _IndicatorSessionUsersDbusPrivate
{
  Login1Manager * login1_manager;
  Login1Seat * login1_seat;
  DisplayManagerSeat * dm_seat;
  Accounts * accounts;

  /* hash table of int uids to UserRecord* */
  GHashTable * uid_to_account;

  /* a hashset of int uids of users who are logged in */
  GHashTable * logins;

  /* the user-id of the owner of the active session */
  guint active_uid;

  /* true if this is a live session */
  gboolean is_live;

  GCancellable * cancellable;

  guint update_list_tag;
};

typedef IndicatorSessionUsersDbusPrivate priv_t;

G_DEFINE_TYPE_WITH_PRIVATE(IndicatorSessionUsersDbus, indicator_session_users_dbus, INDICATOR_TYPE_SESSION_USERS)

/***
****
***/

struct UserRecord
{
  AccountsUser * user;

  gulong signal_id;
};

struct UserRecord *
user_record_new (AccountsUser * user, gulong signal_id)
{
  struct UserRecord * rec;
  rec = g_new (struct UserRecord, 1);
  rec->user = g_object_ref (user);
  rec->signal_id = signal_id;
  return rec;
}

static void
user_record_free (struct UserRecord * rec)
{
  g_signal_handler_disconnect (rec->user, rec->signal_id);
  g_object_unref (G_OBJECT (rec->user));
  g_free (rec);
}

/***
****
***/

/* get our private org.freedesktop.Accounts.User proxy for the given uid */
static AccountsUser *
get_user_for_uid (IndicatorSessionUsersDbus * self, guint uid)
{
  struct UserRecord * rec;

  if ((rec = g_hash_table_lookup (self->priv->uid_to_account,
                                  GUINT_TO_POINTER(uid))))
    return rec->user;

  return NULL;
}

static gboolean
is_tracked_uid (IndicatorSessionUsersDbus * self, guint uid)
{
  return get_user_for_uid (self, uid) != NULL;
}

static void
emit_user_added (IndicatorSessionUsersDbus * self, guint32 uid)
{
  if (is_tracked_uid (self, uid))
    indicator_session_users_added (INDICATOR_SESSION_USERS(self), uid);
}

static void
emit_user_changed (IndicatorSessionUsersDbus * self, guint32 uid)
{
  if (is_tracked_uid (self, uid))
    indicator_session_users_changed (INDICATOR_SESSION_USERS(self), uid);
}

static void
emit_user_removed (IndicatorSessionUsersDbus * self, guint32 uid)
{
  indicator_session_users_removed (INDICATOR_SESSION_USERS(self), uid);
}

/***
****
***/

static void
set_is_live_session_flag (IndicatorSessionUsersDbus * self, gboolean b)
{
  priv_t * p = self->priv;

  if (p->is_live != b)
    {
      p->is_live = b;

      indicator_session_users_notify_is_live_session (INDICATOR_SESSION_USERS (self));
    }
}

static void
set_active_uid (IndicatorSessionUsersDbus * self, guint uid)
{
  priv_t * p = self->priv;

  if (p->active_uid != uid)
    {
      const guint old_uid = p->active_uid;

      p->active_uid = uid;

      emit_user_changed (self, old_uid);
      emit_user_changed (self, uid);
    }
}

static void
set_logins (IndicatorSessionUsersDbus * self, GHashTable * logins)
{
  GHashTable * old_logins = self->priv->logins;
  gpointer uid;
  GHashTableIter iter;

  self->priv->logins = g_hash_table_ref (logins);

  /* fire 'user changed' event for users who logged out */
  g_hash_table_iter_init (&iter, old_logins);
  while ((g_hash_table_iter_next (&iter, &uid, NULL)))
    if (!g_hash_table_contains (logins, uid))
      emit_user_changed (self, GPOINTER_TO_UINT(uid));

  /* fire 'user changed' event for users who logged in */
  g_hash_table_iter_init (&iter, logins);
  while ((g_hash_table_iter_next (&iter, &uid, NULL)))
    if (!g_hash_table_contains (old_logins, uid))
      emit_user_changed (self, GPOINTER_TO_UINT(uid));

  g_hash_table_destroy (old_logins);
}

/***
****  User Account Tracking
***/

static void create_user_proxy_for_path (IndicatorSessionUsersDbus *, const char *);

/* called when a user proxy gets the 'Changed' signal */
static void
on_user_changed (AccountsUser * user, gpointer gself)
{
  /* Accounts.User doesn't update properties in the standard way,
   * so create a new proxy to pull in the new properties.
   * The older proxy is freed when it's replaced in our accounts hash */
  const char * path = g_dbus_proxy_get_object_path (G_DBUS_PROXY(user));
  create_user_proxy_for_path (gself, path);
}

static void
track_user (IndicatorSessionUsersDbus * self,
            AccountsUser              * user)
{
  const guint32 uid = accounts_user_get_uid (user);
  priv_t * p = self->priv;
  gulong id;
  const gboolean already_had_user = is_tracked_uid (self, uid);

  id  = g_signal_connect (user, "changed", G_CALLBACK(on_user_changed), self);
  g_hash_table_insert (p->uid_to_account,
                       GUINT_TO_POINTER (uid),
                       user_record_new (user, id));

  if (already_had_user)
    emit_user_changed (self, uid);
  else
    emit_user_added (self, uid);
}

static void
untrack_user (IndicatorSessionUsersDbus * self,
              const gchar               * path)
{
  guint uid;
  gpointer key;
  gpointer val;
  GHashTableIter iter;
  priv_t * p = self->priv;

  /* find the uid matching this object path */
  uid = 0;
  g_hash_table_iter_init (&iter, p->uid_to_account);
  while (!uid && g_hash_table_iter_next (&iter, &key, &val))
    {
      struct UserRecord * rec = val;
      GDBusProxy * proxy = G_DBUS_PROXY (rec->user);
      if (!g_strcmp0 (path, g_dbus_proxy_get_object_path (proxy)))
        uid = GPOINTER_TO_UINT (key);
    }

  if (uid)
    {
      g_hash_table_remove (p->uid_to_account, GUINT_TO_POINTER(uid));

      emit_user_removed (self, uid);
    }
}

/* We got a new org.freedesktop.Accounts.User proxy.
   If it's one we want to remember, pass it to track_user() */
static void
on_user_proxy_ready (GObject       * o G_GNUC_UNUSED,
                     GAsyncResult  * res,
                     gpointer        self)
{
  GError * err;
  AccountsUser * user;

  err = NULL;
  user = accounts_user_proxy_new_for_bus_finish (res, &err);
  if (err != NULL)
    {
      if (!g_error_matches (err, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        g_warning ("%s: %s", G_STRFUNC, err->message);

      g_error_free (err);
    }
  else
    {
      if (!accounts_user_get_system_account (user))
        track_user (self, user);

      g_object_unref (user);
    }
}

static void
create_user_proxy_for_path (IndicatorSessionUsersDbus * self,
                            const char                * path)
{
  accounts_user_proxy_new_for_bus (G_BUS_TYPE_SYSTEM,
                                   G_DBUS_PROXY_FLAGS_GET_INVALIDATED_PROPERTIES,
                                   "org.freedesktop.Accounts",
                                   path,
                                   self->priv->cancellable,
                                   on_user_proxy_ready, self);
}

/* create proxy objects for everything in Account's user-list */
static void
on_user_list_ready (GObject * o, GAsyncResult * res, gpointer gself)
{
  GError * err;
  gchar ** paths;

  err = NULL;
  paths = NULL;
  accounts_call_list_cached_users_finish (ACCOUNTS(o), &paths, res, &err);
  if (err != NULL)
    {
      if (!g_error_matches (err, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        g_warning ("%s %s: %s", G_STRLOC, G_STRFUNC, err->message);

      g_error_free (err);
    }
  else
    {
      int i;

      for (i=0; paths && paths[i]; ++i)
        create_user_proxy_for_path (gself, paths[i]);

      g_strfreev (paths);
    }
}

static void
set_account_manager (IndicatorSessionUsersDbus * self, Accounts * a)
{
  priv_t * p = self->priv;

  if (p->accounts != NULL)
    {
      g_signal_handlers_disconnect_by_data (p->accounts, self);
      g_clear_object (&p->accounts);
    }

  if (a != NULL)
    {
      p->accounts = g_object_ref (a);

      accounts_call_list_cached_users (a,
                                       self->priv->cancellable,
                                       on_user_list_ready,
                                       self);

      g_signal_connect_swapped (a, "user-added",
                                G_CALLBACK(create_user_proxy_for_path), self);

      g_signal_connect_swapped (a, "user-deleted",
                                G_CALLBACK(untrack_user), self);
    }
}

/***
****
***/

/* Based on the login1 manager's list of current sessions,
   update our 'logins', 'is_live', and 'active_uid' fields */
static void
on_login1_manager_session_list_ready (GObject      * o,
                                      GAsyncResult * res,
                                      gpointer       gself)
{
  GVariant * sessions;
  GError * err;

  sessions = NULL;
  err = NULL;
  login1_manager_call_list_sessions_finish (LOGIN1_MANAGER(o),
                                            &sessions,
                                            res,
                                            &err);

  if (err != NULL)
    {
      if (!g_error_matches (err, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        g_warning ("%s: %s", G_STRFUNC, err->message);

      g_error_free (err);
    }
  else
    {
      const gchar * const current_seat_id = g_getenv ("XDG_SEAT");
      const gchar * const current_session_id = g_getenv ("XDG_SESSION_ID");
      IndicatorSessionUsersDbus * self = INDICATOR_SESSION_USERS_DBUS (gself);
      const gchar * session_id = NULL;
      guint32 uid = 0;
      const gchar * user_name = NULL;
      const gchar * seat_id = NULL;
      const gchar * path = NULL;
      gboolean is_live_session = FALSE;
      GHashTable * logins = g_hash_table_new (g_direct_hash, g_direct_equal);
      GVariantIter iter;

      g_variant_iter_init (&iter, sessions);
      while (g_variant_iter_loop (&iter, "(&su&s&s&o)", &session_id,
                                                        &uid,
                                                        &user_name,
                                                        &seat_id,
                                                        &path))
        {
          /* only track sessions on our seat */
          if (g_strcmp0 (seat_id, current_seat_id))
            continue;

          if (!g_strcmp0 (session_id, current_session_id))
            {
              set_active_uid (self, uid);

              if ((uid==999) && !g_strcmp0 (user_name, "ayatana"))
                is_live_session = TRUE;
            }

          g_hash_table_add (logins, GINT_TO_POINTER(uid));
        }

      set_is_live_session_flag (self, is_live_session);
      set_logins (self, logins);

      g_hash_table_unref (logins);
      g_variant_unref (sessions);
    }
}

static void
update_session_list (IndicatorSessionUsersDbus * self)
{
  priv_t * p = self->priv;

  if (p->login1_manager != NULL)
    {
      login1_manager_call_list_sessions (p->login1_manager,
                                         p->cancellable,
                                         on_login1_manager_session_list_ready,
                                         self);
    }
}

static gboolean
on_update_session_list_timer (gpointer gself)
{
  IndicatorSessionUsersDbus * self = INDICATOR_SESSION_USERS_DBUS (gself);

  update_session_list (self);

  self->priv->update_list_tag = 0;
  return G_SOURCE_REMOVE;
}

/* A dead session can still show up in list-sessions for a few seconds.
   So just to be safe, queue up a rebuild for a few seconds from now */
static void
update_session_list_twice (IndicatorSessionUsersDbus * self)
{
  priv_t * p = self->priv;

  update_session_list (self);

  if (p->update_list_tag == 0)
    p->update_list_tag = g_timeout_add_seconds (5,
                                                on_update_session_list_timer,
                                                self);
}

static void
set_login1_manager (IndicatorSessionUsersDbus * self,
                    Login1Manager             * login1_manager)
{
  priv_t * p = self->priv;

  if (p->login1_manager != NULL)
    {
      g_signal_handlers_disconnect_by_data (p->login1_manager, self);

      g_clear_object (&p->login1_manager);
    }

  if (login1_manager != NULL)
    {
      p->login1_manager = g_object_ref (login1_manager);

      g_signal_connect_swapped (login1_manager, "session-new",
                                G_CALLBACK(update_session_list), self);
      g_signal_connect_swapped (login1_manager, "session-removed",
                                G_CALLBACK(update_session_list_twice), self);
      g_signal_connect_swapped (login1_manager, "user-new",
                                G_CALLBACK(update_session_list), self);
      g_signal_connect_swapped (login1_manager, "user-removed",
                                G_CALLBACK(update_session_list_twice), self);
      update_session_list (self);
    }
}

static void
set_login1_seat (IndicatorSessionUsersDbus * self,
                 Login1Seat                * login1_seat)
{
  priv_t * p = self->priv;

  if (p->login1_seat != NULL)
    {
      g_signal_handlers_disconnect_by_data (p->login1_seat, self);

      g_clear_object (&p->login1_seat);
    }

  if (login1_seat != NULL)
    {
      p->login1_seat = g_object_ref (login1_seat);

      g_signal_connect_swapped (login1_seat, "notify::active-session",
                                G_CALLBACK(update_session_list), self);
      update_session_list (self);
    }
}

static void
set_display_manager_seat (IndicatorSessionUsersDbus * self,
                          DisplayManagerSeat        * dm_seat)
{
  priv_t * p = self->priv;

  g_clear_object (&p->dm_seat);

  if (dm_seat != NULL)
    p->dm_seat = g_object_ref (dm_seat);
}

/***
****  IndicatorSessionUsers virtual functions
***/

/* switch to (or create) a session for the specified user */
static void
my_activate_user (IndicatorSessionUsers * users, guint uid)
{
  IndicatorSessionUsersDbus * self = INDICATOR_SESSION_USERS_DBUS(users);
  priv_t * p = self->priv;
  AccountsUser * au;
  const char * username;

  au = get_user_for_uid (self, uid);
  username = au ? accounts_user_get_user_name (au) : NULL;

  if (!username)
    {
      g_warning ("%s %s can't find user '%u'", G_STRLOC, G_STRFUNC, uid);
    }
  else
    {
      g_return_if_fail (p->dm_seat != NULL);

      display_manager_seat_call_switch_to_user (p->dm_seat,
                                                username,
                                                "",
                                                p->cancellable,
                                                NULL,
                                                NULL);
    }
}

/* returns true if this is a live session */
static gboolean
my_is_live_session (IndicatorSessionUsers * users)
{
  g_return_val_if_fail (INDICATOR_IS_SESSION_USERS_DBUS(users), FALSE);

  return INDICATOR_SESSION_USERS_DBUS(users)->priv->is_live;
}

/* get a list of our user ids */
static GList *
my_get_uids (IndicatorSessionUsers * users)
{
  IndicatorSessionUsersDbus * self = INDICATOR_SESSION_USERS_DBUS (users);
  return g_hash_table_get_keys (self->priv->uid_to_account);
}

/* build a new struct populated with info on the specified user */
static IndicatorSessionUser *
my_get_user (IndicatorSessionUsers * users, guint uid)
{
  IndicatorSessionUsersDbus * self = INDICATOR_SESSION_USERS_DBUS (users);
  priv_t * p = self->priv;
  IndicatorSessionUser * ret;
  AccountsUser * au;

  ret = NULL;
  au = get_user_for_uid (self, uid);
  if (au && !accounts_user_get_system_account(au))
    {
      g_assert (uid == accounts_user_get_uid (au));

      ret = g_new0 (IndicatorSessionUser, 1);
      ret->uid = uid;
      ret->user_name = g_strdup (accounts_user_get_user_name (au));
      ret->real_name = g_strdup (accounts_user_get_real_name (au));
      ret->icon_file = g_strdup (accounts_user_get_icon_file (au));
      ret->login_frequency = accounts_user_get_login_frequency (au);
      ret->is_logged_in = g_hash_table_contains (p->logins, GINT_TO_POINTER(uid));
      ret->is_current_user = uid == p->active_uid;
    }

  return ret;
}

/***
****  GObject virtual functions
***/

static void
my_dispose (GObject * o)
{
  IndicatorSessionUsersDbus * self = INDICATOR_SESSION_USERS_DBUS (o);
  priv_t * p = self->priv;

  if (p->update_list_tag != 0)
    {
      g_source_remove (p->update_list_tag);
      p->update_list_tag = 0;
    }

  if (p->cancellable)
    {
      g_cancellable_cancel (p->cancellable);
      g_clear_object (&p->cancellable);
    }

  set_account_manager (self, NULL);
  set_display_manager_seat (self, NULL);
  set_login1_seat (self, NULL);
  set_login1_manager (self, NULL);

  g_hash_table_remove_all (p->uid_to_account);

  G_OBJECT_CLASS (indicator_session_users_dbus_parent_class)->dispose (o);
}

static void
my_finalize (GObject * o)
{
  IndicatorSessionUsersDbus * self = INDICATOR_SESSION_USERS_DBUS (o);
  priv_t * p = self->priv;

  g_hash_table_destroy (p->logins);
  g_hash_table_destroy (p->uid_to_account);

  G_OBJECT_CLASS (indicator_session_users_dbus_parent_class)->finalize (o);
}

static void
indicator_session_users_dbus_class_init (IndicatorSessionUsersDbusClass * klass)
{
  GObjectClass * object_class;
  IndicatorSessionUsersClass * users_class;

  object_class = G_OBJECT_CLASS (klass);
  object_class->dispose = my_dispose;
  object_class->finalize = my_finalize;

  users_class = INDICATOR_SESSION_USERS_CLASS (klass);
  users_class->is_live_session = my_is_live_session;
  users_class->get_uids = my_get_uids;
  users_class->get_user = my_get_user;
  users_class->activate_user = my_activate_user;
}

static void
indicator_session_users_dbus_init (IndicatorSessionUsersDbus * self)
{
  priv_t * p;

  p = indicator_session_users_dbus_get_instance_private (self);
  self->priv = p;
  p->cancellable = g_cancellable_new ();

  p->uid_to_account = g_hash_table_new_full (g_direct_hash,
                                             g_direct_equal,
                                             NULL,
                                             (GDestroyNotify)user_record_free);

  p->logins = g_hash_table_new (g_direct_hash, g_direct_equal);
}

/***
****  Public
***/

IndicatorSessionUsers *
indicator_session_users_dbus_new (void)
{
  gpointer o = g_object_new (INDICATOR_TYPE_SESSION_USERS_DBUS, NULL);

  return INDICATOR_SESSION_USERS (o);
}

void
indicator_session_users_dbus_set_proxies (IndicatorSessionUsersDbus * self,
                                          Login1Manager             * login1_manager,
                                          Login1Seat                * login1_seat,
                                          DisplayManagerSeat        * dm_seat,
                                          Accounts                  * accounts)
{
  g_return_if_fail (INDICATOR_IS_SESSION_USERS_DBUS (self));

  set_login1_manager (self, login1_manager);
  set_login1_seat (self, login1_seat);
  set_display_manager_seat (self, dm_seat);
  set_account_manager (self, accounts);
}
