/* livepatch-tray.c
 * Copyright (C) 2019 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 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 St, Fifth Floor
 * Boston, MA  02110-1301 USA.
 */
#include <gio/gdesktopappinfo.h>
#include <glib.h>

#include "update-notifier.h"
#include "livepatch-tray.h"
#include "livepatch-utils.h"
#include "trayappletui.h"

#define LIVEPATCH_COMMON_DIRECTORY "/var/snap/canonical-livepatch/common"
#define LIVEPATCH_SNAP_DIRECTORY "/snap/canonical-livepatch"
#define TIMEOUT_SECONDS_DELAY 2

typedef struct _LivepatchTrayAppletPriv LivepatchTrayAppletPriv;
struct _LivepatchTrayAppletPriv
{
  GtkWidget    *menuitem_enabled;
  GtkWidget    *menuitem_desc;

  GFileMonitor *common_dir_monitor;
  GFileMonitor *snap_dir_monitor;

  guint         timeout_id;
};

static void
on_settings_menuitem_activated(GObject *self, gpointer user_data)
{
  g_autoptr(GDesktopAppInfo) info = NULL;
  g_autoptr(GdkAppLaunchContext) context = NULL;
  g_autoptr(GError) error = NULL;

  info = g_desktop_app_info_new(LIVEPATCH_DESKTOP_FILE);
  if (info == NULL)
    {
      g_warning("Could not find application '%s'", LIVEPATCH_DESKTOP_FILE);
      return;
    }

  context = gdk_display_get_app_launch_context(gdk_display_get_default());
  if (!g_app_info_launch(G_APP_INFO(info), NULL,
                         G_APP_LAUNCH_CONTEXT(context), &error))
    {
      g_warning("Could not launch application '%s'", LIVEPATCH_DESKTOP_FILE);
    }
}


static void
livepatch_trayicon_create_menu(TrayApplet *ta)
{
  LivepatchTrayAppletPriv *priv = (LivepatchTrayAppletPriv *) ta->user_data;
  GtkWidget *menuitem;

  ta->menu = gtk_menu_new();

  priv->menuitem_enabled = gtk_menu_item_new();
  gtk_widget_set_sensitive(priv->menuitem_enabled, FALSE);
  gtk_menu_shell_append(GTK_MENU_SHELL(ta->menu), priv->menuitem_enabled);

  priv->menuitem_desc = gtk_menu_item_new();
  gtk_widget_set_sensitive(priv->menuitem_desc, FALSE);
  gtk_menu_shell_append(GTK_MENU_SHELL(ta->menu), priv->menuitem_desc);

  menuitem = gtk_separator_menu_item_new();
  gtk_menu_shell_append(GTK_MENU_SHELL(ta->menu), menuitem);

  menuitem = gtk_menu_item_new_with_label(_("Livepatch Settings…"));
  gtk_menu_shell_append(GTK_MENU_SHELL(ta->menu), menuitem);
  g_signal_connect(G_OBJECT(menuitem), "activate",
                   G_CALLBACK(on_settings_menuitem_activated), ta);

  gtk_widget_show_all(ta->menu);
}

static gboolean
check_livepatch(TrayApplet *ta)
{
  LivepatchTrayAppletPriv *priv = (LivepatchTrayAppletPriv *) ta->user_data;
  gboolean show_status_icon;
  g_autofree gchar *check_state = NULL;
  g_autofree gchar *state = NULL;
  g_autoptr(GError) error = NULL;

  show_status_icon = g_settings_get_boolean(ta->un->settings,
                                            SETTINGS_KEY_SHOW_LIVEPATCH_ICON);

  if (!show_status_icon ||
      !livepatch_is_supported() || 
      !livepatch_is_running ())
    {
      tray_applet_ui_set_visible(ta, FALSE);

      priv->timeout_id = 0;
      return G_SOURCE_REMOVE;
    }

  tray_applet_ui_set_visible(ta, TRUE);

  gtk_menu_item_set_label(GTK_MENU_ITEM(priv->menuitem_enabled),
                          _("Livepatch is on"));

  check_state = livepatch_get_check_state(&error);
  if (check_state == NULL)
    g_warning("Cannot get Livepatch check-state: %s", error->message);

  state = livepatch_get_state(&error);
  if (state == NULL)
    g_warning("Cannot get Livepatch state: %s", error->message);

  if (!g_strcmp0(check_state, "checked") &&
      (!g_strcmp0(state, "applied") || !g_strcmp0(state, "nothing-to-apply")))
    {
      ssize_t num_fixes;
      g_autofree gchar *label = NULL;

      tray_applet_ui_set_icon(ta, "livepatch_on");

      num_fixes = livepatch_get_num_fixes(&error);
      if (num_fixes == -1)
        {
          g_warning("Cannot get applied Livepatch fixes: %s", error->message);
          num_fixes = 0;
        }

      gtk_widget_set_visible(priv->menuitem_desc, TRUE);

      if (num_fixes == 0)
        label = g_strdup(_("No current updates"));
      else
        label = g_strdup_printf(ngettext("%zd current update",
                                         "%zd current updates",
                                         num_fixes), num_fixes);

      gtk_menu_item_set_label(GTK_MENU_ITEM(priv->menuitem_desc), label);
    }
  else if (!g_strcmp0(check_state, "needs-check") ||
           !g_strcmp0(state, "unapplied") ||
           !g_strcmp0(state, "applying"))
    {
      /* Check livepatch status again */
      return G_SOURCE_CONTINUE;
    }
  else
    {
      tray_applet_ui_set_icon(ta, "livepatch_warning");

      if (check_state == NULL || !g_strcmp0(check_state, "check-failed"))
        {
          gtk_menu_item_set_label(GTK_MENU_ITEM(priv->menuitem_desc),
            _("An error occured when checking for Livepatch updates."));
        }
      else
        {
          gtk_menu_item_set_label(GTK_MENU_ITEM(priv->menuitem_desc),
            _("An error occured when applying Livepatch updates."));
        }
    }

    priv->timeout_id = 0;
    return G_SOURCE_REMOVE;
}

static void
gsettings_visibility_changed_cb(GSettings *settings,
                                gchar     *key,
                                gpointer   user_data)
{
  TrayApplet *ta = (TrayApplet *) user_data;
  LivepatchTrayAppletPriv *priv = (LivepatchTrayAppletPriv *) ta->user_data;

  if (priv->timeout_id <= 0)
    priv->timeout_id = g_timeout_add_seconds(0, (GSourceFunc) check_livepatch, ta);
}

static void
livepatch_directory_changed_cb(GFileMonitor     *monitor,
                               GFile            *file,
                               GFile            *other_file,
                               GFileMonitorEvent event_type,
                               gpointer          user_data)
{
  TrayApplet *ta = (TrayApplet *) user_data;
  LivepatchTrayAppletPriv *priv = (LivepatchTrayAppletPriv *) ta->user_data;

  if (priv->timeout_id <= 0)
    priv->timeout_id = g_timeout_add_seconds(TIMEOUT_SECONDS_DELAY,
                                             (GSourceFunc) check_livepatch, ta);
}

void
livepatch_tray_icon_init(TrayApplet *ta)
{
  g_autoptr(GFile) livepatch_common_dir = NULL;
  g_autoptr(GFile) livepatch_snap_dir = NULL;
  g_autoptr(GFileMonitor) common_dir_monitor = NULL;
  g_autoptr(GFileMonitor) snap_dir_monitor = NULL;
  g_autoptr(GError) error = NULL;
  LivepatchTrayAppletPriv *priv;

  if (!livepatch_is_supported())
    return;

  if (!livepatch_has_settings_ui())
    {
      g_warning("There is no graphical application installed to manage "
                "Livepatch. The livepatch status icon will not be displayed.");
      return;
    }

  /* Monitor the directory LIVEPATCH_COMMON_DIRECTORY for changes to update
     the status of the indicator. */
  livepatch_common_dir = g_file_new_for_path(LIVEPATCH_COMMON_DIRECTORY);
  common_dir_monitor = g_file_monitor_directory(livepatch_common_dir,
                                                G_FILE_MONITOR_NONE,
                                                NULL, &error);
  if (common_dir_monitor == NULL)
    {
        g_warning("Couldn't create directory monitor on %s. Error: %s\n",
                  LIVEPATCH_COMMON_DIRECTORY, error->message);
        return;
    }

  /* We also need to monitor the directory LIVEPATCH_SNAP_DIRECTORY in order to
     detect if the snap was enabled/disabled. This consider the case
     canonical-livepatch is enabled but the snap is disabled. */
  livepatch_snap_dir = g_file_new_for_path(LIVEPATCH_SNAP_DIRECTORY);
  snap_dir_monitor = g_file_monitor_directory(livepatch_snap_dir,
                                              G_FILE_MONITOR_WATCH_MOUNTS,
                                              NULL, &error);
  if (snap_dir_monitor == NULL)
    {
        g_warning("Couldn't create directory monitor on %s. Error: %s\n",
                  LIVEPATCH_SNAP_DIRECTORY, error->message);
        return;
    }

  priv = g_new0(LivepatchTrayAppletPriv, 1);
  priv->common_dir_monitor = g_steal_pointer(&common_dir_monitor);
  priv->snap_dir_monitor = g_steal_pointer(&snap_dir_monitor);
  ta->user_data = priv;

  tray_applet_ui_ensure(ta);

  /* Menu initialization */
  livepatch_trayicon_create_menu(ta);
  tray_applet_ui_set_menu(ta, ta->menu);

  g_signal_connect(ta->un->settings,
                   "changed::"SETTINGS_KEY_SHOW_LIVEPATCH_ICON,
                   G_CALLBACK(gsettings_visibility_changed_cb), ta);
  g_signal_connect(priv->common_dir_monitor,
                   "changed",
                   G_CALLBACK(livepatch_directory_changed_cb), ta);
  g_signal_connect(priv->snap_dir_monitor,
                   "changed",
                   G_CALLBACK(livepatch_directory_changed_cb), ta);

  /* We always run check_livepatch in a timeout beacuse this allows us to easily
     check again the status of livepatch if it is in a transistion state (e.g.
     it is downloading the patches or applying them, etc.) */
  priv->timeout_id = g_timeout_add_seconds(TIMEOUT_SECONDS_DELAY,
                                           (GSourceFunc) check_livepatch, ta);
}
