/*
 * Copyright (C) 2019 Purism SPC
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 *
 * Author: Guido Günther <agx@sigxcpu.org>
 */

#define G_LOG_DOMAIN "phosh-notify-manager"

#include "phosh-config.h"

#include <gio/gdesktopappinfo.h>

#include "dbus-notification.h"
#include "notification-banner.h"
#include "notification-list.h"
#include "notify-manager.h"
#include "notify-feedback.h"
#include "shell-priv.h"
#include "phosh-enums.h"
#include "util.h"

#include <gmobile.h>

#define NOTIFICATIONS_KEY_APP_CHILDREN "application-children"

#define NOTIFICATIONS_APP_SCHEMA_ID PHOSH_NOTIFICATIONS_SCHEMA_ID ".application"
#define NOTIFICATIONS_APP_PREFIX "/org/gnome/desktop/notifications/application"
#define NOTIFICATIONS_APP_KEY_SHOW_BANNERS "show-banners"
#define NOTIFICATIONS_APP_KEY_APP_ID "application-id"
#define NOTIFICATIONS_APP_KEY_ENABLE "enable"

#define NOTIFICATIONS_SPEC_VERSION "1.2"

/**
 * PhoshNotifyManager:
 *
 * Manages notifications
 *
 * #PhoshNotifyManager manages notifications sent from the shell
 * itself and via the org.freedesktop.Notification DBus interface.
 * See https://developer.gnome.org/notification-spec/
 *
 * It maintains a list of notifications via a #PhoshNotificationList.
 */

#define NOTIFY_DBUS_NAME "org.freedesktop.Notifications"

enum {
  NEW_NOTIFICATION,
  NOTIFICATION_ACTIVATED,
  N_SIGNALS
};
static guint signals[N_SIGNALS] = { 0 };

typedef struct _PhoshNotifyManager
{
  PhoshDBusNotificationsSkeleton parent;

  int dbus_name_id;
  guint next_id;
  guint unknown_source;
  gboolean show_banners;
  GStrv app_children;

  GSettings *settings;

  /* Notification to be handled on unlock */
  struct {
    PhoshNotification *notification;
    char              *action;
  } unlock_notify;

  PhoshNotificationList *list;
  PhoshNotifyFeedback *feedback;
} PhoshNotifyManager;

static void phosh_notify_manager_notify_iface_init (PhoshDBusNotificationsIface *iface);
G_DEFINE_TYPE_WITH_CODE (PhoshNotifyManager,
                         phosh_notify_manager,
                         PHOSH_DBUS_TYPE_NOTIFICATIONS_SKELETON,
                         G_IMPLEMENT_INTERFACE (PHOSH_DBUS_TYPE_NOTIFICATIONS,
                                                phosh_notify_manager_notify_iface_init));


static gboolean
handle_close_notification (PhoshDBusNotifications *skeleton,
                           GDBusMethodInvocation  *invocation,
                           guint                   arg_id)
{
  PhoshNotification *notification = NULL;
  PhoshNotifyManager *self = PHOSH_NOTIFY_MANAGER (skeleton);

  g_return_val_if_fail (PHOSH_IS_NOTIFY_MANAGER (self), FALSE);
  g_debug ("DBus call CloseNotification %u", arg_id);

  notification = phosh_notification_list_get_by_id (self->list, arg_id);

  /*
   * ignore errors when closing non-existent notification, at least qt 5.11 is not
   * happy about it.
   */
  if (notification && PHOSH_IS_NOTIFICATION (notification)) {
    phosh_notification_close (notification, PHOSH_NOTIFICATION_REASON_CLOSED);
  } else {
    phosh_dbus_notifications_emit_notification_closed (PHOSH_DBUS_NOTIFICATIONS (self),
                                                       arg_id,
                                                       PHOSH_NOTIFICATION_REASON_CLOSED);
  }

  phosh_dbus_notifications_complete_close_notification (skeleton, invocation);

  return TRUE;
}


static gboolean
handle_get_capabilities (PhoshDBusNotifications *skeleton, GDBusMethodInvocation *invocation)
{
  const char *const capabilities[] = {
    "body", "body-markup", "actions", "icon-static", "sound", NULL,
  };

  g_debug ("DBus call GetCapabilities");
  phosh_dbus_notifications_complete_get_capabilities (skeleton, invocation, capabilities);
  return TRUE;
}


static gboolean
handle_get_server_information (PhoshDBusNotifications *skeleton, GDBusMethodInvocation *invocation)
{
  g_debug ("DBus call GetServerInformation");
  phosh_dbus_notifications_complete_get_server_information (skeleton,
                                                            invocation,
                                                            "Phosh Notify Daemon",
                                                            "Phosh",
                                                            PHOSH_VERSION,
                                                            NOTIFICATIONS_SPEC_VERSION);
  return TRUE;
}


static void
on_notification_expired (PhoshNotifyManager *self,
                         PhoshNotification  *notification)
{
  guint id = 0;

  g_return_if_fail (PHOSH_IS_NOTIFY_MANAGER (self));
  g_return_if_fail (PHOSH_IS_NOTIFICATION (notification));

  id = phosh_notification_get_id (notification);

  g_debug ("Notification %u expired", id);

  /* Transient notifications are closed rather than staying in the tray */
  if (phosh_notification_get_transient (notification)) {
    phosh_notification_close (notification,
                              PHOSH_NOTIFICATION_REASON_EXPIRED);
  }
}


static void
forget_unlock_notify (PhoshNotifyManager *self)
{
  if (!self->unlock_notify.notification)
    return;

  g_clear_object (&self->unlock_notify.notification);
  g_clear_pointer (&self->unlock_notify.action, g_free);
}


static void
invoke_action (PhoshNotifyManager *self, PhoshNotification *notification, const char *action)
{
  guint id;

  id = phosh_notification_get_id (notification);

  g_return_if_fail (id);
  g_debug ("Emitting ActionInvoked: %d, %s", id, action);

  phosh_notification_do_action (notification, id, action);

  /* Resident notifications stay after being actioned */
  if (!phosh_notification_get_resident (notification)) {
    phosh_notification_close (notification,
                              PHOSH_NOTIFICATION_REASON_DISMISSED);
  }
  g_signal_emit (self, signals[NOTIFICATION_ACTIVATED], 0, notification);
}



static void
on_shell_lock_changed (PhoshNotifyManager* self, GParamSpec *pspec, PhoshShell *shell)
{
  gboolean locked;

  g_return_if_fail (PHOSH_IS_NOTIFY_MANAGER (self));

  locked = phosh_shell_get_locked (shell);
  if (!locked && self->unlock_notify.notification) {
    invoke_action (self, self->unlock_notify.notification, self->unlock_notify.action);
    forget_unlock_notify (self);
  }
}


static void
on_notification_actioned (PhoshNotifyManager *self,
                          const char         *action,
                          PhoshNotification  *notification)
{
  PhoshShell *shell = phosh_shell_get_default();

  g_return_if_fail (PHOSH_IS_NOTIFY_MANAGER (self));
  g_return_if_fail (PHOSH_IS_NOTIFICATION (notification));

  /*
   * Postpone action when shell is locked.
   * Only do this for the default action atm as other actions on the lock screen
   * must be a result of action filters and hence don't need unlock
   * TODO: Ensure this is really a result of a filtered action
   * Eventually we want to allow other actions that need unlock:
   * https://gitlab.freedesktop.org/xdg/xdg-specs/-/issues/103
   */
  if (phosh_shell_get_locked (shell) && g_strcmp0 (action, "default") == 0) {
    PhoshLockscreenManager *lm = phosh_shell_get_lockscreen_manager (shell);

    /* Forget any pending actions */
    forget_unlock_notify (self);

    self->unlock_notify.notification = g_object_ref (notification);
    self->unlock_notify.action = g_strdup (action);

    /* Scroll to unlock page */
    phosh_lockscreen_manager_set_page (lm, PHOSH_LOCKSCREEN_PAGE_UNLOCK);
  } else {
    invoke_action (self, notification, action);
  }
}


static void
on_notification_closed (PhoshNotifyManager      *self,
                        PhoshNotificationReason  reason,
                        PhoshNotification       *notification)
{
  guint id;

  g_return_if_fail (PHOSH_IS_NOTIFY_MANAGER (self));
  g_return_if_fail (PHOSH_IS_NOTIFICATION (notification));

  id = phosh_notification_get_id (notification);

  g_debug ("Emitting NotificationClosed: %d, %d", id, reason);

  phosh_dbus_notifications_emit_notification_closed (PHOSH_DBUS_NOTIFICATIONS (self), id, reason);
}


static gboolean
phosh_notify_manager_is_notification_enabled (PhoshNotification *notification)
{
  g_autofree char *munged_id = NULL;
  g_autofree char *path = NULL;
  g_autoptr (GSettings) settings = NULL;
  GAppInfo *info;
  const char *id;

  info = phosh_notification_get_app_info (notification);
  if (!info)
    return TRUE;

  id = g_app_info_get_id (info);
  if (!info)
    return TRUE;

  if (gm_str_is_null_or_empty (id))
    return TRUE;

  munged_id = phosh_munge_app_id (id);
  path = g_strconcat (NOTIFICATIONS_APP_PREFIX, "/", munged_id, "/", NULL);
  settings = g_settings_new_with_path (NOTIFICATIONS_APP_SCHEMA_ID, path);

  return g_settings_get_boolean (settings, NOTIFICATIONS_APP_KEY_ENABLE);
}


static GIcon *
parse_icon_data (GVariant *variant)
{
  GVariant *wrapped_data = NULL;
  guchar *data = NULL;
  GIcon *icon = NULL;
  int width = 0;
  int height = 0;
  int row_stride = 0;
  int has_alpha = 0;
  int sample_size = 0;
  int channels = 0;

  if (g_variant_is_of_type (variant, G_VARIANT_TYPE ("(iiibiiay)"))) {
    gsize size_should_be;

    g_variant_get (variant,
                   "(iiibii@ay)",
                   &width,
                   &height,
                   &row_stride,
                   &has_alpha,
                   &sample_size,
                   &channels,
                   &wrapped_data);

    size_should_be = (height - 1) * row_stride + width * ((channels * sample_size + 7) / 8);

    if (size_should_be != g_variant_get_size (wrapped_data)) {
      g_warning ("Rejecting image, %" G_GSIZE_FORMAT
                 " (expected) != %" G_GSIZE_FORMAT,
                 size_should_be, g_variant_get_size (wrapped_data));

      return NULL;
    }

    /* Extract a copy of the raw data */
    data = (guchar *) g_memdup2 (g_variant_get_data (wrapped_data), size_should_be);

    icon = G_ICON (gdk_pixbuf_new_from_data (data,
                                             GDK_COLORSPACE_RGB,
                                             has_alpha,
                                             sample_size,
                                             width,
                                             height,
                                             row_stride,
                                             (GdkPixbufDestroyNotify) g_free,
                                             NULL));
  }

  return icon;
}


static GIcon *
parse_icon_string (const char *string)
{
  g_autoptr (GFile) file = NULL;
  GIcon *icon = NULL;

  if (string == NULL || strlen (string) < 1) {
    return NULL;
  }

  if (g_str_has_prefix (string, "file://")) {
    file = g_file_new_for_uri (string);
    icon = g_file_icon_new (file);
  } else if (g_str_has_prefix (string, "/")) {
    file = g_file_new_for_path (string);
    icon = g_file_icon_new (file);
  } else {
    icon = g_themed_icon_new (string);
  }

  return icon;
}


static void
phosh_notify_manager_add_application (PhoshNotifyManager *self, GAppInfo *info)
{
  g_autofree char *munged_id = NULL;
  g_autofree char *path = NULL;
  g_autoptr (GSettings) settings = NULL;
  g_autoptr(GPtrArray) new_apps = NULL;
  const char *id;

  id = g_app_info_get_id(info);
  munged_id = phosh_munge_app_id (id);
  if (g_strv_contains ((const char * const *)self->app_children, munged_id))
    return;

  g_debug ("Adding new application: %s/%s", id, munged_id);
  new_apps = g_ptr_array_sized_new (g_strv_length (self->app_children) + 1);
  for (int i = 0; i < g_strv_length (self->app_children); i++)
    g_ptr_array_add (new_apps, self->app_children[i]);

  g_ptr_array_add (new_apps, munged_id);
  g_ptr_array_add (new_apps, NULL);

  path = g_strconcat (NOTIFICATIONS_APP_PREFIX, "/", munged_id, "/", NULL);
  settings = g_settings_new_with_path (NOTIFICATIONS_APP_SCHEMA_ID, path);
  g_settings_set_string (settings, NOTIFICATIONS_APP_KEY_APP_ID, id);
  g_settings_set_strv (self->settings, NOTIFICATIONS_KEY_APP_CHILDREN,
                       (const char * const *)new_apps->pdata);
}


static gboolean
handle_notify (PhoshDBusNotifications *skeleton,
               GDBusMethodInvocation  *invocation,
               const char             *app_name,
               guint                   replaces_id,
               const char             *app_icon,
               const char             *summary,
               const char             *body,
               const char *const      *actions,
               GVariant               *hints,
               int                     expire_timeout)
{
  PhoshNotifyManager *self = PHOSH_NOTIFY_MANAGER (skeleton);
  PhoshNotification *notification = NULL;
  GVariant *item;
  GVariantIter iter;
  guint id;
  g_autofree char *desktop_id = NULL;
  g_autofree char *source_id = NULL;
  g_autofree char *escaped_body = NULL;
  g_autoptr (GAppInfo) info = NULL;
  PhoshNotificationUrgency urgency = PHOSH_NOTIFICATION_URGENCY_NORMAL;
  g_autoptr (GIcon) data_gicon = NULL;
  g_autoptr (GIcon) path_gicon = NULL;
  g_autoptr (GIcon) app_gicon = NULL;
  g_autoptr (GIcon) old_data_gicon = NULL;
  gboolean transient = FALSE;
  gboolean resident = FALSE;
  g_autofree char *category = NULL;
  g_autofree char *profile = NULL;
  g_autofree char *sound_file = NULL;
  GIcon *icon = NULL;
  GIcon *image = NULL;

  g_return_val_if_fail (PHOSH_IS_NOTIFY_MANAGER (self), FALSE);

  g_debug ("DBus call Notify: %s (%u): %s (%s), %s, %d", app_name, replaces_id, summary, body, app_icon, expire_timeout);

  app_gicon = parse_icon_string (app_icon);

  g_variant_iter_init (&iter, hints);
  while ((item = g_variant_iter_next_value (&iter))) {
    g_autofree char *key = NULL;
    g_autoptr(GVariant) value = NULL;

    g_variant_get (item, "{sv}", &key, &value);

    if (g_strcmp0 (key, "urgency") == 0) {
      if (g_variant_is_of_type (value, G_VARIANT_TYPE_BYTE) &&
          (g_variant_get_byte(value) == PHOSH_NOTIFICATION_URGENCY_CRITICAL)) {
        expire_timeout = 0;
        urgency = PHOSH_NOTIFICATION_URGENCY_CRITICAL;
      }
    } else if ((g_strcmp0 (key, "image-data") == 0) ||
               (g_strcmp0 (key, "image_data") == 0)) {
      data_gicon = parse_icon_data (value);
    } else if ((g_strcmp0 (key, "image-path") == 0) ||
               (g_strcmp0 (key, "image_path") == 0)) {
      if (g_variant_is_of_type (value, G_VARIANT_TYPE_STRING)) {
        path_gicon = parse_icon_string (g_variant_get_string (value, NULL));
      }
    } else if (g_strcmp0 (key, "icon_data") == 0) {
      old_data_gicon = parse_icon_data (value);
    } else if ((g_strcmp0 (key, "desktop_entry") == 0) ||
               (g_strcmp0 (key, "desktop-entry") == 0)) {
      if (g_variant_is_of_type (value, G_VARIANT_TYPE_STRING))
        desktop_id = g_variant_dup_string (value, NULL);
    } else if ((g_strcmp0 (key, "transient") == 0)) {
      if (g_variant_is_of_type (value, G_VARIANT_TYPE_BOOLEAN))
        transient = g_variant_get_boolean (value);
      else if (g_variant_is_of_type (value, G_VARIANT_TYPE_INT32))
        transient = !!g_variant_get_int32 (value);
    } else if ((g_strcmp0 (key, "resident") == 0)) {
      if (g_variant_is_of_type (value, G_VARIANT_TYPE_BOOLEAN))
        resident = g_variant_get_boolean (value);
    } else if ((g_strcmp0 (key, "category") == 0)) {
      if (g_variant_is_of_type (value, G_VARIANT_TYPE_STRING))
        category = g_variant_dup_string (value, NULL);
    } else if ((g_strcmp0 (key, "x-phosh-fb-profile") == 0)) {
      if (g_variant_is_of_type (value, G_VARIANT_TYPE_STRING))
        profile = g_variant_dup_string (value, NULL);
    } else if ((g_strcmp0 (key, "sound-file") == 0)) {
      if (g_variant_is_of_type (value, G_VARIANT_TYPE_STRING))
        sound_file = g_variant_dup_string (value, NULL);
    } else if ((g_strcmp0 (key, "suppress-sound") == 0)) {
      if (g_variant_is_of_type (value, G_VARIANT_TYPE_BOOLEAN))
        profile = g_strdup ("quiet");
    }

    g_variant_unref (item);
  }

  if (data_gicon) {
    image = data_gicon;
  } else if (path_gicon) {
    image = path_gicon;
  } else if (old_data_gicon) {
    image = old_data_gicon;
  } else {
    image = NULL;
  }

  icon = app_gicon;

  if (desktop_id) {
    GDesktopAppInfo *desktop_info;
    source_id = g_strdup_printf ("%s.desktop", desktop_id);

    desktop_info = phosh_get_desktop_app_info_for_app_id (desktop_id);
    if (desktop_info)
      info = G_APP_INFO (desktop_info);
  } else if (app_name && g_strcmp0 (app_name, "notify-send")) {
    /* When the app name is set (and isn't notify-send) use that
       as it's better than nothing  */
    source_id = g_strdup_printf ("legacy-app-%s", app_name);
  } else {
    /* Worse case: The notification gets it's own group, we don't know
       where it came from */
    source_id = g_strdup_printf ("unknown-app-%i", self->unknown_source++);
  }

  if (replaces_id)
    notification = phosh_notification_list_get_by_id (self->list, replaces_id);

  escaped_body = phosh_util_escape_markup (body, TRUE);

  if (notification) {
    id = replaces_id;

    g_object_set (notification,
                  "app_name", app_name,
                  "summary", summary,
                  "body", escaped_body,
                  "app-icon", icon,
                  "app-info", info,
                  "image", image,
                  "urgency", urgency,
                  "actions", actions,
                  "timestamp", NULL,
                  "sound-file", sound_file,
                  NULL);
  } else {
    g_autoptr (PhoshDBusNotification) dbus_notification = NULL;

    if (info)
      phosh_notify_manager_add_application (self, info);

    id = phosh_notify_manager_get_notification_id (self);
    dbus_notification = phosh_dbus_notification_new (id,
                                                     app_name,
                                                     info,
                                                     summary,
                                                     escaped_body,
                                                     icon,
                                                     image,
                                                     urgency,
                                                     (GStrv) actions,
                                                     transient,
                                                     resident,
                                                     category,
                                                     profile,
                                                     NULL);
    phosh_notification_set_sound_file (PHOSH_NOTIFICATION (dbus_notification), sound_file);

    phosh_notify_manager_add_notification (self,
                                           source_id,
                                           expire_timeout,
                                           PHOSH_NOTIFICATION (dbus_notification));
  }

  phosh_dbus_notifications_complete_notify (skeleton, invocation, id);

  return TRUE;
}


static void
phosh_notify_manager_notify_iface_init (PhoshDBusNotificationsIface *iface)
{
  iface->handle_close_notification = handle_close_notification;
  iface->handle_get_capabilities = handle_get_capabilities;
  iface->handle_get_server_information = handle_get_server_information;
  iface->handle_notify = handle_notify;
}


static void
on_notifications_setting_changed (PhoshNotifyManager *self,
                                  const char         *key,
                                  GSettings          *settings)
{
  g_return_if_fail (PHOSH_IS_NOTIFY_MANAGER (self));
  g_return_if_fail (G_IS_SETTINGS (settings));

  self->show_banners = g_settings_get_boolean (settings, PHOSH_NOTIFICATIONS_KEY_SHOW_BANNERS);
}


static void
on_notification_apps_setting_changed (PhoshNotifyManager *self,
                                      const char         *key,
                                      GSettings          *settings)
{
  g_return_if_fail (PHOSH_IS_NOTIFY_MANAGER (self));
  g_return_if_fail (G_IS_SETTINGS (settings));

  g_strfreev (self->app_children);
  self->app_children = g_settings_get_strv (settings, NOTIFICATIONS_KEY_APP_CHILDREN);
}


static void
on_name_acquired (GDBusConnection *connection,
                  const char      *name,
                  gpointer         user_data)
{
  PhoshNotifyManager *self = PHOSH_NOTIFY_MANAGER (user_data);

  g_debug ("Acquired name %s", name);
  g_return_if_fail (PHOSH_IS_NOTIFY_MANAGER (self));
}


static void
on_name_lost (GDBusConnection *connection,
              const char      *name,
              gpointer         user_data)
{
  g_debug ("Lost or failed to acquire name %s", name);
}


static void
on_bus_acquired (GDBusConnection *connection,
                 const char      *name,
                 gpointer         user_data)
{
  PhoshNotifyManager *self = user_data;

  g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self),
                                    connection,
                                    "/org/freedesktop/Notifications",
                                    NULL);
}


static void
phosh_notify_manager_dispose (GObject *object)
{
  PhoshNotifyManager *self = PHOSH_NOTIFY_MANAGER (object);

  g_clear_handle_id (&self->dbus_name_id, g_bus_unown_name);

  if (g_dbus_interface_skeleton_get_object_path (G_DBUS_INTERFACE_SKELETON (self)))
    g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self));

  g_clear_object (&self->settings);
  g_clear_object (&self->feedback);
  g_clear_object (&self->list);

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


static void
phosh_notify_manager_finalize (GObject *object)
{
  PhoshNotifyManager *self = PHOSH_NOTIFY_MANAGER (object);

  g_strfreev (self->app_children);

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



static void
phosh_notify_manager_constructed (GObject *object)
{
  PhoshNotifyManager *self = PHOSH_NOTIFY_MANAGER (object);
  PhoshShell *shell = phosh_shell_get_default ();

  G_OBJECT_CLASS (phosh_notify_manager_parent_class)->constructed (object);
  self->dbus_name_id = g_bus_own_name (G_BUS_TYPE_SESSION,
                                       NOTIFY_DBUS_NAME,
                                       G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT |
                                       G_BUS_NAME_OWNER_FLAGS_REPLACE,
                                       on_bus_acquired,
                                       on_name_acquired,
                                       on_name_lost,
                                       self,
                                       NULL);

  self->settings = g_settings_new (PHOSH_NOTIFICATIONS_SCHEMA_ID);
  g_signal_connect_swapped (self->settings, "changed::" PHOSH_NOTIFICATIONS_KEY_SHOW_BANNERS,
                            G_CALLBACK (on_notifications_setting_changed), self);
  on_notifications_setting_changed (self, NULL, self->settings);

  g_signal_connect_swapped (self->settings, "changed::" NOTIFICATIONS_KEY_APP_CHILDREN,
                            G_CALLBACK (on_notification_apps_setting_changed), self);
  on_notification_apps_setting_changed (self, NULL, self->settings);

  g_signal_connect_swapped (shell, "notify::locked", G_CALLBACK (on_shell_lock_changed), self);

  self->feedback = phosh_notify_feedback_new (self->list);
}


static void
phosh_notify_manager_class_init (PhoshNotifyManagerClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->constructed = phosh_notify_manager_constructed;
  object_class->dispose = phosh_notify_manager_dispose;
  object_class->finalize = phosh_notify_manager_finalize;

  /**
   * PhoshNotifyManager::new-notification:
   * @self: the #PhoshNotifyManager
   * @notification: the new #PhoshNotification
   *
   * Emitted when a new notification is received and a banner should (possibly)
   * be shown
   */
  signals[NEW_NOTIFICATION] = g_signal_new ("new-notification",
                                            G_TYPE_FROM_CLASS (klass),
                                            G_SIGNAL_RUN_LAST,
                                            0,
                                            NULL,
                                            NULL,
                                            g_cclosure_marshal_VOID__OBJECT,
                                            G_TYPE_NONE,
                                            1,
                                            PHOSH_TYPE_NOTIFICATION);

  /**
   * PhoshNotifyManager::notification-activated:
   * @self: the #PhoshNotifyManager
   * @notification: the #PhoshNotification
   *
   * Emitted when the action on a notification gets activated.
   */
  signals[NOTIFICATION_ACTIVATED] = g_signal_new ("notification-activated",
                                                  G_TYPE_FROM_CLASS (klass),
                                                  G_SIGNAL_RUN_LAST,
                                                  0,
                                                  NULL,
                                                  NULL,
                                                  g_cclosure_marshal_VOID__OBJECT,
                                                  G_TYPE_NONE,
                                                  1,
                                                  PHOSH_TYPE_NOTIFICATION);
}


static void
phosh_notify_manager_init (PhoshNotifyManager *self)
{
  self->next_id = 1;

  self->list = phosh_notification_list_new ();
}

/**
 * phosh_notify_manager_get_default:
 *
 * Get the notify manager singleton
 *
 * Returns:(transfer none): The notify manager singleton
 */
PhoshNotifyManager *
phosh_notify_manager_get_default (void)
{
  static PhoshNotifyManager *instance;

  if (instance == NULL) {
    instance = g_object_new (PHOSH_TYPE_NOTIFY_MANAGER, NULL);
    g_object_add_weak_pointer (G_OBJECT (instance), (gpointer *)&instance);
  }

  return instance;
}

/**
 * phosh_notify_manager_get_list:
 * @self: the #PhoshNotifyManager
 *
 * Get the #PhoshNotificationList of current notifications
 *
 * Returns:(transfer none): The #PhoshNotificationList
 */
PhoshNotificationList *
phosh_notify_manager_get_list (PhoshNotifyManager *self)
{
  g_return_val_if_fail (PHOSH_IS_NOTIFY_MANAGER (self), NULL);

  return self->list;
}

/**
 * phosh_notify_manager_get_show_banners:
 * @self: the #PhoshNotifyManager
 *
 * Are notififcation banners enabled
 *
 * Returns: %TRUE if banners should be shown, otherwise %FALSE
 */
gboolean
phosh_notify_manager_get_show_banners (PhoshNotifyManager *self)
{
  g_return_val_if_fail (PHOSH_IS_NOTIFY_MANAGER (self), FALSE);

  return self->show_banners;
}

/**
 * phosh_notify_manager_get_notification_id:
 * @self: the #PhoshNotifyManager
 *
 * Get a notification id
 *
 * Returns: a notification id that can be used to create new
 * notifications.
 */
guint
phosh_notify_manager_get_notification_id (PhoshNotifyManager *self)
{
  g_return_val_if_fail (PHOSH_IS_NOTIFY_MANAGER (self), FALSE);

  return self->next_id++;
}

/**
 * phosh_notify_manager_add_notification
 * @self: the #PhoshNotifyManager
 * @source_id: The notification source's app_id
 * @expire_timeout: When the notification should expire
 * @notification: The notification
 *
 * Adds @notification to the current list of notifications. This is the single
 * entry point to submit notifications.
 */
void
phosh_notify_manager_add_notification (PhoshNotifyManager *self,
                                       const char         *source_id,
                                       int                 expire_timeout,
                                       PhoshNotification  *notification)
{
  g_return_if_fail (PHOSH_IS_NOTIFY_MANAGER (self));
  g_return_if_fail (PHOSH_IS_NOTIFICATION (notification));
  g_return_if_fail (source_id);

  if (!phosh_notify_manager_is_notification_enabled (notification))
    return;

  if (expire_timeout == -1)
    expire_timeout = PHOSH_NOTIFICATION_DEFAULT_TIMEOUT;

  phosh_notification_list_add (self->list, source_id, notification);

  g_signal_connect_object (notification,
                           "expired",
                           G_CALLBACK (on_notification_expired),
                           self,
                           G_CONNECT_SWAPPED);
  g_signal_connect_object (notification,
                           "actioned",
                           G_CALLBACK (on_notification_actioned),
                           self,
                           G_CONNECT_SWAPPED);
  g_signal_connect_object (notification,
                           "closed",
                           G_CALLBACK (on_notification_closed),
                           self,
                           G_CONNECT_SWAPPED);

  if (expire_timeout)
    phosh_notification_expires (notification, expire_timeout);

  g_signal_emit (self, signals[NEW_NOTIFICATION], 0, notification);
}


gboolean
phosh_notify_manager_close_notification_by_id (PhoshNotifyManager *self,
                                               int id,
                                               PhoshNotificationReason reason)
{
  PhoshNotification *notification = NULL;

  g_return_val_if_fail (PHOSH_IS_NOTIFY_MANAGER (self), FALSE);

  notification = phosh_notification_list_get_by_id (self->list, id);
  if (!notification)
    return FALSE;

  phosh_notification_close (notification, reason);
  return TRUE;
}

/**
 * phosh_notify_manager_get_show_notfication_banner:
 * @self: the #PhoshNotifyManager
 * @notification: the #PhoshNotification in question
 *
 * Checks whether a #PhoshNotificationBanner should be displayed
 * for the given #PhoshNotification according to current policy.
 *
 * Returns: %TRUE if the banner should be shown, otherwise %FALSE
 */
gboolean
phosh_notify_manager_get_show_notification_banner (PhoshNotifyManager *self,
                                                   PhoshNotification  *notification)
{
  g_autoptr (GSettings) settings = NULL;
  g_autofree char *path = NULL;
  g_autofree char *munged_id = NULL;
  GAppInfo *app_info;
  gboolean show;

  g_return_val_if_fail (PHOSH_IS_NOTIFY_MANAGER (self), FALSE);

  if (!self->show_banners)
    return FALSE;

  if (phosh_notification_get_urgency (notification) == PHOSH_NOTIFICATION_URGENCY_CRITICAL)
    return TRUE;

  app_info = phosh_notification_get_app_info (notification);
  if (!app_info)
    return TRUE;

  munged_id = phosh_munge_app_id (g_app_info_get_id(app_info));
  path = g_strconcat (NOTIFICATIONS_APP_PREFIX, "/", munged_id, "/", NULL);
  settings = g_settings_new_with_path (NOTIFICATIONS_APP_SCHEMA_ID, path);
  show = g_settings_get_boolean (settings, NOTIFICATIONS_APP_KEY_SHOW_BANNERS);

  g_debug ("Show banners for %s: %d", munged_id, show);
  return show;
}

/**
 * phosh_notify_manager_close_all:
 * @self: the #PhoshNotifyManager
 * @rease: the #PhoshNotificationReason
 *
 * Closes all notifications using #PhoshNotificationReason as reason.
 */
void
phosh_notify_manager_close_all_notifications (PhoshNotifyManager      *self,
                                              PhoshNotificationReason  reason)
{
  GListModel *source_list;

  source_list = G_LIST_MODEL (phosh_notify_manager_get_list (self));

  for (int i = g_list_model_get_n_items(G_LIST_MODEL (source_list)); i > 0; i--) {
    g_autoptr (GListModel) notif_list = G_LIST_MODEL (g_list_model_get_object (source_list, i-1));

    for (int j = g_list_model_get_n_items(G_LIST_MODEL (notif_list)); j > 0; j--) {
      g_autoptr (PhoshNotification) notification = PHOSH_NOTIFICATION (g_list_model_get_object (notif_list, j-1));
      phosh_notification_close (notification, PHOSH_NOTIFICATION_REASON_DISMISSED);
    }
  }
}

/**
 * phosh_notify_manager_add_shell_notification:
 * @self: The #PhoshNotifyManager
 * @notification: The notification to add
 * @id: The id
 * @expire_timeout: The expiration timeout
 *
 * Adds a notification to the list of notifications. If `id` is not `0`
 * an existing notification is replaced.
 *
 * If the notification has no `app-name` or `app-icon` set then a shell
 * default is filled in.
 *
 * If the expire_timeout is greater than `0` the notification is
 * marked as transient.
 *
 * The returned `id` can be used at a later point to retract or
 * replace the notification.
 *
 * Returns: The id of the notification
 */
guint
phosh_notify_manager_add_shell_notification (PhoshNotifyManager *self,
                                             PhoshNotification  *notification,
                                             guint               id,
                                             int                 expire_timeout)
{
  g_autoptr (GDesktopAppInfo) info = NULL;

  info = g_desktop_app_info_new (PHOSH_APP_ID ".desktop");
  g_return_val_if_fail (G_IS_DESKTOP_APP_INFO (info), 0);

  if (!id) {
    id = phosh_notify_manager_get_notification_id (self);
    phosh_notification_set_id (notification, id);
  }

  /* We don't just set the app_info but name and icon separately to
   * not overwrite values set by the caller */
  if (g_strcmp0 (phosh_notification_get_app_name (notification), _("Notification")) == 0) {
    const char *name = g_app_info_get_name (G_APP_INFO (info));

    phosh_notification_set_app_name (notification, name);
  }

  if (!phosh_notification_get_app_icon (notification)) {
    GIcon *icon = g_app_info_get_icon (G_APP_INFO (info));

    phosh_notification_set_app_icon (notification, icon);
  }

  phosh_notification_set_transient (notification, !!expire_timeout);

  phosh_notify_manager_add_notification (self, PHOSH_APP_ID ".desktop",
                                         expire_timeout,
                                         PHOSH_NOTIFICATION (notification));
  return id;
}
