/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */

#include "config.h"

#include <string.h>

#include <glib/gi18n-lib.h>

#include <meta/display.h>
#include <meta/meta-workspace-manager.h>
#include <meta/meta-x11-display.h>

#include "shell-app-private.h"
#include "shell-enum-types.h"
#include "shell-global.h"
#include "shell-util.h"
#include "shell-app-system-private.h"
#include "shell-window-tracker-private.h"
#include "st.h"
#include "gtkactionmuxer.h"
#include "org-gtk-application.h"
#include "switcheroo-control.h"

#ifdef HAVE_SYSTEMD
#include <systemd/sd-journal.h>
#include <errno.h>
#include <unistd.h>
#endif

/* This is mainly a memory usage optimization - the user is going to
 * be running far fewer of the applications at one time than they have
 * installed.  But it also just helps keep the code more logically
 * separated.
 */
typedef struct {
  guint refcount;

  /* Signal connection to dirty window sort list on workspace changes */
  gulong workspace_switch_id;

  gulong icon_changed_id;

  GSList *windows;

  guint interesting_windows;

  /* Whether or not we need to resort the windows; this is done on demand */
  guint window_sort_stale : 1;

  /* See GApplication documentation */
  GtkActionMuxer   *muxer;
  char             *unique_bus_name;
  GDBusConnection  *session;

  /* GDBus Proxy for getting application busy state */
  ShellOrgGtkApplication *application_proxy;
  GCancellable           *cancellable;

} ShellAppRunningState;

/**
 * SECTION:shell-app
 * @short_description: Object representing an application
 *
 * This object wraps a #GDesktopAppInfo, providing methods and signals
 * primarily useful for running applications.
 */
struct _ShellApp
{
  GObject parent;

  int started_on_workspace;

  ShellAppState state;

  GDesktopAppInfo *info; /* If NULL, this app is backed by one or more
                          * MetaWindow.  For purposes of app title
                          * etc., we use the first window added,
                          * because it's most likely to be what we
                          * want (e.g. it will be of TYPE_NORMAL from
                          * the way shell-window-tracker.c works).
                          */
  GIcon *fallback_icon;

  ShellAppRunningState *running_state;

  char *window_id_string;
  char *name_collation_key;
};

enum {
  PROP_0,
  PROP_STATE,
  PROP_BUSY,
  PROP_ID,
  PROP_DBUS_ID,
  PROP_ACTION_GROUP,
  PROP_ICON,
  PROP_APP_INFO
};

enum {
  WINDOWS_CHANGED,
  LAST_SIGNAL
};

static guint shell_app_signals[LAST_SIGNAL] = { 0 };

static void create_running_state (ShellApp *app);
static void unref_running_state (ShellAppRunningState *state);

G_DEFINE_TYPE (ShellApp, shell_app, G_TYPE_OBJECT)

static void
shell_app_get_property (GObject    *gobject,
                        guint       prop_id,
                        GValue     *value,
                        GParamSpec *pspec)
{
  ShellApp *app = SHELL_APP (gobject);

  switch (prop_id)
    {
    case PROP_STATE:
      g_value_set_enum (value, app->state);
      break;
    case PROP_BUSY:
      g_value_set_boolean (value, shell_app_get_busy (app));
      break;
    case PROP_ID:
      g_value_set_string (value, shell_app_get_id (app));
      break;
    case PROP_ICON:
      g_value_set_object (value, shell_app_get_icon (app));
      break;
    case PROP_ACTION_GROUP:
      if (app->running_state)
        g_value_set_object (value, app->running_state->muxer);
      break;
    case PROP_APP_INFO:
      if (app->info)
        g_value_set_object (value, app->info);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
      break;
    }
}

static void
shell_app_set_property (GObject      *gobject,
                        guint         prop_id,
                        const GValue *value,
                        GParamSpec   *pspec)
{
  ShellApp *app = SHELL_APP (gobject);

  switch (prop_id)
    {
    case PROP_APP_INFO:
      _shell_app_set_app_info (app, g_value_get_object (value));
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
      break;
    }
}

const char *
shell_app_get_id (ShellApp *app)
{
  if (app->info)
    return g_app_info_get_id (G_APP_INFO (app->info));
  return app->window_id_string;
}

static MetaWindow *
window_backed_app_get_window (ShellApp     *app)
{
  g_assert (app->info == NULL);
  if (app->running_state)
    {
      g_assert (app->running_state->windows);
      return app->running_state->windows->data;
    }
  else
    return NULL;
}

static GIcon *
x11_window_create_fallback_gicon (MetaWindow *window)
{
  StTextureCache *texture_cache;
  cairo_surface_t *surface;

  g_object_get (window, "icon", &surface, NULL);

  texture_cache = st_texture_cache_get_default ();
  return st_texture_cache_load_cairo_surface_to_gicon (texture_cache, surface);
}

static void
on_window_icon_changed (GObject          *object,
                        const GParamSpec *pspec,
                        gpointer          user_data)
{
  MetaWindow *window = META_WINDOW (object);
  ShellApp *app = user_data;

  g_clear_object (&app->fallback_icon);
  app->fallback_icon = x11_window_create_fallback_gicon (window);

  g_object_notify (G_OBJECT (app), "icon");
}

/**
 * shell_app_get_icon:
 *
 * Look up the icon for this application
 *
 * Return value: (transfer none): A #GIcon
 */
GIcon *
shell_app_get_icon (ShellApp *app)
{
  MetaWindow *window = NULL;

  g_return_val_if_fail (SHELL_IS_APP (app), NULL);

  if (app->info)
    return g_app_info_get_icon (G_APP_INFO (app->info));

  if (app->fallback_icon)
    return app->fallback_icon;

  /* During a state transition from running to not-running for
   * window-backend apps, it's possible we get a request for the icon.
   * Avoid asserting here and just return a fallback icon
   */
  if (app->running_state != NULL)
    window = window_backed_app_get_window (app);

  if (window &&
      meta_window_get_client_type (window) == META_WINDOW_CLIENT_TYPE_X11)
    {
      app->fallback_icon = x11_window_create_fallback_gicon (window);
      app->running_state->icon_changed_id =
        g_signal_connect (G_OBJECT (window),
                         "notify::icon", G_CALLBACK (on_window_icon_changed), app);
    }
  else
    {
      app->fallback_icon = g_themed_icon_new ("application-x-executable");
    }

  return app->fallback_icon;
}

/**
 * shell_app_create_icon_texture:
 *
 * Look up the icon for this application, and create a #ClutterActor
 * for it at the given size.
 *
 * Return value: (transfer none): A floating #ClutterActor
 */
ClutterActor *
shell_app_create_icon_texture (ShellApp   *app,
                               int         size)
{
  ClutterActor *ret;

  ret = st_icon_new ();
  st_icon_set_icon_size (ST_ICON (ret), size);
  st_icon_set_fallback_icon_name (ST_ICON (ret), "application-x-executable");

  g_object_bind_property (app, "icon", ret, "gicon", G_BINDING_SYNC_CREATE);

  if (shell_app_is_window_backed (app))
    st_widget_add_style_class_name (ST_WIDGET (ret), "fallback-app-icon");

  return ret;
}

const char *
shell_app_get_name (ShellApp *app)
{
  if (app->info)
    return g_app_info_get_name (G_APP_INFO (app->info));
  else
    {
      MetaWindow *window = window_backed_app_get_window (app);
      const char *name = NULL;

      if (window)
        name = meta_window_get_wm_class (window);
      if (!name)
        name = C_("program", "Unknown");
      return name;
    }
}

const char *
shell_app_get_description (ShellApp *app)
{
  if (app->info)
    return g_app_info_get_description (G_APP_INFO (app->info));
  else
    return NULL;
}

/**
 * shell_app_is_window_backed:
 *
 * A window backed application is one which represents just an open
 * window, i.e. there's no .desktop file association, so we don't know
 * how to launch it again.
 */
gboolean
shell_app_is_window_backed (ShellApp *app)
{
  return app->info == NULL;
}

typedef struct {
  MetaWorkspace *workspace;
  GSList **transients;
} CollectTransientsData;

static gboolean
collect_transients_on_workspace (MetaWindow *window,
                                 gpointer    datap)
{
  CollectTransientsData *data = datap;

  if (data->workspace && meta_window_get_workspace (window) != data->workspace)
    return TRUE;

  *data->transients = g_slist_prepend (*data->transients, window);
  return TRUE;
}

/* The basic idea here is that when we're targeting a window,
 * if it has transients we want to pick the most recent one
 * the user interacted with.
 * This function makes raising GEdit with the file chooser
 * open work correctly.
 */
static MetaWindow *
find_most_recent_transient_on_same_workspace (MetaDisplay *display,
                                              MetaWindow  *reference)
{
  GSList *transients, *transients_sorted, *iter;
  MetaWindow *result;
  CollectTransientsData data;

  transients = NULL;
  data.workspace = meta_window_get_workspace (reference);
  data.transients = &transients;

  meta_window_foreach_transient (reference, collect_transients_on_workspace, &data);

  transients_sorted = meta_display_sort_windows_by_stacking (display, transients);
  /* Reverse this so we're top-to-bottom (yes, we should probably change the order
   * returned from the sort_windows_by_stacking function)
   */
  transients_sorted = g_slist_reverse (transients_sorted);
  g_slist_free (transients);
  transients = NULL;

  result = NULL;
  for (iter = transients_sorted; iter; iter = iter->next)
    {
      MetaWindow *window = iter->data;
      MetaWindowType wintype = meta_window_get_window_type (window);

      /* Don't want to focus UTILITY types, like the Gimp toolbars */
      if (wintype == META_WINDOW_NORMAL ||
          wintype == META_WINDOW_DIALOG)
        {
          result = window;
          break;
        }
    }
  g_slist_free (transients_sorted);
  return result;
}

static MetaWorkspace *
get_active_workspace (void)
{
  ShellGlobal *global = shell_global_get ();
  MetaDisplay *display = shell_global_get_display (global);
  MetaWorkspaceManager *workspace_manager =
    meta_display_get_workspace_manager (display);

  return meta_workspace_manager_get_active_workspace (workspace_manager);
}

/**
 * shell_app_activate_window:
 * @app: a #ShellApp
 * @window: (nullable): Window to be focused
 * @timestamp: Event timestamp
 *
 * Bring all windows for the given app to the foreground,
 * but ensure that @window is on top.  If @window is %NULL,
 * the window with the most recent user time for the app
 * will be used.
 *
 * This function has no effect if @app is not currently running.
 */
void
shell_app_activate_window (ShellApp     *app,
                           MetaWindow   *window,
                           guint32       timestamp)
{
  GSList *windows;

  if (shell_app_get_state (app) != SHELL_APP_STATE_RUNNING)
    return;

  windows = shell_app_get_windows (app);
  if (window == NULL && windows)
    window = windows->data;

  if (!g_slist_find (windows, window))
    return;
  else
    {
      GSList *windows_reversed, *iter;
      ShellGlobal *global = shell_global_get ();
      MetaDisplay *display = shell_global_get_display (global);
      MetaWorkspace *active = get_active_workspace ();
      MetaWorkspace *workspace = meta_window_get_workspace (window);
      guint32 last_user_timestamp = meta_display_get_last_user_time (display);
      MetaWindow *most_recent_transient;

      if (meta_display_xserver_time_is_before (display, timestamp, last_user_timestamp))
        {
          meta_window_set_demands_attention (window);
          return;
        }

      /* Now raise all the other windows for the app that are on
       * the same workspace, in reverse order to preserve the stacking.
       */
      windows_reversed = g_slist_copy (windows);
      windows_reversed = g_slist_reverse (windows_reversed);
      for (iter = windows_reversed; iter; iter = iter->next)
        {
          MetaWindow *other_window = iter->data;

          if (other_window != window && meta_window_get_workspace (other_window) == workspace)
            meta_window_raise (other_window);
        }
      g_slist_free (windows_reversed);

      /* If we have a transient that the user's interacted with more recently than
       * the window, pick that.
       */
      most_recent_transient = find_most_recent_transient_on_same_workspace (display, window);
      if (most_recent_transient
          && meta_display_xserver_time_is_before (display,
                                                  meta_window_get_user_time (window),
                                                  meta_window_get_user_time (most_recent_transient)))
        window = most_recent_transient;


      if (active != workspace)
        meta_workspace_activate_with_focus (workspace, window, timestamp);
      else
        meta_window_activate (window, timestamp);
    }
}


void
shell_app_update_window_actions (ShellApp *app, MetaWindow *window)
{
  const char *object_path;

  object_path = meta_window_get_gtk_window_object_path (window);
  if (object_path != NULL)
    {
      GActionGroup *actions;

      actions = g_object_get_data (G_OBJECT (window), "actions");
      if (actions == NULL)
        {
          actions = G_ACTION_GROUP (g_dbus_action_group_get (app->running_state->session,
                                                             meta_window_get_gtk_unique_bus_name (window),
                                                             object_path));
          g_object_set_data_full (G_OBJECT (window), "actions", actions, g_object_unref);
        }

      g_assert (app->running_state->muxer);
      gtk_action_muxer_insert (app->running_state->muxer, "win", actions);
      g_object_notify (G_OBJECT (app), "action-group");
    }
}

/**
 * shell_app_activate:
 * @app: a #ShellApp
 *
 * Like shell_app_activate_full(), but using the default workspace and
 * event timestamp.
 */
void
shell_app_activate (ShellApp      *app)
{
  return shell_app_activate_full (app, -1, 0);
}

/**
 * shell_app_activate_full:
 * @app: a #ShellApp
 * @workspace: launch on this workspace, or -1 for default. Ignored if
 *   activating an existing window
 * @timestamp: Event timestamp
 *
 * Perform an appropriate default action for operating on this application,
 * dependent on its current state.  For example, if the application is not
 * currently running, launch it.  If it is running, activate the most
 * recently used NORMAL window (or if that window has a transient, the most
 * recently used transient for that window).
 */
void
shell_app_activate_full (ShellApp      *app,
                         int            workspace,
                         guint32        timestamp)
{
  ShellGlobal *global;

  global = shell_global_get ();

  if (timestamp == 0)
    timestamp = shell_global_get_current_time (global);

  switch (app->state)
    {
      case SHELL_APP_STATE_STOPPED:
        {
          GError *error = NULL;
          if (!shell_app_launch (app, timestamp, workspace, SHELL_APP_LAUNCH_GPU_APP_PREF, &error))
            {
              char *msg;
              msg = g_strdup_printf (_("Failed to launch “%s”"), shell_app_get_name (app));
              shell_global_notify_error (global,
                                         msg,
                                         error->message);
              g_free (msg);
              g_clear_error (&error);
            }
        }
        break;
      case SHELL_APP_STATE_STARTING:
        break;
      case SHELL_APP_STATE_RUNNING:
        shell_app_activate_window (app, NULL, timestamp);
        break;
      default:
        g_assert_not_reached();
        break;
    }
}

/**
 * shell_app_open_new_window:
 * @app: a #ShellApp
 * @workspace: open on this workspace, or -1 for default
 *
 * Request that the application create a new window.
 */
void
shell_app_open_new_window (ShellApp      *app,
                           int            workspace)
{
  GActionGroup *group = NULL;
  const char * const *actions;

  g_return_if_fail (app->info != NULL);

  /* First check whether the application provides a "new-window" desktop
   * action - it is a safe bet that it will open a new window, and activating
   * it will trigger startup notification if necessary
   */
  actions = g_desktop_app_info_list_actions (G_DESKTOP_APP_INFO (app->info));

  if (g_strv_contains (actions, "new-window"))
    {
      shell_app_launch_action (app, "new-window", 0, workspace);
      return;
    }

  /* Next, check whether the app exports an explicit "new-window" action
   * that we can activate on the bus - the muxer will add startup notification
   * information to the platform data, so this should work just as well as
   * desktop actions.
   */
  group = app->running_state ? G_ACTION_GROUP (app->running_state->muxer)
                             : NULL;

  if (group &&
      g_action_group_has_action (group, "app.new-window") &&
      g_action_group_get_action_parameter_type (group, "app.new-window") == NULL)
    {
      g_action_group_activate_action (group, "app.new-window", NULL);

      return;
    }

  /* Lastly, just always launch the application again, even if we know
   * it was already running.  For most applications this
   * should have the effect of creating a new window, whether that's
   * a second process (in the case of Calculator) or IPC to existing
   * instance (Firefox).  There are a few less-sensical cases such
   * as say Pidgin.
   */
  shell_app_launch (app, 0, workspace, SHELL_APP_LAUNCH_GPU_APP_PREF, NULL);
}

/**
 * shell_app_can_open_new_window:
 * @app: a #ShellApp
 *
 * Returns %TRUE if the app supports opening a new window through
 * shell_app_open_new_window() (ie, if calling that function will
 * result in actually opening a new window and not something else,
 * like presenting the most recently active one)
 */
gboolean
shell_app_can_open_new_window (ShellApp *app)
{
  ShellAppRunningState *state;
  MetaWindow *window;
  GDesktopAppInfo *desktop_info;
  const char * const *desktop_actions;

  /* Apps that are stopped can always open new windows, because
   * activating them would open the first one; if they are starting,
   * we cannot tell whether they can open additional windows until
   * they are running */
  if (app->state != SHELL_APP_STATE_RUNNING)
    return app->state == SHELL_APP_STATE_STOPPED;

  state = app->running_state;

  /* If the app has an explicit new-window action, then it can
     (or it should be able to) ...
  */
  if (g_action_group_has_action (G_ACTION_GROUP (state->muxer), "app.new-window"))
    return TRUE;

  /* If the app doesn't have a desktop file, then nothing is possible */
  if (!app->info)
    return FALSE;

  desktop_info = G_DESKTOP_APP_INFO (app->info);

  /* If the app is explicitly telling us, then we know for sure */
  if (g_desktop_app_info_has_key (desktop_info, "X-GNOME-SingleWindow"))
    return !g_desktop_app_info_get_boolean (desktop_info,
                                            "X-GNOME-SingleWindow");

  /* If it has a new-window desktop action, it should be able to */
  desktop_actions = g_desktop_app_info_list_actions (desktop_info);
  if (desktop_actions && g_strv_contains (desktop_actions, "new-window"))
    return TRUE;

  /* If this is a unique GtkApplication, and we don't have a new-window, then
     probably we can't

     We don't consider non-unique GtkApplications here to handle cases like
     evince, which don't export a new-window action because each window is in
     a different process. In any case, in a non-unique GtkApplication each
     Activate() knows nothing about the other instances, so it will show a
     new window.
  */

  window = state->windows->data;

  if (state->unique_bus_name != NULL &&
      meta_window_get_gtk_application_object_path (window) != NULL)
    {
      if (meta_window_get_gtk_application_id (window) != NULL)
        return FALSE;
      else
        return TRUE;
    }

  /* In all other cases, we don't have a reliable source of information
     or a decent heuristic, so we err on the compatibility side and say
     yes.
  */
  return TRUE;
}

/**
 * shell_app_get_state:
 * @app: a #ShellApp
 *
 * Returns: State of the application
 */
ShellAppState
shell_app_get_state (ShellApp *app)
{
  return app->state;
}

typedef struct {
  ShellApp *app;
  MetaWorkspace *active_workspace;
} CompareWindowsData;

static int
shell_app_compare_windows (gconstpointer   a,
                           gconstpointer   b,
                           gpointer        datap)
{
  MetaWindow *win_a = (gpointer)a;
  MetaWindow *win_b = (gpointer)b;
  CompareWindowsData *data = datap;
  gboolean ws_a, ws_b;
  gboolean vis_a, vis_b;

  ws_a = meta_window_get_workspace (win_a) == data->active_workspace;
  ws_b = meta_window_get_workspace (win_b) == data->active_workspace;

  if (ws_a && !ws_b)
    return -1;
  else if (!ws_a && ws_b)
    return 1;

  vis_a = meta_window_showing_on_its_workspace (win_a);
  vis_b = meta_window_showing_on_its_workspace (win_b);

  if (vis_a && !vis_b)
    return -1;
  else if (!vis_a && vis_b)
    return 1;

  return meta_window_get_user_time (win_b) - meta_window_get_user_time (win_a);
}

/**
 * shell_app_get_windows:
 * @app:
 *
 * Get the windows which are associated with this application. The
 * returned list will be sorted first by whether they're on the
 * active workspace, then by whether they're visible, and finally
 * by the time the user last interacted with them.
 *
 * Returns: (transfer none) (element-type MetaWindow): List of windows
 */
GSList *
shell_app_get_windows (ShellApp *app)
{
  if (app->running_state == NULL)
    return NULL;

  if (app->running_state->window_sort_stale)
    {
      CompareWindowsData data;
      data.app = app;
      data.active_workspace = get_active_workspace ();
      app->running_state->windows = g_slist_sort_with_data (app->running_state->windows, shell_app_compare_windows, &data);
      app->running_state->window_sort_stale = FALSE;
    }

  return app->running_state->windows;
}

guint
shell_app_get_n_windows (ShellApp *app)
{
  if (app->running_state == NULL)
    return 0;
  return g_slist_length (app->running_state->windows);
}

gboolean
shell_app_is_on_workspace (ShellApp *app,
                           MetaWorkspace   *workspace)
{
  GSList *iter;

  if (shell_app_get_state (app) == SHELL_APP_STATE_STARTING)
    {
      if (app->started_on_workspace == -1 ||
          meta_workspace_index (workspace) == app->started_on_workspace)
        return TRUE;
      else
        return FALSE;
    }

  if (app->running_state == NULL)
    return FALSE;

  for (iter = app->running_state->windows; iter; iter = iter->next)
    {
      if (meta_window_get_workspace (iter->data) == workspace)
        return TRUE;
    }

  return FALSE;
}

static int
shell_app_get_last_user_time (ShellApp *app)
{
  GSList *iter;
  guint32 last_user_time;

  last_user_time = 0;

  if (app->running_state != NULL)
    {
      for (iter = app->running_state->windows; iter; iter = iter->next)
        last_user_time = MAX (last_user_time, meta_window_get_user_time (iter->data));
    }

  return (int)last_user_time;
}

static gboolean
shell_app_is_minimized (ShellApp *app)
{
  GSList *iter;

  if (app->running_state == NULL)
    return FALSE;

  for (iter = app->running_state->windows; iter; iter = iter->next)
    {
      if (meta_window_showing_on_its_workspace (iter->data))
        return FALSE;
    }

  return TRUE;
}

/**
 * shell_app_compare:
 * @app:
 * @other: A #ShellApp
 *
 * Compare one #ShellApp instance to another, in the following way:
 *   - Running applications sort before not-running applications.
 *   - If one of them has non-minimized windows and the other does not,
 *     the one with visible windows is first.
 *   - Finally, the application which the user interacted with most recently
 *     compares earlier.
 */
int
shell_app_compare (ShellApp *app,
                   ShellApp *other)
{
  gboolean min_app, min_other;

  if (app->state != other->state)
    {
      if (app->state == SHELL_APP_STATE_RUNNING)
        return -1;
      return 1;
    }

  min_app = shell_app_is_minimized (app);
  min_other = shell_app_is_minimized (other);

  if (min_app != min_other)
    {
      if (min_other)
        return -1;
      return 1;
    }

  if (app->state == SHELL_APP_STATE_RUNNING)
    {
      if (app->running_state->windows && !other->running_state->windows)
        return -1;
      else if (!app->running_state->windows && other->running_state->windows)
        return 1;

      return shell_app_get_last_user_time (other) - shell_app_get_last_user_time (app);
    }

  return 0;
}

ShellApp *
_shell_app_new_for_window (MetaWindow      *window)
{
  ShellApp *app;

  app = g_object_new (SHELL_TYPE_APP, NULL);

  app->window_id_string = g_strdup_printf ("window:%d", meta_window_get_stable_sequence (window));

  _shell_app_add_window (app, window);

  return app;
}

ShellApp *
_shell_app_new (GDesktopAppInfo *info)
{
  ShellApp *app;

  app = g_object_new (SHELL_TYPE_APP,
                      "app-info", info,
                      NULL);

  return app;
}

void
_shell_app_set_app_info (ShellApp        *app,
                         GDesktopAppInfo *info)
{
  g_set_object (&app->info, info);

  g_clear_pointer (&app->name_collation_key, g_free);
  if (app->info)
    app->name_collation_key = g_utf8_collate_key (shell_app_get_name (app), -1);
}

static void
shell_app_state_transition (ShellApp      *app,
                            ShellAppState  state)
{
  if (app->state == state)
    return;
  g_return_if_fail (!(app->state == SHELL_APP_STATE_RUNNING &&
                      state == SHELL_APP_STATE_STARTING));
  app->state = state;

  _shell_app_system_notify_app_state_changed (shell_app_system_get_default (), app);

  g_object_notify (G_OBJECT (app), "state");
}

static void
shell_app_on_unmanaged (MetaWindow      *window,
                        ShellApp *app)
{
  _shell_app_remove_window (app, window);
}

static void
shell_app_on_user_time_changed (MetaWindow *window,
                                GParamSpec *pspec,
                                ShellApp   *app)
{
  g_assert (app->running_state != NULL);

  /* Ideally we don't want to emit windows-changed if the sort order
   * isn't actually changing. This check catches most of those.
   */
  if (window != app->running_state->windows->data)
    {
      app->running_state->window_sort_stale = TRUE;
      g_signal_emit (app, shell_app_signals[WINDOWS_CHANGED], 0);
    }
}

static void
shell_app_sync_running_state (ShellApp *app)
{
  g_return_if_fail (app->running_state != NULL);

  if (app->state != SHELL_APP_STATE_STARTING)
    {
      if (app->running_state->interesting_windows == 0)
        shell_app_state_transition (app, SHELL_APP_STATE_STOPPED);
      else
        shell_app_state_transition (app, SHELL_APP_STATE_RUNNING);
    }
}


static void
shell_app_on_skip_taskbar_changed (MetaWindow *window,
                                   GParamSpec *pspec,
                                   ShellApp   *app)
{
  g_assert (app->running_state != NULL);

  /* we rely on MetaWindow:skip-taskbar only being notified
   * when it actually changes; when that assumption breaks,
   * we'll have to track the "interesting" windows themselves
   */
  if (meta_window_is_skip_taskbar (window))
    app->running_state->interesting_windows--;
  else
    app->running_state->interesting_windows++;

  shell_app_sync_running_state (app);
}

static void
shell_app_on_ws_switch (MetaWorkspaceManager *workspace_manager,
                        int                   from,
                        int                   to,
                        MetaMotionDirection   direction,
                        gpointer              data)
{
  ShellApp *app = SHELL_APP (data);

  g_assert (app->running_state != NULL);

  app->running_state->window_sort_stale = TRUE;

  g_signal_emit (app, shell_app_signals[WINDOWS_CHANGED], 0);
}

gboolean
shell_app_get_busy (ShellApp *app)
{
  if (app->running_state != NULL &&
      app->running_state->application_proxy != NULL &&
      shell_org_gtk_application_get_busy (app->running_state->application_proxy))
    return TRUE;

  return FALSE;
}

static void
busy_changed_cb (GObject    *object,
                 GParamSpec *pspec,
                 gpointer    user_data)
{
  ShellApp *app = user_data;

  g_assert (SHELL_IS_APP (app));

  g_object_notify (G_OBJECT (app), "busy");
}

static void
get_application_proxy (GObject      *source,
                       GAsyncResult *result,
                       gpointer      user_data)
{
  ShellApp *app = user_data;
  ShellOrgGtkApplication *proxy;
  g_autoptr (GError) error = NULL;

  g_assert (SHELL_IS_APP (app));

  proxy = shell_org_gtk_application_proxy_new_finish (result, &error);
  if (proxy != NULL)
    {
      app->running_state->application_proxy = proxy;
      g_signal_connect (proxy,
                        "notify::busy",
                        G_CALLBACK (busy_changed_cb),
                        app);
      if (shell_org_gtk_application_get_busy (proxy))
        g_object_notify (G_OBJECT (app), "busy");
    }

  if (app->running_state != NULL &&
      !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
    g_clear_object (&app->running_state->cancellable);

  g_object_unref (app);
}

static void
shell_app_ensure_busy_watch (ShellApp *app)
{
  ShellAppRunningState *running_state = app->running_state;
  MetaWindow *window;
  const gchar *object_path;

  if (running_state->application_proxy != NULL ||
      running_state->cancellable != NULL)
    return;

  if (running_state->unique_bus_name == NULL)
    return;

  window = g_slist_nth_data (running_state->windows, 0);
  object_path = meta_window_get_gtk_application_object_path (window);

  if (object_path == NULL)
    return;

  running_state->cancellable = g_cancellable_new();
  /* Take a reference to app to make sure it isn't finalized before
     get_application_proxy runs */
  shell_org_gtk_application_proxy_new (running_state->session,
                                       G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
                                       running_state->unique_bus_name,
                                       object_path,
                                       running_state->cancellable,
                                       get_application_proxy,
                                       g_object_ref (app));
}

void
_shell_app_add_window (ShellApp        *app,
                       MetaWindow      *window)
{
  if (app->running_state && g_slist_find (app->running_state->windows, window))
    return;

  g_object_freeze_notify (G_OBJECT (app));

  if (!app->running_state)
      create_running_state (app);

  app->running_state->window_sort_stale = TRUE;
  app->running_state->windows = g_slist_prepend (app->running_state->windows, g_object_ref (window));
  g_signal_connect_object (window, "unmanaged", G_CALLBACK(shell_app_on_unmanaged), app, 0);
  g_signal_connect_object (window, "notify::user-time", G_CALLBACK(shell_app_on_user_time_changed), app, 0);
  g_signal_connect_object (window, "notify::skip-taskbar", G_CALLBACK(shell_app_on_skip_taskbar_changed), app, 0);

  shell_app_update_app_actions (app, window);
  shell_app_ensure_busy_watch (app);

  if (!meta_window_is_skip_taskbar (window))
    app->running_state->interesting_windows++;
  shell_app_sync_running_state (app);

  g_object_thaw_notify (G_OBJECT (app));

  g_signal_emit (app, shell_app_signals[WINDOWS_CHANGED], 0);
}

void
_shell_app_remove_window (ShellApp   *app,
                          MetaWindow *window)
{
  g_assert (app->running_state != NULL);

  if (!g_slist_find (app->running_state->windows, window))
    return;

  g_signal_handlers_disconnect_by_func (window, G_CALLBACK(shell_app_on_unmanaged), app);
  g_signal_handlers_disconnect_by_func (window, G_CALLBACK(shell_app_on_user_time_changed), app);
  g_signal_handlers_disconnect_by_func (window, G_CALLBACK(shell_app_on_skip_taskbar_changed), app);
  app->running_state->windows = g_slist_remove (app->running_state->windows, window);

  g_clear_signal_handler (&app->running_state->icon_changed_id, window);

  if (!meta_window_is_skip_taskbar (window))
    app->running_state->interesting_windows--;
  shell_app_sync_running_state (app);

  g_object_unref (window);

  if (app->running_state->windows == NULL)
    g_clear_pointer (&app->running_state, unref_running_state);

  g_signal_emit (app, shell_app_signals[WINDOWS_CHANGED], 0);
}

/**
 * shell_app_get_pids:
 * @app: a #ShellApp
 *
 * Returns: (transfer container) (element-type int): An unordered list of process identifiers associated with this application.
 */
GSList *
shell_app_get_pids (ShellApp *app)
{
  GSList *result;
  GSList *iter;

  result = NULL;
  for (iter = shell_app_get_windows (app); iter; iter = iter->next)
    {
      MetaWindow *window = iter->data;
      pid_t pid = meta_window_get_pid (window);

      if (pid < 1)
        continue;

      /* Note in the (by far) common case, app will only have one pid, so
       * we'll hit the first element, so don't worry about O(N^2) here.
       */
      if (!g_slist_find (result, GINT_TO_POINTER (pid)))
        result = g_slist_prepend (result, GINT_TO_POINTER (pid));
    }
  return result;
}

void
_shell_app_handle_startup_sequence (ShellApp            *app,
                                    MetaStartupSequence *sequence)
{
  gboolean starting = !meta_startup_sequence_get_completed (sequence);

  /* The Shell design calls for on application launch, the app title
   * appears at top, and no X window is focused.  So when we get
   * a startup-notification for this app, transition it to STARTING
   * if it's currently stopped, set it as our application focus,
   * but focus the no_focus window.
   */
  if (starting && shell_app_get_state (app) == SHELL_APP_STATE_STOPPED)
    {
      MetaDisplay *display = shell_global_get_display (shell_global_get ());

      shell_app_state_transition (app, SHELL_APP_STATE_STARTING);
      meta_display_unset_input_focus (display,
                                      meta_startup_sequence_get_timestamp (sequence));
      app->started_on_workspace = meta_startup_sequence_get_workspace (sequence);
    }

  if (!starting)
    {
      if (app->running_state && app->running_state->windows)
        shell_app_state_transition (app, SHELL_APP_STATE_RUNNING);
      else /* application have > 1 .desktop file */
        shell_app_state_transition (app, SHELL_APP_STATE_STOPPED);
    }
}

/**
 * shell_app_request_quit:
 * @app: A #ShellApp
 *
 * Initiate an asynchronous request to quit this application.
 * The application may interact with the user, and the user
 * might cancel the quit request from the application UI.
 *
 * This operation may not be supported for all applications.
 *
 * Returns: %TRUE if a quit request is supported for this application
 */
gboolean
shell_app_request_quit (ShellApp   *app)
{
  GActionGroup *group = NULL;
  GSList *iter;

  if (shell_app_get_state (app) != SHELL_APP_STATE_RUNNING)
    return FALSE;

  /* First, check whether the app exports an explicit "quit" action
   * that we can activate on the bus
   */
  group = G_ACTION_GROUP (app->running_state->muxer);

  if (g_action_group_has_action (group, "app.quit") &&
      g_action_group_get_action_parameter_type (group, "app.quit") == NULL)
    {
      g_action_group_activate_action (group, "app.quit", NULL);

      return TRUE;
    }

  /* Otherwise, fall back to closing all the app's windows */
  for (iter = app->running_state->windows; iter; iter = iter->next)
    {
      MetaWindow *win = iter->data;

      if (!meta_window_can_close (win))
        continue;

      meta_window_delete (win, shell_global_get_current_time (shell_global_get ()));
    }
  return TRUE;
}

#if !defined(HAVE_GIO_DESKTOP_LAUNCH_URIS_WITH_FDS) && defined(HAVE_SYSTEMD)
/* This sets up the launched application to log to the journal
 * using its own identifier, instead of just "gnome-session".
 */
static void
app_child_setup (gpointer user_data)
{
  const char *appid = user_data;
  int res;
  int journalfd = sd_journal_stream_fd (appid, LOG_INFO, FALSE);
  if (journalfd >= 0)
    {
      do
        res = dup2 (journalfd, 1);
      while (G_UNLIKELY (res == -1 && errno == EINTR));
      do
        res = dup2 (journalfd, 2);
      while (G_UNLIKELY (res == -1 && errno == EINTR));
      (void) close (journalfd);
    }
}
#endif

static void
wait_pid (GDesktopAppInfo *appinfo,
          GPid             pid,
          gpointer         user_data)
{
  g_child_watch_add (pid, (GChildWatchFunc) g_spawn_close_pid, NULL);
}

static void
apply_discrete_gpu_env (GAppLaunchContext *context,
                        ShellGlobal       *global)
{
  GDBusProxy *proxy;
  GVariant* variant;
  guint num_children, i;

  proxy = shell_global_get_switcheroo_control (global);
  if (!proxy)
    {
      g_warning ("Could not apply discrete GPU environment, switcheroo-control not available");
      return;
    }

  variant = shell_net_hadess_switcheroo_control_get_gpus (SHELL_NET_HADESS_SWITCHEROO_CONTROL (proxy));
  if (!variant)
    {
      g_warning ("Could not apply discrete GPU environment, no GPUs in list");
      return;
    }

  num_children = g_variant_n_children (variant);
  for (i = 0; i < num_children; i++)
    {
      g_autoptr(GVariant) gpu = NULL;
      g_autoptr(GVariant) env = NULL;
      g_autoptr(GVariant) default_variant = NULL;
      g_autofree const char **env_s = NULL;
      guint j;

      gpu = g_variant_get_child_value (variant, i);
      if (!gpu ||
          !g_variant_is_of_type (gpu, G_VARIANT_TYPE ("a{s*}")))
        continue;

      /* Skip over the default GPU */
      default_variant = g_variant_lookup_value (gpu, "Default", NULL);
      if (!default_variant || g_variant_get_boolean (default_variant))
        continue;

      env = g_variant_lookup_value (gpu, "Environment", NULL);
      if (!env)
        continue;

      env_s = g_variant_get_strv (env, NULL);
      for (j = 0; env_s[j] != NULL; j = j + 2)
        g_app_launch_context_setenv (context, env_s[j], env_s[j+1]);
      return;
    }

  g_debug ("Could not find discrete GPU in switcheroo-control, not applying environment");
}

/**
 * shell_app_launch:
 * @timestamp: Event timestamp, or 0 for current event timestamp
 * @workspace: Start on this workspace, or -1 for default
 * @gpu_pref: the GPU to prefer launching on
 * @error: A #GError
 */
gboolean
shell_app_launch (ShellApp           *app,
                  guint               timestamp,
                  int                 workspace,
                  ShellAppLaunchGpu   gpu_pref,
                  GError            **error)
{
  ShellGlobal *global;
  GAppLaunchContext *context;
  gboolean ret;
  GSpawnFlags flags;
  gboolean discrete_gpu = FALSE;

  if (app->info == NULL)
    {
      MetaWindow *window = window_backed_app_get_window (app);
      /* We don't use an error return if there no longer any windows, because the
       * user attempting to activate a stale window backed app isn't something
       * we would expect the caller to meaningfully handle or display an error
       * message to the user.
       */
      if (window)
        meta_window_activate (window, timestamp);
      return TRUE;
    }

  global = shell_global_get ();
  context = shell_global_create_app_launch_context (global, timestamp, workspace);
  if (gpu_pref == SHELL_APP_LAUNCH_GPU_APP_PREF)
    discrete_gpu = g_desktop_app_info_get_boolean (app->info, "PrefersNonDefaultGPU");
  else
    discrete_gpu = (gpu_pref == SHELL_APP_LAUNCH_GPU_DISCRETE);

  if (discrete_gpu)
    apply_discrete_gpu_env (context, global);

  /* Set LEAVE_DESCRIPTORS_OPEN in order to use an optimized gspawn
   * codepath. The shell's open file descriptors should be marked CLOEXEC
   * so that they are automatically closed even with this flag set.
   */
  flags = G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD |
          G_SPAWN_LEAVE_DESCRIPTORS_OPEN;

#ifdef HAVE_GIO_DESKTOP_LAUNCH_URIS_WITH_FDS
  /* Optimized spawn path, avoiding a child_setup function */
  {
    int journalfd = -1;

#ifdef HAVE_SYSTEMD
    journalfd = sd_journal_stream_fd (shell_app_get_id (app), LOG_INFO, FALSE);
#endif /* HAVE_SYSTEMD */

    ret = g_desktop_app_info_launch_uris_as_manager_with_fds (app->info, NULL,
                                                              context,
                                                              flags,
                                                              NULL, NULL,
                                                              wait_pid, NULL,
                                                              -1,
                                                              journalfd,
                                                              journalfd,
                                                              error);

    if (journalfd >= 0)
      (void) close (journalfd);
  }
#else /* !HAVE_GIO_DESKTOP_LAUNCH_URIS_WITH_FDS */
  ret = g_desktop_app_info_launch_uris_as_manager (app->info, NULL,
                                                   context,
                                                   flags,
#ifdef HAVE_SYSTEMD
                                                   app_child_setup, (gpointer)shell_app_get_id (app),
#else
                                                   NULL, NULL,
#endif
                                                   wait_pid, NULL,
                                                   error);
#endif /* HAVE_GIO_DESKTOP_LAUNCH_URIS_WITH_FDS */
  g_object_unref (context);

  return ret;
}

/**
 * shell_app_launch_action:
 * @app: the #ShellApp
 * @action_name: the name of the action to launch (as obtained by
 *               g_desktop_app_info_list_actions())
 * @timestamp: Event timestamp, or 0 for current event timestamp
 * @workspace: Start on this workspace, or -1 for default
 */
void
shell_app_launch_action (ShellApp        *app,
                         const char      *action_name,
                         guint            timestamp,
                         int              workspace)
{
  ShellGlobal *global;
  GAppLaunchContext *context;

  global = shell_global_get ();
  context = shell_global_create_app_launch_context (global, timestamp, workspace);

  g_desktop_app_info_launch_action (G_DESKTOP_APP_INFO (app->info),
                                    action_name, context);

  g_object_unref (context);
}

/**
 * shell_app_get_app_info:
 * @app: a #ShellApp
 *
 * Returns: (transfer none): The #GDesktopAppInfo for this app, or %NULL if backed by a window
 */
GDesktopAppInfo *
shell_app_get_app_info (ShellApp *app)
{
  return app->info;
}

static void
create_running_state (ShellApp *app)
{
  MetaDisplay *display = shell_global_get_display (shell_global_get ());
  MetaWorkspaceManager *workspace_manager =
    meta_display_get_workspace_manager (display);

  g_assert (app->running_state == NULL);

  app->running_state = g_slice_new0 (ShellAppRunningState);
  app->running_state->refcount = 1;
  app->running_state->workspace_switch_id =
    g_signal_connect (workspace_manager, "workspace-switched",
                      G_CALLBACK (shell_app_on_ws_switch), app);

  app->running_state->session = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL);
  g_assert (app->running_state->session != NULL);
  app->running_state->muxer = gtk_action_muxer_new ();
}

void
shell_app_update_app_actions (ShellApp   *app,
                              MetaWindow *window)
{
  const gchar *unique_bus_name;

  /* We assume that 'gtk-application-object-path' and
   * 'gtk-app-menu-object-path' are the same for all windows which
   * have it set.
   *
   * It could be possible, however, that the first window we see
   * belonging to the app didn't have them set.  For this reason, we
   * take the values from the first window that has them set and ignore
   * all the rest (until the app is stopped and restarted).
   */

  unique_bus_name = meta_window_get_gtk_unique_bus_name (window);

  if (g_strcmp0 (app->running_state->unique_bus_name, unique_bus_name) != 0)
    {
      const gchar *application_object_path;
      GDBusActionGroup *actions;

      application_object_path = meta_window_get_gtk_application_object_path (window);

      if (application_object_path == NULL || unique_bus_name == NULL)
        return;

      g_clear_pointer (&app->running_state->unique_bus_name, g_free);
      app->running_state->unique_bus_name = g_strdup (unique_bus_name);
      actions = g_dbus_action_group_get (app->running_state->session, unique_bus_name, application_object_path);
      gtk_action_muxer_insert (app->running_state->muxer, "app", G_ACTION_GROUP (actions));
      g_object_unref (actions);
    }
}

static void
unref_running_state (ShellAppRunningState *state)
{
  MetaDisplay *display = shell_global_get_display (shell_global_get ());
  MetaWorkspaceManager *workspace_manager =
    meta_display_get_workspace_manager (display);

  g_assert (state->refcount > 0);

  state->refcount--;
  if (state->refcount > 0)
    return;

  g_clear_signal_handler (&state->workspace_switch_id, workspace_manager);

  g_clear_object (&state->application_proxy);

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

  g_clear_object (&state->muxer);
  g_clear_object (&state->session);
  g_clear_pointer (&state->unique_bus_name, g_free);

  g_slice_free (ShellAppRunningState, state);
}

/**
 * shell_app_compare_by_name:
 * @app: One app
 * @other: The other app
 *
 * Order two applications by name.
 *
 * Returns: -1, 0, or 1; suitable for use as a comparison function
 * for e.g. g_slist_sort()
 */
int
shell_app_compare_by_name (ShellApp *app, ShellApp *other)
{
  return strcmp (app->name_collation_key, other->name_collation_key);
}

static void
shell_app_init (ShellApp *self)
{
  self->state = SHELL_APP_STATE_STOPPED;
}

static void
shell_app_dispose (GObject *object)
{
  ShellApp *app = SHELL_APP (object);

  g_clear_object (&app->info);
  g_clear_object (&app->fallback_icon);

  while (app->running_state)
    _shell_app_remove_window (app, app->running_state->windows->data);

  /* We should have been transitioned when we removed all of our windows */
  g_assert (app->state == SHELL_APP_STATE_STOPPED);
  g_assert (app->running_state == NULL);

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

static void
shell_app_finalize (GObject *object)
{
  ShellApp *app = SHELL_APP (object);

  g_free (app->window_id_string);

  g_free (app->name_collation_key);

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

static void
shell_app_class_init(ShellAppClass *klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);

  gobject_class->get_property = shell_app_get_property;
  gobject_class->set_property = shell_app_set_property;
  gobject_class->dispose = shell_app_dispose;
  gobject_class->finalize = shell_app_finalize;

  shell_app_signals[WINDOWS_CHANGED] = g_signal_new ("windows-changed",
                                     SHELL_TYPE_APP,
                                     G_SIGNAL_RUN_LAST,
                                     0,
                                     NULL, NULL, NULL,
                                     G_TYPE_NONE, 0);

  /**
   * ShellApp:state:
   *
   * The high-level state of the application, effectively whether it's
   * running or not, or transitioning between those states.
   */
  g_object_class_install_property (gobject_class,
                                   PROP_STATE,
                                   g_param_spec_enum ("state",
                                                      "State",
                                                      "Application state",
                                                      SHELL_TYPE_APP_STATE,
                                                      SHELL_APP_STATE_STOPPED,
                                                      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  /**
   * ShellApp:busy:
   *
   * Whether the application has marked itself as busy.
   */
  g_object_class_install_property (gobject_class,
                                   PROP_BUSY,
                                   g_param_spec_boolean ("busy",
                                                         "Busy",
                                                         "Busy state",
                                                         FALSE,
                                                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  /**
   * ShellApp:id:
   *
   * The id of this application (a desktop filename, or a special string
   * like window:0xabcd1234)
   */
  g_object_class_install_property (gobject_class,
                                   PROP_ID,
                                   g_param_spec_string ("id",
                                                        "Application id",
                                                        "The desktop file id of this ShellApp",
                                                        NULL,
                                                        G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  /**
   * ShellApp:icon:
   *
   * The #GIcon representing this ShellApp
   */
  g_object_class_install_property (gobject_class,
                                   PROP_ICON,
                                   g_param_spec_object ("icon",
                                                        "GIcon",
                                                        "The GIcon representing this app",
                                                        G_TYPE_ICON,
                                                        G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  /**
   * ShellApp:action-group:
   *
   * The #GDBusActionGroup associated with this ShellApp, if any. See the
   * documentation of #GApplication and #GActionGroup for details.
   */
  g_object_class_install_property (gobject_class,
                                   PROP_ACTION_GROUP,
                                   g_param_spec_object ("action-group",
                                                        "Application Action Group",
                                                        "The action group exported by the remote application",
                                                        G_TYPE_ACTION_GROUP,
                                                        G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
  /**
   * ShellApp:app-info:
   *
   * The #GDesktopAppInfo associated with this ShellApp, if any.
   */
  g_object_class_install_property (gobject_class,
                                   PROP_APP_INFO,
                                   g_param_spec_object ("app-info",
                                                        "DesktopAppInfo",
                                                        "The DesktopAppInfo associated with this app",
                                                        G_TYPE_DESKTOP_APP_INFO,
                                                        G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

}
