/*
 * Copyright (C) 2024 Canonical Ltd.
 *
 * 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 or version 3 of the License.
 * See http://www.gnu.org/copyleft/lgpl.html the full text of the license.
 */

#include "snapd-notice.h"

/**
 * SECTION: snapd-notice
 * @short_description: Notices element
 * @include: snapd-glib/snapd-glib.h
 *
 * A #SnapdNotice contains information about a notice that is notified.
 */

/**
 * SnapdNotice:
 *
 * #SnapdNotice contains information on a notification element.
 *
 * Since: 1.65
 */

enum {
  PROP_ID = 1,
  PROP_USER_ID,
  PROP_TYPE,
  PROP_KEY,
  PROP_FIRST_OCCURRED,
  PROP_LAST_OCCURRED,
  PROP_LAST_OCCURRED_NANO,
  PROP_LAST_REPEATED,
  PROP_OCCURRENCES,
  PROP_LAST_DATA,
  PROP_REPEAT_AFTER,
  PROP_EXPIRE_AFTER,
  PROP_LAST
};

struct _SnapdNotice {
  GObject parent_instance;

  gchar *id;
  gchar *userid;
  SnapdNoticeType type;
  gchar *key;
  GDateTime *first_occurred;
  GDateTime *last_occurred;
  // last_ocurred_internal contains the same date/time, but without
  // microseconds. Useful to speedup comparisons.
  GDateTime *last_occurred_internal;
  int last_occurred_nanosecond;
  GDateTime *last_repeated;
  GHashTable *data;
  gint64 occurrences;
  GTimeSpan repeat_after;
  GTimeSpan expire_after;
};

G_DEFINE_TYPE(SnapdNotice, snapd_notice, G_TYPE_OBJECT)

/**
 * snapd_notice_get_id:
 * @notice: a #SnapdNotice.
 *
 * Get the unique ID for this notice.
 *
 * Returns: an ID.
 *
 * Since: 1.65
 */
const gchar *snapd_notice_get_id(SnapdNotice *self) {
  g_return_val_if_fail(SNAPD_IS_NOTICE(self), NULL);
  return self->id;
}

/**
 * snapd_notice_get_user_id:
 * @notice: a #SnapdNotice.
 *
 * Get the user ID for this notice, or NULL if no user is defined
 *
 * Returns: an user ID.
 *
 * Since: 1.65
 */
const gchar *snapd_notice_get_user_id(SnapdNotice *self) {
  g_return_val_if_fail(SNAPD_IS_NOTICE(self), NULL);
  return self->userid;
}

/**
 * snapd_notice_get_notice_type:
 * @notice: a #SnapdNotice.
 *
 * Gets the type of notice this is.
 *
 * Returns: the type of notice.
 *
 * Since: 1.65
 */
SnapdNoticeType snapd_notice_get_notice_type(SnapdNotice *self) {
  g_return_val_if_fail(SNAPD_IS_NOTICE(self), SNAPD_NOTICE_TYPE_UNKNOWN);
  return self->type;
}

/**
 * snapd_notice_get_key:
 * @notice: a #SnapdNotice.
 *
 * Get the notice-id or the instance-name, depending on the type.
 *
 * Returns: a string with the key.
 *
 * Since: 1.65
 */
const gchar *snapd_notice_get_key(SnapdNotice *self) {
  g_return_val_if_fail(SNAPD_IS_NOTICE(self), NULL);
  return self->key;
}

/**
 * snapd_notice_get_last_data:
 * @notice: a #SnapdNotice.
 *
 * Get the data of the notice.
 *
 * Returns: (transfer container): a HashTable with the data elements.
 *
 * Since: 1.65
 * Deprecated: 1.66: Use snapd_notice_get_last_data2()
 */
GHashTable *snapd_notice_get_last_data(SnapdNotice *self) {
  g_return_val_if_fail(SNAPD_IS_NOTICE(self), NULL);
  return g_hash_table_ref(self->data);
}

/**
 * snapd_notice_get_last_data2: (rename-to snapd_notice_get_last_data)
 * @notice: a #SnapdNotice.
 *
 * Get the data of the notice.
 *
 * Returns: (transfer none): a HashTable with the data elements.
 *
 * Since: 1.66
 */
GHashTable *snapd_notice_get_last_data2(SnapdNotice *self) {
  g_return_val_if_fail(SNAPD_IS_NOTICE(self), NULL);
  return self->data;
}

/**
 * snapd_notice_get_first_occurred:
 * @notice: a #SnapdNotice.
 *
 * Get the time this notification first occurred.
 *
 * Returns: (transfer full): a #GDateTime.
 *
 * Since: 1.65
 * Deprecated: 1.66: Use snapd_notice_get_first_occurred2()
 */
GDateTime *snapd_notice_get_first_occurred(SnapdNotice *self) {
  g_return_val_if_fail(SNAPD_IS_NOTICE(self), NULL);
  if (self->first_occurred == NULL)
    return NULL;
  return g_date_time_ref(self->first_occurred);
}

/**
 * snapd_notice_get_first_occurred2: (rename-to snapd_notice_get_first_occurred)
 * @notice: a #SnapdNotice.
 *
 * Get the time this notification first occurred.
 *
 * Returns: (transfer none): a #GDateTime.
 *
 * Since: 1.66
 */
GDateTime *snapd_notice_get_first_occurred2(SnapdNotice *self) {
  g_return_val_if_fail(SNAPD_IS_NOTICE(self), NULL);
  return self->first_occurred;
}

/**
 * snapd_notice_get_last_occurred:
 * @notice: a #SnapdNotice.
 *
 * Get the time this notification last occurred.
 *
 * Returns: (transfer full): a #GDateTime.
 *
 * Since: 1.65
 * Deprecated: 1.66: Use snapd_notice_get_last_occurred2()
 */
GDateTime *snapd_notice_get_last_occurred(SnapdNotice *self) {
  g_return_val_if_fail(SNAPD_IS_NOTICE(self), NULL);
  if (self->last_occurred == NULL)
    return NULL;
  return g_date_time_ref(self->last_occurred);
}

/**
 * snapd_notice_get_last_occurred2: (rename-to snapd_notice_get_last_occurred)
 * @notice: a #SnapdNotice.
 *
 * Get the time this notification last occurred.
 *
 * Returns: (transfer none): a #GDateTime.
 *
 * Since: 1.66
 */
GDateTime *snapd_notice_get_last_occurred2(SnapdNotice *self) {
  g_return_val_if_fail(SNAPD_IS_NOTICE(self), NULL);
  return self->last_occurred;
}

/**
 * snapd_notice_get_last_occurred_nanoseconds:
 * @notice: a #SnapdNotice.
 *
 * Get the nanoseconds value of *last-occurred*, exactly as sent by
 * snapd. Useful when combined with #snapd_client_notices_set_since_nanoseconds,
 * and used internally by snapd_client_notices_set_after_notice, to ensure the
 * maximum possible precission when dealing with timestamps.
 *
 * Returns: the nanosecond value between 0 and 999999999, or -1 if no
 * nanosecond value was set.
 *
 * Since: 1.66
 */
int snapd_notice_get_last_occurred_nanoseconds(SnapdNotice *self) {
  g_return_val_if_fail(SNAPD_IS_NOTICE(self), 0);
  return self->last_occurred_nanosecond;
}

/**
 * snapd_notice_get_last_repeated:
 * @notice: a #SnapdNotice.
 *
 * Get the time this notification last repeated.
 *
 * Returns: (transfer full): a #GDateTime.
 *
 * Since: 1.65
 * Deprecated: 1.66: Use snapd_notice_get_last_repeated2()
 */
GDateTime *snapd_notice_get_last_repeated(SnapdNotice *self) {
  g_return_val_if_fail(SNAPD_IS_NOTICE(self), NULL);
  if (self->last_repeated == NULL)
    return NULL;
  return g_date_time_ref(self->last_repeated);
}

/**
 * snapd_notice_get_last_repeated2: (rename-to snapd_notice_get_last_repeated)
 * @notice: a #SnapdNotice.
 *
 * Get the time this notification last repeated.
 *
 * Returns: (transfer none): a #GDateTime.
 *
 * Since: 1.66
 */
GDateTime *snapd_notice_get_last_repeated2(SnapdNotice *self) {
  g_return_val_if_fail(SNAPD_IS_NOTICE(self), NULL);
  return self->last_repeated;
}

/**
 * snapd_notice_get_occurrences:
 * @notice: a #SnapdNotice.
 *
 * Get the number of times that this notification has been triggered.
 *
 * Returns: a count.
 *
 * Since: 1.65
 */
gint64 snapd_notice_get_occurrences(SnapdNotice *self) {
  g_return_val_if_fail(SNAPD_IS_NOTICE(self), 0);
  return self->occurrences;
}

/**
 * snapd_notice_get_repeat_after:
 * @notice: a #SnapdNotice.
 *
 * Get the time interval after this notification can be repeated.
 *
 * Returns: a #GTimeSpan.
 *
 * Since: 1.65
 */
GTimeSpan snapd_notice_get_repeat_after(SnapdNotice *self) {
  g_return_val_if_fail(SNAPD_IS_NOTICE(self), 0);
  return self->repeat_after;
}

/**
 * snapd_notice_get_expire_after:
 * @notice: a #SnapdNotice.
 *
 * Get the time interval after this notification can expire.
 *
 * Returns: a #GTimeSpan.
 *
 * Since: 1.65
 */
GTimeSpan snapd_notice_get_expire_after(SnapdNotice *self) {
  g_return_val_if_fail(SNAPD_IS_NOTICE(self), 0);
  return self->expire_after;
}

/**
 * snapd_notice_compare_last_occurred:
 * @notice: a #SnapdNotice.
 * @notice_to_compare: another #SnapdNotice.
 *
 * Compare the last_occurred fields (and the last_occurred_nanosecond if
 * available) of both notices, and returns whether the first one is before, same
 * or after the second one.
 *
 * Returns: -1 if the first one is before the second one; 0 if both are the same
 * time instant, and 1 if the first one is after the second one.
 *
 * Since: 1.66
 */
gint snapd_notice_compare_last_occurred(SnapdNotice *self,
                                        SnapdNotice *notice_to_compare) {
  g_return_val_if_fail(SNAPD_IS_NOTICE(self), 0);
  g_return_val_if_fail(SNAPD_IS_NOTICE(notice_to_compare), 0);

  // first, compare without micro/nanoseconds. This is a must to avoid errors
  // due to rounding, because the g_date_time objects are created using a
  // gdouble.
  gint c = g_date_time_compare(self->last_occurred_internal,
                               notice_to_compare->last_occurred_internal);
  if (c != 0)
    return c;

  // now take into account the nanoseconds if available
  int self_nano = self->last_occurred_nanosecond;
  if (self_nano == -1)
    self_nano = 1000 * g_date_time_get_microsecond(self->last_occurred);
  int notice_nano = notice_to_compare->last_occurred_nanosecond;
  if (notice_nano == -1)
    notice_nano =
        1000 * g_date_time_get_microsecond(notice_to_compare->last_occurred);
  if (self_nano < notice_nano)
    return -1;
  if (self_nano == notice_nano)
    return 0;
  return 1;
}

static void snapd_notice_set_property(GObject *object, guint prop_id,
                                      const GValue *value, GParamSpec *pspec) {
  SnapdNotice *self = SNAPD_NOTICE(object);
  GDateTime *tmpdate;

  switch (prop_id) {
  case PROP_ID:
    g_free(self->id);
    self->id = g_strdup(g_value_get_string(value));
    break;
  case PROP_USER_ID:
    g_free(self->userid);
    self->userid = g_strdup(g_value_get_string(value));
    break;
  case PROP_TYPE:
    self->type = g_value_get_uint(value);
    break;
  case PROP_KEY:
    g_free(self->key);
    self->key = g_strdup(g_value_get_string(value));
    break;
  case PROP_FIRST_OCCURRED:
    g_clear_pointer(&self->first_occurred, g_date_time_unref);
    tmpdate = g_value_get_boxed(value);
    self->first_occurred = tmpdate == NULL ? NULL : g_date_time_ref(tmpdate);
    break;
  case PROP_LAST_OCCURRED:
    g_clear_pointer(&self->last_occurred, g_date_time_unref);
    g_clear_pointer(&self->last_occurred_internal, g_date_time_unref);
    if (g_value_get_boxed(value) != NULL) {
      self->last_occurred = g_date_time_ref(g_value_get_boxed(value));
      // store the same date/time, but without microseconds
      self->last_occurred_internal =
          g_date_time_add(self->last_occurred,
                          -g_date_time_get_microsecond(self->last_occurred));
    }
    break;
  case PROP_LAST_OCCURRED_NANO:
    self->last_occurred_nanosecond = g_value_get_int(value);
    break;
  case PROP_LAST_REPEATED:
    g_clear_pointer(&self->last_repeated, g_date_time_unref);
    if (g_value_get_boxed(value) != NULL)
      self->last_repeated = g_date_time_ref(g_value_get_boxed(value));
    break;
  case PROP_OCCURRENCES:
    self->occurrences = g_value_get_int64(value);
    break;
  case PROP_LAST_DATA:
    g_clear_pointer(&self->data, g_hash_table_unref);
    if (g_value_get_boxed(value) != NULL)
      self->data = g_hash_table_ref(g_value_get_boxed(value));
    else
      self->data =
          g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
    break;
  case PROP_EXPIRE_AFTER:
    self->expire_after = g_value_get_int64(value);
    break;
  case PROP_REPEAT_AFTER:
    self->repeat_after = g_value_get_int64(value);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
    break;
  }
}

static void snapd_notice_get_property(GObject *object, guint prop_id,
                                      GValue *value, GParamSpec *pspec) {
  SnapdNotice *self = SNAPD_NOTICE(object);

  switch (prop_id) {
  case PROP_ID:
    g_value_set_string(value, self->id);
    break;
  case PROP_USER_ID:
    g_value_set_string(value, self->userid);
    break;
  case PROP_TYPE:
    g_value_set_uint(value, self->type);
    break;
  case PROP_KEY:
    g_value_set_string(value, self->key);
    break;
  case PROP_FIRST_OCCURRED:
    g_value_set_boxed(value, self->first_occurred);
    break;
  case PROP_LAST_OCCURRED:
    g_value_set_boxed(value, self->last_occurred);
    break;
  case PROP_LAST_OCCURRED_NANO:
    g_value_set_int(value, self->last_occurred_nanosecond);
    break;
  case PROP_LAST_REPEATED:
    g_value_set_boxed(value, self->last_repeated);
    break;
  case PROP_OCCURRENCES:
    g_value_set_int(value, self->occurrences);
    break;
  case PROP_LAST_DATA:
    g_value_set_boxed(value, self->data);
    break;
  case PROP_EXPIRE_AFTER:
    g_value_set_int64(value, self->expire_after);
    break;
  case PROP_REPEAT_AFTER:
    g_value_set_int64(value, self->repeat_after);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
    break;
  }
}

static void snapd_notice_finalize(GObject *object) {
  SnapdNotice *self = SNAPD_NOTICE(object);

  g_clear_pointer(&self->id, g_free);
  g_clear_pointer(&self->userid, g_free);
  g_clear_pointer(&self->key, g_free);
  g_clear_pointer(&self->first_occurred, g_date_time_unref);
  g_clear_pointer(&self->last_occurred, g_date_time_unref);
  g_clear_pointer(&self->last_occurred_internal, g_date_time_unref);
  g_clear_pointer(&self->last_repeated, g_date_time_unref);
  g_clear_pointer(&self->data, g_hash_table_unref);

  G_OBJECT_CLASS(snapd_notice_parent_class)->finalize(object);
}

static void snapd_notice_class_init(SnapdNoticeClass *klass) {
  GObjectClass *gobject_class = G_OBJECT_CLASS(klass);

  gobject_class->set_property = snapd_notice_set_property;
  gobject_class->get_property = snapd_notice_get_property;
  gobject_class->finalize = snapd_notice_finalize;

  g_object_class_install_property(
      gobject_class, PROP_ID,
      g_param_spec_string("id", "id", "ID of notice", NULL,
                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
                              G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
                              G_PARAM_STATIC_BLURB));
  g_object_class_install_property(
      gobject_class, PROP_USER_ID,
      g_param_spec_string(
          "user-id", "user-id",
          "UserID of the user who may view this notice (NULL means all users)",
          NULL,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_NAME |
              G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB));
  g_object_class_install_property(
      gobject_class, PROP_TYPE,
      g_param_spec_uint(
          "notice-type", "notice-type", "Type of notice", 0, G_MAXUINT, 0,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_NAME |
              G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB));
  g_object_class_install_property(
      gobject_class, PROP_KEY,
      g_param_spec_string("key", "key", "Key of notice", NULL,
                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
                              G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
                              G_PARAM_STATIC_BLURB));
  g_object_class_install_property(
      gobject_class, PROP_FIRST_OCCURRED,
      g_param_spec_boxed("first-occurred", "first-occurred",
                         "Time this notice first occurred", G_TYPE_DATE_TIME,
                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
                             G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
                             G_PARAM_STATIC_BLURB));
  g_object_class_install_property(
      gobject_class, PROP_LAST_OCCURRED,
      g_param_spec_boxed("last-occurred", "last-occurred",
                         "Time this notice last occurred", G_TYPE_DATE_TIME,
                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
                             G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
                             G_PARAM_STATIC_BLURB));
  g_object_class_install_property(
      gobject_class, PROP_LAST_OCCURRED_NANO,
      g_param_spec_int("last-occurred-nanoseconds", "last-occurred-nanoseconds",
                       "Time this notice last ocurred, in string format and "
                       "with nanosecond accuracy",
                       -1, 999999999, -1,
                       G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
                           G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
                           G_PARAM_STATIC_BLURB));
  g_object_class_install_property(
      gobject_class, PROP_LAST_REPEATED,
      g_param_spec_boxed("last-repeated", "last-repeated",
                         "Time this notice was last repeated", G_TYPE_DATE_TIME,
                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
                             G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
                             G_PARAM_STATIC_BLURB));
  g_object_class_install_property(
      gobject_class, PROP_OCCURRENCES,
      g_param_spec_int64("occurrences", "occurrences",
                         "Number of time one of these notices has occurred",
                         G_MININT, G_MAXINT, -1,
                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
                             G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
                             G_PARAM_STATIC_BLURB));
  g_object_class_install_property(
      gobject_class, PROP_LAST_DATA,
      g_param_spec_boxed(
          "last-data", "last-data", "Data for this notice", G_TYPE_HASH_TABLE,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_NAME |
              G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB));
  g_object_class_install_property(
      gobject_class, PROP_REPEAT_AFTER,
      g_param_spec_int64("repeat-after", "repeat-after",
                         "Time (in ms) after one of these was last repeated "
                         "should we allow it to repeat",
                         G_MININT64, G_MAXINT64, 0,
                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
                             G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
                             G_PARAM_STATIC_BLURB));
  g_object_class_install_property(
      gobject_class, PROP_EXPIRE_AFTER,
      g_param_spec_int64("expire-after", "expire-after",
                         "Time (in ms) since one of these last occurred until "
                         "we should drop the notice",
                         G_MININT64, G_MAXINT64, 0,
                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
                             G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
                             G_PARAM_STATIC_BLURB));
}

static void snapd_notice_init(SnapdNotice *self) {
  self->data = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
}
