/*
   Kickshaw - A Menu Editor for Openbox

   Copyright (c) 2010–2024        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 <setjmp.h>
#include <stdlib.h> // Sometimes this might have to be included explicitly.
#include <string.h> // Sometimes this might have to be included explicitly.

#include "declarations_definitions_and_enumerations.h"
#include "save_menu.h"

jmp_buf buf;

typedef struct {
    GOutputStream *menu_file_output_stream;
    GtkTreeModel *filter_model;
    GError **error;
    guint8 saving_stage;
    GtkTreeIter filter_iter[2];
    gint filter_path_depth_prev;
    gchar *save_txts[NUMBER_OF_TXT_FIELDS];
} SaveMenuArgsData;

// SM = Save Menu, indicating that the enums are only used here.
enum { SM_MENUS, SM_ROOT_MENU, SM_IND_OF_SAVING_STAGE, SM_NO_SEPARATE_ROOT_MENU };
enum { SM_TOPLEVEL, SM_FILTER_LEVEL, SM_IND_OF_LEVEL };
enum { SM_MENU_OR_PIPE_MENU, SM_ITEM_OR_ACTION, SM_SEPARATOR };
enum { SM_CURRENT, SM_PREV, SM_NUMBER_OF_ITER_ARRAY_ELM };

static gboolean closing_tags (const gboolean          filter_iteration_completed, 
                                    GtkTreeModel     *filter_model, 
                                    SaveMenuArgsData *save_menu_args);
static void get_field_values (gchar        **txt_fields_array, 
                              GtkTreeModel  *current_model, 
                              GtkTreeIter   *current_iter);
static void process_menu_or_item (GtkTreeIter      *process_iter, 
                                  SaveMenuArgsData *save_menu_args);
static gboolean treestore_save_process_iteration (GtkTreeModel     *filter_model, 
                                                  GtkTreePath      *filter_path, 
                                                  GtkTreeIter      *filter_iter, 
                                                  SaveMenuArgsData *save_menu_args);
static gboolean visualize_invisible_children (GtkTreeModel *filter_model, 
                                              GtkTreePath  *filter_path, 
                                              GtkTreeIter  *filter_iter, 
                                              gpointer      root_has_label_ptr);
static void write_tag (const guint8            saving_stage, 
                       const guint8            level, 
                             gchar           **tag_elements, 
                             GtkTreeModel      *local_model,  
                             GtkTreeIter       *local_iter, 
                       const guint8             type, 
                             SaveMenuArgsData  *save_menu_args);

/* 

    Writes closing tags.

*/

static gboolean closing_tags (const gboolean          filter_iteration_completed, 
                                    GtkTreeModel     *filter_model, 
                                    SaveMenuArgsData *save_menu_args)
{
    GOutputStream *menu_file_output_stream = save_menu_args->menu_file_output_stream;
    GError **error = save_menu_args->error;
    guint8 saving_stage = save_menu_args->saving_stage;
    GtkTreeIter *filter_iter = save_menu_args->filter_iter;
    gint filter_path_depth_prev = save_menu_args->filter_path_depth_prev;

    g_autoptr(GString) file_string = g_string_new (NULL);

    const gchar *indentation = (ks.settings.tabs_for_indentations) ? "\t" : "    ";

    /*
        The number of indentations for elements outside and inside the root menu differs.
        A menu with the menu ID "root_menu" has to be written, into which all root elements are packed.
        Since the root menu itself is not visualized inside the tree view, there has to be an additional
        indentation written into the menu file for all those elements.

        --- MENU DEFINITIONS ---

        <menu id="menu"> -> Path depth counting starts from this level
            <item label="menu">
                <action name="Exit">
                    <prompt>yes</prompt>
                </action>
            </item>
        </menu>

        --- ROOT MENU ---

        <menu id="root_menu"> -> Not visualized inside the tree view
            <menu id="menu"> -> Path depth counting starts from this level
                 <item label="root_menu1">
                    <action name="Exit">
                        <prompt>yes</prompt>
                    </action>
                </item>
            </menu>
    */
    const guint8 offset = (saving_stage == SM_MENUS); // TRUE = 1, FALSE = 0.

    gchar *ts_txt[SM_NUMBER_OF_ITER_ARRAY_ELM][2]; // COL_MENU_ELEMENT & COL_TYPE for current and previous iter.

    /* 
        action_closing_tag_subtraction contains the number of indentations 
        that have to be removed for the closing action tag after the last option of an action.

        1 = option of action (='command' and 'prompt')
        <item label="Program">
             <action="Execute">
                <command>program</command>
            </action> = removed 1 in comparison to 'command'
        </item>
        <item="New item">
        ...

        2 = startupnotify option (='enabled', 'name', 'wmclass', and 'icon')
        <item label="Program">
            <action="Execute">
                 <command>program</command>
                <startupnotify>
                    <enabled>yes</enabled>
                <startupnotify />
             </action> = removed 2 in comparison to 'enabled'
        <item>
        <item label="New item">
        ...

        The value is set to zero if the action is a self-closing action without an option (='Reconfigure')

        <item label="Reconfigure">
            <action="Reconfigure" /> = 0, self-closing action without an option
        </item> 
        <item label="New item"> 
        ...
  
        action_closing_tag_subtraction defaults to -1. This is done for the case that additional </menu> 
        closing tags have to be added. The reason for setting the default value to -1 is explained there.
    */
    gint8 action_closing_tag_subtraction = -1; // Default

    gint path_depth_prev_cnt, subtr_path_depth_prev_cnt;
    gint8 array_cnt;

    for (array_cnt = SM_CURRENT; array_cnt < SM_NUMBER_OF_ITER_ARRAY_ELM; ++array_cnt) {
        gtk_tree_model_get (filter_model, &filter_iter[array_cnt], 
                            TS_MENU_ELEMENT, &ts_txt[array_cnt][COL_MENU_ELEMENT], 
                            TS_TYPE, &ts_txt[array_cnt][COL_TYPE],
                            -1);
    }

    // startupnotify
    if ((STREQ (ts_txt[SM_PREV][COL_TYPE], "option") && 
        streq_any (ts_txt[SM_PREV][COL_MENU_ELEMENT], "enabled", "name", "wmclass", "icon", NULL)) &&
         (!(STREQ (ts_txt[SM_CURRENT][COL_TYPE], "option") && 
        streq_any (ts_txt[SM_CURRENT][COL_MENU_ELEMENT], "enabled", "name", "wmclass", "icon", NULL)) || 
        filter_iteration_completed)) {
        for (path_depth_prev_cnt = offset; path_depth_prev_cnt < filter_path_depth_prev; ++path_depth_prev_cnt) {
            g_string_append (file_string, indentation);
        }

        g_string_append (file_string, "</startupnotify>\n");
    }

    // action
    if (streq_any (ts_txt[SM_PREV][COL_TYPE], "option", "option block", NULL) && 
        (!streq_any (ts_txt[SM_CURRENT][COL_TYPE], "option", "option block", NULL) || filter_iteration_completed)) {
        action_closing_tag_subtraction = (streq_any (ts_txt[SM_PREV][COL_MENU_ELEMENT], // 2 if startupnotify option.
                                          "command", "prompt", "startupnotify", NULL)) ? 1 : 2;
        for (subtr_path_depth_prev_cnt = offset;
            subtr_path_depth_prev_cnt <= filter_path_depth_prev - action_closing_tag_subtraction;
            ++subtr_path_depth_prev_cnt) {
            g_string_append (file_string, indentation);
        }

        g_string_append (file_string, "</action>\n");
    }

    // action without option
    if (STREQ (ts_txt[SM_PREV][COL_TYPE], "action") && !gtk_tree_model_iter_has_child (filter_model, &filter_iter[SM_PREV])) {
        action_closing_tag_subtraction = 0;
    }

    // item
    if (!(streq_any (ts_txt[SM_PREV][COL_TYPE], "menu", "pipe menu", "separator", NULL) ||
        (STREQ (ts_txt[SM_PREV][COL_TYPE], "item") && 
        !gtk_tree_model_iter_has_child (filter_model, &filter_iter[SM_PREV]))) && 
        (streq_any (ts_txt[SM_CURRENT][COL_TYPE], "menu", "pipe menu", "item", "separator", NULL) || 
        filter_iteration_completed)) {
        /* 
            If an open action has been closed (or it has been a self-closing action) and no new action follows, 
            an </item> tag is written.
            The calculation of the subtraction for this tag is  
             filter_path_depth_prev - action_closing_tag_subtraction - 1, 
            since the </item> closing tag is one indentation level below the one of the </action> closing tag:
            Either 

                    <prompt>yes</prompt> (example)
                </action>
            </item>

            or

                        <enabled>yes</enabled> (example)
                    </startupnotify>
                </action>
            </item> 
        */
        for (subtr_path_depth_prev_cnt = offset; 
             subtr_path_depth_prev_cnt <= filter_path_depth_prev - action_closing_tag_subtraction - 1;
             ++subtr_path_depth_prev_cnt) {
            g_string_append (file_string, indentation);
        }

        g_string_append (file_string, "</item>\n");
    }

    // menu
    if (saving_stage == SM_MENUS || saving_stage == SM_NO_SEPARATE_ROOT_MENU) {
        g_autoptr(GtkTreePath) filter_path = gtk_tree_model_get_path (filter_model, &filter_iter[SM_CURRENT]);
        const gint filter_path_depth = (filter_iteration_completed) ? 1 : gtk_tree_path_get_depth (filter_path);

        /* 
            If the current element is a menu, pipe menu, item or separator or the filter iteration has been completed and 
            the current path depth is lower than the previous one, addtional </menu> tags have to be added, 
            as for example in these cases:

            Case 1:

                                                                    Depth:  
            <menu id="menu level 1a" label="menu level 1a">         1
                <menu id="menu level 2" label="menu level 2">       2
                    <menu id="menu level 3" label="menu level 3">   3
                        <item label="1" />                          4
                        <item label="2">                            4
                            <action="Execute">                      5
                                <command>anything</command>         6
                                <startupnotify>                     6
                                    <enabled>no</enabled>           7 previous visible element inside the tree
                                </startupnotify>                    6
                            </action>                               5
                        </item>                                     4
                    </menu>                                         3
                </menu>                                             2 
             </menu>                                                1
            <menu id="menu level 1b" label>                         1 current visible element inside the tree
            ...

            1 < filter_path_depth_prev (=7, 'enabled') - action_closing_tag_subtraction (here: 2) - 1 -> 1 < 4
            4 - 1 = 3 menu closing tags to write.

            Case 2:

                                                                    Depth:
            <menu id="menu level 1" label="menu level 1">           1
                <menu id="menu level 2a" label="menu level 2a">     2
                    <separator />                                   3 previous visible element inside the tree
       
                </menu> has to be added                             2
            <menu id="menu lebel 2b">                               2 current visible element inside the tree
            ...

            2 < filter_path_depth_prev (=3, 'separator') - action_closing_tag_substraction (here: -1) - 1 -> 2 < 3
            3 - 2 = 1 closing tag to write.

            If the current element is a menu/pipe menu/item/separator or the filter iteration has been completed, 
            its path depth is compared to the one of the previous element. If the path depth of the current element is 
            lower than the one of the previous element, there are still open menu tags and additional </menu> tags have to 
            be written until the level of the current element or, in the case of a last row, the base level.
            If the previous element is an option of an action or a self-closing action, the value of the previous path depth 
            has to be adjusted to the additional closing tags resulting from this, since there have to be written less 
            </menu> closing tags then. 
            For this case the action_closing_tag_substraction variable has been set above, and considering there is also 
            a closing item tag following afterwards, the calculation is 
            filter_path_depth_prev - action_closing_tag_subtraction - 1 
             (see above for the values for action_closing_tag subtraction). 
            If the previous element is not an option of an action or self-closing action, 
             but a self-closing menu/item, a pipe menu or separator, there is not only no action closing tag subtraction, 
            but the additional reduction for the closing item tag has also to be taken to account. 
            That is why action_closing_tag_subtraction defaults to -1 for this case, since this evaluates to    
            filter_path_depth_prev - (-1) - 1 = filter_path_depth_prev
            so reductions that would be caused by additional closing tags don't come to effect.
        */
        if ((streq_any (ts_txt[SM_CURRENT][COL_TYPE], "menu", "pipe menu", "item", "separator", NULL) || 
            filter_iteration_completed) && 
            filter_path_depth < filter_path_depth_prev - action_closing_tag_subtraction - 1) {
            /*
                The closing menu tags are written for each path depth from 
                filter_path_depth_prev - action_closing_tag_subtraction - 2
                down to
                filter_path_depth 
            */
            gint path_depth_of_closing_menu_tag = filter_path_depth_prev - action_closing_tag_subtraction - 2;
            gint menu_closing_tags_cnt;

            /* If there is no separate root menu, there has to be one additional indentation.
               This is done by the expression menu_closing_tags_cnt = (ks.separate_root_menu),  
               which evaluates to menu_closing_tags_cnt = 0 (FALSE) in the case of !ks.separate_root_menu. */
            while (path_depth_of_closing_menu_tag >= filter_path_depth) {
                for (menu_closing_tags_cnt = (ks.settings.separate_root_menu); 
                     menu_closing_tags_cnt <= path_depth_of_closing_menu_tag; 
                     ++menu_closing_tags_cnt) {
                    g_string_append (file_string, indentation);
                }

                g_string_append (file_string, "</menu>\n");
                --path_depth_of_closing_menu_tag;
            }
        }
    }

    g_output_stream_write (menu_file_output_stream, file_string->str, strlen (file_string->str), NULL, error);

    // Cleanup
    for (array_cnt = SM_CURRENT; array_cnt < SM_NUMBER_OF_ITER_ARRAY_ELM; ++array_cnt) {
        g_free (ts_txt[array_cnt][COL_MENU_ELEMENT]);
        g_free (ts_txt[array_cnt][COL_TYPE]);
    }

    return (*error == NULL);
}

/* 

    Writes an (opening) menu, item, separator or action tag into the menu XML file.

*/

static void write_tag (const guint8             saving_stage, 
                       const guint8             level, 
                             gchar            **tag_elements, 
                             GtkTreeModel      *local_model, 
                             GtkTreeIter       *local_iter, 
                       const guint8             type, 
                             SaveMenuArgsData  *save_menu_args)
{
    GOutputStream *menu_file_output_stream = save_menu_args->menu_file_output_stream;
    GError **error = save_menu_args->error;
    /*
        For actions and options there is no distinction made between SM_MENUS and SM_ROOT_MENU, 
        the value is always SM_IND_OF_SAVING_STAGE. 
        If the current saving stage is SM_ROOT_MENU and a menu or item has to be written, this function was called 
        directly from the save_menu and not from the treestore_save_process_iteration function.
        This means that there has no indentation been done yet (this is done inside the latter function), 
        so there's one added for the mentioned case.
    */
    const gchar *indentation = (ks.settings.tabs_for_indentations) ? "\t" : "    ";
    g_autoptr(GString) file_string = g_string_new ((saving_stage == SM_ROOT_MENU || 
                                                    saving_stage == SM_NO_SEPARATE_ROOT_MENU) ? indentation : NULL);

    if (type == SM_MENU_OR_PIPE_MENU || type == SM_ITEM_OR_ACTION) {
        if (type == SM_MENU_OR_PIPE_MENU) {
            g_string_append_printf (file_string, "<menu id=\"%s\"", tag_elements[MENU_ID_TXT]);
            if (tag_elements[MENU_ELEMENT_TXT] && 
                !(saving_stage == SM_ROOT_MENU && STREQ (tag_elements[TYPE_TXT], "menu"))) {
                g_string_append_printf (file_string, " label=\"%s\"", tag_elements[MENU_ELEMENT_TXT]);
            }
            if (STREQ (tag_elements[TYPE_TXT], "pipe menu")) {
                g_string_append_printf (file_string, " execute=\"%s\"", 
                (tag_elements[EXECUTE_TXT]) ? (tag_elements[EXECUTE_TXT]) : "");
            }
            if (tag_elements[ICON_PATH_TXT] && (saving_stage == SM_ROOT_MENU || level == SM_FILTER_LEVEL)) { // icon="" is saved back.
                g_string_append_printf (file_string, " icon=\"%s\"", tag_elements[ICON_PATH_TXT]);
            }
        }
        else { // Item or action
            g_string_append_printf (file_string, "<%s", (STREQ (tag_elements[TYPE_TXT], "item")) ? "item" : "action name");
            if (STREQ (tag_elements[TYPE_TXT], "item")) {
                if (tag_elements[MENU_ELEMENT_TXT]) {
                    g_string_append_printf (file_string, " label=\"%s\"", tag_elements[MENU_ELEMENT_TXT]);
                }
                if (tag_elements[ICON_PATH_TXT]) { // icon="" is saved back.
                    g_string_append_printf (file_string, " icon=\"%s\"", tag_elements[ICON_PATH_TXT]);
                }
            }
            else { // Action
                g_string_append_printf (file_string, "=\"%s\"", tag_elements[MENU_ELEMENT_TXT]);
            }
        }
        g_string_append_printf (file_string, "%s>\n", 
                                ((saving_stage == SM_ROOT_MENU && type == SM_MENU_OR_PIPE_MENU) || 
                                 !gtk_tree_model_iter_has_child (local_model, local_iter)) ? "/" : "");
    }
    else { // Separator
        if (tag_elements[MENU_ELEMENT_TXT]) {
            g_string_append_printf (file_string, "<separator label=\"%s\"/>\n", tag_elements[MENU_ELEMENT_TXT]);
        }
        else {
            g_string_append (file_string, "<separator/>\n");
        }
    }

    g_output_stream_write (menu_file_output_stream, file_string->str, strlen (file_string->str), NULL, error);

    if (G_LIKELY (!(*error))) {
        return;
    }
    else {
        longjmp (buf, 1);
    }
}

/* 

    Gets all values of a row needed for saving and escapes their special characters.

*/

static void get_field_values (gchar        **txt_fields_array, 
                              GtkTreeModel  *current_model, 
                              GtkTreeIter   *current_iter)
{
    for (guint8 txt_cnt = 0; txt_cnt < NUMBER_OF_TXT_FIELDS; ++txt_cnt) {
        g_autofree gchar *unescaped_save_txt;

        gtk_tree_model_get (current_model, current_iter, txt_cnt + TS_ICON_PATH, &unescaped_save_txt, -1);
        txt_fields_array[txt_cnt] = (unescaped_save_txt) ? g_markup_escape_text (unescaped_save_txt, -1) : NULL;
    }
}

/* 

    Iterates through the elements of a menu or item for processing 
    of each node and generating lines for the xml file.

*/

static gboolean treestore_save_process_iteration (GtkTreeModel     *filter_model, 
                                                  GtkTreePath      *filter_path, 
                                                  GtkTreeIter      *filter_iter, 
                                                  SaveMenuArgsData *save_menu_args)
{
    GOutputStream *menu_file_output_stream = save_menu_args->menu_file_output_stream;
    GError **error = save_menu_args->error;
    gchar **save_txts = save_menu_args->save_txts;

    const gint filter_path_depth = gtk_tree_path_get_depth (filter_path);

    gint path_depth_cnt;

    save_menu_args->filter_iter[SM_CURRENT] = *filter_iter;

    // Write closing tag(s).
    if (filter_path_depth < save_menu_args->filter_path_depth_prev) {
        if (G_UNLIKELY (!(closing_tags (FALSE, filter_model, save_menu_args)))) { // FALSE = filter iteration not yet completed.
            longjmp (buf, 1);
        }
    }

    get_field_values (save_txts, filter_model, filter_iter);

    /*
        Create leading whitespace for indenting.

        The number of indentations for elements outside and inside the root menu differs.
        A menu with the menu ID "root_menu" has to be written, into which all root elements are packed.
        Since the root menu itself is not visualized inside the tree view, there has to be an additional indentation 
        written into the menu file for all those elements.

        --- MENU DEFINITIONS ---

        <menu id="menu"> -> Path depth counting starts from this level
            <item label="menu">
                <action name="Exit">
                    <prompt>yes</prompt>
                </action>
            </item>
        </menu>

        --- ROOT MENU ---

        <menu id="root_menu"> -> Not visualized inside the tree view
            <menu id="menu"> -> Path depth counting starts from this level
                <item label="root_menu1">
                    <action name="Exit">
                        <prompt>yes</prompt>
                    </action>
                </item>
            </menu>
    */
    gchar *indentation = (ks.settings.tabs_for_indentations) ? "\t" : "    ";

    for (path_depth_cnt = (gint) (save_menu_args->saving_stage == SM_MENUS); // TRUE = 1, FALSE = 0.
         path_depth_cnt <= filter_path_depth;
         ++path_depth_cnt) {
        if (G_UNLIKELY (!(g_output_stream_write (menu_file_output_stream, indentation, strlen (indentation), NULL, error)))) {
            longjmp (buf, 1);
        }
    }

    if (streq_any (save_txts[TYPE_TXT], "menu", "pipe menu", NULL)) {
        write_tag (SM_MENUS, SM_FILTER_LEVEL, save_txts, filter_model, 
                   filter_iter, SM_MENU_OR_PIPE_MENU, save_menu_args);
    }
    else if (streq_any (save_txts[TYPE_TXT], "item", "action", "separator", NULL)) {
        write_tag (SM_IND_OF_SAVING_STAGE, SM_IND_OF_LEVEL, 
                   save_txts, filter_model, filter_iter, 
                   STREQ (save_txts[TYPE_TXT], "separator") ? SM_SEPARATOR : SM_ITEM_OR_ACTION, 
                   save_menu_args);
    }
    else { // Options
        // Option with value or "startupnotify" option block with child(ren)
        if (NOT_NULL_AND_NOT_EMPTY (save_txts[VALUE_TXT]) || 
            gtk_tree_model_iter_has_child (filter_model, filter_iter)) {
            if (STREQ (save_txts[TYPE_TXT], "option")) {
                if (G_UNLIKELY (!(g_output_stream_printf (menu_file_output_stream, NULL, NULL, error, 
                                                          "<%s>%s</%s>\n", save_txts[MENU_ELEMENT_TXT], 
                                                                           save_txts[VALUE_TXT],
                                                                           save_txts[MENU_ELEMENT_TXT])))) {
                    longjmp (buf, 1);
                }
            }
            else if (G_UNLIKELY (!(g_output_stream_write (menu_file_output_stream, "<startupnotify>\n", 
                                                          strlen ("<startupnotify>\n"), NULL, error)))) {
                longjmp (buf, 1);
            }
        }
        /*
            Option without specified value or "startupnotify" option block without child(ren) 
            (self-closing tag written in both cases)
        */
        else if (G_UNLIKELY (!(g_output_stream_printf (menu_file_output_stream, NULL, NULL, error, 
                                                      "<%s/>\n", save_txts[MENU_ELEMENT_TXT])))) {
            longjmp (buf, 1);
        }
    }

    save_menu_args->filter_path_depth_prev = filter_path_depth;
    save_menu_args->filter_iter[SM_PREV] = *filter_iter;

    // Cleanup
    free_elements_of_static_string_array (save_txts, NUMBER_OF_TXT_FIELDS, TRUE);

    return (*error != NULL); // FALSE = continue iterating.
}

/* 

    Processes all subrows of a menu or item.

*/

static void process_menu_or_item (GtkTreeIter      *process_iter, 
                                  SaveMenuArgsData *save_menu_args) 
{
    // g_autoptr can't be used here, since auto cleanup does not work when using longjmp ().
    GtkTreePath *path = gtk_tree_model_get_path (ks.ts_model, process_iter);
    GtkTreeModel **filter_model = &(save_menu_args->filter_model);
    GError **error = save_menu_args->error;

    *filter_model = gtk_tree_model_filter_new (ks.ts_model, path);

    // Cleanup
    gtk_tree_path_free (path);

    gtk_tree_model_foreach (*filter_model, (GtkTreeModelForeachFunc) treestore_save_process_iteration, save_menu_args);

    if (G_UNLIKELY (*error || !(closing_tags (TRUE, *filter_model, save_menu_args)))) { // TRUE = filter iteration completed.
        longjmp (buf, 1);
    }

    save_menu_args->filter_path_depth_prev = 0; // Reset

    // Cleanup
    g_clear_pointer (filter_model, (GDestroyNotify) g_object_unref);
}

/*

    If a menu that contained invisible orphaned menus is saved with the "Keep Root Menu Separate in Saved Menu File" being disabled, 
    this filter callback function renders the children of such menus visible, provided that they have a label. 
    If they don't have a label, child menus, pipe menus, and items will remain invisible and are marked as such. The same applies 
    if a former invisible orphaned menu didn't have a label; it is now no longer orphaned, but due to the missing label it 
    remains invisible.

*/

static gboolean visualize_invisible_children (GtkTreeModel *filter_model, 
                                              GtkTreePath  *filter_path, 
                                              GtkTreeIter  *filter_iter, 
                                              gpointer      root_has_label_ptr)
{
    g_autofree gchar *type_txt;

    gtk_tree_model_get (filter_model, filter_iter, TS_TYPE, &type_txt, -1);

    if (streq_any (type_txt, "menu", "pipe menu", "item", "separator", NULL)) {
        g_autofree gchar *menu_element_txt;
        GtkTreeIter model_iter;
        const gboolean root_has_label = GPOINTER_TO_UINT (root_has_label_ptr);
        const gboolean ancestor_has_no_label = check_if_invisible_ancestor_exists (filter_model, filter_path);

        gtk_tree_model_get (filter_model, filter_iter, TS_MENU_ELEMENT, &menu_element_txt, -1);

        gtk_tree_model_filter_convert_iter_to_child_iter ((GtkTreeModelFilter *) filter_model, &model_iter, filter_iter);

        if (ancestor_has_no_label || !root_has_label) {
            gtk_tree_store_set (ks.treestore, &model_iter, TS_ELEMENT_VISIBILITY, "invisible dsct. of invisible menu", -1);
        }
        else { 
            gtk_tree_store_set (ks.treestore, &model_iter, TS_ELEMENT_VISIBILITY, 
                                ((menu_element_txt) || STREQ (type_txt, "separator")) ? "visible" : 
                                ((streq_any (type_txt, "menu", "pipe menu", NULL)) ? "invisible menu" : 
                                "invisible item"), 
                                -1);                
        }
    }

    return CONTINUE_ITERATING;
}

/* 

    Saves the currently edited menu.

*/

void save_menu (gchar *save_as_filename)
{
    gchar *preliminary_filename = (save_as_filename) ? save_as_filename : ks.filename;
    g_autoptr(GFile) menu_file = g_file_new_for_path (preliminary_filename);
    g_autoptr(GError) error = NULL;
    SaveMenuArgsData save_menu_args = {
        .save_txts = { NULL }
    };
    gchar *save_txts_toplevel[NUMBER_OF_TXT_FIELDS] = { NULL };

    GtkTreeIter save_menu_iter;
    gboolean valid;

    // Create menu file.

    g_autoptr(GFileOutputStream) menu_file_output_stream;

    if (G_UNLIKELY (!(menu_file_output_stream = g_file_replace (menu_file, NULL, ks.settings.create_backup, G_FILE_CREATE_NONE, NULL, &error)))) {
        g_autofree gchar *err_msg = g_strdup_printf (_("<b>An error occurred during the creation of the menu file</b> "
                                                       "<tt>%s</tt> <b>:</b>\n%s"), 
                                                     preliminary_filename, error->message);

        show_errmsg (err_msg);
    }

    // The program will return here in case of an error during the writing process.
    if (G_UNLIKELY (setjmp (buf))) {
        g_autofree gchar *err_msg = g_strdup_printf (_("<b>An error occurred during the menu file writing process:</b>\n%s"), 
                                                     error->message);

        show_errmsg (err_msg);
    }

    if (G_LIKELY (!error)) {
        save_menu_args = (SaveMenuArgsData) {
            .menu_file_output_stream = G_OUTPUT_STREAM (menu_file_output_stream), 
            .filter_model = NULL, 
            .error = &error, 
            .saving_stage = (ks.settings.separate_root_menu) ? SM_MENUS : SM_NO_SEPARATE_ROOT_MENU,
            .filter_path_depth_prev = 0
        };
    }
    else {
        // Cleanup
        // If save_menu is called directly, save_as_filename is NULL, thus g_free () will do effectively nothing.
        g_free (save_as_filename);
        /*
            Either the elemets of the string arrays have a value and can thus be freed, 
            or they don't and are set to NULL, which means that these functions do effectively nothing.
        */
        free_elements_of_static_string_array (save_txts_toplevel, NUMBER_OF_TXT_FIELDS, FALSE);
        free_elements_of_static_string_array (save_menu_args.save_txts, NUMBER_OF_TXT_FIELDS, FALSE);
        g_object_unref (save_menu_args.filter_model);

        return;
    }


    // --- Write menu file. ---


    const gchar *output_str = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<openbox_menu>\n";

    if (G_UNLIKELY (!(g_output_stream_write (G_OUTPUT_STREAM (menu_file_output_stream), 
                                             output_str, strlen (output_str), NULL, &error)))) {
        longjmp (buf, 1);
    }

    if (ks.settings.separate_root_menu) {
        gboolean at_least_one_menu_before_the_root_menu_written = FALSE; // Default

        // Menus
        valid = gtk_tree_model_get_iter_first (ks.ts_model, &save_menu_iter);
        while (valid) {
            get_field_values (save_txts_toplevel, ks.ts_model, &save_menu_iter);

            if (STREQ (save_txts_toplevel[TYPE_TXT], "menu") || 
                (STREQ (save_txts_toplevel[TYPE_TXT], "pipe menu") && 
                 STREQ (save_txts_toplevel[ELEMENT_VISIBILITY_TXT], "invisible orphaned menu"))) {
                // Write an additional newline if there is at least one menu to write before the root menu.
                if (!at_least_one_menu_before_the_root_menu_written) {
                    output_str = "\n";
                    if (G_UNLIKELY (!(g_output_stream_write (G_OUTPUT_STREAM (menu_file_output_stream), 
                                                             output_str, strlen (output_str), NULL, &error)))) {
                        longjmp (buf, 1);
                    }
                    at_least_one_menu_before_the_root_menu_written = TRUE;
                }
                write_tag (SM_MENUS, SM_TOPLEVEL, save_txts_toplevel, ks.ts_model, 
                           &save_menu_iter, SM_MENU_OR_PIPE_MENU, &save_menu_args);
                if (gtk_tree_model_iter_has_child (ks.ts_model, &save_menu_iter)) {
                    process_menu_or_item (&save_menu_iter, &save_menu_args);
                    if (G_UNLIKELY (!(g_output_stream_write (G_OUTPUT_STREAM (menu_file_output_stream), 
                                                             "</menu>\n", strlen ("</menu>\n"), NULL, &error)))) {
                        longjmp (buf, 1);
                    }
                }
            }
            valid = gtk_tree_model_iter_next (ks.ts_model, &save_menu_iter);

            free_elements_of_static_string_array (save_txts_toplevel, NUMBER_OF_TXT_FIELDS, TRUE);
        }

        // Root menu
        output_str = "\n<menu id=\"root-menu\" label=\"Openbox 3\">\n";
        if (G_UNLIKELY (!(g_output_stream_write (G_OUTPUT_STREAM (menu_file_output_stream), 
                                                 output_str, strlen (output_str), NULL, &error)))) {
            longjmp (buf, 1);
        }

        save_menu_args.saving_stage = SM_ROOT_MENU;

        valid = gtk_tree_model_get_iter_first (ks.ts_model, &save_menu_iter);
        while (valid) {
            get_field_values (save_txts_toplevel, ks.ts_model, &save_menu_iter);

            if (streq_any (save_txts_toplevel[TYPE_TXT], "menu", "pipe menu", NULL) && 
                !STREQ (save_txts_toplevel[ELEMENT_VISIBILITY_TXT], "invisible orphaned menu")) {
                write_tag (SM_ROOT_MENU, SM_TOPLEVEL, save_txts_toplevel, ks.ts_model, 
                           &save_menu_iter, SM_MENU_OR_PIPE_MENU, &save_menu_args);
            }
            else if (streq_any (save_txts_toplevel[TYPE_TXT], "item", "separator", NULL)) {
                write_tag (SM_ROOT_MENU, SM_IND_OF_LEVEL, save_txts_toplevel, ks.ts_model, &save_menu_iter, 
                           STREQ (save_txts_toplevel[TYPE_TXT], "item") ? SM_ITEM_OR_ACTION : SM_SEPARATOR, 
                           &save_menu_args);
                if (gtk_tree_model_iter_has_child (ks.ts_model, &save_menu_iter)) { // = non-empty item (= containing action(s))
                    process_menu_or_item (&save_menu_iter, &save_menu_args);
                }
            }

            valid = gtk_tree_model_iter_next (ks.ts_model, &save_menu_iter);

            free_elements_of_static_string_array (save_txts_toplevel, NUMBER_OF_TXT_FIELDS, TRUE);
        }
    }
    else { // No separate root menu
        output_str = "\n<menu id=\"root-menu\" label=\"Openbox 3\">\n";
        if (G_UNLIKELY (!(g_output_stream_write (G_OUTPUT_STREAM (menu_file_output_stream), 
                                                 output_str, strlen (output_str), NULL, &error)))) {
            longjmp (buf, 1);
        }

        valid = gtk_tree_model_get_iter_first (ks.ts_model, &save_menu_iter);
        while (valid) {
            get_field_values (save_txts_toplevel, ks.ts_model, &save_menu_iter);

            if (streq_any (save_txts_toplevel[TYPE_TXT], "menu", "pipe menu", NULL)) {
                write_tag (SM_NO_SEPARATE_ROOT_MENU, SM_IND_OF_LEVEL, save_txts_toplevel, ks.ts_model, &save_menu_iter, SM_MENU_OR_PIPE_MENU, &save_menu_args);
            }
            else {
                write_tag (SM_NO_SEPARATE_ROOT_MENU, SM_IND_OF_LEVEL, save_txts_toplevel, ks.ts_model, &save_menu_iter, 
                           (STREQ (save_txts_toplevel[TYPE_TXT], "item")) ? SM_ITEM_OR_ACTION : SM_SEPARATOR, 
                           &save_menu_args);
            }
            if (gtk_tree_model_iter_has_child (ks.ts_model, &save_menu_iter)) {
                process_menu_or_item (&save_menu_iter, &save_menu_args);
                if (STREQ (save_txts_toplevel[TYPE_TXT], "menu")) {
                    g_autofree gchar *indented_closing_menu_tag = g_strdup_printf ("%s</menu>\n", 
                                                                                   (ks.settings.tabs_for_indentations) ? "\t" : "    ");

                    if (G_UNLIKELY (!(g_output_stream_write (G_OUTPUT_STREAM (menu_file_output_stream), 
                                                             indented_closing_menu_tag, strlen (indented_closing_menu_tag), 
                                                             NULL, &error)))) {
                        longjmp (buf, 1);
                    }
                }
            }

            valid = gtk_tree_model_iter_next (ks.ts_model, &save_menu_iter);

            free_elements_of_static_string_array (save_txts_toplevel, NUMBER_OF_TXT_FIELDS, TRUE);
        }
    }

    output_str = "</menu>\n\n</openbox_menu>";
    if (G_UNLIKELY (!(g_output_stream_write (G_OUTPUT_STREAM (menu_file_output_stream), output_str, 
                                             strlen (output_str), NULL, &error)))) {
        longjmp (buf, 1);
    }

    g_autofree gchar *filename_prior_to_saving = g_strdup (ks.filename);

    if (save_as_filename) {
        set_filename_and_window_title (save_as_filename);
    }

    /* The stream has to be closed manually before reconfiguring Openbox, 
       otherwise the reconfiguration occasionally won't work. */ 
    if (G_UNLIKELY (!g_output_stream_close (G_OUTPUT_STREAM (menu_file_output_stream), NULL, &error))) {
        longjmp (buf, 1);
    }

    g_autofree gchar *standard_file_path = g_build_filename (ks.home_dir, ".config/openbox/menu.xml", NULL);

    if (STREQ (ks.filename, standard_file_path)) {
        g_autofree gchar *standard_output = NULL;

        if (G_UNLIKELY (!g_spawn_command_line_sync ("ps cax", &standard_output, NULL, NULL, &error))) {
            g_autofree gchar *err_msg = g_strdup_printf (_("<b>The menu has been saved, but it wasn't possible to obtain a list "
                                                           "of the running processes to determine if Openbox is currently running.\n\n"
                                                           "Error:</b> %s"), 
                                                         error->message);

            show_errmsg (err_msg);
        }
        else if (strstr (standard_output, "openbox")) {
            g_autofree gchar *standard_output = NULL;

            g_spawn_command_line_sync ("openbox --reconfigure", &standard_output, NULL, NULL, &error);

            // Error messages for openbox --reconfigure go to standard output.
            if (G_UNLIKELY (*standard_output || error)) {
                g_autofree gchar *err_msg = g_strdup_printf (_("<b>The menu has been saved, but there was a problem "
                                                               "with the reconfiguration of Openbox.\n\nError:</b> %s"),
                                                             (error) ? error->message : standard_output);

                show_errmsg (err_msg);
            }
        }
    }

    if (!(ks.settings.separate_root_menu)) {
        gboolean invisible_orphaned_menu_has_been_converted_to_visible = FALSE; // Default

        /* Invisible orphaned menus are placed at the bottom by default, so it would be conventient to start from 
           there and iterate to the top. However, since there is no gtk_tree_model_get_iter_last() (at least as 
           public function), the program starts from the top. This is hardly slowing down the program though, 
           because the iteration is only done over top level menu elements. */
        valid = gtk_tree_model_get_iter_first (ks.ts_model, &save_menu_iter);
        while (valid) {
            get_field_values (save_txts_toplevel, ks.ts_model, &save_menu_iter);

            if (G_UNLIKELY (streq_any (save_txts_toplevel[TYPE_TXT], "menu", "pipe menu", NULL) && 
                            STREQ (save_txts_toplevel[ELEMENT_VISIBILITY_TXT], "invisible orphaned menu"))) {
                gtk_tree_store_set (GTK_TREE_STORE (ks.ts_model), &save_menu_iter, TS_ELEMENT_VISIBILITY,
                                    (save_txts_toplevel[MENU_ELEMENT_TXT]) ? "visible" : "invisible menu", -1);

                g_autoptr(GtkTreePath) path = gtk_tree_model_get_path (ks.ts_model, &save_menu_iter);
                g_autoptr(GtkTreeModel) filter_model = gtk_tree_model_filter_new (ks.ts_model, path);

                gtk_tree_model_foreach (filter_model, (GtkTreeModelForeachFunc) visualize_invisible_children, 
                                        GUINT_TO_POINTER ((save_txts_toplevel[MENU_ELEMENT_TXT] != NULL)));

                if (!invisible_orphaned_menu_has_been_converted_to_visible) {
                    invisible_orphaned_menu_has_been_converted_to_visible = TRUE;
                }
            }

            valid = gtk_tree_model_iter_next (ks.ts_model, &save_menu_iter);

            free_elements_of_static_string_array (save_txts_toplevel, NUMBER_OF_TXT_FIELDS, TRUE);
        }

        if (invisible_orphaned_menu_has_been_converted_to_visible) {
            show_message_dialog (_("Former Invisible Orphaned Menus Are Now Visible"), "gtk-info", 
                                 _("This menu contained invisible orphaned menus. Since this menu has been saved "
                                   "right now with the \"Keep Root Menu Separate in Saved Menu File\" option "
                                   "being <b>disabled</b>, these menus have become part of the root menu now and, "
                                   "<b>provided that they have a label</b>, are therefore no longer invisible."), 
                                 _("_Close"));
        }
    }

    /* If the file is saved to the standard path .config/openbox/menu.xml, Kickshaw will open this file 
       automatically after a restart. It is thus not necessary to keep an autosave after saving in this case. */    
    if (G_LIKELY (g_regex_match_simple (".*\\.config/openbox/menu\\.xml$", ks.filename, 0, 0))) {
        delete_autosave ();
    }

    if (G_UNLIKELY (ks.restored_autosave_that_hasnt_been_saved_yet)) {
        ks.restored_autosave_that_hasnt_been_saved_yet = FALSE;
    }

    ks.last_save_pos = ks.pos_inside_undo_stack;

    // Reset values.
    memset (&ks.loading_process_edit_types, 0, sizeof ks.loading_process_edit_types);
    ks.change_done = FALSE;

    if (G_UNLIKELY (!(STREQ (filename_prior_to_saving, ks.filename)))) {
        // Replace the current undo stack item with a new one that contains the new file name.
        push_new_item_on_undo_stack (AFTER_SAVE);
    }
    // Change sensitivity of save button and remove additional save button tooltip texts, if they were shown.
    row_selected ();
}

/* 

    Asks for a file name prior to saving.

*/

void save_menu_as (void)
{
    GtkWidget *dialog;

    create_file_dialog (&dialog, SAVE_FILE);

    if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT) {
        gchar *new_filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog));

        gtk_widget_destroy (dialog);

        save_menu (new_filename);
    }
    else {
        gtk_widget_destroy (dialog);
    }
}
