/* ide-session.c
 *
 * Copyright 2018-2019 Christian Hergert <chergert@redhat.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#define G_LOG_DOMAIN "ide-session"

#include "config.h"

#include <libpeas/peas.h>
#include <libide-plugins.h>
#include <libide-gui.h>
#include <libide-threading.h>

#include "ide-session-addin.h"
#include "ide-session-private.h"

#include "ide-gui-private.h"

struct _IdeSession
{
  IdeObject               parent_instance;
  GPtrArray              *addins;
};

typedef struct
{
  GPtrArray      *addins;
  GVariantBuilder pages_state;
  guint           active;
  IdeGrid        *grid;
} Save;

typedef struct
{
  GPtrArray *addins;
  GVariant  *state;
  IdeGrid   *grid;
  GArray    *pages;
  guint      active;
} Restore;

typedef struct
{
  guint            column;
  guint            row;
  guint            depth;
  IdeSessionAddin *addin;
  GVariant        *state;
  IdePage         *restored_page;
} RestoreItem;

G_DEFINE_FINAL_TYPE (IdeSession, ide_session, IDE_TYPE_OBJECT)

static void
restore_free (Restore *r)
{
  g_assert (r != NULL);
  g_assert (r->active == 0);

  g_clear_pointer (&r->state, g_variant_unref);
  g_clear_pointer (&r->pages, g_array_unref);

  g_slice_free (Restore, r);
}

static void
save_free (Save *s)
{
  g_assert (s != NULL);
  g_assert (s->active == 0);

  g_slice_free (Save, s);
}

static void
restore_item_clear (RestoreItem *item)
{
  g_assert (item != NULL);

  g_clear_pointer (&item->state, g_variant_unref);
}

static gint
compare_restore_items (gconstpointer a,
                       gconstpointer b)
{
  const RestoreItem *item_a = a;
  const RestoreItem *item_b = b;
  gint ret;

  if (!(ret = item_a->column - item_b->column))
    {
      if (!(ret = item_a->row - item_b->row))
        ret = item_a->depth - item_b->depth;
    }

  return ret;
}

static void
collect_addins_cb (IdeExtensionSetAdapter *set,
                   PeasPluginInfo         *plugin_info,
                   PeasExtension          *exten,
                   gpointer                user_data)
{
  GPtrArray *ar = user_data;

  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
  g_assert (plugin_info != NULL);
  g_assert (IDE_IS_SESSION_ADDIN (exten));
  g_assert (ar != NULL);

  g_ptr_array_add (ar, g_object_ref (exten));
}

static IdeSessionAddin *
find_suitable_addin_for_page (IdePage   *page,
                              GPtrArray *addins)
{
  for (guint i = 0; i < addins->len; i++)
    {
      IdeSessionAddin *addin = g_ptr_array_index (addins, i);
      if (ide_session_addin_can_save_page (addin, page))
        return addin;
    }
  return NULL;
}

static void
on_session_autosaved_cb (GObject      *object,
                         GAsyncResult *result,
                         gpointer      user_data)
{
  IdeSession *session = (IdeSession *)object;
  g_autoptr(GError) error = NULL;

  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (IDE_IS_SESSION (session));
  g_assert (G_IS_ASYNC_RESULT (result));

  if (!ide_session_save_finish (session, result, &error))
    g_warning ("Couldn't autosave session: %s", error->message);
}

typedef struct {
  IdeSession *session;
  IdeGrid    *grid;
  guint       session_autosave_source;
} AutosaveGrid;

static void
autosave_grid_free (gpointer data,
                    GClosure *closure)
{
  AutosaveGrid *self = (AutosaveGrid *)data;

  if (self->session_autosave_source)
    {
      g_source_remove (self->session_autosave_source);
      self->session_autosave_source = 0;
    }
  g_slice_free (AutosaveGrid, self);
}

static gboolean
on_session_autosave_timeout_cb (gpointer user_data)
{
  AutosaveGrid *autosave_grid = (AutosaveGrid *)user_data;

  g_assert (IDE_IS_SESSION (autosave_grid->session));
  g_assert (IDE_IS_GRID (autosave_grid->grid));

  ide_session_save_async (autosave_grid->session,
                          autosave_grid->grid,
                          NULL,
                          on_session_autosaved_cb,
                          NULL);

  autosave_grid->session_autosave_source = 0;

  return G_SOURCE_REMOVE;
}

static void
schedule_session_autosave_timeout (AutosaveGrid *autosave_grid)
{
  if (!autosave_grid->session_autosave_source)
    {
      /* We don't want to be saving the state on each (small) change, so introduce a small
       * timeout so changes are grouped when saving.
       */
      autosave_grid->session_autosave_source =
        g_timeout_add_seconds (30,
                               on_session_autosave_timeout_cb,
                               autosave_grid);
    }
}

static void
on_autosave_property_changed_cb (GObject    *gobject,
                                 GParamSpec *pspec,
                                 gpointer    user_data)
{
  schedule_session_autosave_timeout ((AutosaveGrid *)user_data);
}

static void
watch_pages_session_autosave (AutosaveGrid *autosave_grid,
                              guint         start_pos,
                              guint         end_pos)
{
  GListModel *list = (GListModel *)autosave_grid->grid;
  IdeSession *session = (IdeSession *)autosave_grid->session;

  g_assert (IDE_IS_SESSION (session));
  g_assert (G_IS_LIST_MODEL (list));
  g_assert (g_type_is_a (g_list_model_get_item_type (list), IDE_TYPE_PAGE));
  g_assert (start_pos <= end_pos);

  for (guint i = start_pos; i < end_pos; i++)
    {
      IdePage *page = IDE_PAGE (g_list_model_get_object (list, i));
      IdeSessionAddin *addin;
      g_auto(GStrv) props = NULL;

      if ((addin = find_suitable_addin_for_page (page, session->addins)) &&
          (props = ide_session_addin_get_autosave_properties (addin)))
        {
          for (guint j = 0; props[j] != NULL; j++)
            {
              char detailed_signal[256];
              g_snprintf (detailed_signal, sizeof detailed_signal, "notify::%s", props[j]);

              g_signal_connect (page, detailed_signal, G_CALLBACK (on_autosave_property_changed_cb), autosave_grid);
            }
        }
    }
}

static void
on_grid_items_changed_cb (GListModel *list,
                          guint       position,
                          guint       removed,
                          guint       added,
                          gpointer    user_data)
{
  AutosaveGrid *autosave_grid = (AutosaveGrid *)user_data;

  g_assert (G_IS_LIST_MODEL (list));
  g_assert (g_type_is_a (g_list_model_get_item_type (list), IDE_TYPE_PAGE));

  /* We've nothing to do when no page were added here as signals are
   * automatically disconnected, so avoid extra work by stopping here early.
   */
  if (added > 0)
    watch_pages_session_autosave (autosave_grid, position, position + added);

  /* Handles autosaving both when closing/opening a page and when moving a page in the grid. */
  schedule_session_autosave_timeout (autosave_grid);
}

static void
ide_session_destroy (IdeObject *object)
{
  IdeSession *self = (IdeSession *)object;

  IDE_ENTRY;

  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (IDE_IS_SESSION (self));

  g_clear_pointer (&self->addins, g_ptr_array_unref);

  IDE_OBJECT_CLASS (ide_session_parent_class)->destroy (object);

  IDE_EXIT;
}

static void
ide_session_parent_set (IdeObject *object,
                        IdeObject *parent)
{
  IdeSession *self = (IdeSession *)object;
  g_autoptr(IdeExtensionSetAdapter) extension_set = NULL;

  g_assert (IDE_IS_MAIN_THREAD ());
  g_assert (IDE_IS_SESSION (self));
  g_assert (!parent || IDE_IS_OBJECT (parent));

  if (parent == NULL)
    return;

  extension_set = ide_extension_set_adapter_new (IDE_OBJECT (self),
                                                 peas_engine_get_default (),
                                                 IDE_TYPE_SESSION_ADDIN,
                                                 NULL, NULL);

  self->addins = g_ptr_array_new_with_free_func (g_object_unref);
  ide_extension_set_adapter_foreach (extension_set, collect_addins_cb, self->addins);
}

static void
ide_session_class_init (IdeSessionClass *klass)
{
  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);

  i_object_class->destroy = ide_session_destroy;
  i_object_class->parent_set = ide_session_parent_set;
}

static void
ide_session_init (IdeSession *self)
{
}

static void
restore_pages_to_grid (GArray  *r_items,
                       IdeGrid *grid)
{
  IDE_ENTRY;
  for (guint i = 0; i < r_items->len; i++)
    {
      RestoreItem *item = &g_array_index (r_items, RestoreItem, i);
      IdeGridColumn *column;
      IdeFrame *stack;

      /* Ignore pages that couldn't be restored. */
      if (item->restored_page == NULL)
        continue;

      /* This relies on the fact that the items are sorted. */
      column = ide_grid_get_nth_column (grid, item->column);
      stack = _ide_grid_get_nth_stack_for_column (grid, column, item->row);

      gtk_container_add (GTK_CONTAINER (stack),
                         GTK_WIDGET (item->restored_page));
    }
  IDE_EXIT;
}

typedef struct
{
  IdeTask     *task;
  RestoreItem *item;
} RestorePage;

static void
on_session_addin_page_restored_cb (GObject      *object,
                                   GAsyncResult *result,
                                   gpointer      user_data)
{
  IdeSessionAddin *addin = (IdeSessionAddin *)object;
  RestorePage *r_page = user_data;
  g_autoptr(IdeTask) task = r_page->task;
  g_autoptr(GError) error = NULL;
  RestoreItem *item = r_page->item;
  Restore *r;

  IDE_ENTRY;

  g_assert (IDE_IS_SESSION_ADDIN (addin));
  g_assert (G_IS_ASYNC_RESULT (result));
  g_assert (IDE_IS_TASK (task));

  r = ide_task_get_task_data (task);

  g_assert (r != NULL);
  g_assert (r->addins != NULL);
  g_assert (r->active > 0);
  g_assert (r->state != NULL);

  if (!(item->restored_page = ide_session_addin_restore_page_finish (addin, result, &error)))
    g_warning ("Couldn't restore page with addin %s: %s", G_OBJECT_TYPE_NAME (addin), error->message);

  r->active--;

  if (r->active == 0)
    {
      restore_pages_to_grid (r->pages, r->grid);

      ide_task_return_boolean (task, TRUE);
    }

  IDE_EXIT;
}

static IdeSessionAddin *
get_addin_for_name (GPtrArray  *addins,
                    const char *addin_name)
{
  GType addin_type = g_type_from_name (addin_name);
  for (guint i = 0; i < addins->len; i++)
    {
      if (G_OBJECT_TYPE (addins->pdata[i]) == addin_type)
        return addins->pdata[i];
    }
  return NULL;
}

static void
load_restore_items (Restore *r,
                    GArray  *items)
{
  GVariantIter iter;
  RestoreItem item;
  GVariant *page_state = NULL;

  g_assert (r != NULL);
  g_assert (r->state != NULL);
  g_assert (r->addins != NULL);
  g_assert (items != NULL);

  g_variant_iter_init (&iter, r->state);
  while ((page_state = g_variant_iter_next_value (&iter)))
    {
      const char *addin_name = NULL;

      g_variant_lookup (page_state, "column", "u", &item.column);
      g_variant_lookup (page_state, "row", "u", &item.row);
      g_variant_lookup (page_state, "depth", "u", &item.depth);
      g_variant_lookup (page_state, "addin_name", "&s", &addin_name);
      g_variant_lookup (page_state, "addin_page_state", "v", &item.state);

      item.addin = get_addin_for_name (r->addins, addin_name);
      g_array_append_val (items, item);

      g_variant_unref (page_state);
    }
}

static GVariant *
migrate_pre_api_rework (GVariant *pages_variant)
{
  GVariantIter iter;
  const char *uri = NULL;
  int column, row, depth;
  /* Freed in the loop. */
  GVariant *search_variant;

  GVariantDict version_wrapper_dict;
  GVariantBuilder addins_states;

  g_variant_dict_init (&version_wrapper_dict, NULL);
  /* Migrate old format to first version of the new format. */
  g_variant_dict_insert (&version_wrapper_dict, "version", "u", (guint32) 1);

  g_variant_builder_init (&addins_states, G_VARIANT_TYPE ("aa{sv}"));

  g_debug ("Handling migration of the project's session.gvariant, from prior to the Session API rework…");

  g_variant_iter_init (&iter, pages_variant);
  while (g_variant_iter_next (&iter, "(&siiiv)", &uri, &column, &row, &depth, &search_variant))
    {
      GVariantDict addin_state;
      GVariantDict editor_session_state;

      g_variant_dict_init (&addin_state, NULL);
      g_variant_dict_insert (&addin_state, "column", "u", (guint32) column);
      g_variant_dict_insert (&addin_state, "row", "u", (guint32) row);
      g_variant_dict_insert (&addin_state, "depth", "u", (guint32) depth);
      g_variant_dict_insert (&addin_state, "addin_name", "s", "GbpEditorSessionAddin");

      /* Since we need to migrate the data for the new API, let's also migrate to a dictionary
       * instead of a tuple, for greater flexibility and extensibility in the future.
       */
      g_variant_dict_init (&editor_session_state, NULL);
      g_variant_dict_insert (&editor_session_state, "uri", "s", uri);
      /* Unbox the search_variant since we don't want to bother with multiple levels of variants,
       * just have an a{sv}
       */
      g_variant_dict_insert_value (&editor_session_state, "search", g_variant_get_variant (search_variant));
      g_variant_dict_insert (&addin_state, "addin_page_state", "v", g_variant_dict_end (&editor_session_state));
      g_variant_builder_add_value (&addins_states, g_variant_dict_end (&addin_state));

      g_variant_unref (search_variant);
    }

  g_variant_dict_insert_value (&version_wrapper_dict, "data", g_variant_builder_end (&addins_states));

  g_debug ("Successfully migrated old session.gvariant to new format.");

  return g_variant_take_ref (g_variant_dict_end (&version_wrapper_dict));
}

static GVariant *
load_state_with_migrations (GBytes *bytes)
{
  g_autoptr(GVariant) variant = NULL;
  /* This is the value of the "data" key in the final @variant. */
  g_autoptr(GVariant) migrated_state = NULL;
  GVariantDict state;
  gboolean fully_migrated = FALSE;
  GVariant *old_api_state = NULL;

  g_assert (bytes != NULL);

  variant = g_variant_take_ref (g_variant_new_from_bytes (G_VARIANT_TYPE_VARDICT, bytes, FALSE));

  if (!variant)
    {
      g_warning ("Couldn't load the array of pages' states from session.gvariant!");
      return NULL;
    }

  g_variant_dict_init (&state, variant);

  /* Handle migrations from prior to the Session API rework, where there was only GbpEditorSessionAddin that used it */
  old_api_state = g_variant_dict_lookup_value (&state, "GbpEditorSessionAddin", G_VARIANT_TYPE ("a(siiiv)"));
  if (old_api_state)
    migrated_state = migrate_pre_api_rework (old_api_state);
  else
    migrated_state = g_steal_pointer (&variant);

  while (!fully_migrated)
    {
      guint32 version;
      g_autoptr(GVariant) versioned_data = NULL;

      if (!g_variant_lookup (migrated_state, "version", "u", &version))
        {
          g_warning ("session.gvariant isn't using the old format but doesn't have a version field, so cannot load it!");
          fully_migrated = TRUE;
          migrated_state = NULL;
          break;
        }

      if (!(versioned_data = g_variant_lookup_value (migrated_state, "data", G_VARIANT_TYPE ("aa{sv}"))))
        {
          g_warning ("session.gvariant had a version field but the actual versioned data wasn't found, so cannot load it!");
          fully_migrated = TRUE;
          migrated_state = NULL;
          break;
        }

      switch (version)
        {
          /* It's the current format so the rest of the code understands it natively. */
          case 1:
            migrated_state = g_steal_pointer (&migrated_state);
            fully_migrated = TRUE;
            break;

          default:
            g_warning ("Version %d of session.gvariant data is not known to Builder!", version);
            migrated_state = NULL;
            fully_migrated = TRUE;
        }
    }

  if (migrated_state)
    /* The current format (version 1) is an `aa{sv}` (array of dictionaries) with the dict's keys being:
     * guint32 column, row, depth;
     * char *addin_name;
     * GVariant *addin_page_state;
     */
    return g_variant_lookup_value (migrated_state, "data", NULL);
  else
    return NULL;
}

static void
on_session_cache_loaded_cb (GObject      *object,
                            GAsyncResult *result,
                            gpointer      user_data)
{
  GFile *file = (GFile *)object;
  g_autoptr(IdeTask) task = user_data;
  g_autoptr(GError) error = NULL;
  g_autoptr(GBytes) bytes = NULL;
  GArray *items = NULL;
  GCancellable *cancellable;
  Restore *r;

  IDE_ENTRY;

  g_assert (G_IS_FILE (file));
  g_assert (G_IS_ASYNC_RESULT (result));
  g_assert (IDE_IS_TASK (task));

  r = ide_task_get_task_data (task);
  cancellable = ide_task_get_cancellable (task);

  g_assert (r != NULL);
  g_assert (r->addins != NULL);
  g_assert (r->addins->len > 0);
  g_assert (r->state == NULL);
  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));

  if (!(bytes = g_file_load_bytes_finish (file, result, NULL, &error)))
    {
      if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
        ide_task_return_boolean (task, TRUE);
      else
        ide_task_return_error (task, g_steal_pointer (&error));
      IDE_EXIT;
    }

  if (g_bytes_get_size (bytes) == 0)
    {
      ide_task_return_boolean (task, TRUE);
      IDE_EXIT;
    }

  r->state = load_state_with_migrations (bytes);

  if (r->state == NULL)
    {
      ide_task_return_new_error (task,
                                 G_IO_ERROR,
                                 G_IO_ERROR_INVALID_DATA,
                                 "Failed to decode session state");
      IDE_EXIT;
    }

  items = g_array_new (FALSE, FALSE, sizeof (RestoreItem));
  g_array_set_clear_func (items, (GDestroyNotify)restore_item_clear);
  load_restore_items (r, items);
  r->pages = items;
  r->active = items->len;
  g_array_sort (items, compare_restore_items);

  for (guint i = 0; i < items->len; i++)
    {
      RestoreItem *item = &g_array_index (items, RestoreItem, i);
      RestorePage *r_page = g_slice_new0 (RestorePage);
      r_page->task = g_object_ref (task);
      r_page->item = item;

      ide_session_addin_restore_page_async (item->addin,
                                            item->state,
                                            cancellable,
                                            on_session_addin_page_restored_cb,
                                            r_page);
    }

  if (r->active == 0)
    {
      ide_task_return_boolean (task, TRUE);
      IDE_EXIT;
    }

  IDE_EXIT;
}

/**
 * ide_session_restore_async:
 * @self: an #IdeSession
 * @grid: an #IdeGrid
 * @cancellable: (nullable): a #GCancellable or %NULL
 * @callback: the callback to execute upon completion
 * @user_data: user data for callback
 *
 * This function will asynchronously restore the state of the project to
 * the point it was last saved (typically upon shutdown). This includes
 * open documents and editor splits to the degree possible. Adding support
 * for a new page type requires implementing an #IdeSessionAddin.
 *
 * Since: 41
 */
void
ide_session_restore_async (IdeSession          *self,
                           IdeGrid             *grid,
                           GCancellable        *cancellable,
                           GAsyncReadyCallback  callback,
                           gpointer             user_data)
{
  g_autoptr(IdeTask) task = NULL;
  g_autoptr(GFile) file = NULL;
  g_autoptr(GSettings) settings = NULL;
  IdeContext *context;
  Restore *r;

  IDE_ENTRY;

  g_return_if_fail (IDE_IS_SESSION (self));
  g_return_if_fail (IDE_IS_GRID (grid));
  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));

  task = ide_task_new (self, cancellable, callback, user_data);
  ide_task_set_source_tag (task, ide_session_restore_async);

  r = g_slice_new0 (Restore);
  r->addins = self->addins;
  r->grid = grid;
  ide_task_set_task_data (task, r, restore_free);

  settings = g_settings_new ("org.gnome.builder");
  if (!g_settings_get_boolean (settings, "restore-previous-files"))
    {
      ide_task_return_boolean (task, TRUE);
      return;
    }

  context = ide_object_get_context (IDE_OBJECT (self));
  file = ide_context_cache_file (context, "session.gvariant", NULL);

  g_file_load_bytes_async (file,
                           cancellable,
                           on_session_cache_loaded_cb,
                           g_steal_pointer (&task));

  IDE_EXIT;
}

gboolean
ide_session_restore_finish (IdeSession    *self,
                            GAsyncResult  *result,
                            GError       **error)
{
  gboolean ret;
  Restore *r;
  GListModel *list;
  AutosaveGrid *autosave_grid;

  IDE_ENTRY;

  g_return_val_if_fail (IDE_IS_SESSION (self), FALSE);
  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);

  r = ide_task_get_task_data (IDE_TASK (result));
  g_assert (r != NULL);
  list = G_LIST_MODEL (r->grid);

  autosave_grid = g_slice_new0 (AutosaveGrid);
  autosave_grid->grid = r->grid;
  autosave_grid->session = self;
  autosave_grid->session_autosave_source = 0;

  watch_pages_session_autosave (autosave_grid,
                                0, g_list_model_get_n_items (list));
  g_signal_connect_data (list,
                         "items-changed",
                         G_CALLBACK (on_grid_items_changed_cb),
                         autosave_grid,
                         autosave_grid_free,
                         0);

  ret = ide_task_propagate_boolean (IDE_TASK (result), error);

  IDE_RETURN (ret);
}

static void
on_state_saved_to_cache_file_cb (GObject      *object,
                                 GAsyncResult *result,
                                 gpointer      user_data)
{
  GFile *file = (GFile *)object;
  g_autoptr(GError) error = NULL;
  g_autoptr(IdeTask) task = user_data;

  IDE_ENTRY;

  g_assert (G_IS_FILE (file));
  g_assert (G_IS_ASYNC_RESULT (result));
  g_assert (IDE_IS_TASK (task));

  if (!g_file_replace_contents_finish (file, result, NULL, &error))
    ide_task_return_error (task, g_steal_pointer (&error));
  else
    ide_task_return_boolean (task, TRUE);

  IDE_EXIT;
}

static void
get_page_position (IdePage *page,
                   guint   *out_column,
                   guint   *out_row,
                   guint   *out_depth)
{
  GtkWidget *frame_pages_stack;
  GtkWidget *frame;
  GtkWidget *grid_column;
  GtkWidget *grid;

  g_assert (IDE_IS_PAGE (page));
  g_assert (out_column != NULL);
  g_assert (out_row != NULL);
  g_assert (out_depth != NULL);

  frame_pages_stack = gtk_widget_get_ancestor (GTK_WIDGET (page), GTK_TYPE_STACK);
  frame = gtk_widget_get_ancestor (GTK_WIDGET (frame_pages_stack), IDE_TYPE_FRAME);
  grid_column = gtk_widget_get_ancestor (GTK_WIDGET (frame), IDE_TYPE_GRID_COLUMN);
  grid = gtk_widget_get_ancestor (GTK_WIDGET (grid_column), IDE_TYPE_GRID);

  /* When this page is the currently visible one for this frame, we want to keep it on top when
   * restoring so that there's no need to switch back to the pages we were working on. We need to
   * do this because the stack's "position" child property only refers to the order in which the
   * pages were initially opened, not the most-recently-used order.
   */
  if (ide_frame_get_visible_child (IDE_FRAME (frame)) == page)
    {
      *out_depth = g_list_model_get_n_items (G_LIST_MODEL (frame));
    }
  else
    {
      gtk_container_child_get (GTK_CONTAINER (frame_pages_stack), GTK_WIDGET (page),
                               "position", out_depth,
                               NULL);
      *out_depth = MAX (*out_depth, 0);
    }

  gtk_container_child_get (GTK_CONTAINER (grid_column), GTK_WIDGET (frame),
                           "index", out_row,
                           NULL);
  *out_row = MAX (*out_row, 0);

  gtk_container_child_get (GTK_CONTAINER (grid), GTK_WIDGET (grid_column),
                           "index", out_column,
                           NULL);
  *out_column = MAX (*out_column, 0);
}

typedef struct {
  IdeTask *task;
  IdePage *page;
} SavePage;

static void
save_state_to_disk (IdeSession      *self,
                    IdeTask         *task,
                    GVariantBuilder *pages_state)
{
  g_autoptr(GVariant) state = NULL;
  g_autoptr(GBytes) bytes = NULL;
  g_autoptr(GFile) file = NULL;
  GCancellable *cancellable;
  IdeContext *context;
  GVariantDict final_dict;

  IDE_ENTRY;

  g_assert (IDE_IS_SESSION (self));
  g_assert (IDE_IS_TASK (task));
  g_assert (pages_state != NULL);

  cancellable = ide_task_get_cancellable (task);

  g_variant_dict_init (&final_dict, NULL);
  g_variant_dict_insert (&final_dict, "version", "u", (guint32) 1);
  g_variant_dict_insert_value (&final_dict, "data", g_variant_builder_end (pages_state));

  state = g_variant_ref_sink (g_variant_dict_end (&final_dict));
  bytes = g_variant_get_data_as_bytes (state);

#ifdef IDE_ENABLE_TRACE
  {
    g_autofree char *str = g_variant_print (state, TRUE);
    IDE_TRACE_MSG ("Saving session state to %s", str);
  }
#endif

  context = ide_object_get_context (IDE_OBJECT (self));
  file = ide_context_cache_file (context, "session.gvariant", NULL);

  if (!ide_task_return_error_if_cancelled (task))
    g_file_replace_contents_bytes_async (file,
                                         bytes,
                                         NULL,
                                         FALSE,
                                         G_FILE_CREATE_NONE,
                                         cancellable,
                                         on_state_saved_to_cache_file_cb,
                                         g_object_ref (task));

  IDE_EXIT;
}

static void
on_session_addin_page_saved_cb (GObject      *object,
                                GAsyncResult *result,
                                gpointer      user_data)
{
  IdeSessionAddin *addin = (IdeSessionAddin *)object;
  g_autoptr(GVariant) page_state = NULL;
  SavePage *save_page = user_data;
  g_autoptr(IdeTask) task = save_page->task;
  IdePage *page = save_page->page;
  g_autoptr(GError) error = NULL;
  IdeSession *self;
  Save *s;

  IDE_ENTRY;

  g_assert (IDE_IS_SESSION_ADDIN (addin));
  g_assert (G_IS_ASYNC_RESULT (result));
  g_assert (IDE_IS_TASK (task));

  self = ide_task_get_source_object (task);
  s = ide_task_get_task_data (task);

  g_assert (IDE_IS_SESSION (self));
  g_assert (s != NULL);
  g_assert (s->active > 0);

  page_state = ide_session_addin_save_page_finish (addin, result, &error);

  if (error != NULL)
    g_warning ("Could not save page with addin %s: %s", G_OBJECT_TYPE_NAME (addin), error->message);

  if (page_state != NULL)
    {
      guint frame_column, frame_row, frame_depth;
      GVariantDict state_dict;

      g_assert (!g_variant_is_floating (page_state));

      get_page_position (page, &frame_column, &frame_row, &frame_depth);

      g_variant_dict_init (&state_dict, NULL);
      g_variant_dict_insert (&state_dict, "column", "u", frame_column);
      g_variant_dict_insert (&state_dict, "row", "u", frame_row);
      g_variant_dict_insert (&state_dict, "depth", "u", frame_depth);
      g_variant_dict_insert (&state_dict, "addin_name", "s", G_OBJECT_TYPE_NAME (addin));
      g_variant_dict_insert (&state_dict, "addin_page_state", "v", page_state);

      g_variant_builder_add_value (&s->pages_state, g_variant_dict_end (&state_dict));
    }
  g_free (save_page);

  s->active--;

  if (s->active == 0)
    save_state_to_disk (self, task, &s->pages_state);

  IDE_EXIT;
}

static void
foreach_page_in_grid_save_cb (GtkWidget *widget,
                              gpointer   user_data)
{
  IdePage *page = IDE_PAGE (widget);
  IdeTask *task = user_data;
  IdeSessionAddin *addin;
  SavePage *save_page = NULL;
  Save *s;

  g_assert (IDE_IS_PAGE (page));
  g_assert (IDE_IS_TASK (task));

  s = ide_task_get_task_data (task);

  g_assert (s != NULL);
  g_assert (s->addins != NULL);

  if (!(addin = find_suitable_addin_for_page (page, s->addins)))
    {
      /* It's not a saveable page. */
      s->active--;
      return;
    }

  save_page = g_slice_new0 (SavePage);
  save_page->task = g_object_ref (task);
  save_page->page = page;

  ide_session_addin_save_page_async (addin,
                                     page,
                                     ide_task_get_cancellable (task),
                                     on_session_addin_page_saved_cb,
                                     g_steal_pointer (&save_page));
}

/**
 * ide_session_save_async:
 * @self: an #IdeSession
 * @grid: an #IdeGrid
 * @cancellable: (nullable): a #GCancellable, or %NULL
 * @callback: a callback to execute upon completion
 * @user_data: user data for @callback
 *
 * This function will save the position and content of the pages in the @grid,
 * which can then be restored with ide_session_restore_async(), asking the
 * content of the pages to the appropriate #IdeSessionAddin.
 *
 * Since: 41
 */
void
ide_session_save_async (IdeSession          *self,
                        IdeGrid             *grid,
                        GCancellable        *cancellable,
                        GAsyncReadyCallback  callback,
                        gpointer             user_data)
{
  g_autoptr(IdeTask) task = NULL;
  Save *s;

  IDE_ENTRY;

  g_return_if_fail (IDE_IS_SESSION (self));
  g_return_if_fail (IDE_IS_GRID (grid));
  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));

  task = ide_task_new (self, cancellable, callback, user_data);
  ide_task_set_source_tag (task, ide_session_save_async);

  s = g_slice_new0 (Save);
  s->addins = self->addins;
  s->grid = grid;
  s->active = ide_grid_count_pages (s->grid);

  g_variant_builder_init (&s->pages_state, G_VARIANT_TYPE ("aa{sv}"));
  ide_task_set_task_data (task, s, save_free);

  ide_grid_foreach_page (s->grid,
                         foreach_page_in_grid_save_cb,
                         task);

  g_assert (s != NULL);

  /* Save the empty pages state there too because it wouldn't have
   * been done in foreach_page_in_grid_save_cb() since there's no
   * pages to save.
   */
  if (s->active == 0)
    save_state_to_disk (self, task, &s->pages_state);

  IDE_EXIT;
}

gboolean
ide_session_save_finish (IdeSession    *self,
                         GAsyncResult  *result,
                         GError       **error)
{
  gboolean ret;

  IDE_ENTRY;

  g_return_val_if_fail (IDE_IS_SESSION (self), FALSE);
  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);

  ret = ide_task_propagate_boolean (IDE_TASK (result), error);

  IDE_RETURN (ret);
}

IdeSession *
ide_session_new (void)
{
  return g_object_new (IDE_TYPE_SESSION, NULL);
}
