/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
 *
 * Copyright 2025 GNOME Foundation, Inc.
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 * 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.1 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * Authors:
 *  - Philip Withnall <pwithnall@gnome.org>
 */

#include "config.h"

#include <act/act.h>
#include <glib.h>
#include <glib-object.h>
#include <glib/gi18n-lib.h>
#include <gio/gio.h>
#include <libmalcontent/user.h>

#include "enums.h"
#include "user-private.h"


/**
 * MctUser:
 *
 * A user account on the system.
 *
 * Many of the properties on the account may change over time if the user
 * database is edited. If so, [signal@GObject.Object::notify] will be emitted
 * on them. [property@Malcontent.User:uid] and
 * [property@Malcontent.User:username] cannot change after construction.
 *
 * Since: 0.14.0
 */
struct _MctUser
{
  GObject parent_instance;

  ActUser *user;  /* (owned) (not nullable) */
  unsigned long user_changed_id;
};

G_DEFINE_TYPE (MctUser, mct_user, G_TYPE_OBJECT)

typedef enum
{
  PROP_UID = 1,
  PROP_USERNAME,
  PROP_REAL_NAME,
  PROP_DISPLAY_NAME,
  PROP_USER_TYPE,
  PROP_ICON_PATH,
  PROP_LOGIN_TIME,
  PROP_LOCALE,
} MctUserProperty;

static GParamSpec *props[PROP_LOCALE + 1] = { NULL, };

static void
mct_user_init (MctUser *self)
{
  /* Nothing to do here. */
}

static void
mct_user_get_property (GObject    *object,
                       guint       property_id,
                       GValue     *value,
                       GParamSpec *spec)
{
  MctUser *self = MCT_USER (object);

  switch ((MctUserProperty) property_id)
    {
    case PROP_UID:
      g_value_set_uint (value, mct_user_get_uid (self));
      break;
    case PROP_USERNAME:
      g_value_set_string (value, mct_user_get_username (self));
      break;
    case PROP_REAL_NAME:
      g_value_set_string (value, mct_user_get_real_name (self));
      break;
    case PROP_DISPLAY_NAME:
      g_value_set_string (value, mct_user_get_display_name (self));
      break;
    case PROP_USER_TYPE:
      g_value_set_enum (value, mct_user_get_user_type (self));
      break;
    case PROP_ICON_PATH:
      g_value_set_string (value, mct_user_get_icon_path (self));
      break;
    case PROP_LOGIN_TIME:
      g_value_set_uint64 (value, mct_user_get_login_time (self));
      break;
    case PROP_LOCALE:
      g_value_set_string (value, mct_user_get_locale (self));
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, spec);
      break;
    }
}

static void
mct_user_set_property (GObject      *object,
                       guint         property_id,
                       const GValue *value,
                       GParamSpec   *spec)
{
  switch ((MctUserProperty) property_id)
    {
    case PROP_UID:
    case PROP_USERNAME:
    case PROP_REAL_NAME:
    case PROP_DISPLAY_NAME:
    case PROP_USER_TYPE:
    case PROP_ICON_PATH:
    case PROP_LOGIN_TIME:
    case PROP_LOCALE:
      /* Read-only. */
      g_assert_not_reached ();
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, spec);
      break;
    }
}

static void
mct_user_dispose (GObject *object)
{
  MctUser *self = MCT_USER (object);

  g_clear_signal_handler (&self->user_changed_id, self->user);
  g_clear_object (&self->user);

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

static void
mct_user_class_init (MctUserClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->dispose = mct_user_dispose;
  object_class->get_property = mct_user_get_property;
  object_class->set_property = mct_user_set_property;

  /**
   * MctUser:uid:
   *
   * The user’s UID.
   *
   * This will not change after the [class@Malcontent.User] is constructed.
   *
   * Since: 0.14.0
   */
  props[PROP_UID] = g_param_spec_uint ("uid",
                                       NULL, NULL,
                                       0, G_MAXUINT, 0,
                                       G_PARAM_READABLE |
                                       G_PARAM_STATIC_STRINGS |
                                       G_PARAM_EXPLICIT_NOTIFY);

  /**
   * MctUser:username:
   *
   * The user’s username.
   *
   * This will not change after the [class@Malcontent.User] is constructed.
   *
   * Since: 0.14.0
   */
  props[PROP_USERNAME] = g_param_spec_string ("username",
                                              NULL, NULL,
                                              "unknown",
                                              G_PARAM_READABLE |
                                              G_PARAM_STATIC_STRINGS |
                                              G_PARAM_EXPLICIT_NOTIFY);

  /**
   * MctUser:real-name: (nullable)
   *
   * The user’s real name.
   *
   * This may be `NULL` if not set on the system. If you need to use a
   * non-`NULL` string for display purposes, see
   * [property@Malcontent.User:display-name].
   *
   * Since: 0.14.0
   */
  props[PROP_REAL_NAME] = g_param_spec_string ("real-name",
                                               NULL, NULL,
                                               "unknown",
                                               G_PARAM_READABLE |
                                               G_PARAM_STATIC_STRINGS |
                                               G_PARAM_EXPLICIT_NOTIFY);

  /**
   * MctUser:display-name: (not nullable)
   *
   * The user’s real name or username if the real name is unset.
   *
   * This is guaranteed to not be `NULL`, and should be used in user interfaces
   * when you need a non-`NULl` human readable name for a user.
   *
   * Since: 0.14.0
   */
  props[PROP_DISPLAY_NAME] = g_param_spec_string ("display-name",
                                                  NULL, NULL,
                                                  "unknown",
                                                  G_PARAM_READABLE |
                                                  G_PARAM_STATIC_STRINGS |
                                                  G_PARAM_EXPLICIT_NOTIFY);

  /**
   * MctUser:user-type:
   *
   * Type of the user.
   *
   * Since: 0.14.0
   */
  props[PROP_USER_TYPE] = g_param_spec_enum ("user-type",
                                             NULL, NULL,
                                             MCT_TYPE_USER_TYPE,
                                             MCT_USER_TYPE_UNKNOWN,
                                             G_PARAM_READABLE |
                                             G_PARAM_STATIC_STRINGS |
                                             G_PARAM_EXPLICIT_NOTIFY);

  /**
   * MctUser:icon-path: (type filename) (nullable)
   *
   * Path to the user’s icon/avatar image.
   *
   * This may be `NULL` if not set on the system.
   *
   * Since: 0.14.0
   */
  props[PROP_ICON_PATH] = g_param_spec_string ("icon-path",
                                               NULL, NULL,
                                               NULL,
                                               G_PARAM_READABLE |
                                               G_PARAM_STATIC_STRINGS |
                                               G_PARAM_EXPLICIT_NOTIFY);

  /**
   * MctUser:login-time:
   *
   * The last login time for this user, in seconds since the Unix epoch.
   *
   * If the user has never logged in, this will be zero.
   *
   * Since: 0.14.0
   */
  props[PROP_LOGIN_TIME] = g_param_spec_uint64 ("login-time",
                                                NULL, NULL,
                                                0, G_MAXUINT64, 0,
                                                G_PARAM_READABLE |
                                                G_PARAM_STATIC_STRINGS |
                                                G_PARAM_EXPLICIT_NOTIFY);


  /**
   * MctUser:locale: (nullable)
   *
   * The user’s login locale.
   *
   * This is in the format `language[_territory][.codeset][@modifier]`, where
   * `language` is an ISO 639 language code, `territory` is an ISO 3166 country
   * code, and `codeset` is a character set or encoding identifier like
   * `ISO-8859-1` or `UTF-8`; as specified by
   * [`setlocale(3)`](man:setlocale(3)).
   *
   * This may be `NULL` if not set on the system. If it’s the empty string
   * (`""`) then the user is using the system default locale.
   *
   * Since: 0.14.0
   */
  props[PROP_LOCALE] = g_param_spec_string ("locale",
                                            NULL, NULL,
                                            NULL,
                                            G_PARAM_READABLE |
                                            G_PARAM_STATIC_STRINGS |
                                            G_PARAM_EXPLICIT_NOTIFY);

  g_object_class_install_properties (object_class,
                                     G_N_ELEMENTS (props),
                                     props);
}

static void
user_changed_cb (ActUser *act_user,
                 void    *user_data)
{
  MctUser *self = MCT_USER (user_data);

  /* Unfortunately accountssservice doesn’t give more detail about *what*
   * changed, so notify for all the properties which could possibly change.
   * UID and username cannot change. */
  g_object_freeze_notify (G_OBJECT (self));
  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_REAL_NAME]);
  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DISPLAY_NAME]);
  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_USER_TYPE]);
  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_PATH]);
  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_LOGIN_TIME]);
  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_LOCALE]);
  g_object_thaw_notify (G_OBJECT (self));
}

/**
 * mct_user_new_from_act_user:
 * @act_user: an AccountsService user
 *
 * Create a new [class@Malcontent.User] from the given @act_user.
 *
 * This is an internal constructor.
 *
 * Returns: (transfer full) (not nullable): a new user object
 * Since: 0.14.0
 */
MctUser *
mct_user_new_from_act_user (ActUser *act_user)
{
  g_autoptr(MctUser) user = NULL;

  g_return_val_if_fail (ACT_IS_USER (act_user), NULL);

  user = g_object_new (MCT_TYPE_USER, NULL);
  user->user = g_object_ref (act_user);
  user->user_changed_id = g_signal_connect (user->user, "changed",
                                            G_CALLBACK (user_changed_cb),
                                            user);

  return g_steal_pointer (&user);
}

/**
 * mct_user_get_uid:
 * @self: a user
 *
 * Get the value of the [property@Malcontent.User:uid] property.
 *
 * Returns: the user’s ID
 * Since: 0.14.0
 */
uid_t
mct_user_get_uid (MctUser *self)
{
  g_return_val_if_fail (MCT_IS_USER (self), (uid_t) -1);

  return act_user_get_uid (self->user);
}

/**
 * mct_user_get_username:
 * @self: a user
 *
 * Get the value of the [property@Malcontent.User:username] property.
 *
 * Returns: (not nullable): the user’s username
 * Since: 0.14.0
 */
const char *
mct_user_get_username (MctUser *self)
{
  g_return_val_if_fail (MCT_IS_USER (self), "unknown");

  return act_user_get_user_name (self->user);
}

/**
 * mct_user_get_real_name:
 * @self: a user
 *
 * Get the value of the [property@Malcontent.User:real-name] property.
 *
 * Returns: (nullable): the user’s real name, or `NULL` if not set
 * Since: 0.14.0
 */
const char *
mct_user_get_real_name (MctUser *self)
{
  g_return_val_if_fail (MCT_IS_USER (self), NULL);

  return act_user_get_real_name (self->user);
}

/**
 * mct_user_get_display_name:
 * @self: a user
 *
 * Get the value of the [property@Malcontent.User:display-name] property.
 *
 * Returns: (not nullable): the user’s display name
 * Since: 0.14.0
 */
const char *
mct_user_get_display_name (MctUser *self)
{
  const char *name;

  g_return_val_if_fail (MCT_IS_USER (self), NULL);

  name = mct_user_get_real_name (self);
  if (name != NULL)
    return name;

  return mct_user_get_username (self);
}

/**
 * mct_user_get_user_type:
 * @self: a user
 *
 * Get the value of the [property@Malcontent.User:user-type] property.
 *
 * Returns: the user’s account type
 * Since: 0.14.0
 */
MctUserType
mct_user_get_user_type (MctUser *self)
{
  g_return_val_if_fail (MCT_IS_USER (self), MCT_USER_TYPE_UNKNOWN);

  if (act_user_is_system_account (self->user))
    return MCT_USER_TYPE_SYSTEM;

  /* FIXME: Link this to the data in com.endlessm.ParentalControls.AccountInfo?
   * Or drop that settings interface. */
  switch (act_user_get_account_type (self->user))
    {
    case ACT_USER_ACCOUNT_TYPE_STANDARD:
      return MCT_USER_TYPE_CHILD;
    case ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR:
      return MCT_USER_TYPE_PARENT;
    default:
      return MCT_USER_TYPE_UNKNOWN;
    }
}

/**
 * mct_user_get_icon_path:
 * @self: a user
 *
 * Get the value of the [property@Malcontent.User:icon-path] property.
 *
 * Returns: (nullable): the user’s avatar icon path, or `NULL` if not set
 * Since: 0.14.0
 */
const char *
mct_user_get_icon_path (MctUser *self)
{
  g_return_val_if_fail (MCT_IS_USER (self), NULL);

  return act_user_get_icon_file (self->user);
}

/**
 * mct_user_get_login_time:
 * @self: a user
 *
 * Get the value of the [property@Malcontent.User:login-time] property.
 *
 * Returns: the login time
 * Since: 0.14.0
 */
uint64_t
mct_user_get_login_time (MctUser *self)
{
  g_return_val_if_fail (MCT_IS_USER (self), 0);

  return act_user_get_login_time (self->user);
}

/**
 * mct_user_get_locale:
 * @self: a user
 *
 * Get the value of the [property@Malcontent.User:locale] property.
 *
 * Returns: (nullable): the user’s locale, `""` if the system default, or `NULL`
 *   if not set
 * Since: 0.14.0
 */
const char *
mct_user_get_locale (MctUser *self)
{
  g_return_val_if_fail (MCT_IS_USER (self), NULL);

  return act_user_get_language (self->user);
}

/**
 * mct_user_get_act_user:
 * @self: a user
 *
 * Get the AccountsService user object from @self.
 *
 * This is an internal getter method.
 *
 * Returns: (not nullable) (transfer none): an AccountsService user
 * Since: 0.14.0
 */
ActUser *
mct_user_get_act_user (MctUser *self)
{
  g_return_val_if_fail (MCT_IS_USER (self), NULL);

  return self->user;
}

/**
 * mct_user_equal:
 * @a: a user
 * @b: another user
 *
 * Check whether two users are equal.
 *
 * This compares their user IDs for equality, and is equivalent to:
 * ```c
 * mct_user_get_uid (a) == mct_user_get_uid (b)
 * ```
 *
 * User IDs are a primary key for users in Unix.
 *
 * Returns: true if @a and @b are the same user, false otherwise
 * Since: 0.14.0
 */
gboolean
mct_user_equal (MctUser *a,
                MctUser *b)
{
  g_return_val_if_fail (MCT_IS_USER (a), FALSE);
  g_return_val_if_fail (MCT_IS_USER (b), FALSE);

  return mct_user_get_uid (a) == mct_user_get_uid (b);
}

/**
 * mct_user_is_in_same_family:
 * @self: a user
 * @other: another user
 *
 * Calculate whether two users are in the same family.
 *
 * See the documentation for [class@Malcontent.UserManager] for the definition
 * of a family.
 *
 * Returns: true if @self and @other are in the same family, false otherwise
 * Since: 0.14.0
 */
gboolean
mct_user_is_in_same_family (MctUser *self,
                            MctUser *other)
{
  g_return_val_if_fail (MCT_IS_USER (self), FALSE);
  g_return_val_if_fail (MCT_IS_USER (other), FALSE);

  /* FIXME: For now, we support very minimal family setups. All standard and
   * admin users on the system are in the same family. System users are not in
   * a family.
   *
   * Eventually we want to support more complicated family groups like
   * https://gitlab.gnome.org/Teams/Design/app-mockups/-/raw/fedd34d5c2eff8a95951807bf0a6de3e184d6065/family/family-overview.png
   * (from https://gitlab.gnome.org/Teams/Design/app-mockups/-/issues/118). */
  return (!act_user_is_system_account (self->user) &&
          !act_user_is_system_account (other->user));
}

/**
 * mct_user_is_parent_of:
 * @self: a user
 * @other: another user
 *
 * Calculate whether one user is the parent of another user.
 *
 * See the documentation for [class@Malcontent.UserManager] for the definition
 * of a family and family relationships.
 *
 * Returns: true if @self is a parent of @other; false otherwise
 * Since: 0.14.0
 */
gboolean
mct_user_is_parent_of (MctUser *self,
                       MctUser *other)
{
  return (mct_user_is_in_same_family (self, other) &&
          mct_user_get_user_type (self) == MCT_USER_TYPE_PARENT &&
          mct_user_get_user_type (other) == MCT_USER_TYPE_CHILD);
}
