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

/* Mutter Session Management */

/*
 * Copyright (C) 2001 Havoc Pennington (some code in here from
 * libgnomeui, (C) Tom Tromey, Carsten Schaar)
 * Copyright (C) 2004, 2005 Elijah Newren
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include "x11/session.h"

#include <sys/wait.h>
#include <time.h>
#include <X11/Xatom.h>

#include "core/util-private.h"
#include "meta/main.h"
#include "x11/meta-x11-display-private.h"

#ifndef HAVE_SM
void
meta_session_init (const char *client_id,
                   const char *save_file)
{
  meta_topic (META_DEBUG_SM, "Compiled without session management support\n");
}

const MetaWindowSessionInfo*
meta_window_lookup_saved_state (MetaWindow *window)
{
  return NULL;
}

void
meta_window_release_saved_state (const MetaWindowSessionInfo *info)
{
  ;
}
#else /* HAVE_SM */

#include <errno.h>
#include <fcntl.h>
#include <glib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <X11/ICE/ICElib.h>
#include <X11/SM/SMlib.h>

#include "core/display-private.h"
#include "meta/main.h"
#include "meta/util.h"
#include "meta/workspace.h"

static void ice_io_error_handler (IceConn connection);

static void new_ice_connection (IceConn connection, IcePointer client_data,
				Bool opening, IcePointer *watch_data);

static void        save_state         (void);
static char*       load_state         (const char *previous_save_file);
static void        regenerate_save_file (void);
static const char* full_save_file       (void);
static void        warn_about_lame_clients_and_finish_interact (gboolean shutdown);
static void        disconnect         (void);

/* This is called when data is available on an ICE connection.  */
static gboolean
process_ice_messages (GIOChannel *channel,
                      GIOCondition condition,
                      gpointer client_data)
{
  IceConn connection = (IceConn) client_data;
  IceProcessMessagesStatus status;

  /* This blocks infinitely sometimes. I don't know what
   * to do about it. Checking "condition" just breaks
   * session management.
   */
  status = IceProcessMessages (connection, NULL, NULL);

  if (status == IceProcessMessagesIOError)
    {
#if 0
      IcePointer context = IceGetConnectionContext (connection);
#endif

      /* We were disconnected; close our connection to the
       * session manager, this will result in the ICE connection
       * being cleaned up, since it is owned by libSM.
       */
      disconnect ();
      meta_quit (META_EXIT_SUCCESS);

      return FALSE;
    }

  return TRUE;
}

/* This is called when a new ICE connection is made.  It arranges for
   the ICE connection to be handled via the event loop.  */
static void
new_ice_connection (IceConn connection, IcePointer client_data, Bool opening,
		    IcePointer *watch_data)
{
  guint input_id;

  if (opening)
    {
      /* Make sure we don't pass on these file descriptors to any
       * exec'ed children
       */
      GIOChannel *channel;

      fcntl (IceConnectionNumber (connection), F_SETFD,
             fcntl (IceConnectionNumber (connection), F_GETFD, 0) | FD_CLOEXEC);

      channel = g_io_channel_unix_new (IceConnectionNumber (connection));

      input_id = g_io_add_watch (channel,
                                 G_IO_IN | G_IO_ERR,
                                 process_ice_messages,
                                 connection);

      g_io_channel_unref (channel);

      *watch_data = (IcePointer) GUINT_TO_POINTER (input_id);
    }
  else
    {
      input_id = GPOINTER_TO_UINT ((gpointer) *watch_data);

      g_clear_handle_id (&input_id, g_source_remove);
    }
}

static IceIOErrorHandler ice_installed_handler;

/* We call any handler installed before (or after) gnome_ice_init but
   avoid calling the default libICE handler which does an exit() */
static void
ice_io_error_handler (IceConn connection)
{
    if (ice_installed_handler)
      (*ice_installed_handler) (connection);
}

static void
ice_init (void)
{
  static gboolean ice_initted = FALSE;

  if (! ice_initted)
    {
      IceIOErrorHandler default_handler;

      ice_installed_handler = IceSetIOErrorHandler (NULL);
      default_handler = IceSetIOErrorHandler (ice_io_error_handler);

      if (ice_installed_handler == default_handler)
	ice_installed_handler = NULL;

      IceAddConnectionWatch (new_ice_connection, NULL);

      ice_initted = TRUE;
    }
}

typedef enum
{
  STATE_DISCONNECTED,
  STATE_IDLE,
  STATE_SAVING_PHASE_1,
  STATE_WAITING_FOR_PHASE_2,
  STATE_SAVING_PHASE_2,
  STATE_WAITING_FOR_INTERACT,
  STATE_DONE_WITH_INTERACT,
  STATE_SKIPPING_GLOBAL_SAVE,
  STATE_FROZEN,
  STATE_REGISTERING
} ClientState;

static void save_phase_2_callback       (SmcConn   smc_conn,
                                         SmPointer client_data);
static void interact_callback           (SmcConn   smc_conn,
                                         SmPointer client_data);
static void shutdown_cancelled_callback (SmcConn   smc_conn,
                                         SmPointer client_data);
static void save_complete_callback      (SmcConn   smc_conn,
                                         SmPointer client_data);
static void die_callback                (SmcConn   smc_conn,
                                         SmPointer client_data);
static void save_yourself_callback      (SmcConn   smc_conn,
                                         SmPointer client_data,
                                         int       save_style,
                                         Bool      shutdown,
                                         int       interact_style,
                                         Bool      fast);
static void set_clone_restart_commands  (void);

static char *client_id = NULL;
static gpointer session_connection = NULL;
static ClientState current_state = STATE_DISCONNECTED;
static gboolean interaction_allowed = FALSE;

void
meta_session_init (const char *previous_client_id,
                   const char *previous_save_file)
{
  /* Some code here from twm */
  char buf[256];
  unsigned long mask;
  SmcCallbacks callbacks;
  char *saved_client_id;

  meta_topic (META_DEBUG_SM, "Initializing session with save file '%s'\n",
              previous_save_file ? previous_save_file : "(none)");

  if (previous_save_file)
    {
      saved_client_id = load_state (previous_save_file);
      previous_client_id = saved_client_id;
    }
  else if (previous_client_id)
    {
      char *save_file = g_strconcat (previous_client_id, ".ms", NULL);
      saved_client_id = load_state (save_file);
      g_free (save_file);
    }
  else
    {
      saved_client_id = NULL;
    }

  ice_init ();

  mask = SmcSaveYourselfProcMask | SmcDieProcMask |
    SmcSaveCompleteProcMask | SmcShutdownCancelledProcMask;

  callbacks.save_yourself.callback = save_yourself_callback;
  callbacks.save_yourself.client_data = NULL;

  callbacks.die.callback = die_callback;
  callbacks.die.client_data = NULL;

  callbacks.save_complete.callback = save_complete_callback;
  callbacks.save_complete.client_data = NULL;

  callbacks.shutdown_cancelled.callback = shutdown_cancelled_callback;
  callbacks.shutdown_cancelled.client_data = NULL;

  session_connection =
    SmcOpenConnection (NULL, /* use SESSION_MANAGER env */
                       NULL, /* means use existing ICE connection */
                       SmProtoMajor,
                       SmProtoMinor,
                       mask,
                       &callbacks,
                       (char*) previous_client_id,
                       &client_id,
                       255, buf);

  if (session_connection == NULL)
    {
      meta_topic (META_DEBUG_SM,
                  "Failed to a open connection to a session manager, so window positions will not be saved: %s\n",
                  buf);

      goto out;
    }
  else
    {
      if (client_id == NULL)
        meta_bug ("Session manager gave us a NULL client ID?");
      meta_topic (META_DEBUG_SM, "Obtained session ID '%s'\n", client_id);
    }

  if (previous_client_id && strcmp (previous_client_id, client_id) == 0)
    current_state = STATE_IDLE;
  else
    current_state = STATE_REGISTERING;

  {
    SmProp prop1, prop2, prop3, prop4, prop5, prop6, *props[6];
    SmPropValue prop1val, prop2val, prop3val, prop4val, prop5val, prop6val;
    char pid[32];
    /* Historically, this was SmRestartImmediately, which made sense
     * for a stateless window manager, but we don't really control
     * what embedders do, and it's all around better if gnome-session
     * handles this.
     */
    char hint = SmRestartIfRunning;
    char priority = 20; /* low to run before other apps */
    const char *prgname;

    prgname = g_get_prgname ();

    prop1.name = (char *)SmProgram;
    prop1.type = (char *)SmARRAY8;
    prop1.num_vals = 1;
    prop1.vals = &prop1val;
    prop1val.value = (char *)prgname;
    prop1val.length = strlen (prgname);

    /* twm sets getuid() for this, but the SM spec plainly
     * says pw_name, twm is on crack
     */
    prop2.name = (char *)SmUserID;
    prop2.type = (char *)SmARRAY8;
    prop2.num_vals = 1;
    prop2.vals = &prop2val;
    prop2val.value = (char*) g_get_user_name ();
    prop2val.length = strlen (prop2val.value);

    prop3.name = (char *)SmRestartStyleHint;
    prop3.type = (char *)SmCARD8;
    prop3.num_vals = 1;
    prop3.vals = &prop3val;
    prop3val.value = &hint;
    prop3val.length = 1;

    sprintf (pid, "%d", getpid ());
    prop4.name = (char *)SmProcessID;
    prop4.type = (char *)SmARRAY8;
    prop4.num_vals = 1;
    prop4.vals = &prop4val;
    prop4val.value = pid;
    prop4val.length = strlen (prop4val.value);

    /* Always start in home directory */
    prop5.name = (char *)SmCurrentDirectory;
    prop5.type = (char *)SmARRAY8;
    prop5.num_vals = 1;
    prop5.vals = &prop5val;
    prop5val.value = (char*) g_get_home_dir ();
    prop5val.length = strlen (prop5val.value);

    prop6.name = (char *)"_GSM_Priority";
    prop6.type = (char *)SmCARD8;
    prop6.num_vals = 1;
    prop6.vals = &prop6val;
    prop6val.value = &priority;
    prop6val.length = 1;

    props[0] = &prop1;
    props[1] = &prop2;
    props[2] = &prop3;
    props[3] = &prop4;
    props[4] = &prop5;
    props[5] = &prop6;

    SmcSetProperties (session_connection, 6, props);
  }

 out:
  g_free (saved_client_id);
}

static void
disconnect (void)
{
  SmcCloseConnection (session_connection, 0, NULL);
  session_connection = NULL;
  current_state = STATE_DISCONNECTED;
}

static void
save_yourself_possibly_done (gboolean shutdown,
                             gboolean successful)
{
  meta_topic (META_DEBUG_SM,
              "save possibly done shutdown = %d success = %d\n",
              shutdown, successful);

  if (current_state == STATE_SAVING_PHASE_1)
    {
      Status status;

      status = SmcRequestSaveYourselfPhase2 (session_connection,
                                             save_phase_2_callback,
                                             GINT_TO_POINTER (shutdown));

      if (status)
        current_state = STATE_WAITING_FOR_PHASE_2;

      meta_topic (META_DEBUG_SM,
                  "Requested phase 2, status = %d\n", status);
    }

  if (current_state == STATE_SAVING_PHASE_2 &&
      interaction_allowed)
    {
      Status status;

      status = SmcInteractRequest (session_connection,
                                   /* ignore this feature of the protocol by always
                                    * claiming normal
                                    */
                                   SmDialogNormal,
                                   interact_callback,
                                   GINT_TO_POINTER (shutdown));

      if (status)
        current_state = STATE_WAITING_FOR_INTERACT;

      meta_topic (META_DEBUG_SM,
                  "Requested interact, status = %d\n", status);
    }

  if (current_state == STATE_SAVING_PHASE_1 ||
      current_state == STATE_SAVING_PHASE_2 ||
      current_state == STATE_DONE_WITH_INTERACT ||
      current_state == STATE_SKIPPING_GLOBAL_SAVE)
    {
      meta_topic (META_DEBUG_SM, "Sending SaveYourselfDone\n");

      SmcSaveYourselfDone (session_connection,
                           successful);

      if (shutdown)
        current_state = STATE_FROZEN;
      else
        current_state = STATE_IDLE;
    }
}

static void
save_phase_2_callback (SmcConn smc_conn, SmPointer client_data)
{
  gboolean shutdown;

  meta_topic (META_DEBUG_SM, "Phase 2 save");

  shutdown = GPOINTER_TO_INT (client_data);

  current_state = STATE_SAVING_PHASE_2;

  save_state ();

  save_yourself_possibly_done (shutdown, TRUE);
}

static void
save_yourself_callback (SmcConn   smc_conn,
                        SmPointer client_data,
                        int       save_style,
                        Bool      shutdown,
                        int       interact_style,
                        Bool      fast)
{
  gboolean successful;

  meta_topic (META_DEBUG_SM, "SaveYourself received");

  successful = TRUE;

  /* The first SaveYourself after registering for the first time
   * is a special case (SM specs 7.2).
   */

#if 0 /* I think the GnomeClient rationale for this doesn't apply */
  if (current_state == STATE_REGISTERING)
    {
      current_state = STATE_IDLE;
      /* Double check that this is a section 7.2 SaveYourself: */

      if (save_style == SmSaveLocal &&
	  interact_style == SmInteractStyleNone &&
	  !shutdown && !fast)
	{
	  /* The protocol requires this even if xsm ignores it. */
	  SmcSaveYourselfDone (session_connection, successful);
	  return;
	}
    }
#endif

  /* ignore Global style saves
   *
   * This interpretaion of the Local/Global/Both styles
   * was discussed extensively on the xdg-list. See:
   *
   * https://listman.redhat.com/pipermail/xdg-list/2002-July/000615.html
   */
  if (save_style == SmSaveGlobal)
    {
      current_state = STATE_SKIPPING_GLOBAL_SAVE;
      save_yourself_possibly_done (shutdown, successful);
      return;
    }

  interaction_allowed = interact_style != SmInteractStyleNone;

  current_state = STATE_SAVING_PHASE_1;

  regenerate_save_file ();

  set_clone_restart_commands ();

  save_yourself_possibly_done (shutdown, successful);
}


static void
die_callback (SmcConn smc_conn, SmPointer client_data)
{
  meta_topic (META_DEBUG_SM, "Disconnecting from session manager");
  disconnect ();
  /* We don't actually exit here - we will simply go away with the X
   * server on logout, when we lose the X connection and libx11 kills
   * us.  It looks like *crap* on logout if the user sees their
   * windows lose the decorations, etc.
   *
   * Anything that wants us to go away outside of session management
   * can use kill().
   */

  /* All of that is true - unless we're a wayland compositor. In which
   * case the X server won't go down until we do, so we must die first.
   */
  if (meta_is_wayland_compositor ())
    meta_quit (0);
}

static void
save_complete_callback (SmcConn smc_conn, SmPointer client_data)
{
  /* nothing */
  meta_topic (META_DEBUG_SM, "SaveComplete received\n");
}

static void
shutdown_cancelled_callback (SmcConn smc_conn, SmPointer client_data)
{
  meta_topic (META_DEBUG_SM, "Shutdown cancelled received\n");

  if (session_connection != NULL &&
      (current_state != STATE_IDLE && current_state != STATE_FROZEN))
    {
      SmcSaveYourselfDone (session_connection, True);
      current_state = STATE_IDLE;
    }
}

static void
interact_callback (SmcConn smc_conn, SmPointer client_data)
{
  /* nothing */
  gboolean shutdown;

  meta_topic (META_DEBUG_SM, "Interaction permission received\n");

  shutdown = GPOINTER_TO_INT (client_data);

  current_state = STATE_DONE_WITH_INTERACT;

  warn_about_lame_clients_and_finish_interact (shutdown);
}

static void
set_clone_restart_commands (void)
{
  char *restartv[10];
  char *clonev[10];
  char *discardv[10];
  int i;
  SmProp prop1, prop2, prop3, *props[3];
  const char *prgname;

  prgname = g_get_prgname ();

  /* Restart (use same client ID) */

  prop1.name = (char *)SmRestartCommand;
  prop1.type = (char *)SmLISTofARRAY8;

  g_return_if_fail (client_id);

  i = 0;
  restartv[i] = (char *)prgname;
  ++i;
  restartv[i] = (char *)"--sm-client-id";
  ++i;
  restartv[i] = client_id;
  ++i;
  restartv[i] = NULL;

  prop1.vals = g_new (SmPropValue, i);
  i = 0;
  while (restartv[i])
    {
      prop1.vals[i].value = restartv[i];
      prop1.vals[i].length = strlen (restartv[i]);
      ++i;
    }
  prop1.num_vals = i;

  /* Clone (no client ID) */

  i = 0;
  clonev[i] = (char *)prgname;
  ++i;
  clonev[i] = NULL;

  prop2.name = (char *)SmCloneCommand;
  prop2.type = (char *)SmLISTofARRAY8;

  prop2.vals = g_new (SmPropValue, i);
  i = 0;
  while (clonev[i])
    {
      prop2.vals[i].value = clonev[i];
      prop2.vals[i].length = strlen (clonev[i]);
      ++i;
    }
  prop2.num_vals = i;

  /* Discard */

  i = 0;
  discardv[i] = (char *)"rm";
  ++i;
  discardv[i] = (char *)"-f";
  ++i;
  discardv[i] = (char*) full_save_file ();
  ++i;
  discardv[i] = NULL;

  prop3.name = (char *)SmDiscardCommand;
  prop3.type = (char *)SmLISTofARRAY8;

  prop3.vals = g_new (SmPropValue, i);
  i = 0;
  while (discardv[i])
    {
      prop3.vals[i].value = discardv[i];
      prop3.vals[i].length = strlen (discardv[i]);
      ++i;
    }
  prop3.num_vals = i;


  props[0] = &prop1;
  props[1] = &prop2;
  props[2] = &prop3;

  SmcSetProperties (session_connection, 3, props);

  g_free (prop1.vals);
  g_free (prop2.vals);
  g_free (prop3.vals);
}

/* The remaining code in this file actually loads/saves the session,
 * while the code above this comment handles chatting with the
 * session manager.
 */

static const char*
window_type_to_string (MetaWindowType type)
{
  switch (type)
    {
    case META_WINDOW_NORMAL:
      return "normal";
    case META_WINDOW_DESKTOP:
      return "desktop";
    case META_WINDOW_DOCK:
      return "dock";
    case META_WINDOW_DIALOG:
      return "dialog";
    case META_WINDOW_MODAL_DIALOG:
      return "modal_dialog";
    case META_WINDOW_TOOLBAR:
      return "toolbar";
    case META_WINDOW_MENU:
      return "menu";
    case META_WINDOW_SPLASHSCREEN:
      return "splashscreen";
    case META_WINDOW_UTILITY:
      return "utility";
    case META_WINDOW_DROPDOWN_MENU:
      return "dropdown_menu";
    case META_WINDOW_POPUP_MENU:
      return "popup_menu";
    case META_WINDOW_TOOLTIP:
      return "tooltip";
    case META_WINDOW_NOTIFICATION:
      return "notification";
    case META_WINDOW_COMBO:
      return "combo";
    case META_WINDOW_DND:
      return "dnd";
    case META_WINDOW_OVERRIDE_OTHER:
      return "override_redirect";
    }

  return "";
}

static MetaWindowType
window_type_from_string (const char *str)
{
  if (strcmp (str, "normal") == 0)
    return META_WINDOW_NORMAL;
  else if (strcmp (str, "desktop") == 0)
    return META_WINDOW_DESKTOP;
  else if (strcmp (str, "dock") == 0)
    return META_WINDOW_DOCK;
  else if (strcmp (str, "dialog") == 0)
    return META_WINDOW_DIALOG;
  else if (strcmp (str, "modal_dialog") == 0)
    return META_WINDOW_MODAL_DIALOG;
  else if (strcmp (str, "toolbar") == 0)
    return META_WINDOW_TOOLBAR;
  else if (strcmp (str, "menu") == 0)
    return META_WINDOW_MENU;
  else if (strcmp (str, "utility") == 0)
    return META_WINDOW_UTILITY;
  else if (strcmp (str, "splashscreen") == 0)
    return META_WINDOW_SPLASHSCREEN;
  else
    return META_WINDOW_NORMAL;
}

static int
window_gravity_from_string (const char *str)
{
  if (strcmp (str, "META_GRAVITY_NORTH_WEST") == 0)
    return META_GRAVITY_NORTH_WEST;
  else if (strcmp (str, "META_GRAVITY_NORTH") == 0)
    return META_GRAVITY_NORTH;
  else if (strcmp (str, "META_GRAVITY_NORTH_EAST") == 0)
    return META_GRAVITY_NORTH_EAST;
  else if (strcmp (str, "META_GRAVITY_WEST") == 0)
    return META_GRAVITY_WEST;
  else if (strcmp (str, "META_GRAVITY_CENTER") == 0)
    return META_GRAVITY_CENTER;
  else if (strcmp (str, "META_GRAVITY_EAST") == 0)
    return META_GRAVITY_EAST;
  else if (strcmp (str, "META_GRAVITY_SOUTH_WEST") == 0)
    return META_GRAVITY_SOUTH_WEST;
  else if (strcmp (str, "META_GRAVITY_SOUTH") == 0)
    return META_GRAVITY_SOUTH;
  else if (strcmp (str, "META_GRAVITY_SOUTH_EAST") == 0)
    return META_GRAVITY_SOUTH_EAST;
  else if (strcmp (str, "META_GRAVITY_STATIC") == 0)
    return META_GRAVITY_STATIC;
  else
    return META_GRAVITY_NORTH_WEST;
}

static char*
encode_text_as_utf8_markup (const char *text)
{
  /* text can be any encoding, and is nul-terminated.
   * we pretend it's Latin-1 and encode as UTF-8
   */
  GString *str;
  const char *p;
  char *escaped;

  str = g_string_new ("");

  p = text;
  while (*p)
    {
      g_string_append_unichar (str, *p);
      ++p;
    }

  escaped = g_markup_escape_text (str->str, str->len);
  g_string_free (str, TRUE);

  return escaped;
}

static char*
decode_text_from_utf8 (const char *text)
{
  /* Convert back from the encoded (but not escaped) UTF-8 */
  GString *str;
  const char *p;

  str = g_string_new ("");

  p = text;
  while (*p)
    {
      /* obviously this barfs if the UTF-8 contains chars > 255 */
      g_string_append_c (str, g_utf8_get_char (p));

      p = g_utf8_next_char (p);
    }

  return g_string_free (str, FALSE);
}

static void
save_state (void)
{
  char *muffin_dir;
  char *session_dir;
  FILE *outfile;
  GSList *windows;
  GSList *tmp;
  int stack_position;

  g_assert (client_id);

  outfile = NULL;

  /*
   * g_get_user_config_dir() is guaranteed to return an existing directory.
   * Eventually, if SM stays with the WM, I'd like to make this
   * something like <config>/window_placement in a standard format.
   * Future optimisers should note also that by the time we get here
   * we probably already have full_save_path figured out and therefore
   * can just use the directory name from that.
   */
  muffin_dir = g_strconcat (g_get_user_config_dir (),
                              G_DIR_SEPARATOR_S "muffin",
                              NULL);

  session_dir = g_strconcat (muffin_dir,
                             G_DIR_SEPARATOR_S "sessions",
                             NULL);

  if (mkdir (muffin_dir, 0700) < 0 &&
      errno != EEXIST)
    {
      meta_warning ("Could not create directory '%s': %s\n",
                    muffin_dir, g_strerror (errno));
    }

  if (mkdir (session_dir, 0700) < 0 &&
      errno != EEXIST)
    {
      meta_warning ("Could not create directory '%s': %s\n",
                    session_dir, g_strerror (errno));
    }

  meta_topic (META_DEBUG_SM, "Saving session to '%s'\n", full_save_file ());

  outfile = fopen (full_save_file (), "w");

  if (outfile == NULL)
    {
      meta_warning ("Could not open session file '%s' for writing: %s\n",
                    full_save_file (), g_strerror (errno));
      goto out;
    }

  /* The file format is:
   * <muffin_session id="foo">
   *   <window id="bar" class="XTerm" name="xterm" title="/foo/bar" role="blah" type="normal" stacking="5">
   *     <workspace index="2"/>
   *     <workspace index="4"/>
   *     <sticky/> <minimized/> <maximized/>
   *     <geometry x="100" y="100" width="200" height="200" gravity="northwest"/>
   *   </window>
   * </muffin_session>
   *
   * Note that attributes on <window> are the match info we use to
   * see if the saved state applies to a restored window, and
   * child elements are the saved state to be applied.
   *
   */

  fprintf (outfile, "<muffin_session id=\"%s\">\n",
           client_id);

  windows = meta_display_list_windows (meta_get_display (), META_LIST_DEFAULT);
  stack_position = 0;

  windows = g_slist_sort (windows, meta_display_stack_cmp);
  tmp = windows;
  stack_position = 0;

  while (tmp != NULL)
    {
      MetaWindow *window;

      window = tmp->data;

      if (window->sm_client_id)
        {
          char *sm_client_id;
          char *res_class;
          char *res_name;
          char *role;
          char *title;

          /* client id, class, name, role are not expected to be
           * in UTF-8 (I think they are in XPCS which is Latin-1?
           * in practice they are always ascii though.)
           */

          sm_client_id = encode_text_as_utf8_markup (window->sm_client_id);
          res_class = window->res_class ?
            encode_text_as_utf8_markup (window->res_class) : NULL;
          res_name = window->res_name ?
            encode_text_as_utf8_markup (window->res_name) : NULL;
          role = window->role ?
            encode_text_as_utf8_markup (window->role) : NULL;
          if (window->title)
            title = g_markup_escape_text (window->title, -1);
          else
            title = NULL;

          meta_topic (META_DEBUG_SM, "Saving session managed window %s, client ID '%s'\n",
                      window->desc, window->sm_client_id);

          fprintf (outfile,
                   "  <window id=\"%s\" class=\"%s\" name=\"%s\" title=\"%s\" role=\"%s\" type=\"%s\" stacking=\"%d\">\n",
                   sm_client_id,
                   res_class ? res_class : "",
                   res_name ? res_name : "",
                   title ? title : "",
                   role ? role : "",
                   window_type_to_string (window->type),
                   stack_position);

          g_free (sm_client_id);
          g_free (res_class);
          g_free (res_name);
          g_free (role);
          g_free (title);

          /* Sticky */
          if (window->on_all_workspaces_requested)
            {
              fputs ("    <sticky/>\n", outfile);
            } else {
              int n;
              if (window->workspace)
                n = meta_workspace_index (window->workspace);
              else
                n = window->initial_workspace;
              fprintf (outfile,
                       "    <workspace index=\"%d\"/>\n", n);
            }


          /* Minimized */
          if (window->minimized)
            fputs ("    <minimized/>\n", outfile);

          /* Maximized */
          if (META_WINDOW_MAXIMIZED (window))
            {
              fprintf (outfile,
                       "    <maximized saved_x=\"%d\" saved_y=\"%d\" saved_width=\"%d\" saved_height=\"%d\"/>\n",
                       window->saved_rect.x,
                       window->saved_rect.y,
                       window->saved_rect.width,
                       window->saved_rect.height);
            }

          /* Gravity */
          {
            int x, y, w, h;
            meta_window_get_session_geometry (window, &x, &y, &w, &h);

            fprintf (outfile,
                     "    <geometry x=\"%d\" y=\"%d\" width=\"%d\" height=\"%d\" gravity=\"%s\"/>\n",
                     x, y, w, h,
                     meta_gravity_to_string (window->size_hints.win_gravity));
          }

          fputs ("  </window>\n", outfile);
        }
      else
        {
          meta_topic (META_DEBUG_SM, "Not saving window '%s', not session managed\n",
                      window->desc);
        }

      tmp = tmp->next;
      ++stack_position;
    }

  g_slist_free (windows);

  fputs ("</muffin_session>\n", outfile);

 out:
  if (outfile)
    {
      /* FIXME need a dialog for this */
      if (ferror (outfile))
        {
          meta_warning ("Error writing session file '%s': %s\n",
                        full_save_file (), g_strerror (errno));
        }
      if (fclose (outfile))
        {
          meta_warning ("Error closing session file '%s': %s\n",
                        full_save_file (), g_strerror (errno));
        }
    }

  g_free (muffin_dir);
  g_free (session_dir);
}

typedef enum
{
  WINDOW_TAG_NONE,
  WINDOW_TAC_DESKTOP,
  WINDOW_TAG_STICKY,
  WINDOW_TAG_MINIMIZED,
  WINDOW_TAG_MAXIMIZED,
  WINDOW_TAG_GEOMETRY
} WindowTag;

typedef struct
{
  MetaWindowSessionInfo *info;
  char *previous_id;
} ParseData;

static void                   session_info_free (MetaWindowSessionInfo *info);
static MetaWindowSessionInfo* session_info_new  (void);

static void start_element_handler (GMarkupParseContext  *context,
                                   const gchar          *element_name,
                                   const gchar         **attribute_names,
                                   const gchar         **attribute_values,
                                   gpointer              user_data,
                                   GError              **error);
static void end_element_handler   (GMarkupParseContext  *context,
                                   const gchar          *element_name,
                                   gpointer              user_data,
                                   GError              **error);
static void text_handler          (GMarkupParseContext  *context,
                                   const gchar          *text,
                                   gsize                 text_len,
                                   gpointer              user_data,
                                   GError              **error);

static GMarkupParser muffin_session_parser = {
  start_element_handler,
  end_element_handler,
  text_handler,
  NULL,
  NULL
};

static GSList *window_info_list = NULL;

static char*
load_state (const char *previous_save_file)
{
  GMarkupParseContext *context;
  GError *error;
  ParseData parse_data;
  char *text;
  gsize length;
  char *session_file;

  session_file = g_strconcat (g_get_user_config_dir (),
                              G_DIR_SEPARATOR_S "muffin"
                              G_DIR_SEPARATOR_S "sessions" G_DIR_SEPARATOR_S,
                              previous_save_file,
                              NULL);

  error = NULL;
  if (!g_file_get_contents (session_file,
                            &text,
                            &length,
                            &error))
    {
      char *canonical_session_file = session_file;

      /* Maybe they were doing it the old way, with ~/.muffin */
      session_file = g_strconcat (g_get_home_dir (),
                                  G_DIR_SEPARATOR_S ".muffin"
                                  G_DIR_SEPARATOR_S "sessions"
                                  G_DIR_SEPARATOR_S,
                                  previous_save_file,
                                  NULL);

      if (!g_file_get_contents (session_file,
                                &text,
                                &length,
                                NULL))
        {
          /* oh, just give up */

          g_error_free (error);
          g_free (session_file);
          g_free (canonical_session_file);
          return NULL;
        }

      g_free (canonical_session_file);
    }

  meta_topic (META_DEBUG_SM, "Parsing saved session file %s\n", session_file);
  g_free (session_file);
  session_file = NULL;

  parse_data.info = NULL;
  parse_data.previous_id = NULL;

  context = g_markup_parse_context_new (&muffin_session_parser,
                                        0, &parse_data, NULL);

  error = NULL;
  if (!g_markup_parse_context_parse (context,
                                     text,
                                     length,
                                     &error))
    goto error;


  error = NULL;
  if (!g_markup_parse_context_end_parse (context, &error))
    goto error;

  g_markup_parse_context_free (context);

  goto out;

 error:

  meta_warning ("Failed to parse saved session file: %s\n",
                error->message);
  g_error_free (error);

  if (parse_data.info)
    session_info_free (parse_data.info);

  g_free (parse_data.previous_id);
  parse_data.previous_id = NULL;

 out:

  g_free (text);

  return parse_data.previous_id;
}

/* FIXME this isn't very robust against bogus session files */
static void
start_element_handler  (GMarkupParseContext *context,
                        const gchar         *element_name,
                        const gchar        **attribute_names,
                        const gchar        **attribute_values,
                        gpointer             user_data,
                        GError             **error)
{
  ParseData *pd;

  pd = user_data;

  if (strcmp (element_name, "muffin_session") == 0)
    {
      /* Get previous ID */
      int i;

      i = 0;
      while (attribute_names[i])
        {
          const char *name;
          const char *val;

          name = attribute_names[i];
          val = attribute_values[i];

          if (pd->previous_id)
            {
              g_set_error (error,
                           G_MARKUP_ERROR,
                       G_MARKUP_ERROR_PARSE,
                           "<muffin_session> attribute seen but we already have the session ID");
              return;
            }

          if (strcmp (name, "id") == 0)
            {
              pd->previous_id = decode_text_from_utf8 (val);
            }
          else
            {
              g_set_error (error,
                           G_MARKUP_ERROR,
                           G_MARKUP_ERROR_UNKNOWN_ATTRIBUTE,
                           "Unknown attribute %s on <%s> element",
                           name, "muffin_session");
              return;
            }

          ++i;
        }
    }
  else if (strcmp (element_name, "window") == 0)
    {
      int i;

      if (pd->info)
        {
          g_set_error (error,
                       G_MARKUP_ERROR,
                       G_MARKUP_ERROR_PARSE,
                       "nested <window> tag");
          return;
        }

      pd->info = session_info_new ();

      i = 0;
      while (attribute_names[i])
        {
          const char *name;
          const char *val;

          name = attribute_names[i];
          val = attribute_values[i];

          if (strcmp (name, "id") == 0)
            {
              if (*val)
                pd->info->id = decode_text_from_utf8 (val);
            }
          else if (strcmp (name, "class") == 0)
            {
              if (*val)
                pd->info->res_class = decode_text_from_utf8 (val);
            }
          else if (strcmp (name, "name") == 0)
            {
              if (*val)
                pd->info->res_name = decode_text_from_utf8 (val);
            }
          else if (strcmp (name, "title") == 0)
            {
              if (*val)
                pd->info->title = g_strdup (val);
            }
          else if (strcmp (name, "role") == 0)
            {
              if (*val)
                pd->info->role = decode_text_from_utf8 (val);
            }
          else if (strcmp (name, "type") == 0)
            {
              if (*val)
                pd->info->type = window_type_from_string (val);
            }
          else if (strcmp (name, "stacking") == 0)
            {
              if (*val)
                {
                  pd->info->stack_position = atoi (val);
                  pd->info->stack_position_set = TRUE;
                }
            }
          else
            {
              g_set_error (error,
                           G_MARKUP_ERROR,
                           G_MARKUP_ERROR_UNKNOWN_ATTRIBUTE,
                           "Unknown attribute %s on <%s> element",
                           name, "window");
              session_info_free (pd->info);
              pd->info = NULL;
              return;
            }

          ++i;
        }
    }
  else if (strcmp (element_name, "workspace") == 0)
    {
      int i;

      i = 0;
      while (attribute_names[i])
        {
          const char *name;

          name = attribute_names[i];

          if (strcmp (name, "index") == 0)
            {
              pd->info->workspace_indices =
                g_slist_prepend (pd->info->workspace_indices,
                                 GINT_TO_POINTER (atoi (attribute_values[i])));
            }
          else
            {
              g_set_error (error,
                           G_MARKUP_ERROR,
                           G_MARKUP_ERROR_UNKNOWN_ATTRIBUTE,
                           "Unknown attribute %s on <%s> element",
                           name, "window");
              session_info_free (pd->info);
              pd->info = NULL;
              return;
            }

          ++i;
        }
    }
  else if (strcmp (element_name, "sticky") == 0)
    {
      pd->info->on_all_workspaces = TRUE;
      pd->info->on_all_workspaces_set = TRUE;
    }
  else if (strcmp (element_name, "minimized") == 0)
    {
      pd->info->minimized = TRUE;
      pd->info->minimized_set = TRUE;
    }
  else if (strcmp (element_name, "maximized") == 0)
    {
      int i;

      i = 0;
      pd->info->maximized = TRUE;
      pd->info->maximized_set = TRUE;
      while (attribute_names[i])
        {
          const char *name;
          const char *val;

          name = attribute_names[i];
          val = attribute_values[i];

          if (strcmp (name, "saved_x") == 0)
            {
              if (*val)
                {
                  pd->info->saved_rect.x = atoi (val);
                  pd->info->saved_rect_set = TRUE;
                }
            }
          else if (strcmp (name, "saved_y") == 0)
            {
              if (*val)
                {
                  pd->info->saved_rect.y = atoi (val);
                  pd->info->saved_rect_set = TRUE;
                }
            }
          else if (strcmp (name, "saved_width") == 0)
            {
              if (*val)
                {
                  pd->info->saved_rect.width = atoi (val);
                  pd->info->saved_rect_set = TRUE;
                }
            }
          else if (strcmp (name, "saved_height") == 0)
            {
              if (*val)
                {
                  pd->info->saved_rect.height = atoi (val);
                  pd->info->saved_rect_set = TRUE;
                }
            }
          else
            {
              g_set_error (error,
                           G_MARKUP_ERROR,
                           G_MARKUP_ERROR_UNKNOWN_ATTRIBUTE,
                           "Unknown attribute %s on <%s> element",
                           name, "maximized");
              return;
            }

          ++i;
        }

      if (pd->info->saved_rect_set)
        meta_topic (META_DEBUG_SM, "Saved unmaximized size %d,%d %dx%d \n",
                    pd->info->saved_rect.x,
                    pd->info->saved_rect.y,
                    pd->info->saved_rect.width,
                    pd->info->saved_rect.height);
    }
  else if (strcmp (element_name, "geometry") == 0)
    {
      int i;

      pd->info->geometry_set = TRUE;

      i = 0;
      while (attribute_names[i])
        {
          const char *name;
          const char *val;

          name = attribute_names[i];
          val = attribute_values[i];

          if (strcmp (name, "x") == 0)
            {
              if (*val)
                pd->info->rect.x = atoi (val);
            }
          else if (strcmp (name, "y") == 0)
            {
              if (*val)
                pd->info->rect.y = atoi (val);
            }
          else if (strcmp (name, "width") == 0)
            {
              if (*val)
                pd->info->rect.width = atoi (val);
            }
          else if (strcmp (name, "height") == 0)
            {
              if (*val)
                pd->info->rect.height = atoi (val);
            }
          else if (strcmp (name, "gravity") == 0)
            {
              if (*val)
                pd->info->gravity = window_gravity_from_string (val);
            }
          else
            {
              g_set_error (error,
                           G_MARKUP_ERROR,
                           G_MARKUP_ERROR_UNKNOWN_ATTRIBUTE,
                           "Unknown attribute %s on <%s> element",
                           name, "geometry");
              return;
            }

          ++i;
        }

      meta_topic (META_DEBUG_SM, "Loaded geometry %d,%d %dx%d gravity %s\n",
                  pd->info->rect.x,
                  pd->info->rect.y,
                  pd->info->rect.width,
                  pd->info->rect.height,
                  meta_gravity_to_string (pd->info->gravity));
    }
  else
    {
      g_set_error (error,
                   G_MARKUP_ERROR,
                   G_MARKUP_ERROR_UNKNOWN_ELEMENT,
                   "Unknown element %s",
                   element_name);
      return;
    }
}

static void
end_element_handler    (GMarkupParseContext *context,
                        const gchar         *element_name,
                        gpointer             user_data,
                        GError             **error)
{
  ParseData *pd;

  pd = user_data;

  if (strcmp (element_name, "window") == 0)
    {
      g_assert (pd->info);

      window_info_list = g_slist_prepend (window_info_list,
                                          pd->info);

      meta_topic (META_DEBUG_SM, "Loaded window info from session with class: %s name: %s role: %s\n",
                  pd->info->res_class ? pd->info->res_class : "(none)",
                  pd->info->res_name ? pd->info->res_name : "(none)",
                  pd->info->role ? pd->info->role : "(none)");

      pd->info = NULL;
    }
}

static void
text_handler           (GMarkupParseContext *context,
                        const gchar         *text,
                        gsize                text_len,
                        gpointer             user_data,
                        GError             **error)
{
  /* Right now we don't have any elements where we care about their
   * content
   */
}

static gboolean
both_null_or_matching (const char *a,
                       const char *b)
{
  if (a == NULL && b == NULL)
    return TRUE;
  else if (a && b && strcmp (a, b) == 0)
    return TRUE;
  else
    return FALSE;
}

static GSList*
get_possible_matches (MetaWindow *window)
{
  /* Get all windows with this client ID */
  GSList *retval;
  GSList *tmp;
  gboolean ignore_client_id;

  retval = NULL;

  ignore_client_id = g_getenv ("MUFFIN_DEBUG_SM") != NULL;

  tmp = window_info_list;
  while (tmp != NULL)
    {
      MetaWindowSessionInfo *info;

      info = tmp->data;

      if ((ignore_client_id ||
           both_null_or_matching (info->id, window->sm_client_id)) &&
          both_null_or_matching (info->res_class, window->res_class) &&
          both_null_or_matching (info->res_name, window->res_name) &&
          both_null_or_matching (info->role, window->role))
        {
          meta_topic (META_DEBUG_SM, "Window %s may match saved window with class: %s name: %s role: %s\n",
                      window->desc,
                      info->res_class ? info->res_class : "(none)",
                      info->res_name ? info->res_name : "(none)",
                      info->role ? info->role : "(none)");

          retval = g_slist_prepend (retval, info);
        }
      else
        {
          if (meta_is_verbose ())
            {
              if (!both_null_or_matching (info->id, window->sm_client_id))
                meta_topic (META_DEBUG_SM, "Window %s has SM client ID %s, saved state has %s, no match\n",
                            window->desc,
                            window->sm_client_id ? window->sm_client_id : "(none)",
                            info->id ? info->id : "(none)");
              else if (!both_null_or_matching (info->res_class, window->res_class))
                meta_topic (META_DEBUG_SM, "Window %s has class %s doesn't match saved class %s, no match\n",
                            window->desc,
                            window->res_class ? window->res_class : "(none)",
                            info->res_class ? info->res_class : "(none)");

              else if (!both_null_or_matching (info->res_name, window->res_name))
                meta_topic (META_DEBUG_SM, "Window %s has name %s doesn't match saved name %s, no match\n",
                            window->desc,
                            window->res_name ? window->res_name : "(none)",
                            info->res_name ? info->res_name : "(none)");
              else if (!both_null_or_matching (info->role, window->role))
                meta_topic (META_DEBUG_SM, "Window %s has role %s doesn't match saved role %s, no match\n",
                            window->desc,
                            window->role ? window->role : "(none)",
                            info->role ? info->role : "(none)");
              else
                meta_topic (META_DEBUG_SM, "???? should not happen - window %s doesn't match saved state %s for no good reason\n",
                            window->desc, info->id);
            }
        }

      tmp = tmp->next;
    }

  return retval;
}

static const MetaWindowSessionInfo*
find_best_match (GSList     *infos,
                 MetaWindow *window)
{
  GSList *tmp;
  const MetaWindowSessionInfo *matching_title;
  const MetaWindowSessionInfo *matching_type;

  matching_title = NULL;
  matching_type = NULL;

  tmp = infos;
  while (tmp != NULL)
    {
      MetaWindowSessionInfo *info;

      info = tmp->data;

      if (matching_title == NULL &&
          both_null_or_matching (info->title, window->title))
        matching_title = info;

      if (matching_type == NULL &&
          info->type == window->type)
        matching_type = info;

      tmp = tmp->next;
    }

  /* Prefer same title, then same type of window, then
   * just pick something. Eventually we could enhance this
   * to e.g. break ties by geometry hint similarity,
   * or other window features.
   */

  if (matching_title)
    return matching_title;
  else if (matching_type)
    return matching_type;
  else
    return infos->data;
}

const MetaWindowSessionInfo*
meta_window_lookup_saved_state (MetaWindow *window)
{
  GSList *possibles;
  const MetaWindowSessionInfo *info;

  /* Window is not session managed.
   * I haven't yet figured out how to deal with these
   * in a way that doesn't cause broken side effects in
   * situations other than on session restore.
   */
  if (window->sm_client_id == NULL)
    {
      meta_topic (META_DEBUG_SM,
                  "Window %s is not session managed, not checking for saved state\n",
                  window->desc);
      return NULL;
    }

  possibles = get_possible_matches (window);

  if (possibles == NULL)
    {
      meta_topic (META_DEBUG_SM, "Window %s has no possible matches in the list of saved window states\n",
                  window->desc);
      return NULL;
    }

  info = find_best_match (possibles, window);

  g_slist_free (possibles);

  return info;
}

void
meta_window_release_saved_state (const MetaWindowSessionInfo *info)
{
  /* We don't want to use the same saved state again for another
   * window.
   */
  window_info_list = g_slist_remove (window_info_list, info);

  session_info_free ((MetaWindowSessionInfo*) info);
}

static void
session_info_free (MetaWindowSessionInfo *info)
{
  g_free (info->id);
  g_free (info->res_class);
  g_free (info->res_name);
  g_free (info->title);
  g_free (info->role);

  g_slist_free (info->workspace_indices);

  g_free (info);
}

static MetaWindowSessionInfo*
session_info_new (void)
{
  MetaWindowSessionInfo *info;

  info = g_new0 (MetaWindowSessionInfo, 1);

  info->type = META_WINDOW_NORMAL;
  info->gravity = META_GRAVITY_NORTH_WEST;

  return info;
}

static char* full_save_path = NULL;

static void
regenerate_save_file (void)
{
  g_free (full_save_path);

  if (client_id)
    full_save_path = g_strconcat (g_get_user_config_dir (),
                                  G_DIR_SEPARATOR_S "muffin"
                                  G_DIR_SEPARATOR_S "sessions" G_DIR_SEPARATOR_S,
                                  client_id,
                                  ".ms",
                                  NULL);
  else
    full_save_path = NULL;
}

static const char*
full_save_file (void)
{
  return full_save_path;
}

static int
windows_cmp_by_title (MetaWindow *a,
                      MetaWindow *b)
{
  return g_utf8_collate (a->title, b->title);
}

static void
finish_interact (gboolean shutdown)
{
  if (current_state == STATE_DONE_WITH_INTERACT) /* paranoia */
    {
      SmcInteractDone (session_connection, False /* don't cancel logout */);

      save_yourself_possibly_done (shutdown, TRUE);
    }
}

static void
dialog_closed (GPid pid, int status, gpointer user_data)
{
  gboolean shutdown = GPOINTER_TO_INT (user_data);

  if (WIFEXITED (status) && WEXITSTATUS (status) == 0) /* pressed "OK" */
    {
      finish_interact (shutdown);
    }
}

static void
warn_about_lame_clients_and_finish_interact (gboolean shutdown)
{
  GSList *lame = NULL;
  GSList *windows;
  GSList *lame_details = NULL;
  GSList *tmp;
  GSList *columns = NULL;
  GPid pid;

  windows = meta_display_list_windows (meta_get_display (), META_LIST_DEFAULT);
  tmp = windows;
  while (tmp != NULL)
    {
      MetaWindow *window;

      window = tmp->data;

      /* only complain about normal windows, the others
       * are kind of dumb to worry about
       */
      if (window->sm_client_id == NULL &&
          window->type == META_WINDOW_NORMAL)
        lame = g_slist_prepend (lame, window);

      tmp = tmp->next;
    }

  g_slist_free (windows);

  if (lame == NULL)
    {
      /* No lame apps. */
      finish_interact (shutdown);
      return;
    }

  columns = g_slist_prepend (columns, (gpointer)"Window");
  columns = g_slist_prepend (columns, (gpointer)"Class");

  lame = g_slist_sort (lame, (GCompareFunc) windows_cmp_by_title);

  tmp = lame;
  while (tmp != NULL)
    {
      MetaWindow *w = tmp->data;

      lame_details = g_slist_prepend (lame_details,
                                      w->res_class ? w->res_class : (gpointer)"");
      lame_details = g_slist_prepend (lame_details,
                                      w->title);

      tmp = tmp->next;
    }
  g_slist_free (lame);

  pid = meta_show_dialog("--list",
                         _("These windows do not support “save current setup” "
                           "and will have to be restarted manually next time "
                           "you log in."),
                         "240",
                         meta_get_display()->x11_display->screen_name,
                         NULL, NULL,
                         None,
                         columns,
                         lame_details);

  g_slist_free (lame_details);

  g_child_watch_add (pid, dialog_closed, GINT_TO_POINTER (shutdown));
}

#endif /* HAVE_SM */
