/*
   Kickshaw - A Menu Editor for Openbox

   Copyright (c) 2010–2025        Marcus Schätzle

   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 Kickshaw. If not, see http://www.gnu.org/licenses/.
*/

#include <gtk/gtk.h>

#include <stdlib.h> // May need to be included explicitly in some environments.
#include <string.h> // May need to be included explicitly in some environments.

#include "declarations_definitions_and_enumerations.h"
#include "undo_redo_autosave.h"

static void show_errmsg_and_restore_position_index (const gchar  *action, 
                                                          GError *error);
static void update_pos_inside_undo_stack_pointer (void);
static void write_autosave_callback (      GObject      *source_object, 
                                           GAsyncResult *result, 
                                     const gchar        *autosave_file_path);
static gboolean write_row_information (GtkTreeModel  *model, 
                                       GtkTreePath   *path, 
                                       GtkTreeIter   *iter, 
                                       GOutputStream *compressed_undo_file_output_stream);

/*

    Shows a message about an error that occurred while creating or writing
    a new undo stack item, and resets the position index if applicable.

*/

static void show_errmsg_and_restore_position_index (const gchar  *action, 
                                                          GError *error)
{
    const gchar *err_msg_markup;

    if (STREQ (action, "creation")) {
        err_msg_markup = _("<b>Could not create undo stack item</b> <tt>%1$i</tt> <b>inside tmp folder</b> <tt>%2$s</tt> <b>!\nError:</b> %3$s");
    }
    else if (STREQ (action, "writing")) {
        err_msg_markup = _("<b>An error occurred during the writing process of the new undo stack item</b> "
                           "<tt>%1$i</tt> <b>inside tmp folder</b> <tt>%2$s</tt> <b>!\nError:</b> %3$s");
    }
    else { // deletion
        err_msg_markup = _("<b>Could not remove incomplete undo stack item</b> "
                           "<tt>%1$i</tt> <b>inside tmp folder</b> <tt>%2$s</tt> <b>!\nError:</b> %3$s");
    }

    g_autofree gchar *err_msg = g_strdup_printf (err_msg_markup, 
                                                 (!STREQ (action, "deletion")) ? ks.pos_inside_undo_stack-- : ks.pos_inside_undo_stack, 
                                                 ks.tmp_path, error->message);

    show_errmsg (err_msg);
}

/*

    Writes all relevant data needed to restore the content and appearance of a single tree view row, including:
    - Tree store fields (of the image-related fields, only the file path is stored,
      because the image file might have changed between saving and restoring).
    - Existence of a label
    - Path depth
    - Selection status
    - Expansion status

*/

static gboolean write_row_information (GtkTreeModel  *model,
                                       GtkTreePath   *path, 
                                       GtkTreeIter   *iter, 
                                       GOutputStream *compressed_undo_file_output_stream)
{
    GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (ks.treeview));
    g_autoptr(GString) row_info = g_string_new (NULL);
    g_autoptr(GError) error = NULL;

    gchar *undo_txt_fields[NUMBER_OF_TXT_FIELDS];

    for (guint8 tree_store_loop = 0; tree_store_loop < NUMBER_OF_TXT_FIELDS; ++tree_store_loop) {
        /*
            Reading of the tree store data has to start from TS_ICON_PATH,
            since TS_ICON_IMG, TS_ICON_IMG_STATUS, and TS_ICON_MODIFICATION_TIME aren't used.
        */
        gtk_tree_model_get (model, iter, tree_store_loop + TS_ICON_PATH, &undo_txt_fields[tree_store_loop], -1);
        g_string_append_printf (row_info, "%s%c", 
                                (undo_txt_fields[tree_store_loop]) ? undo_txt_fields[tree_store_loop] : "\0", 
                                31); // 31 = unit separator
    }

    g_string_append_printf (row_info, "%s%c%i%c%s%c%s%c", 
                            (streq_any (undo_txt_fields[TYPE_TXT], "menu", "pipe menu", "item", "separator", NULL) && 
                             undo_txt_fields[MENU_ELEMENT_TXT]) ? "TRUE" : "FALSE", // Existence of a label
                            31, // 31 = unit separator
                            gtk_tree_path_get_depth (path), // Path depth
                            31, // 31 = unit separator
                            (gtk_tree_selection_iter_is_selected (selection, iter)) ? "TRUE" : "FALSE", // Selection status
                            31, // 31 = unit separator
                            // Expansion status
                            (gtk_tree_view_row_expanded (GTK_TREE_VIEW (ks.treeview), path)) ? "TRUE" : "FALSE",
                            30); // 30 = record/row separator

    if (G_UNLIKELY (!(g_output_stream_write (compressed_undo_file_output_stream, 
                                             row_info->str, strlen (row_info->str), NULL, &error)))) {
        show_errmsg_and_restore_position_index ("writing", error);
    }

    // Cleanup
    free_elements_of_static_string_array (undo_txt_fields, NUMBER_OF_TXT_FIELDS, FALSE);

    return (error != NULL); // TRUE = stop iterating.
}

/*

    Pushes the tree view data onto the undo stack
    by writing it to a compressed file in the tmp folder.

*/

void push_new_item_on_undo_stack (const gboolean after_save)
{
    g_autoptr(GError) error = NULL;

    /*
        Applying a change starting from a position earlier than the latest undo file creates a new "time line".
        This means that all later undo files starting from the current position have to be removed.
    */
    if (ks.pos_inside_undo_stack < ks.number_of_undo_stack_items) {
        for (gint stack_file_cnt = ks.pos_inside_undo_stack + 1; stack_file_cnt <= ks.number_of_undo_stack_items; ++stack_file_cnt) {
            g_autofree gchar *later_undo_file_str = g_strdup_printf ("%s/%i", ks.tmp_path, stack_file_cnt);
            g_autoptr(GFile) later_undo_file = g_file_new_for_path (later_undo_file_str);

            if (G_UNLIKELY (!(g_file_delete (later_undo_file, NULL, &error)))) {
                g_autofree gchar *err_msg = g_strdup_printf (_("<b>Could not remove undo stack item</b> <tt>%1$i</tt> "
                                                               "<b>inside tmp folder</b> <tt>%2$s</tt> <b>!\nError:</b> %3$s\n\n"
                                                               "--- Note ---\n\n"
                                                               "All undo stack items after the current position must be "
                                                               "deleted when a change is applied prior to the latest change, "
                                                               "i.e. from a position within the undo stack.\n"
                                                               "A new undo stack item '<tt>%4$i</tt>' for this change will not be saved, "
                                                               "so this change cannot be recalled with undo or redo."), 
                                                             stack_file_cnt, ks.tmp_path, error->message, ks.pos_inside_undo_stack + 1);

                show_errmsg (err_msg);

                return;
            }
        }

        ks.number_of_undo_stack_items = ks.pos_inside_undo_stack;
    }

    if (!after_save) {
        ks.pos_inside_undo_stack = ks.number_of_undo_stack_items + 1;
    }

    g_autofree gchar *compressed_undo_file_str = g_strdup_printf ("%s/%i", ks.tmp_path, ks.pos_inside_undo_stack);
    g_autoptr(GFile) compressed_undo_file = g_file_new_for_path (compressed_undo_file_str);

    g_autoptr(GOutputStream) undo_file_output_stream;

    if (!after_save) {
        if (G_UNLIKELY (!((undo_file_output_stream = G_OUTPUT_STREAM (g_file_create (compressed_undo_file, G_FILE_CREATE_NONE, 
                                                                                     FALSE, &error)))))) {
            show_errmsg_and_restore_position_index ("creation", error);

            return;
        }
    }
    else {
        if (G_UNLIKELY (!((undo_file_output_stream = G_OUTPUT_STREAM (g_file_replace (compressed_undo_file, NULL, FALSE, 
                                                                                      G_FILE_CREATE_NONE, NULL, &error)))))) {
            g_autofree gchar *err_msg = g_strdup_printf (_("<b>Could not remove undo stack item</b> <tt>%1$i</tt> "
                                                           "<b>inside tmp folder</b> <tt>%2$s</tt> <b>!\nError:</b> %3$s"), 
                                                         ks.pos_inside_undo_stack, ks.tmp_path, error->message);

            show_errmsg (err_msg);

            return;
        }
    }

    g_autoptr(GConverter) compressor = G_CONVERTER (g_zlib_compressor_new (G_ZLIB_COMPRESSOR_FORMAT_RAW, -1));
    GOutputStream *compressed_undo_file_output_stream = g_converter_output_stream_new (undo_file_output_stream, compressor);

    g_autofree gchar *filename_header = g_strdup_printf ("%s%c", (ks.filename) ? (ks.filename) : "\0", 31); // 31 = unit separator

    if (G_UNLIKELY (!(g_output_stream_write (compressed_undo_file_output_stream, filename_header, strlen (filename_header), NULL, &error)))) {
        show_errmsg_and_restore_position_index ("writing", error);
    }

    g_autoptr(GString) global_data = g_string_new (NULL);

    for (guint8 edit_booleans_cnt = 0; edit_booleans_cnt < NUMBER_OF_LOADING_PROCESS_EDIT_TYPES; ++edit_booleans_cnt) {
        g_string_append_printf (global_data, "%s%c", 
                                (ks.loading_process_edit_types[edit_booleans_cnt]) ? "TRUE" : "FALSE", 
                                (edit_booleans_cnt < NUMBER_OF_LOADING_PROCESS_EDIT_TYPES - 1) ? '|' : 31); // 31 = unit separator
    }
    // 31 = unit separator, 30 = record/row separator
    g_string_append_printf (global_data, "%s%c%i%c", (ks.change_done) ? "TRUE" : "FALSE", 31, ks.last_save_pos, 30);

    if (G_UNLIKELY (!(g_output_stream_write (compressed_undo_file_output_stream, global_data->str, strlen (global_data->str), NULL, &error)))) {
        show_errmsg_and_restore_position_index ("writing", error);
    }

    if (G_LIKELY (!error)) {
        gtk_tree_model_foreach (ks.ts_model, (GtkTreeModelForeachFunc) write_row_information, compressed_undo_file_output_stream);

        // The position index has been reset by the callback function write_row_information (), meaning that there has been an error.
        if (G_UNLIKELY (ks.pos_inside_undo_stack == ks.number_of_undo_stack_items && 
                        !(g_file_delete (compressed_undo_file, NULL, &error)))) {
            show_errmsg_and_restore_position_index ("deletion", error);
        }
    }

    // There has been an error, do not increase ks.number_of_undo_stack_items.
    if (G_UNLIKELY (!after_save && ks.pos_inside_undo_stack == ks.number_of_undo_stack_items)) {
        return;
    }

    ks.number_of_undo_stack_items = ks.pos_inside_undo_stack;

    update_pos_inside_undo_stack_pointer ();

    // Cleanup. This has to be unrefed here, or the new undo stack file can't be copied as new autosave file.
    g_clear_object (&compressed_undo_file_output_stream);

    write_autosave (compressed_undo_file);
}

/*
    Undoes the last change or redoes the next change from the current undo stack position.
    Also restores an autosave if requested.
*/

void undo_redo_restore (const gchar *action)
{
    // new_pos_inside_undo_stack is not used in case action = "restore".
    const gint new_pos_inside_undo_stack = (STREQ (action, "undo")) ? ks.pos_inside_undo_stack - 1 : ks.pos_inside_undo_stack + 1;
    g_autofree gchar *compressed_undo_file_str = (!(STREQ (action, "restore"))) ? 
                                                 g_strdup_printf ("%s/%i", ks.tmp_path, new_pos_inside_undo_stack) : 
                                                 g_build_filename (ks.home_dir, ".kickshaw_autosave", NULL);
    g_autoptr(GFile) compressed_undo_file = g_file_new_for_path (compressed_undo_file_str);
    g_autoptr(GFileInputStream) undo_file_input_stream;
    g_autoptr(GError) error = NULL;

    if (G_UNLIKELY (!((undo_file_input_stream = g_file_read (compressed_undo_file, NULL, &error))))) {
        g_autofree gchar *err_msg;

        if (!(STREQ (action, "restore"))) {
            err_msg = g_strdup_printf (_("<b>Could not open undo stack item</b> <tt>%1$i</tt> "
                                         "<b>inside tmp folder</b> <tt>%2$s</tt> <b>for reading!\n"
                                         "Error:</b> %3$s"), 
                                       new_pos_inside_undo_stack, ks.tmp_path, error->message);
        }
        else {
            err_msg = g_strdup_printf (_("<b>Could not open autosave file</b> <tt>%s</tt> <b>!\nError:</b> %s"), 
                                       compressed_undo_file_str, error->message);
        }

        show_errmsg (err_msg);

        return;
    }

    GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (ks.treeview));

    g_signal_handler_block (selection, ks.handler_id_row_selected);


    // --- Clear global data. ---


#if GLIB_CHECK_VERSION(2,64,0)
    g_clear_slist (&ks.menu_ids, (GDestroyNotify) g_free);
#else
    g_slist_free_full (ks.menu_ids, (GDestroyNotify) g_free);
    ks.menu_ids = NULL;
#endif

    if (ks.rows_with_icons) {
        stop_timeout ();
    }

    gtk_tree_store_clear (ks.treestore);

    ks.statusbar_msg_shown = FALSE;


    // --- Restore tree store from undo file. ---


    g_autoptr(GConverter) decompressor = G_CONVERTER (g_zlib_decompressor_new (G_ZLIB_COMPRESSOR_FORMAT_RAW));
    g_autoptr(GInputStream) decom_undo_file_input_stream = g_converter_input_stream_new (G_INPUT_STREAM (undo_file_input_stream), 
                                                                                         decompressor);

    g_autoptr(GString) record = g_string_new (NULL);
    gchar buffer[2];

    // The fields that are stored inside an undo file.
    enum { UN_ICON_PATH, UN_MENU_ELEMENT, UN_TYPE, UN_VALUE, UN_MENU_ID, UN_EXECUTE, UN_ELEMENT_VISIBILITY, 
           UN_MENU_ITEM_OR_SEP_WITH_LABEL, UN_PATH_DEPTH, UN_SELECTED, UN_EXPANDED };

    g_autofree GtkTreeIter *iters = g_malloc (sizeof (GtkTreeIter));

#if GLIB_CHECK_VERSION(2,56,0)
    g_autoslist(GtkTreePath) expanded_paths = NULL;
    g_autoslist(GtkTreePath) selected_paths = NULL;
#else
    GSList *expanded_paths = NULL;
    GSList *selected_paths = NULL;
#endif

    guint max_path_depth = 1; // Default value.

    // x1E (dec. 30) = record/row separator
    while (g_input_stream_read (decom_undo_file_input_stream, buffer, 1, NULL, &error) > 0 && !(STREQ (buffer, "\x1E"))) { 
        g_string_append (record, buffer);
    }

    // Global data

    if (STREQ (action, "restore")) {
        g_auto(GStrv) tokens = g_strsplit (record->str, "\x1F", -1); // x1F (dec. 31) = unit separator
        if (*(tokens[0])) {
            set_filename_and_window_title (g_strdup (tokens[0]));
        }
        else {
            if (!ks.settings.use_header_bar) {
                gtk_window_set_title (GTK_WINDOW (ks.window), _("Unsaved Menu"));
            }
            else {
                gtk_label_set_label (GTK_LABEL (ks.basename_label), _("Unsaved Menu"));
                gtk_widget_hide (ks.subtitle_label);
            }
        }

        g_auto(GStrv) loading_process_edit_tokens = g_strsplit (tokens[1], "|", -1);

        for (guint8 edit_booleans_cnt = 0; edit_booleans_cnt < NUMBER_OF_LOADING_PROCESS_EDIT_TYPES; ++edit_booleans_cnt) {
            ks.loading_process_edit_types[edit_booleans_cnt] = (STREQ (loading_process_edit_tokens[edit_booleans_cnt], "TRUE"));
        }

        ks.change_done = (STREQ (tokens[2], "TRUE"));
        ks.last_save_pos = atoi (tokens[3]);
    }

    g_string_assign (record, "");

    // Menu data

    while (g_input_stream_read (decom_undo_file_input_stream, buffer, 1, NULL, &error) > 0) {
        if (!(STREQ (buffer, "\x1E"))) { // x1E (dec. 30) = record/row separator
            g_string_append (record, buffer);
        }
        else { 
            g_auto(GStrv) tokens = g_strsplit (record->str, "\x1F", -1); // x1F (dec. 31) = unit separator
            const guint path_depth = atoi (tokens[UN_PATH_DEPTH]);
            const guint iter_idx = path_depth - 1;

            if (path_depth > max_path_depth) {
                max_path_depth = path_depth;
                iters = g_realloc (iters, sizeof (GtkTreeIter) * max_path_depth);
            }

            gtk_tree_store_insert (ks.treestore, &iters[iter_idx], (path_depth == 1) ? NULL : &iters[iter_idx - 1], -1);

            for (guint8 tokens_cnt = 0; tokens_cnt <= UN_ELEMENT_VISIBILITY; ++tokens_cnt) {
                gtk_tree_store_set (ks.treestore, &iters[iter_idx], tokens_cnt + TS_ICON_PATH, 
                                    (!STREQ (tokens[tokens_cnt], "\0")) ? tokens[tokens_cnt] : 
                                     /*
                                        Menus, items, and separators may have empty labels or no labels at all.
                                        Menu IDs may consist of empty strings.
                                     */
                                     (((tokens_cnt == UN_MENU_ELEMENT && STREQ (tokens[UN_MENU_ITEM_OR_SEP_WITH_LABEL], "TRUE")) ||
                                      tokens_cnt == UN_MENU_ID) ? 
                                      "\0" : NULL), 
                                      -1);
            }

            if (streq_any (tokens[UN_TYPE], "menu", "pipe menu", NULL)) {
                ks.menu_ids = g_slist_prepend (ks.menu_ids, g_strdup (tokens[UN_MENU_ID]));
            }

            if (*tokens[UN_ICON_PATH]) {
                g_autoptr(GFile) icon_path_file = g_file_new_for_path (tokens[UN_ICON_PATH]);

                if (G_LIKELY (g_file_query_exists (icon_path_file, NULL))) {
                    // This is a separate if-statement, because after a succesful call of set_icon() the 
                    // else-statement below should not be called.
                    if (G_UNLIKELY (!(set_icon (&iters[iter_idx], tokens[UN_ICON_PATH], FALSE, TRUE)))) {
                        g_autofree gchar *time_stamp = get_modification_time_for_icon (tokens[UN_ICON_PATH]);

                        gtk_tree_store_set (ks.treestore, &iters[iter_idx],
                                            TS_ICON_IMG, ks.invalid_icon_imgs[INVALID_FILE_ICON], 
                                            TS_ICON_IMG_STATUS, INVALID_FILE, 
                                            TS_ICON_MODIFICATION_TIME, time_stamp, 
                                            -1);
                    }
                }
                else {
                    gtk_tree_store_set (ks.treestore, &iters[iter_idx],
                                        TS_ICON_IMG, ks.invalid_icon_imgs[INVALID_PATH_ICON], 
                                        TS_ICON_IMG_STATUS, INVALID_PATH, 
                                        TS_ICON_MODIFICATION_TIME, NULL, 
                                        -1);
                }
            }

            g_autoptr(GtkTreePath) path = gtk_tree_model_get_path (ks.ts_model, &iters[iter_idx]);

            if (STREQ (tokens[UN_EXPANDED], "TRUE")) {
                expanded_paths = g_slist_prepend (expanded_paths, gtk_tree_path_copy (path));
            }

            if (STREQ (tokens[UN_SELECTED], "TRUE")) {
                selected_paths = g_slist_prepend (selected_paths, gtk_tree_path_copy (path));
            }

            g_string_assign (record, "");
        }
    }

    if (G_LIKELY (!error)) {
        GSList *expanded_paths_loop, *selected_paths_loop;

        expanded_paths = g_slist_reverse (expanded_paths); // Expand from top to bottom

        FOREACH_IN_LIST (expanded_paths, expanded_paths_loop) {
            gtk_tree_view_expand_row (GTK_TREE_VIEW (ks.treeview), expanded_paths_loop->data, FALSE);
        }

        FOREACH_IN_LIST (selected_paths, selected_paths_loop) {
            gtk_tree_selection_select_path (selection, selected_paths_loop->data);
        }
    }

    g_signal_handler_unblock (selection, ks.handler_id_row_selected);

#if !(GLIB_CHECK_VERSION(2,56,0))
    // Cleanup
    g_slist_free_full (expanded_paths, (GDestroyNotify) gtk_tree_path_free);
    g_slist_free_full (selected_paths, (GDestroyNotify) gtk_tree_path_free);
#endif

    if (G_UNLIKELY (error)) {
        g_autofree gchar *err_msg;

        if (!STREQ (action, "restore")) {
            err_msg = g_strdup_printf (_("<b>Could not read data from undo stack item</b> <tt>%1$i</tt> "
                                         "<b>inside tmp folder</b> <tt>%2$s</tt> <b>!\n"
                                         "Error:</b> %3$s"), 
                                       new_pos_inside_undo_stack, ks.tmp_path, error->message);
        }
        else {
            err_msg = g_strdup_printf (_("<b>Could not read data from autosave file</b> <tt>%s</tt> <b>!\nError:</b> %s"), 
                                       compressed_undo_file_str, error->message);
        }

        show_errmsg (err_msg);

        return;
    }

    // If this was a restore of an autosave, setting ks.pos_inside_undo_stack is done later when the first undo item is pushed on the stack.
    if (!(STREQ (action, "restore"))) {
        ks.pos_inside_undo_stack = new_pos_inside_undo_stack;
        update_pos_inside_undo_stack_pointer ();
        write_autosave (compressed_undo_file);
    }

    // If this was a restore of an autosave for a new menu that had not been saved yet, the ability to save (not save as) should be disabled.
    if (STREQ (action, "restore") && !ks.filename) {
        ks.restored_autosave_that_hasnt_been_saved_yet = TRUE;
    }

    gtk_tree_model_foreach (ks.ts_model, (GtkTreeModelForeachFunc) add_icon_occurrence_to_list, NULL);
    if (ks.rows_with_icons) {
        ks.timeout_id = g_timeout_add_seconds (1, (GSourceFunc) check_for_external_file_and_settings_changes, "timeout");
    }

    if (!(STREQ (action, "restore"))) {
        ks.change_done = TRUE;
    }
    row_selected ();
}

/*

    Creates a symbolic link to the undo stack file representing the current position.
    Intended for use after a restart or irregular termination of the program.

*/

static void update_pos_inside_undo_stack_pointer (void)
{
    g_autofree gchar *pos_inside_undo_stack_str = g_strdup_printf ("%i", ks.pos_inside_undo_stack);
    g_autofree gchar *pos_inside_undo_stack_path = g_build_filename (ks.tmp_path, pos_inside_undo_stack_str, NULL);
    g_autofree gchar *symlink_pos_inside_undo_stack_path = g_build_filename (ks.tmp_path, "pos_inside_undo_stack_symlink", NULL);
    g_autoptr(GFile) symlink_pos_inside_undo_stack = g_file_new_for_path (symlink_pos_inside_undo_stack_path);

    g_autoptr(GError) error = NULL;

    if (g_file_query_exists (symlink_pos_inside_undo_stack, NULL)) {
        g_file_delete (symlink_pos_inside_undo_stack, NULL, &error);
    }

    if (G_UNLIKELY (error)) {
        g_autofree gchar *err_msg = g_strdup_printf (_("<b>Could not delete symbolic link to undo stack file</b> <tt>%s</tt> <b>!\n"
                                                       "Error:</b> %s"), 
                                                     pos_inside_undo_stack_path, error->message);

        show_errmsg (err_msg);

        return;
    }

    if (G_UNLIKELY (!(g_file_make_symbolic_link (symlink_pos_inside_undo_stack, pos_inside_undo_stack_path, NULL, &error)))) {
        g_autofree gchar *err_msg = g_strdup_printf (_("<b>Could not create symbolic link to undo stack file</b> <tt>%s</tt> <b>!\n"
                                                       "Error:</b> %s"), 
                                                     pos_inside_undo_stack_path, error->message);

        show_errmsg (err_msg);
    }
}

/*

    Creates an autosave file by copying the latest undo stack file to the home folder.

*/

void write_autosave (GFile *compressed_undo_file)
{
    g_autofree gchar *autosave_file_path = g_build_filename (ks.home_dir, ".kickshaw_autosave", NULL);
    g_autoptr(GFile) autosave_file = g_file_new_for_path (autosave_file_path);

    g_file_copy_async (compressed_undo_file, autosave_file, G_FILE_COPY_OVERWRITE, G_PRIORITY_DEFAULT, NULL, NULL, NULL, 
                       (GAsyncReadyCallback) write_autosave_callback, autosave_file_path);
}

/*

    Called after the latest undo stack item has been copied as an autosave,
    to check for errors.

*/

static void write_autosave_callback (      GObject      *source_object, 
                                           GAsyncResult *result, 
                                     const gchar        *autosave_file_path)
{
    g_autoptr(GError) error = NULL;

    g_file_copy_finish ((GFile *) source_object, result, &error);

    if (G_UNLIKELY (error)) {
        g_autofree gchar *err_msg = g_strdup_printf (_("<b>Could not create the autosave file</b> <tt>%s</tt> <b>!\nError:</b> %s"), 
                                                       autosave_file_path, error->message);

        show_errmsg (err_msg);
    }
}
