/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
 *
 * Copyright 2025 GNOME Foundation, 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:
 *  - Ignacy Kuchciński <ignacykuchcinski@gnome.org>
 */

#include "config.h"

#include <glib/gi18n.h>
#include <stdint.h>

#include "screen-time-statistics-row.h"

/**
 * MctScreenTimeStatisticsRow:
 *
 * A widget which shows screen time bar chart for screen time usage
 * records for the selected user.
 *
 * Since: 0.14.0
 */
struct _MctScreenTimeStatisticsRow
{
  CcScreenTimeStatisticsRow parent;

  unsigned long usage_changed_id;

  GDBusConnection *connection; /* (owned) */
  uid_t uid;
};

G_DEFINE_TYPE (MctScreenTimeStatisticsRow, mct_screen_time_statistics_row, CC_TYPE_SCREEN_TIME_STATISTICS_ROW)

typedef enum
{
  PROP_CONNECTION = 1,
  PROP_UID,
} MctScreenTimeStatisticsRowProperty;

static GParamSpec *properties[PROP_UID + 1];

static void
mct_screen_time_statistics_row_get_property (GObject      *object,
                                             unsigned int  property_id,
                                             GValue       *value,
                                             GParamSpec   *spec)
{
  MctScreenTimeStatisticsRow *self = MCT_SCREEN_TIME_STATISTICS_ROW (object);

  switch ((MctScreenTimeStatisticsRowProperty) property_id)
  {
    case PROP_CONNECTION:
      g_value_set_object (value, self->connection);
      break;

    case PROP_UID:
      g_value_set_uint (value, self->uid);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, spec);
      break;
  }
}

static void
mct_screen_time_statistics_row_set_property (GObject      *object,
                                             unsigned int  property_id,
                                             const GValue *value,
                                             GParamSpec   *spec)
{
  MctScreenTimeStatisticsRow *self = MCT_SCREEN_TIME_STATISTICS_ROW (object);

  switch ((MctScreenTimeStatisticsRowProperty) property_id)
  {
    case PROP_CONNECTION:
      /* Construct-only. May not be %NULL. */
      g_assert (self->connection == NULL);
      self->connection = g_value_dup_object (value);
      g_assert (self->connection != NULL);
      break;

    case PROP_UID:
      /* Construct-only. */
      g_assert (self->uid == 0);
      self->uid = g_value_get_uint (value);
      g_assert (self->uid != 0);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, spec);
      break;
  }
}

static void
mct_screen_time_statistics_row_constructed (GObject *object)
{
  MctScreenTimeStatisticsRow *self = MCT_SCREEN_TIME_STATISTICS_ROW (object);

  g_assert (self->connection != NULL);
  g_assert (self->uid != 0);

  G_OBJECT_CLASS (mct_screen_time_statistics_row_parent_class)->constructed (object);
}

static void
mct_screen_time_statistics_row_dispose (GObject *object)
{
  MctScreenTimeStatisticsRow *self = MCT_SCREEN_TIME_STATISTICS_ROW (object);

  if (self->connection != NULL && self->usage_changed_id != 0)
  {
    g_dbus_connection_signal_unsubscribe (self->connection, self->usage_changed_id);
    self->usage_changed_id = 0;
  }

  g_clear_object (&self->connection);

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

typedef struct
{
  GDate *start_date;
  size_t n_days;
  double *screen_time_per_day;
} LoadDataData;

static void
load_data_data_free (LoadDataData *data)
{
  g_date_free (data->start_date);
  g_free (data->screen_time_per_day);
  g_free (data);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC (LoadDataData, load_data_data_free)

/**
 * load_data_data_new:
 * @start_date: (transfer none): an initialized [struct@GLib.Date]
 * @n_days: number of days
 * @screen_time_per_day: (transfer full): screen time per day
 */
static LoadDataData *
load_data_data_new (GDate  *start_date,
                    size_t  n_days,
                    double *screen_time_per_day)
{
  g_autoptr(LoadDataData) data = g_new0 (LoadDataData, 1);

  data->start_date = g_date_copy (start_date);
  data->n_days = n_days;
  data->screen_time_per_day = screen_time_per_day;

  return g_steal_pointer (&data);
}

static void
query_usage_cb (GObject      *object,
                GAsyncResult *result,
                void         *user_data);
static void
usage_changed_cb (GDBusConnection *connection,
                  const char      *sender_name,
                  const char      *object_path,
                  const char      *interface_name,
                  const char      *signal_name,
                  GVariant        *parameters,
                  void            *user_data);

static void
mct_screen_time_statistics_row_load_data_async (CcScreenTimeStatisticsRow *screen_time_statistics_row,
                                                GCancellable              *cancellable,
                                                GAsyncReadyCallback        callback,
                                                void                      *user_data)
{
  MctScreenTimeStatisticsRow *self = MCT_SCREEN_TIME_STATISTICS_ROW (screen_time_statistics_row);
  g_autoptr(GTask) task = NULL;

  task = g_task_new (self, cancellable, callback, user_data);
  g_task_set_source_tag (task, mct_screen_time_statistics_row_load_data_async);

  /* Load and parse the screen time usage periods of the child account using
   * the org.freedesktop.MalcontentTimer1.Parent malcontent-timerd interface.
   * See `timeLimitsManager.js` in gnome-shell for the code which records the
   * usage using org.freedesktop.MalcontentTimer1.Child interface. */
  g_dbus_connection_call (self->connection,
                          "org.freedesktop.MalcontentTimer1",
                          "/org/freedesktop/MalcontentTimer1",
                          "org.freedesktop.MalcontentTimer1.Parent",
                          "QueryUsage",
                          g_variant_new ("(uss)",
                                         self->uid,
                                         "login-session",
                                         ""),
                          (const GVariantType *) "(a(tt))",
                          G_DBUS_CALL_FLAGS_NONE,
                          -1,
                          cancellable,
                          query_usage_cb,
                          g_steal_pointer (&task));

  /* Start watching for screen time usage changes coming from the daemon. */
  self->usage_changed_id =
    g_dbus_connection_signal_subscribe (self->connection,
                                        "org.freedesktop.MalcontentTimer1",
                                        "org.freedesktop.MalcontentTimer1.Parent",
                                        "UsageChanged",
                                        "/org/freedesktop/MalcontentTimer1",
                                        NULL,
                                        G_DBUS_SIGNAL_FLAGS_NONE,
                                        usage_changed_cb,
                                        self,
                                        NULL);
}

static void allocate_duration_to_days (const GDate *model_start_date,
                                       GArray      *model_screen_time_per_day,
                                       uint64_t     start_wall_time_secs,
                                       uint64_t     duration_secs);

static void
query_usage_cb (GObject      *object,
                GAsyncResult *result,
                void         *user_data)
{
  GDBusConnection *connection = G_DBUS_CONNECTION (object);
  g_autoptr(GTask) task = G_TASK (g_steal_pointer (&user_data));
  GDate new_model_start_date;
  size_t new_model_n_days;
  g_autoptr(GArray) new_model_screen_time_per_day = NULL;  /* (element-type double) */
  g_autoptr(GVariant) result_variant = NULL;
  g_autoptr(GError) local_error = NULL;
  g_autoptr(GVariantIter) entries_iter = NULL;
  uint64_t start_wall_time_secs, end_wall_time_secs;

  g_date_clear (&new_model_start_date, 1);

  result_variant = g_dbus_connection_call_finish (connection, result, &local_error);

  if (result_variant == NULL)
    {
      g_task_return_error (task, g_steal_pointer (&local_error));
      return;
    }

  g_variant_get (result_variant, "(a(tt))", &entries_iter);
  while (g_variant_iter_loop (entries_iter, "(tt)", &start_wall_time_secs, &end_wall_time_secs))
    {
      /* Set up the model if this is the first iteration */
      if (!g_date_valid (&new_model_start_date))
        {
          g_date_set_time_t (&new_model_start_date, start_wall_time_secs);
          new_model_screen_time_per_day = g_array_new (FALSE, TRUE, sizeof (double));
        }

      /* Interpret the data */
      uint64_t duration_secs = end_wall_time_secs - start_wall_time_secs;
      allocate_duration_to_days (&new_model_start_date, new_model_screen_time_per_day,
                                 start_wall_time_secs, duration_secs);
    }

  /* Was the data empty? */
  if (new_model_screen_time_per_day == NULL || new_model_screen_time_per_day->len == 0)
    {
      g_set_error (&local_error, G_FILE_ERROR, G_FILE_ERROR_NOENT,
                   _("Failed to load session history data: %s"),
                   _("Data is empty"));
      g_task_return_error (task, g_steal_pointer (&local_error));
      return;
    }

  new_model_n_days = new_model_screen_time_per_day->len;

  LoadDataData *data = load_data_data_new (&new_model_start_date,
                                           new_model_n_days,
                                           (double *) g_array_free (g_steal_pointer (&new_model_screen_time_per_day), FALSE));
  g_task_return_pointer (task, data, (GDestroyNotify) load_data_data_free);
}

static void allocate_duration_to_day (const GDate *model_start_date,
                                      GArray      *model_screen_time_per_day,
                                      GDateTime   *start_date_time,
                                      uint64_t     duration_secs);

/* Take the time period [start_wall_time_secs, start_wall_time_secs + duration_secs]
 * and add it to the model, splitting it between day boundaries if needed, and
 * extending the `GArray` if needed. */
static void
allocate_duration_to_days (const GDate *model_start_date,
                           GArray      *model_screen_time_per_day,
                           uint64_t     start_wall_time_secs,
                           uint64_t     duration_secs)
{
  g_autoptr(GDateTime) start_date_time = NULL;

  start_date_time = g_date_time_new_from_unix_local (start_wall_time_secs);

  while (duration_secs > 0)
    {
      g_autoptr(GDateTime) start_of_day = NULL, start_of_next_day = NULL;
      g_autoptr(GDateTime) new_start_date_time = NULL;
      GTimeSpan span_usecs;
      uint64_t span_secs;

      start_of_day = g_date_time_new_local (g_date_time_get_year (start_date_time),
                                            g_date_time_get_month (start_date_time),
                                            g_date_time_get_day_of_month (start_date_time),
                                            0, 0, 0);
      g_assert (start_of_day != NULL);
      start_of_next_day = g_date_time_add_days (start_of_day, 1);
      g_assert (start_of_next_day != NULL);

      span_usecs = g_date_time_difference (start_of_next_day, start_date_time);
      span_secs = span_usecs / G_USEC_PER_SEC;
      if (span_secs > duration_secs)
        span_secs = duration_secs;

      allocate_duration_to_day (model_start_date, model_screen_time_per_day,
                                start_date_time, span_secs);

      duration_secs -= span_secs;
      new_start_date_time = g_date_time_add_seconds (start_date_time, span_secs);
      g_date_time_unref (start_date_time);
      start_date_time = g_steal_pointer (&new_start_date_time);
    }
}

/* Take the time period [start_date_time, start_date_time + duration_secs]
 * and add it to the model, extending the `GArray` if needed. The time period
 * *must not* cross a day boundary, i.e. it’s invalid to call this function
 * with `start_date_time` as 23:00 on a day, and `duration_secs` as 2h.
 *
 * Note that @model_screen_time_per_day is in minutes, whereas @duration_secs
 * is in seconds. */
static void
allocate_duration_to_day (const GDate *model_start_date,
                          GArray      *model_screen_time_per_day,
                          GDateTime   *start_date_time,
                          uint64_t     duration_secs)
{
  GDate start_date;
  int diff_days;
  double *element;

  g_date_clear (&start_date, 1);
  g_date_set_dmy (&start_date,
                  g_date_time_get_day_of_month (start_date_time),
                  g_date_time_get_month (start_date_time),
                  g_date_time_get_year (start_date_time));

  diff_days = g_date_days_between (model_start_date, &start_date);
  g_assert (diff_days >= 0);

  /* If the new day is outside the range of the model, insert it at the right
   * index. This will automatically create the indices between, and initialise
   * them to zero, which is what we want. */
  if ((unsigned int) diff_days >= model_screen_time_per_day->len)
    {
      const double new_val = 0.0;
      g_array_insert_val (model_screen_time_per_day, diff_days, new_val);
    }

  element = &g_array_index (model_screen_time_per_day, double, diff_days);
  *element += duration_secs / 60.0;
}

static gboolean
mct_screen_time_statistics_row_load_data_finish (CcScreenTimeStatisticsRow  *screen_time_statistics_row,
                                                 GAsyncResult               *result,
                                                 GDate                      *out_new_model_start_date,
                                                 size_t                     *out_new_model_n_days,
                                                 double                    **out_new_model_screen_time_per_day,
                                                 GError                    **error)
{
  g_autoptr(LoadDataData) data = NULL;

  g_return_val_if_fail (g_task_is_valid (result, screen_time_statistics_row), FALSE);

  /* Set up in case of error. */
  if (out_new_model_start_date != NULL)
    g_date_clear (out_new_model_start_date, 1);
  if (out_new_model_n_days != NULL)
    *out_new_model_n_days = 0;
  if (out_new_model_screen_time_per_day != NULL)
    *out_new_model_screen_time_per_day = NULL;

  data = g_task_propagate_pointer (G_TASK (result), error);

  if (data == NULL)
    return FALSE;

  /* Success! */
  if (out_new_model_start_date != NULL)
    *out_new_model_start_date = *data->start_date;
  if (out_new_model_n_days != NULL)
    *out_new_model_n_days = data->n_days;
  if (out_new_model_screen_time_per_day != NULL)
    *out_new_model_screen_time_per_day = g_steal_pointer (&data->screen_time_per_day);

  return TRUE;
}

static void
usage_changed_cb (GDBusConnection *connection,
                  const char      *sender_name,
                  const char      *object_path,
                  const char      *interface_name,
                  const char      *signal_name,
                  GVariant        *parameters,
                  void            *user_data)
{
  MctScreenTimeStatisticsRow *self = MCT_SCREEN_TIME_STATISTICS_ROW (user_data);

  cc_screen_time_statistics_row_update_model (CC_SCREEN_TIME_STATISTICS_ROW (self));
}

static void
mct_screen_time_statistics_row_class_init (MctScreenTimeStatisticsRowClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  CcScreenTimeStatisticsRowClass *screen_time_statistics_row_class = CC_SCREEN_TIME_STATISTICS_ROW_CLASS (klass);

  object_class->get_property = mct_screen_time_statistics_row_get_property;
  object_class->set_property = mct_screen_time_statistics_row_set_property;
  object_class->constructed = mct_screen_time_statistics_row_constructed;
  object_class->dispose = mct_screen_time_statistics_row_dispose;

  screen_time_statistics_row_class->load_data_async = mct_screen_time_statistics_row_load_data_async;
  screen_time_statistics_row_class->load_data_finish = mct_screen_time_statistics_row_load_data_finish;

  /**
   * MctScreenTimeStatisticsRow:connection: (not nullable)
   *
   * A connection to the system bus, where malcontent-timerd runs.
   *
   * It’s provided to allow an existing connection to be re-used and for testing
   * purposes.
   *
   * Since 0.14.0
   */
  properties[PROP_CONNECTION] = g_param_spec_object ("connection", NULL, NULL,
                                                     G_TYPE_DBUS_CONNECTION,
                                                     G_PARAM_READWRITE |
                                                     G_PARAM_CONSTRUCT_ONLY |
                                                     G_PARAM_STATIC_STRINGS);

  /**
   * MctScreenTimeStatisticsRow:uid:
   *
   * The selected user’s UID.
   *
   * Since 0.14.0
   */
  properties[PROP_UID] = g_param_spec_uint ("uid", NULL, NULL,
                                            0, G_MAXUINT, 0,
                                            G_PARAM_READWRITE |
                                            G_PARAM_CONSTRUCT_ONLY |
                                            G_PARAM_STATIC_STRINGS);

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

static void
mct_screen_time_statistics_row_init (MctScreenTimeStatisticsRow *self)
{
}

/**
 * mct_screen_time_statistics_row_new:
 * @connection: (transfer none): a D-Bus connection to use
 * @uid: user ID to use
 *
 * Create a new [class@Malcontent.ScreenTimeStatisticsRow] widget.
 *
 * Returns: (transfer full): a new screen time statistics row
 * Since: 0.14.0
 */
MctScreenTimeStatisticsRow *
mct_screen_time_statistics_row_new (GDBusConnection *connection,
                                    uid_t            uid)
{
  g_return_val_if_fail (G_IS_DBUS_CONNECTION (connection), NULL);

  return g_object_new (MCT_TYPE_SCREEN_TIME_STATISTICS_ROW,
                       "connection", connection,
                       "uid", uid,
                       NULL);
}
