/*
 * Copyright 2013 Canonical Ltd.
 *
 * Authors:
 *   Charles Kerr <charles.kerr@canonical.com>
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 3, as published
 * by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranties of
 * MERCHANTABILITY, SATISFACTORY QUALITY, 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/>.
 */

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

#include <libwhoopsie/recoverable-problem.h>

#include "backend.h"
#include "service.h"

#define BUS_NAME "com.canonical.indicator.session"
#define BUS_PATH "/com/canonical/indicator/session"

#define ICON_DEFAULT "system-devices-panel"
#define ICON_INFO    "system-devices-panel-information"
#define ICON_ALERT   "system-devices-panel-alert"

G_DEFINE_TYPE (IndicatorSessionService,
               indicator_session_service,
               G_TYPE_OBJECT)

/* signals enum */
enum
{
  NAME_LOST,
  LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = { 0 };

enum
{
  PROP_0,
  PROP_MAX_USERS,
  PROP_LAST
};

static GParamSpec * properties[PROP_LAST];

enum
{
  SECTION_HEADER    = (1<<0),
  SECTION_ADMIN     = (1<<1),
  SECTION_SETTINGS  = (1<<2),
  SECTION_SWITCH    = (1<<3),
  SECTION_LOGOUT    = (1<<4),
  SECTION_SESSION   = (1<<5)
};

enum
{
  PROFILE_DESKTOP,
  PROFILE_GREETER,
  PROFILE_LOCKSCREEN,
  N_PROFILES
};

static const char * const menu_names[N_PROFILES] =
{
  "desktop",
  "desktop_greeter",
  "desktop_lockscreen"
};

static const char * const usage_mode_schema_name = "com.canonical.Unity8";

struct ProfileMenuInfo
{
  /* the root level -- the header is the only child of this */
  GMenu * menu;

  /* parent of the sections. This is the header's submenu */
  GMenu * submenu;

  guint export_id;
};

struct _IndicatorSessionServicePrivate
{
  guint own_id;
  guint max_users;
  IndicatorSessionUsers * backend_users;
  IndicatorSessionGuest * backend_guest;
  IndicatorSessionActions * backend_actions;
  GSettings * indicator_settings;
  GSettings * keybinding_settings;
  GSettings * usage_mode_settings;
  GSimpleActionGroup * actions;
  guint actions_export_id;
  struct ProfileMenuInfo menus[N_PROFILES];
  GSimpleAction * header_action;
  GSimpleAction * user_switcher_action;
  GSimpleAction * guest_switcher_action;
  GSimpleAction * usage_mode_action;
  GHashTable * users;
  GHashTable * reported_users;
  guint rebuild_id;
  int rebuild_flags;
  GDBusConnection * conn;
  GCancellable * cancellable;
  GVariant * default_icon_serialized;
};

typedef IndicatorSessionServicePrivate priv_t;

static const char * get_current_real_name (IndicatorSessionService * self);

/***
****
***/

static void rebuild_now (IndicatorSessionService * self, int section);
static void rebuild_soon (IndicatorSessionService * self, int section);

static inline void
rebuild_header_soon (IndicatorSessionService * self)
{
  rebuild_soon (self, SECTION_HEADER);
}
static inline void
rebuild_switch_section_soon (IndicatorSessionService * self)
{
  rebuild_soon (self, SECTION_SWITCH);
}
static inline void
rebuild_logout_section_soon (IndicatorSessionService * self)
{
  rebuild_soon (self, SECTION_LOGOUT);
}
static inline void
rebuild_session_section_soon (IndicatorSessionService * self)
{
  rebuild_soon (self, SECTION_SESSION);
}
static inline void
rebuild_settings_section_soon (IndicatorSessionService * self)
{
  rebuild_soon (self, SECTION_SETTINGS);
}
static inline void
rebuild_admin_section_soon (IndicatorSessionService * self)
{
  rebuild_soon (self, SECTION_ADMIN);
}

/***
****
***/

static gboolean
show_user_list (IndicatorSessionService * self)
{
  return g_settings_get_boolean (self->priv->indicator_settings,
                                 "user-show-menu");
}


static GVariant *
action_state_for_header (IndicatorSessionService * self)
{
  const priv_t * const p = self->priv;
  gboolean show_name;
  const gchar * real_name;
  const gchar * label;
  gchar * a11y;
  GVariantBuilder b;
  GVariant * state;

  show_name = g_settings_get_boolean (p->indicator_settings,
                                      "show-real-name-on-panel");

  real_name = get_current_real_name (self);
  label = show_name && real_name ? real_name : "";

  if (*label)
    {
      /* Translators: the name of the menu ("System"), then the user's name */
      a11y = g_strdup_printf (_("System, %s"), label);
    }
  else
    {
      a11y = g_strdup (_("System"));
    }

  /* build the state */
  g_variant_builder_init (&b, G_VARIANT_TYPE("a{sv}"));
  g_variant_builder_add (&b, "{sv}", "accessible-desc", g_variant_new_string (a11y));
  g_variant_builder_add (&b, "{sv}", "icon", p->default_icon_serialized);
  if (label && *label)
    g_variant_builder_add (&b, "{sv}", "label", g_variant_new_string (label));
  g_variant_builder_add (&b, "{sv}", "visible", g_variant_new_boolean (TRUE));
  state = g_variant_builder_end (&b);

  /* cleanup */
  g_free (a11y);

  return state;
}

static void
update_header_action (IndicatorSessionService * self)
{
  g_simple_action_set_state (self->priv->header_action, action_state_for_header (self));
}

/***
****  USERS
***/

static GMenuModel * create_switch_section (IndicatorSessionService * self, int profile);

static void
add_user (IndicatorSessionService * self, guint uid)
{
  IndicatorSessionUser * u;

  if ((u = indicator_session_users_get_user (self->priv->backend_users, uid)))
    {
      /* update our user table */
      g_hash_table_insert (self->priv->users, GUINT_TO_POINTER(uid), u);

      /* queue rebuilds for the affected sections */
      rebuild_switch_section_soon (self);
      if (u->is_current_user)
        rebuild_header_soon (self);
    }
}

static void
on_user_added (IndicatorSessionUsers * backend_users G_GNUC_UNUSED,
               guint                   uid,
               gpointer                gself)
{
  add_user (INDICATOR_SESSION_SERVICE(gself), uid);
}

static void
on_user_changed (IndicatorSessionUsers * backend_users G_GNUC_UNUSED,
                 guint                   uid,
                 gpointer                gself)
{
  add_user (INDICATOR_SESSION_SERVICE(gself), uid);
}

static void
maybe_add_users (IndicatorSessionService * self)
{
  if (show_user_list (self))
    {
      GList * uids, * l;

      uids = indicator_session_users_get_uids (self->priv->backend_users);
      for (l=uids; l!=NULL; l=l->next)
        add_user (self, GPOINTER_TO_UINT(l->data));
      g_list_free (uids);
    }
}


static void
user_show_menu_changed (IndicatorSessionService * self)
{
  if (show_user_list (self))
      maybe_add_users (self);
  else
      g_hash_table_remove_all (self->priv->users);

  rebuild_switch_section_soon (self);
}

static void
on_user_removed (IndicatorSessionUsers * backend_users G_GNUC_UNUSED,
                 guint                   uid,
                 gpointer                gself)
{
  IndicatorSessionService * self = INDICATOR_SESSION_SERVICE (gself);
  g_return_if_fail (self != NULL);

  /* update our user table */
  g_hash_table_remove (self->priv->users, GUINT_TO_POINTER(uid));

  /* enqueue rebuilds for the affected sections */
  rebuild_switch_section_soon (self);
}

static const char *
get_user_label (const IndicatorSessionUser * user)
{
  const char * c;

  /* if real_name exists and is printable, use it */
  c = user->real_name;
  if ((c != NULL) && g_utf8_validate(c, -1, NULL))
    {
      while (*c != '\0')
        {
          if (g_unichar_isgraph(g_utf8_get_char(c)))
            return user->real_name;

          c = g_utf8_next_char(c);
        }
    }

  /* otherwise, use this as a fallback */
  return user->user_name;
}

static const char *
get_current_real_name (IndicatorSessionService * self)
{
  GHashTableIter iter;
  gpointer key, value;

  /* is it the guest? */
  if (indicator_session_guest_is_active (self->priv->backend_guest))
    return _("Guest");

  /* is it a user? */
  g_hash_table_iter_init (&iter, self->priv->users);
  while (g_hash_table_iter_next (&iter, &key, &value))
    {
      IndicatorSessionUser * user = value;
      if (user->is_current_user)
        return get_user_label (user);
    }

  return "";
}

/***
****
***/

static GHashTable*
get_os_release (void)
{
  static const char * const os_release = "/etc/os-release";
  GHashTable * hash;
  GIOChannel * io;

  hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);

  if ((io = g_io_channel_new_file (os_release, "r", NULL)))
    {
      GString * key = g_string_new (NULL);

      for (;;)
        {
          GIOStatus status;
          char * in;
          GError * error;
          gchar * val;

          /* read a line */
          status = g_io_channel_read_line_string (io, key, NULL, NULL);
          if (status == G_IO_STATUS_EOF)
            break;

          /* ignore blank lines & comments */
          if (!key->len || key->str[0]=='#')
            continue;

          /* split into key=value */
          in = strchr(key->str, '=');
          if (!in)
            continue;
          *in++ = '\0';

          /* unmunge the value component */
          g_strstrip(in); /* eat linefeed */
          error = NULL;
          val = g_shell_unquote (in, &error);
          if (error != NULL)
            {
              g_warning("Unable to unquote \"%s\": %s", in, error->message);
              val = g_strdup(in);
              g_clear_error(&error);
            }
 
          g_debug("from \"%s\": key [%s] val [%s]", os_release, key->str, val); 
          g_hash_table_insert (hash, g_strdup(key->str), val); /* hash owns val now */
        }

      g_string_free(key, TRUE);
      g_io_channel_unref(io);
    }

  return hash;
}

static const char*
get_distro_name (void)
{
  static char * distro_name = NULL;

  if (distro_name == NULL)
    {
      GHashTable * os_release = get_os_release();
      gpointer value = g_hash_table_lookup(os_release, "NAME");
      if (value == NULL)
        value = "Ubuntu"; /* fallback value */
      distro_name = g_strdup(value);
      g_hash_table_destroy(os_release);
    }

  return distro_name;
}

static gboolean
usage_mode_to_action_state(GValue *value,
                           GVariant *variant,
                           gpointer unused G_GNUC_UNUSED)
{
  const gchar* usage_mode = g_variant_get_string(variant, NULL);
  GVariant* ret_var = g_variant_new_boolean(g_strcmp0(usage_mode, "Windowed") == 0 ? TRUE : FALSE);
  g_value_set_variant(value, ret_var);
  return TRUE;
}

static GVariant*
action_state_to_usage_mode(const GValue *value,
                           const GVariantType * unused_expected_type G_GNUC_UNUSED,
                           gpointer unused G_GNUC_UNUSED)
{
  GVariant* var = g_value_get_variant(value);
  GVariant* ret = g_variant_new_string(g_variant_get_boolean(var) == TRUE ? "Windowed" : "Staged");
  return ret;
}

static void
on_usage_mode_setting_changed (gpointer gself)
{
  rebuild_admin_section_soon((IndicatorSessionService*)(gself));
}

static GMenuModel *
create_admin_section (IndicatorSessionService * self)
{
  GMenu * menu;
  priv_t * p = self->priv;
  gchar * help_label = g_strdup_printf(_("%s Help…"), get_distro_name());
  menu = g_menu_new ();
  if (g_getenv ("MIR_SOCKET") != NULL) {
      g_menu_append (menu, _("About This Device…"), "indicator.about");
  } else {
      g_menu_append (menu, _("About This Computer"), "indicator.about");
  }
  g_menu_append (menu, help_label, "indicator.help");
  g_free (help_label);

  if (p->usage_mode_action && g_getenv ("MIR_SOCKET") != NULL) // only under unity8
  {
      GMenuItem * menu_item = NULL;
      menu_item = g_menu_item_new(_("Desktop Mode"), "indicator.usage-mode");
      g_menu_item_set_attribute(menu_item, "x-canonical-type", "s", "com.canonical.indicator.switch");
      g_menu_append_item(menu, menu_item);
      g_object_unref(menu_item);
  }

  return G_MENU_MODEL (menu);
}

static GMenuModel *
create_settings_section (IndicatorSessionService * self)
{
  GMenu * menu;
  priv_t * p = self->priv;

  menu = g_menu_new ();
  g_menu_append (menu, _("System Settings…"), "indicator.settings");
  if (indicator_session_actions_has_online_account_error (p->backend_actions))
      g_menu_append (menu, _("Online Accounts…"), "indicator.online-accounts");

  return G_MENU_MODEL (menu);
}

/**
 * The switch-to-guest action's state is a dictionary with these entries:
 *   - "is-active" (boolean)
 *   - "is-logged-in" (boolean)
 */
static GVariant *
create_guest_switcher_state (IndicatorSessionService * self)
{
  GVariant * val;
  GVariantBuilder b;
  IndicatorSessionGuest * const g = self->priv->backend_guest;

  g_variant_builder_init (&b, G_VARIANT_TYPE ("a{sv}"));
  val = g_variant_new_boolean (indicator_session_guest_is_active (g));
  g_variant_builder_add (&b, "{sv}", "is-active", val);
  val = g_variant_new_boolean (indicator_session_guest_is_logged_in (g));
  g_variant_builder_add (&b, "{sv}", "is-logged-in", val);
  return g_variant_builder_end (&b);
}

/**
 * The switch-to-user action's state is a dictionary with these entries: 
 *  - "active-user" (username string)
 *  - "logged-in-users" (array of username strings)
 */
static GVariant *
create_user_switcher_state (IndicatorSessionService * self)
{
  GVariantBuilder a;
  GVariantBuilder b;
  GVariant * val;
  GHashTableIter ht_iter;
  gpointer ht_value;
  const char * current_user;

  current_user = "";
  g_variant_builder_init (&a, G_VARIANT_TYPE("as"));
  g_hash_table_iter_init (&ht_iter, self->priv->users);
  while (g_hash_table_iter_next (&ht_iter, NULL, &ht_value))
    {
      const IndicatorSessionUser * u = ht_value;

      if (u->is_current_user)
        current_user = u->user_name;

      if (u->is_logged_in)
        g_variant_builder_add (&a, "s", u->user_name);
    }

  g_variant_builder_init (&b, G_VARIANT_TYPE("a{sv}"));
  val = g_variant_new_string (current_user);
  g_variant_builder_add (&b, "{sv}", "active-user", val);
  val = g_variant_builder_end (&a);
  g_variant_builder_add (&b, "{sv}", "logged-in-users", val);
  return g_variant_builder_end (&b);
}

static void
update_switch_actions (IndicatorSessionService * self)
{
  g_simple_action_set_state (self->priv->guest_switcher_action,
                             create_guest_switcher_state (self));

  g_simple_action_set_state (self->priv->user_switcher_action,
                             create_user_switcher_state (self));
}

static gboolean
use_ellipsis (IndicatorSessionService * self)
{
  /* does the backend support confirmation prompts? */
  if (!indicator_session_actions_can_prompt (self->priv->backend_actions))
    return FALSE;

  /* has the user disabled prompts? */
  if (g_settings_get_boolean (self->priv->indicator_settings,
                              "suppress-logout-restart-shutdown"))
    return FALSE;

  return TRUE;
}

/* lower index == more useful.
   When there are too many users for the menu,
   we use this to decide which to cull. */
static int
compare_users_by_usefulness (gconstpointer ga, gconstpointer gb)
{
  const IndicatorSessionUser * a = *(const IndicatorSessionUser**)ga;
  const IndicatorSessionUser * b = *(const IndicatorSessionUser**)gb;

  if (a->is_current_user != b->is_current_user)
    return a->is_current_user ? -1 : 1;

  if (a->is_logged_in != b->is_logged_in)
    return a->is_logged_in ? -1 : 1;

  if (a->login_frequency != b->login_frequency)
    return a->login_frequency > b->login_frequency ? -1 : 1;

  return 0;
}

/* sorting them for display in the menu */
static int
compare_users_by_label (gconstpointer ga, gconstpointer gb)
{
  int i;
  const IndicatorSessionUser * a = *(const IndicatorSessionUser**)ga;
  const IndicatorSessionUser * b = *(const IndicatorSessionUser**)gb;

  if ((i = g_strcmp0 (get_user_label (a), get_user_label (b))))
    return i;

  return g_strcmp0 (a->user_name, b->user_name);
}

static GVariant *
serialize_icon_file (const gchar * filename)
{
  GVariant * serialized_icon = NULL;

  if (filename != NULL)
    {
      GFile * file = g_file_new_for_path (filename);
      GIcon * icon = g_file_icon_new (file);

      serialized_icon = g_icon_serialize (icon);

      g_object_unref (icon);
      g_object_unref (file);
    }

  return serialized_icon;
}

static void
report_unusable_user (IndicatorSessionService * self, const IndicatorSessionUser * u)
{
  const priv_t * const p = self->priv;
  gpointer key;

  g_return_if_fail(u != NULL);

  key = GUINT_TO_POINTER(u->uid);

  if (!g_hash_table_contains (p->reported_users, key))
  {
    gchar * uid_str = g_strdup_printf("%u", u->uid);

    const char * properties[] = {
        "uid", uid_str,
        "icon_file", (u->icon_file ? u->icon_file : "(null)"),
        "is_current_user", (u->is_current_user ? "true" : "false"),
        "is_logged_in", (u->is_logged_in ? "true" : "false"),
        "real_name", (u->real_name ? u->real_name : "(null)"),
        "user_name", (u->user_name ? u->user_name : "(null)"),
        NULL
    };

    whoopsie_report_recoverable_problem("indicator-session-unknown-user-error", 0, FALSE, properties);

    /* mark it as reported so that we'll only report it once */
    g_hash_table_add (p->reported_users, key);

    g_free (uid_str);
  }
}

static GMenuModel *
create_switch_section (IndicatorSessionService * self, int profile)
{
  GMenu * menu;
  GMenuItem * item = NULL;
  gboolean want_accel;
  guint i;
  gpointer guser;
  GHashTableIter iter;
  GPtrArray * users;
  const priv_t * const p = self->priv;
  const gboolean ellipsis = use_ellipsis (self);

  menu = g_menu_new ();

  /* lockswitch */
  if (indicator_session_users_is_live_session (p->backend_users))
    {
      const char * action = "indicator.switch-to-screensaver";
      item = g_menu_item_new (_("Start Screen Saver"), action);
      want_accel = TRUE;
    }
  else if (profile == PROFILE_LOCKSCREEN ||
           indicator_session_guest_is_active (p->backend_guest))
    {
      const char * action = "indicator.switch-to-greeter";
      if (indicator_session_actions_can_switch (p->backend_actions))
        item = g_menu_item_new (ellipsis ? _("Switch Account…")
                                         : _("Switch Account"), action);
      want_accel = FALSE;
    }
  else
    {
      const char * lock_switch_action = "indicator.switch-to-screensaver";
      const char * switch_action = "indicator.switch-to-greeter";

      if (g_hash_table_size (p->users) > 1 &&
          indicator_session_actions_can_switch (p->backend_actions))
        {
          if (indicator_session_actions_can_lock (p->backend_actions))
            item = g_menu_item_new (ellipsis ? _("Lock/Switch Account…")
                                             : _("Lock/Switch Account"), lock_switch_action);
          else
            item = g_menu_item_new (ellipsis ? _("Switch Account…")
                                             : _("Switch Account"), switch_action);
        }
      else if (indicator_session_actions_can_lock (p->backend_actions))
          item = g_menu_item_new (_("Lock"), lock_switch_action);

      want_accel = TRUE;
    }

  if (item)
    {
      if (want_accel)
        {
          gchar * str = g_settings_get_string (p->keybinding_settings, "screensaver");
          g_menu_item_set_attribute (item, "accel", "s", str);
          g_free (str);
        }

      g_menu_append_item (menu, item);
      g_object_unref (item);
    }

  if (!indicator_session_actions_can_switch (p->backend_actions))
    return G_MENU_MODEL (menu);

  if (indicator_session_guest_is_allowed (p->backend_guest))
    {
      GMenuItem *item;

      item = g_menu_item_new (_("Guest Session"), "indicator.switch-to-guest");
      g_menu_item_set_attribute (item, "x-canonical-type", "s", "indicator.guest-menu-item");
      g_menu_append_item (menu, item);

      g_object_unref (item);
    }

  /* if we need to show the user list, build an array of all the users we know
   * of, otherwise get out now */
  if (!show_user_list (self))
      return G_MENU_MODEL (menu);

  users = g_ptr_array_new ();
  g_hash_table_iter_init (&iter, p->users);
  while (g_hash_table_iter_next (&iter, NULL, &guser))
    g_ptr_array_add (users, guser);

  /* if there are too many users, cull out the less interesting ones */
  if (users->len > p->max_users)
    {
      g_ptr_array_sort (users, compare_users_by_usefulness);
      g_ptr_array_set_size (users, p->max_users);
    }

  /* sort the users by name */
  g_ptr_array_sort (users, compare_users_by_label);

  /* add the users */
  for (i=0; i<users->len; ++i)
    {
      const IndicatorSessionUser * u = g_ptr_array_index (users, i);
      const char * label;
      GVariant * serialized_icon;

      if (profile == PROFILE_LOCKSCREEN && u->is_current_user)
        continue;

      /* Sometimes we get a user without a username? bus hiccup.
         I can't reproduce it, but let's not confuse users with
         a meaningless menuitem. (see bug #1263228) */
      label = get_user_label (u);
      if (!label || !*label)
      {
        report_unusable_user (self, u);
        continue;
      }

      item = g_menu_item_new (label, NULL);
      g_menu_item_set_action_and_target (item, "indicator.switch-to-user", "s", u->user_name);
      g_menu_item_set_attribute (item, "x-canonical-type", "s", "indicator.user-menu-item");

      if ((serialized_icon = serialize_icon_file (u->icon_file)))
        {
          g_menu_item_set_attribute_value (item, G_MENU_ATTRIBUTE_ICON, serialized_icon);
          g_variant_unref (serialized_icon);
        }

      g_menu_append_item (menu, item);
      g_object_unref (item);
    }

  /* cleanup */
  g_ptr_array_free (users, TRUE);
  return G_MENU_MODEL (menu);
}

static GMenuModel *
create_logout_section (IndicatorSessionService * self)
{
  GMenu * menu;
  const priv_t * const p = self->priv;
  const gboolean ellipsis = use_ellipsis (self);

  menu = g_menu_new ();

  if (indicator_session_actions_can_logout (p->backend_actions))
    {
      const char * label = ellipsis ? _("Log Out…") : _("Log Out");
      g_menu_append (menu, label, "indicator.logout");
    }

  return G_MENU_MODEL (menu);
}

static GMenuModel *
create_session_section (IndicatorSessionService * self, int profile)
{
  GMenu * menu;
  const priv_t * const p = self->priv;
  GSettings * const s = p->indicator_settings;
  const gboolean ellipsis = use_ellipsis (self);

  menu = g_menu_new ();

  if (indicator_session_actions_can_suspend (p->backend_actions))
    g_menu_append (menu, _("Suspend"), "indicator.suspend");

  if (indicator_session_actions_can_hibernate (p->backend_actions))
    g_menu_append (menu, _("Hibernate"), "indicator.hibernate");

  if (profile != PROFILE_LOCKSCREEN && 
    indicator_session_actions_can_reboot (p->backend_actions))
    {
      const char * label = ellipsis ? _("Restart…") : _("Restart");
      g_menu_append (menu, label, "indicator.reboot");
    }

  if (profile != PROFILE_LOCKSCREEN && 
    !g_settings_get_boolean (s, "suppress-shutdown-menuitem"))
    {
      const char * label = ellipsis ? _("Shut Down…") : _("Shut Down");
      g_menu_append (menu, label, "indicator.power-off");
    }

  return G_MENU_MODEL (menu);
}

static void
create_menu (IndicatorSessionService * self, int profile)
{
  GMenu * menu;
  GMenu * submenu;
  GMenuItem * header;
  GMenuModel * sections[16];
  int i;
  int n = 0;

  g_assert (0<=profile && profile<N_PROFILES);
  g_assert (self->priv->menus[profile].menu == NULL);

  if (profile == PROFILE_DESKTOP)
    {
      sections[n++] = create_admin_section (self);
      sections[n++] = create_settings_section (self);
      sections[n++] = create_switch_section (self, profile);
      sections[n++] = create_logout_section (self);
      sections[n++] = create_session_section (self, profile);
    }
  else if (profile == PROFILE_GREETER)
    {
      sections[n++] = create_session_section (self, profile);
    }
  else if (profile == PROFILE_LOCKSCREEN)
    {
      sections[n++] = create_switch_section (self, profile);
      sections[n++] = create_session_section (self, profile);
    }

  /* add sections to the submenu */
  submenu = g_menu_new ();
  for (i=0; i<n; ++i)
    {
      g_menu_append_section (submenu, NULL, sections[i]);
      g_object_unref (sections[i]);
    }

  /* add submenu to the header */
  header = g_menu_item_new (NULL, "indicator._header");
  g_menu_item_set_attribute (header, "x-canonical-type", "s", "com.canonical.indicator.root");
  g_menu_item_set_submenu (header, G_MENU_MODEL (submenu));
  g_object_unref (submenu);

  /* add header to the menu */
  menu = g_menu_new ();
  g_menu_append_item (menu, header);
  g_object_unref (header);

  self->priv->menus[profile].menu = menu;
  self->priv->menus[profile].submenu = submenu;
}

/***
****  GActions
***/

static IndicatorSessionActions *
get_backend_actions (gpointer gself)
{
  return INDICATOR_SESSION_SERVICE(gself)->priv->backend_actions;
}

static void
on_about_activated (GSimpleAction * a      G_GNUC_UNUSED,
                    GVariant      * param  G_GNUC_UNUSED,
                    gpointer        gself)
{
  indicator_session_actions_about (get_backend_actions(gself));
}

static void
on_online_accounts_activated (GSimpleAction * a      G_GNUC_UNUSED,
                              GVariant      * param  G_GNUC_UNUSED,
                              gpointer        gself)
{
  indicator_session_actions_online_accounts (get_backend_actions(gself));
}

static void
on_help_activated (GSimpleAction  * a      G_GNUC_UNUSED,
                   GVariant       * param  G_GNUC_UNUSED,
                   gpointer         gself)
{
  indicator_session_actions_help (get_backend_actions(gself));
}

static void
on_settings_activated (GSimpleAction * a      G_GNUC_UNUSED,
                       GVariant      * param  G_GNUC_UNUSED,
                       gpointer        gself)
{
  indicator_session_actions_settings (get_backend_actions(gself));
}

static void
on_logout_activated (GSimpleAction * a      G_GNUC_UNUSED,
                     GVariant      * param  G_GNUC_UNUSED,
                     gpointer        gself)
{
  indicator_session_actions_logout (get_backend_actions(gself));
}

static void
on_suspend_activated (GSimpleAction * a      G_GNUC_UNUSED,
                      GVariant      * param  G_GNUC_UNUSED,
                      gpointer        gself)
{
  indicator_session_actions_suspend (get_backend_actions(gself));
}

static void
on_hibernate_activated (GSimpleAction * a      G_GNUC_UNUSED,
                        GVariant      * param  G_GNUC_UNUSED,
                        gpointer        gself)
{
  indicator_session_actions_hibernate (get_backend_actions(gself));
}

static void
on_reboot_activated (GSimpleAction * action G_GNUC_UNUSED,
                     GVariant      * param  G_GNUC_UNUSED,
                     gpointer        gself)
{
  indicator_session_actions_reboot (get_backend_actions(gself));
}

static void
on_power_off_activated (GSimpleAction * a     G_GNUC_UNUSED,
                        GVariant      * param G_GNUC_UNUSED,
                        gpointer        gself)
{
  indicator_session_actions_power_off (get_backend_actions(gself));
}

static void
on_guest_activated (GSimpleAction * a     G_GNUC_UNUSED,
                    GVariant      * param G_GNUC_UNUSED,
                    gpointer        gself)
{
  indicator_session_actions_switch_to_guest (get_backend_actions(gself));
}

static void
on_screensaver_activated (GSimpleAction * a      G_GNUC_UNUSED,
                          GVariant      * param  G_GNUC_UNUSED,
                          gpointer        gself)
{
  indicator_session_actions_switch_to_screensaver (get_backend_actions(gself));
}

static void
on_greeter_activated (GSimpleAction * a      G_GNUC_UNUSED,
                      GVariant      * param  G_GNUC_UNUSED,
                      gpointer        gself)
{
  indicator_session_actions_switch_to_greeter (get_backend_actions(gself));
}

static void
on_user_activated (GSimpleAction * a         G_GNUC_UNUSED,
                   GVariant      * param,
                   gpointer        gself)
{
  const char * username = g_variant_get_string (param, NULL);
  indicator_session_actions_switch_to_username (get_backend_actions(gself),
                                                username);
}

static void
init_gactions (IndicatorSessionService * self)
{
  GVariant * v;
  GSimpleAction * a;
  priv_t * p = self->priv;

  GActionEntry entries[] = {
    { "about",                  on_about_activated           },
    { "help",                   on_help_activated            },
    { "hibernate",              on_hibernate_activated       },
    { "logout",                 on_logout_activated          },
    { "online-accounts",        on_online_accounts_activated },
    { "reboot",                 on_reboot_activated          },
    { "settings",               on_settings_activated        },
    { "switch-to-screensaver",  on_screensaver_activated     },
    { "switch-to-greeter",      on_greeter_activated         },
    { "suspend",                on_suspend_activated         },
    { "power-off",              on_power_off_activated       }
  };

  p->actions = g_simple_action_group_new ();

  g_action_map_add_action_entries (G_ACTION_MAP(p->actions),
                                   entries,
                                   G_N_ELEMENTS(entries),
                                   self);

  /* add switch-to-guest action */
  v = create_guest_switcher_state (self);
  a = g_simple_action_new_stateful ("switch-to-guest", NULL, v);
  g_signal_connect (a, "activate", G_CALLBACK(on_guest_activated), self);
  g_action_map_add_action (G_ACTION_MAP (p->actions), G_ACTION(a));
  p->guest_switcher_action = a;

  /* add switch-to-user action... parameter is the username */
  v = create_user_switcher_state (self);
  a = g_simple_action_new_stateful ("switch-to-user", G_VARIANT_TYPE_STRING, v);
  g_signal_connect (a, "activate", G_CALLBACK(on_user_activated), self);
  g_action_map_add_action (G_ACTION_MAP (p->actions), G_ACTION(a));
  p->user_switcher_action = a;

  /* add usage-mode action */
  if (p->usage_mode_settings)
    {
      a = g_simple_action_new_stateful("usage-mode",
                                       NULL,
                                       g_variant_new_boolean(FALSE));
      g_settings_bind_with_mapping(p->usage_mode_settings, "usage-mode",
                                   a, "state",
                                   G_SETTINGS_BIND_DEFAULT,
                                   usage_mode_to_action_state,
                                   action_state_to_usage_mode,
                                   NULL,
                                   NULL);

      g_action_map_add_action(G_ACTION_MAP(p->actions), G_ACTION(a));
      g_signal_connect_swapped(p->usage_mode_settings, "changed::usage-mode",
                               G_CALLBACK(on_usage_mode_setting_changed), self);
      p->usage_mode_action = a;
    }

  /* add the header action */
  a = g_simple_action_new_stateful ("_header", NULL,
                                    action_state_for_header (self));
  g_action_map_add_action (G_ACTION_MAP (p->actions), G_ACTION(a));
  p->header_action = a;

  rebuild_now (self, SECTION_HEADER);
}

/***
****
***/

/**
 * A small helper function for rebuild_now().
 * - removes the previous section
 * - adds and unrefs the new section
 */
static void
rebuild_section (GMenu * parent, int pos, GMenuModel * new_section)
{
  g_menu_remove (parent, pos);
  g_menu_insert_section (parent, pos, NULL, new_section);
  g_object_unref (new_section);
}

static void
rebuild_now (IndicatorSessionService * self, int sections)
{
  priv_t * p = self->priv;
  struct ProfileMenuInfo * desktop = &p->menus[PROFILE_DESKTOP];
  struct ProfileMenuInfo * greeter = &p->menus[PROFILE_GREETER];
  struct ProfileMenuInfo * lockscreen = &p->menus[PROFILE_LOCKSCREEN];

  if (sections & SECTION_HEADER)
    {
      update_header_action (self);
    }

  if (sections & SECTION_ADMIN)
    {
      rebuild_section (desktop->submenu, 0, create_admin_section(self));
    }

  if (sections & SECTION_SETTINGS)
    {
      rebuild_section (desktop->submenu, 1, create_settings_section(self));
    }

  if (sections & SECTION_SWITCH)
    {
      rebuild_section (desktop->submenu, 2, create_switch_section(self, PROFILE_DESKTOP));
      rebuild_section (lockscreen->submenu, 0, create_switch_section(self, PROFILE_LOCKSCREEN));
      update_switch_actions (self);
    }

  if (sections & SECTION_LOGOUT)
    {
      rebuild_section (desktop->submenu, 3, create_logout_section(self));
    }

  if (sections & SECTION_SESSION)
    {
      rebuild_section (desktop->submenu, 4, create_session_section(self, PROFILE_DESKTOP));
      rebuild_section (greeter->submenu, 0, create_session_section(self, PROFILE_GREETER));
      rebuild_section (lockscreen->submenu, 1, create_session_section(self, PROFILE_LOCKSCREEN));
    }
}

static int
rebuild_timeout_func (IndicatorSessionService * self)
{
  priv_t * p = self->priv;
  rebuild_now (self, p->rebuild_flags);
  p->rebuild_flags = 0;
  p->rebuild_id = 0;
  return G_SOURCE_REMOVE;
}

static void
rebuild_soon (IndicatorSessionService * self, int section)
{
  priv_t * p = self->priv;

  p->rebuild_flags |= section;

  if (p->rebuild_id == 0)
    {
      /* Change events seem to come over the bus in small bursts. This msec
         value is an arbitrary number that tries to be large enough to fold
         multiple events into a single rebuild, but small enough that the
         user won't notice any lag. */
      static const int REBUILD_INTERVAL_MSEC = 500;

      p->rebuild_id = g_timeout_add (REBUILD_INTERVAL_MSEC,
                                     (GSourceFunc)rebuild_timeout_func,
                                     self);
    }
}

/***
**** GDBus
***/

static void
on_bus_acquired (GDBusConnection * connection,
                 const gchar     * name,
                 gpointer          gself)
{
  int i;
  guint id;
  GError * err = NULL;
  IndicatorSessionService * self = INDICATOR_SESSION_SERVICE(gself);
  priv_t * p = self->priv;

  g_debug ("bus acquired: %s", name);

  p->conn = g_object_ref (G_OBJECT (connection));

  /* export the actions */
  if ((id = g_dbus_connection_export_action_group (connection,
                                                   BUS_PATH,
                                                   G_ACTION_GROUP (p->actions),
                                                   &err)))
    {
      p->actions_export_id = id;
    }
  else
    {
      g_warning ("cannot export action group: %s", err->message);
      g_clear_error (&err);
    }

  /* export the menus */
  for (i=0; i<N_PROFILES; ++i)
    {
      char * path = g_strdup_printf ("%s/%s", BUS_PATH, menu_names[i]);
      struct ProfileMenuInfo * menu = &p->menus[i];

      if (menu->menu == NULL)
        create_menu (self, i);

      if ((id = g_dbus_connection_export_menu_model (connection,
                                                     path,
                                                     G_MENU_MODEL (menu->menu),
                                                     &err)))
        {
          menu->export_id = id;
        }
      else
        {
          g_warning ("cannot export %s menu: %s", menu_names[i], err->message);
          g_clear_error (&err);
        }

      g_free (path);
    }
}

static void
unexport (IndicatorSessionService * self)
{
  int i;
  priv_t * p = self->priv;

  /* unexport the menus */
  for (i=0; i<N_PROFILES; ++i)
    {
      guint * id = &self->priv->menus[i].export_id;

      if (*id)
        {
          g_dbus_connection_unexport_menu_model (p->conn, *id);
          *id = 0;
        }
    }

  /* unexport the actions */
  if (p->actions_export_id)
    {
      g_dbus_connection_unexport_action_group (p->conn, p->actions_export_id);
      p->actions_export_id = 0;
    }
}

static void
on_name_lost (GDBusConnection * connection G_GNUC_UNUSED,
              const gchar     * name,
              gpointer          gself)
{
  IndicatorSessionService * self = INDICATOR_SESSION_SERVICE (gself);

  g_debug ("%s %s name lost %s", G_STRLOC, G_STRFUNC, name);

  unexport (self);

  g_signal_emit (self, signals[NAME_LOST], 0, NULL);
}

/***
****
***/

static void
/* cppcheck-suppress unusedFunction */
indicator_session_service_init (IndicatorSessionService * self)
{
  priv_t * p;
  gpointer gp;
  GIcon * icon;
  GSettingsSchema * usage_mode_schema;

  /* init our priv pointer */
  p = G_TYPE_INSTANCE_GET_PRIVATE (self,
                                   INDICATOR_TYPE_SESSION_SERVICE,
                                   IndicatorSessionServicePrivate);
  p->indicator_settings = g_settings_new ("com.canonical.indicator.session");
  p->keybinding_settings = g_settings_new ("com.canonical.unity.settings-daemon.plugins.media-keys");

  /* Only use unity8 schema if it's installed; this avoids a hard dependency
     on unity8-schemas */
  usage_mode_schema = g_settings_schema_source_lookup (g_settings_schema_source_get_default (),
                                                       usage_mode_schema_name, TRUE);
  if (usage_mode_schema)
    {
      p->usage_mode_settings = g_settings_new (usage_mode_schema_name);
      g_settings_schema_unref (usage_mode_schema);
    }

  self->priv = p;

  /* init the backend objects */
  p->cancellable = g_cancellable_new ();
  backend_get (p->cancellable, &p->backend_actions,
                               &p->backend_users,
                               &p->backend_guest);

  icon = g_themed_icon_new_with_default_fallbacks (ICON_DEFAULT);
  p->default_icon_serialized = g_icon_serialize (icon);
  g_object_unref (icon);

  /* init our key-to-User table */
  p->users = g_hash_table_new_full (g_direct_hash,
                                    g_direct_equal,
                                    NULL,
                                    (GDestroyNotify)indicator_session_user_free);

  p->reported_users = g_hash_table_new (g_direct_hash, g_direct_equal);

  maybe_add_users (self);

  init_gactions (self);

  /* watch for changes in backend_users */
  gp = p->backend_users;
  g_signal_connect (gp, INDICATOR_SESSION_USERS_SIGNAL_USER_ADDED,
                    G_CALLBACK(on_user_added), self);
  g_signal_connect (gp, INDICATOR_SESSION_USERS_SIGNAL_USER_CHANGED,
                    G_CALLBACK(on_user_changed), self);
  g_signal_connect (gp, INDICATOR_SESSION_USERS_SIGNAL_USER_REMOVED,
                    G_CALLBACK(on_user_removed), self);
  g_signal_connect_swapped (gp, "notify::is-live-session",
                            G_CALLBACK(rebuild_switch_section_soon), self);

  /* watch for changes in backend_guest */
  gp = p->backend_guest;
  g_signal_connect_swapped (gp, "notify::guest-is-active-session",
                            G_CALLBACK(rebuild_header_soon), self);
  g_signal_connect_swapped (gp, "notify",
                            G_CALLBACK(rebuild_switch_section_soon), self);

  /* watch for updates in backend_actions */
  gp = p->backend_actions;
  g_signal_connect_swapped (gp, "notify",
                            G_CALLBACK(rebuild_switch_section_soon), self);
  g_signal_connect_swapped (gp, "notify",
                            G_CALLBACK(rebuild_logout_section_soon), self);
  g_signal_connect_swapped (gp, "notify",
                            G_CALLBACK(rebuild_session_section_soon), self);
  g_signal_connect_swapped (gp, "notify::has-online-account-error",
                            G_CALLBACK(rebuild_header_soon), self);
  g_signal_connect_swapped (gp, "notify::has-online-account-error",
                            G_CALLBACK(rebuild_settings_section_soon), self);

  /* watch for changes in the indicator's settings */
  gp = p->indicator_settings;
  g_signal_connect_swapped (gp, "changed::suppress-logout-restart-shutdown",
                            G_CALLBACK(rebuild_switch_section_soon), self);
  g_signal_connect_swapped (gp, "changed::suppress-logout-restart-shutdown",
                            G_CALLBACK(rebuild_logout_section_soon), self);
  g_signal_connect_swapped (gp, "changed::suppress-logout-restart-shutdown",
                            G_CALLBACK(rebuild_session_section_soon), self);
  g_signal_connect_swapped (gp, "changed::suppress-shutdown-menuitem",
                            G_CALLBACK(rebuild_session_section_soon), self);
  g_signal_connect_swapped (gp, "changed::show-real-name-on-panel",
                            G_CALLBACK(rebuild_header_soon), self);
  g_signal_connect_swapped (gp, "changed::user-show-menu",
                            G_CALLBACK(user_show_menu_changed), self);

  /* watch for changes to the lock keybinding */
  gp = p->keybinding_settings;
  g_signal_connect_swapped (gp, "changed::screensaver",
                            G_CALLBACK(rebuild_switch_section_soon), self);

  self->priv->own_id = g_bus_own_name (G_BUS_TYPE_SESSION,
                                       BUS_NAME,
                                       G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT,
                                       on_bus_acquired,
                                       NULL,
                                       on_name_lost,
                                       self,
                                       NULL);
}

/***
****  GObject plumbing: properties
***/

static void
my_get_property (GObject     * o,
                  guint         property_id,
                  GValue      * value,
                  GParamSpec  * pspec)
{
  IndicatorSessionService * self = INDICATOR_SESSION_SERVICE (o);
 
  switch (property_id)
    {
      case PROP_MAX_USERS:
        g_value_set_uint (value, self->priv->max_users);
        break;
 
      default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (o, property_id, pspec);
    }
}

static void
my_set_property (GObject       * o,
                 guint           property_id,
                 const GValue  * value,
                 GParamSpec    * pspec)
{
  IndicatorSessionService * self = INDICATOR_SESSION_SERVICE (o);

  switch (property_id)
    {
      case PROP_MAX_USERS:
        self->priv->max_users = g_value_get_uint (value);
        rebuild_switch_section_soon (self);
        break;

      default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (o, property_id, pspec);
    }
}

/***
****  GObject plumbing: life cycle
***/

static void
my_dispose (GObject * o)
{
  int i;
  IndicatorSessionService * self = INDICATOR_SESSION_SERVICE(o);
  priv_t * p = self->priv;

  if (p->own_id)
    {
      g_bus_unown_name (p->own_id);
      p->own_id = 0;
    }

  unexport (self);

  if (p->cancellable != NULL)
    {
      g_cancellable_cancel (p->cancellable);
      g_clear_object (&p->cancellable);
    }

  if (p->rebuild_id)
    {
      g_source_remove (p->rebuild_id);
      p->rebuild_id = 0;
    }

  g_clear_pointer (&p->users, g_hash_table_destroy);
  g_clear_pointer (&p->reported_users, g_hash_table_destroy);
  g_clear_object (&p->backend_users);
  g_clear_object (&p->backend_guest);
  g_clear_object (&p->backend_actions);
  g_clear_object (&p->indicator_settings);
  g_clear_object (&p->keybinding_settings);
  g_clear_object (&p->usage_mode_settings);
  g_clear_object (&p->actions);

  for (i=0; i<N_PROFILES; ++i)
    g_clear_object (&p->menus[i].menu);

  g_clear_object (&p->header_action);
  g_clear_object (&p->user_switcher_action);
  g_clear_object (&p->guest_switcher_action);
  g_clear_object (&p->usage_mode_action);
  g_clear_object (&p->conn);

  g_clear_pointer (&p->default_icon_serialized, g_variant_unref);

  G_OBJECT_CLASS (indicator_session_service_parent_class)->dispose (o);
}

static void
/* cppcheck-suppress unusedFunction */
indicator_session_service_class_init (IndicatorSessionServiceClass * klass)
{
  GObjectClass * object_class = G_OBJECT_CLASS (klass);

  object_class->dispose = my_dispose;
  object_class->get_property = my_get_property;
  object_class->set_property = my_set_property;

  g_type_class_add_private (klass, sizeof (IndicatorSessionServicePrivate));

  signals[NAME_LOST] = g_signal_new (INDICATOR_SESSION_SERVICE_SIGNAL_NAME_LOST,
                                     G_TYPE_FROM_CLASS(klass),
                                     G_SIGNAL_RUN_LAST,
                                     G_STRUCT_OFFSET (IndicatorSessionServiceClass, name_lost),
                                     NULL, NULL,
                                     g_cclosure_marshal_VOID__VOID,
                                     G_TYPE_NONE, 0);

  properties[PROP_0] = NULL;

  properties[PROP_MAX_USERS] = g_param_spec_uint ("max-users",
                                                  "Max Users",
                                                  "Max visible users",
                                                  0, INT_MAX, 12,
                                                  G_PARAM_READWRITE |
                                                  G_PARAM_CONSTRUCT |
                                                  G_PARAM_STATIC_STRINGS);

  g_object_class_install_properties (object_class, PROP_LAST, properties);
}

IndicatorSessionService *
indicator_session_service_new (void)
{
  GObject * o = g_object_new (INDICATOR_TYPE_SESSION_SERVICE, NULL);

  return INDICATOR_SESSION_SERVICE (o);
}
