/* ide-notifications.c
 *
 * Copyright 2018-2019 Christian Hergert <chergert@redhat.com>
 *
 * 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 3 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/>.
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#define G_LOG_DOMAIN "ide-notifications"

#include "config.h"

#include "ide-macros.h"
#include "ide-notifications.h"

struct _IdeNotifications
{
  IdeObject parent_instance;
};

typedef struct
{
  gdouble progress;
  guint   total;
  guint   imprecise;
} Progress;

typedef struct
{
  const gchar     *id;
  IdeNotification *notif;
} Find;

static void list_model_iface_init (GListModelInterface *iface);

G_DEFINE_FINAL_TYPE_WITH_CODE (IdeNotifications, ide_notifications, IDE_TYPE_OBJECT,
                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))

enum {
  PROP_0,
  PROP_HAS_PROGRESS,
  PROP_PROGRESS,
  PROP_PROGRESS_IS_IMPRECISE,
  N_PROPS
};

static GParamSpec *properties [N_PROPS];

static void
ide_notifications_get_property (GObject    *object,
                                guint       prop_id,
                                GValue     *value,
                                GParamSpec *pspec)
{
  IdeNotifications *self = IDE_NOTIFICATIONS (object);

  switch (prop_id)
    {
    case PROP_HAS_PROGRESS:
      g_value_set_boolean (value, ide_notifications_get_has_progress (self));
      break;

    case PROP_PROGRESS:
      g_value_set_double (value, ide_notifications_get_progress (self));
      break;

    case PROP_PROGRESS_IS_IMPRECISE:
      g_value_set_boolean (value, ide_notifications_get_progress_is_imprecise (self));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
ide_notifications_child_notify_progress_cb (IdeNotifications *self,
                                            GParamSpec       *pspec,
                                            IdeNotification  *child)
{
  g_assert (IDE_IS_NOTIFICATIONS (self));
  g_assert (IDE_IS_NOTIFICATION (child));

  ide_object_notify_by_pspec (self, properties [PROP_PROGRESS]);
}

static void
ide_notifications_add (IdeObject         *object,
                       IdeObject         *sibling,
                       IdeObject         *child,
                       IdeObjectLocation  location)
{
  IdeNotifications *self = (IdeNotifications *)object;

  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (IDE_IS_NOTIFICATIONS (object));
  g_assert (IDE_IS_OBJECT (child));

  if (!IDE_IS_NOTIFICATION (child))
    {
      g_warning ("Attempt to add something other than an IdeNotification is not allowed");
      return;
    }

  g_signal_connect_object (child,
                           "notify::progress",
                           G_CALLBACK (ide_notifications_child_notify_progress_cb),
                           self,
                           G_CONNECT_SWAPPED);

  IDE_OBJECT_CLASS (ide_notifications_parent_class)->add (object, sibling, child, location);

  g_list_model_items_changed (G_LIST_MODEL (object), ide_object_get_position (child), 0, 1);
  ide_object_notify_by_pspec (self, properties [PROP_HAS_PROGRESS]);
  ide_object_notify_by_pspec (self, properties [PROP_PROGRESS_IS_IMPRECISE]);
  ide_object_notify_by_pspec (self, properties [PROP_PROGRESS]);
}

static void
ide_notifications_remove (IdeObject *object,
                          IdeObject *child)
{
  IdeNotifications *self = (IdeNotifications *)object;
  guint position;

  g_assert (IDE_IS_NOTIFICATIONS (self));
  g_assert (IDE_IS_OBJECT (child));

  g_signal_handlers_disconnect_by_func (child,
                                        G_CALLBACK (ide_notifications_child_notify_progress_cb),
                                        self);

  position = ide_object_get_position (child);

  IDE_OBJECT_CLASS (ide_notifications_parent_class)->remove (object, child);

  g_list_model_items_changed (G_LIST_MODEL (object), position, 1, 0);
  ide_object_notify_by_pspec (self, properties [PROP_HAS_PROGRESS]);
  ide_object_notify_by_pspec (self, properties [PROP_PROGRESS_IS_IMPRECISE]);
  ide_object_notify_by_pspec (self, properties [PROP_PROGRESS]);
}

static void
ide_notifications_class_init (IdeNotificationsClass *klass)
{
  GObjectClass *g_object_class = G_OBJECT_CLASS (klass);
  IdeObjectClass *object_class = IDE_OBJECT_CLASS (klass);

  g_object_class->get_property = ide_notifications_get_property;

  object_class->add = ide_notifications_add;
  object_class->remove = ide_notifications_remove;

  /**
   * IdeNotifications:has-progress:
   *
   * The "has-progress" property denotes if any of the notifications
   * have progress supported.
   *
   * Since: 3.32
   */
  properties [PROP_HAS_PROGRESS] =
    g_param_spec_boolean ("has-progress",
                          "Has Progress",
                          "If any of the notifications have progress",
                          FALSE,
                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  /**
   * IdeNotifications:progress:
   *
   * The "progress" property is the combination of all of the notifications
   * currently monitored. It is updated when child notifications progress
   * changes.
   *
   * Since: 3.32
   */
  properties [PROP_PROGRESS] =
    g_param_spec_double ("progress",
                         "Progress",
                         "The combined process of all child notifications",
                         0.0, 1.0, 0.0,
                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  /**
   * IdeNotifications:progress-is-imprecise:
   *
   * The "progress-is-imprecise" property indicates that all progress-bearing
   * notifications are imprecise.
   *
   * Since: 3.32
   */
  properties [PROP_PROGRESS_IS_IMPRECISE] =
    g_param_spec_boolean ("progress-is-imprecise",
                          "Progress is Imprecise",
                          "If all of the notifications have imprecise progress",
                          FALSE,
                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_properties (g_object_class, N_PROPS, properties);
}

static void
ide_notifications_init (IdeNotifications *self)
{
#if 0
  g_autoptr(IdeNotification) notif = NULL;
  g_autoptr(IdeNotification) notif2 = NULL;
  g_autoptr(IdeNotification) notif3 = NULL;
  g_autoptr(IdeNotification) notif4 = NULL;
  g_autoptr(IdeNotification) notif5 = NULL;
  g_autoptr(IdeNotification) notif6 = NULL;
  g_autoptr(IdeNotification) notif7 = NULL;
  g_autoptr(GIcon) icon1 = NULL;
  g_autoptr(GIcon) icon2 = NULL;
  g_autoptr(GIcon) icon4 = NULL;
  g_autoptr(GIcon) icon5 = NULL;
  g_autoptr(GIcon) icon6 = NULL;
  g_autoptr(GIcon) icon7 = NULL;

  notif = ide_notification_new ();
  ide_notification_set_title (notif, "Builder ready.");
  ide_notification_set_has_progress (notif, FALSE);
  ide_notification_add_button (notif, "Foo", (icon1 = g_icon_new_for_string ("media-playback-pause-symbolic", NULL)), "debugger.pause");
  ide_notifications_add_notification (self, notif);

  notif2 = ide_notification_new ();
  ide_notification_set_title (notif2, "Downloading libdazzle…");
  ide_notification_set_has_progress (notif2, TRUE);
  ide_notification_set_progress (notif2, .75);
  ide_notification_set_default_action (notif2, "win.close");
  ide_notification_add_button (notif2, "Foo", (icon2 = g_icon_new_for_string ("process-stop-symbolic", NULL)), "build-manager.stop");
  ide_notifications_add_notification (self, notif2);

  notif3 = ide_notification_new ();
  ide_notification_set_title (notif3, "SDK Not Installed");
  ide_notification_set_body (notif3, "The org.gnome.Calculator.json build profile requires the org.gnome.Platform runtime. Install it to allow this project to be built.");
  ide_notification_set_has_progress (notif3, FALSE);
  ide_notification_set_progress (notif3, 0);
  ide_notification_add_button (notif3, "Download and Install", NULL, "win.close");
  ide_notification_set_default_action (notif3, "win.close");
  ide_notification_set_urgent (notif3, TRUE);
  ide_notifications_add_notification (self, notif3);

  notif4 = ide_notification_new ();
  ide_notification_set_title (notif4, "Code Analytics Unavailable");
  ide_notification_set_body (notif4, "Code highlighting, error detection, and macros are not fully available, due to this project not being built recently. Rebuild to fully enable these features.");
  ide_notification_set_has_progress (notif4, FALSE);
  ide_notification_set_progress (notif4, 0);
  ide_notification_set_default_action (notif4, "win.close");
  ide_notifications_add_notification (self, notif4);

  notif5 = ide_notification_new ();
  ide_notification_set_title (notif5, "Running Partial Build");
  ide_notification_set_body (notif5, "Diagnostics and autocompletion may be limited until complete.");
  ide_notification_set_has_progress (notif5, TRUE);
  ide_notification_set_progress_is_imprecise (notif5, TRUE);
  ide_notification_set_progress (notif5, 0);
  ide_notification_add_button (notif5, NULL, (icon5 = g_icon_new_for_string ("process-stop-symbolic", NULL)), "win.close");
  ide_notifications_add_notification (self, notif5);

  notif6 = ide_notification_new ();
  ide_notification_set_title (notif6, "Indexing Source Code");
  ide_notification_set_body (notif6, "Search, diagnostics, and autocompletion may be limited until complete.");
  ide_notification_set_has_progress (notif6, TRUE);
  ide_notification_set_progress (notif6, 0);
  ide_notification_set_progress_is_imprecise (notif6, TRUE);
  ide_notification_add_button (notif6, NULL, (icon6 = g_icon_new_for_string ("media-playback-pause-symbolic", NULL)), "win.close");
  ide_notifications_add_notification (self, notif6);

  notif7 = ide_notification_new ();
  ide_notification_set_title (notif7, "Downloading org.gnome.Platform");
  ide_notification_set_body (notif7, "3 minutes remaining");
  ide_notification_set_has_progress (notif7, TRUE);
  ide_notification_set_progress (notif7, 0);
  ide_notification_add_button (notif7, NULL, (icon7 = g_icon_new_for_string ("process-stop-symbolic", NULL)), "win.close");
  ide_notifications_add_notification (self, notif7);

  ide_notification_withdraw_in_seconds (notif, 10);
  ide_notification_withdraw_in_seconds (notif2, 12);
  ide_notification_withdraw_in_seconds (notif3, 14);
  ide_notification_withdraw_in_seconds (notif4, 16);
  ide_notification_withdraw_in_seconds (notif5, 18);
#endif
}

/**
 * ide_notifications_new:
 *
 * Create a new #IdeNotifications.
 *
 * Usually, creating this is not necessary, as the #IdeContext root
 * #IdeObject will create it automatically.
 *
 * Returns: (transfer full): a newly created #IdeNotifications
 *
 * Since: 3.32
 */
IdeNotifications *
ide_notifications_new (void)
{
  return g_object_new (IDE_TYPE_NOTIFICATIONS, NULL);
}

/**
 * ide_notifications_add_notification:
 * @self: an #IdeNotifications
 * @notification: an #IdeNotification
 *
 * Adds @notification as a child of @self, sorting it by priority
 * and urgency.
 *
 * Since: 3.32
 */
void
ide_notifications_add_notification (IdeNotifications *self,
                                    IdeNotification  *notification)
{
  g_return_if_fail (IDE_IS_MAIN_THREAD ());
  g_return_if_fail (IDE_IS_NOTIFICATIONS (self));
  g_return_if_fail (IDE_IS_NOTIFICATION (notification));

  ide_object_insert_sorted (IDE_OBJECT (self),
                            IDE_OBJECT (notification),
                            (GCompareDataFunc)ide_notification_compare,
                            NULL);
}

static GType
ide_notifications_get_item_type (GListModel *model)
{
  return IDE_TYPE_NOTIFICATION;
}

static guint
ide_notifications_get_n_items (GListModel *model)
{
  return ide_object_get_n_children (IDE_OBJECT (model));
}

static gpointer
ide_notifications_get_item (GListModel *model,
                            guint       position)
{
  return ide_object_get_nth_child (IDE_OBJECT (model), position);
}

static void
list_model_iface_init (GListModelInterface *iface)
{
  iface->get_item_type = ide_notifications_get_item_type;
  iface->get_n_items = ide_notifications_get_n_items;
  iface->get_item = ide_notifications_get_item;
}

static void
collect_progress_cb (gpointer item,
                     gpointer user_data)
{
  IdeNotification *notif = item;
  Progress *prog = user_data;

  g_assert (IDE_IS_NOTIFICATION (notif));
  g_assert (prog != NULL);

  if (ide_notification_get_has_progress (notif))
    {
      if (ide_notification_get_progress_is_imprecise (notif))
        prog->imprecise++;
      else
        prog->progress += ide_notification_get_progress (notif);

      prog->total++;
    }
}

/**
 * ide_notifications_get_progress:
 * @self: a #IdeNotifications
 *
 * Gets the combined progress of the notifications contained in this
 * #IdeNotifications object.
 *
 * Returns: A double between 0.0 and 1.0
 *
 * Since: 3.32
 */
gdouble
ide_notifications_get_progress (IdeNotifications *self)
{
  Progress prog = {0};

  g_return_val_if_fail (IDE_IS_NOTIFICATIONS (self), 0.0);

  ide_object_lock (IDE_OBJECT (self));
  ide_object_foreach (IDE_OBJECT (self), collect_progress_cb, &prog);
  ide_object_unlock (IDE_OBJECT (self));

  if (prog.total > 0)
    {
      if (prog.imprecise != prog.total)
        return prog.progress / (gdouble)(prog.total - prog.imprecise);
      else
        return prog.progress / (gdouble)prog.total;
    }

  return 0.0;
}

/**
 * ide_notifications_get_has_progress:
 * @self: a #IdeNotifications
 *
 * Gets if any of the notification support progress updates.
 *
 * Returns: %TRUE if any notification has progress
 *
 * Since: 3.32
 */
gboolean
ide_notifications_get_has_progress (IdeNotifications *self)
{
  Progress prog = {0};

  g_return_val_if_fail (IDE_IS_NOTIFICATIONS (self), 0.0);

  ide_object_lock (IDE_OBJECT (self));
  ide_object_foreach (IDE_OBJECT (self), collect_progress_cb, &prog);
  ide_object_unlock (IDE_OBJECT (self));

  return prog.total > 0;
}

/**
 * ide_notifications_get_progress_is_imprecise:
 * @self: a #IdeNotifications
 *
 * Checks if all of the notifications with progress are imprecise.
 *
 * Returns: %TRUE if all progress-supporting notifications are imprecise.
 *
 * Since: 3.32
 */
gboolean
ide_notifications_get_progress_is_imprecise (IdeNotifications *self)
{
  Progress prog = {0};

  g_return_val_if_fail (IDE_IS_NOTIFICATIONS (self), 0.0);

  ide_object_lock (IDE_OBJECT (self));
  ide_object_foreach (IDE_OBJECT (self), collect_progress_cb, &prog);
  ide_object_unlock (IDE_OBJECT (self));

  if (prog.total > 0)
    return prog.imprecise == prog.total;

  return FALSE;
}

static void
find_by_id (gpointer item,
            gpointer user_data)
{
  IdeNotification *notif = item;
  Find *find = user_data;
  g_autofree gchar *id = NULL;

  if (find->notif)
    return;

  id = ide_notification_dup_id (notif);

  if (ide_str_equal0 (find->id, id))
    find->notif = g_object_ref (notif);
}

/**
 * ide_notifications_find_by_id:
 * @self: a #IdeNotifications
 * @id: the id of the notification
 *
 * Finds the first #IdeNotification registered with @self with
 * #IdeNotification:id of @id.
 *
 * Returns: (transfer full) (nullable): an #IdeNotification or %NULL
 *
 * Since: 3.32
 */
IdeNotification *
ide_notifications_find_by_id (IdeNotifications *self,
                              const gchar      *id)
{
  Find find = { id, NULL };

  g_return_val_if_fail (IDE_IS_NOTIFICATIONS (self), NULL);
  g_return_val_if_fail (id != NULL, NULL);

  ide_object_lock (IDE_OBJECT (self));
  ide_object_foreach (IDE_OBJECT (self), find_by_id, &find);
  ide_object_unlock (IDE_OBJECT (self));

  return g_steal_pointer (&find.notif);
}
