/*
   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 <sys/stat.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 "auxiliary.h"

/*

    -------------------------------------------------------------------
      GENERAL - Auxiliary functions not necessarily bound to Kickshaw
    -------------------------------------------------------------------

*/


/* 

    Version comparison function used in case that glibc is not installed.

*/

#if !__GLIBC__
gint8 strverscmp_nonglibc (const gchar *version1, const gchar *version2) {
    guint major1 = 0, minor1 = 0, bugfix1 = 0;
    guint major2 = 0, minor2 = 0, bugfix2 = 0;

    sscanf (version1, "%u.%u.%u", &major1, &minor1, &bugfix1);
    sscanf (version2, "%u.%u.%u", &major2, &minor2, &bugfix2);

    if (major1 < major2) return -1;
    if (major1 > major2) return 1;
    if (minor1 < minor2) return -1;
    if (minor1 > minor2) return 1;
    if (bugfix1 < bugfix2) return -1;
    if (bugfix1 > bugfix2) return 1;
    return 0;
}
#endif

/* 

    Expands a row by retrieving the required path from a passed iterator.

*/

void expand_row_from_iter (GtkTreeIter *local_iter)
{
    g_autoptr(GtkTreePath) path = gtk_tree_model_get_path (ks.ts_model, local_iter);

    gtk_tree_view_expand_row (GTK_TREE_VIEW (ks.treeview), path, FALSE); // FALSE = just expand immediate children.
}

/* 

   Extracts a substring from a string by using a regular expression.

   This function returns a newly-allocated string that should be freed with g_free () after use.

*/

gchar *extract_substring_via_regex (const gchar *string, const gchar *regex_str)
{
    // No compile and match options, no return location for a GError.
    g_autoptr(GRegex) regex = g_regex_new (regex_str, 0, 0, NULL);
    g_autoptr(GMatchInfo) match_info; // match_info is created even if g_regex_match returns FALSE.
    gchar *match;

    g_regex_match (regex, string, 0, &match_info); // No match options.
    match = g_match_info_fetch (match_info, 0); // 0 = full text of the match.

    return match;
}

/* 

    Frees dyn. alloc. memory of all strings of a static string array.

*/

void free_elements_of_static_string_array (      gchar    **string_array, 
                                                 gint8      number_of_fields, 
                                           const gboolean   set_to_NULL)
{
    do {
        --number_of_fields; // x fields = string_array[0 ... x-1]

        g_free (string_array[number_of_fields]);

        if (set_to_NULL) {
            string_array[number_of_fields] = NULL;
        }
    } while (number_of_fields);
}

/* 

    Retrieves the current font so that it can be used with a Pango font description.

    This function returns a newly-allocated string that should be freed with g_free () after use.

*/

gchar *get_font_name (void)
{
    g_autofree gchar *font_and_size;
    gchar *font_name;

    g_object_get (gtk_settings_get_default (), "gtk-font-name", &font_and_size, NULL);
    font_name = extract_substring_via_regex (font_and_size, ".*(?=( .*)$)"); // e.g. Serif Bold Italic 12 -> Serif Bold Italic

    return font_name;
}

/* 

    Retrieves the current font size so that the size of the icon images can be adjusted to it.

*/

guint get_font_size (void)
{
    g_autofree gchar *font_str, *font_substr;
    guint font_size;

    g_object_get (gtk_settings_get_default (), "gtk-font-name", &font_str, NULL);
    font_substr = extract_substring_via_regex (font_str, "\\d+$"); // e.g. Sans 12 -> 12
    font_size = atoi (font_substr); 

    return font_size;
}

/* 

    Returns the time of the last modification of an icon image file as an RFC 3339 encoded string.

    Since memory has been allocated for it, the string should be freed with g_free () after use.

*/

gchar *get_modification_time_for_icon (const gchar *icon_path)
{
    g_autoptr(GFile) file = g_file_new_for_path (icon_path);
    g_autoptr(GError) error = NULL;
    g_autoptr(GFileInfo) file_info = g_file_query_info (file, G_FILE_ATTRIBUTE_TIME_MODIFIED, G_FILE_QUERY_INFO_NONE, NULL, &error);

    if (G_UNLIKELY (error)) {
        g_autofree gchar *err_msg = g_strdup_printf (_("<b>Could not retrieve the modification time for the icon</b> <tt>%s</tt> <b>!\n"
                                                       "Error:</b> %s"), 
                                                     icon_path, error->message);

        show_errmsg (err_msg);
    }

#if GLIB_CHECK_VERSION(2,62,0)
    g_autoptr(GDateTime) icon_modification_time = g_file_info_get_modification_date_time (file_info);
#else
    GTimeVal icon_modification_time;

    g_file_info_get_modification_time (file_info, &icon_modification_time);
#endif

#if GLIB_CHECK_VERSION(2,62,0)
    gchar *icon_modification_time_str = g_date_time_format_iso8601 (icon_modification_time);

    return icon_modification_time_str;
#else
    return g_time_val_to_iso8601 (&icon_modification_time);
#endif
}

/* 

    Sets an iterator for the toplevel of a given path.

*/

void get_toplevel_iter_from_path (GtkTreeIter *local_iter, 
                                  GtkTreePath *local_path)
{
    g_autofree gchar *toplevel_str = g_strdup_printf ("%i", gtk_tree_path_get_indices (local_path)[0]);

    gtk_tree_model_get_iter_from_string (ks.ts_model, local_iter, toplevel_str);
}

/* 

    Short replacement for the check if a string equals to one of several values
    (strcmp (x,y) == 0 || strcmp (x,z) == 0).

*/

gboolean streq_any (const gchar *string, ...)
{
    va_list arguments;

    va_start (arguments, string);

    while (TRUE) {
        const gchar *check = va_arg (arguments, gchar *);

        if (!check || STREQ (string, check)) {
            va_end (arguments); // Mandatory for safety and implementation/platform neutrality.

            /*
                check != NULL -> TRUE -> one of the string in the argument list is equal to the string to be compared against.
                check != NULL -> FALSE -> none of the strings in the argument list is equal to the string to be compared against.
            */
            return (check != NULL);
        }
    }
}

/* 

    Adds elements to a GPtrArray.

*/

void add_elements_to_ptr_array (GPtrArray *array, ...)
{
    va_list arguments;
    gpointer element;

    va_start (arguments, array);

    while ((element = va_arg (arguments, gpointer))) { // Parantheses avoid compiler warning
        g_ptr_array_add (array, element);
    }

    va_end (arguments); // Mandatory for safety and implementation/platform neutrality.
}

/*

    An equivalent of rm -rf.

    This function makes use of recursion. It is also a particularly suitable case 
    for using the g_autoptr () macro to keep the code tidy.

*/

gboolean rm_rf (GFile         *file, 
                GCancellable  *cancellable, 
                GError       **error)
{
    g_autoptr(GFileEnumerator) enumerator = g_file_enumerate_children (file, G_FILE_ATTRIBUTE_STANDARD_NAME, 
                                            G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
                                            cancellable, NULL);

    while (enumerator) {
        GFile *child;

        if (!(g_file_enumerator_iterate (enumerator, NULL, &child, cancellable, error))) {
            return FALSE;
        }
        if (!child) {
            break;
        }
        if (!(rm_rf (child, cancellable, error))) {
            return FALSE;
        }
    }

    return g_file_delete (file, cancellable, error);
}

/*

    Retrieves a text that is stored inside a GResource element.
    The memory for the returned string has been dynamically allocated and must be freed after use.

*/

gchar *get_txt_from_resources (      GResource *resources, 
                               const gchar     *path)
{
    GBytes *data = g_resource_lookup_data (resources, path, G_RESOURCE_LOOKUP_FLAGS_NONE, NULL);
    g_autoptr(GByteArray) gbarray = g_bytes_unref_to_array (data);
    gchar *resource = g_strdup (g_strchomp ((gchar *) gbarray->data));

    return resource;
}

/* 

    --------------------------------------------------------------------
      KICKSHAW-SPECIFIC - Auxiliary functions for use in Kickshaw only
    --------------------------------------------------------------------

*/

/*

    A save way to set the values of several bits of the settings bit field

*/

void set_settings (gint field, ...)
{
    va_list arguments;

    va_start (arguments, field);

    while (field != -1) {
        const gboolean value = va_arg (arguments, gint);

        switch (field) {
            case M_CREATE_BACKUP_BEFORE_OVERWRITING_MENU:
                ks.settings.create_backup = value;
                break;
            case M_USE_TABS_FOR_INDENTATIONS:
                ks.settings.tabs_for_indentations = value;
                break;
            case M_KEEP_ROOT_MENU_SEPARATE:
                ks.settings.separate_root_menu = value;
                break;
            case M_SORT_EXECUTE_AND_STARTUPN_OPTIONS:
                ks.settings.autosort_options = value;
                break;
            case M_NOTIFY_ABOUT_EXECUTE_OPT_CONVERSIONS:
                ks.settings.always_notify_about_opts_conv = value;
                break;
            case M_USE_HEADER_BAR:
                ks.settings.use_header_bar = value;
                break;
            case M_SHOW_MENU_BUTTON:
                ks.settings.show_menu_button = value;
                break;
        }

        field = va_arg (arguments, gint);
    }

    va_end (arguments);
}

/*

    Deletes all undo stack items.

*/

void delete_undo_stack_items (void)
{
    g_autoptr(GError) error = NULL;

    for (gint stack_file_cnt = 1; stack_file_cnt <= ks.number_of_undo_stack_items; ++stack_file_cnt) {
        g_autofree gchar *undo_stack_item_str = g_strdup_printf ("%s/%i", ks.tmp_path, stack_file_cnt);
        g_autoptr(GFile) undo_stack_item = g_file_new_for_path (undo_stack_item_str);

        if (G_UNLIKELY (!(g_file_delete (undo_stack_item, 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>!\n"
                                                           "Error:</b> %3$s"), 
                                                         stack_file_cnt, ks.tmp_path, error->message);

            show_errmsg (err_msg);

            break;
        }
    }
}

/*

    Deletes the autosave file from the home folder.

*/

void delete_autosave (void)
{
    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);

    /* The autosave file does not exist after ...
       - the immediate start of the program
       - a saving operation when no edit has been done yet. */
    if (g_file_query_exists (autosave_file, NULL)) {
        g_autoptr(GError) error = NULL;

        g_file_delete (autosave_file, NULL, &error);

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

            show_errmsg (err_msg);
        }
    }
}

/* 

    Refreshes the txt_fields array with the values of the currently selected row.

*/

void repopulate_txt_fields_array (void)
{
    // FALSE = Don't set array elements to NULL after freeing.
    free_elements_of_static_string_array (ks.txt_fields, NUMBER_OF_TXT_FIELDS, FALSE);
    gtk_tree_model_get (ks.ts_model, &ks.iter, 
                        TS_ICON_PATH, &ks.txt_fields[ICON_PATH_TXT],
                        TS_MENU_ELEMENT, &ks.txt_fields[MENU_ELEMENT_TXT],
                        TS_TYPE, &ks.txt_fields[TYPE_TXT],
                        TS_VALUE, &ks.txt_fields[VALUE_TXT],
                        TS_MENU_ID, &ks.txt_fields[MENU_ID_TXT],
                        TS_EXECUTE, &ks.txt_fields[EXECUTE_TXT],
                        TS_ELEMENT_VISIBILITY, &ks.txt_fields[ELEMENT_VISIBILITY_TXT],
                        -1);
}

/* 

    Creates a new label with some formattings.

*/

GtkWidget *new_label_with_formattings (const gchar    *label_txt, 
                                       const gboolean  wrap)
{
    return gtk_widget_new (GTK_TYPE_LABEL, "label", label_txt, "xalign", 0.0, "use-markup", TRUE, 
                          "wrap", wrap, "wrap-mode", PANGO_WRAP_WORD_CHAR, "max-width-chars", 1, NULL);
}

/* 

    Convenience function for the creation of a dialog window.

    The dialog can have up to three buttons. If less than three are needed, 
    the "button_txt_2" and/or "button_txt_3" argument should be set to NULL.
    If something else than a label text shall be added to the content area, 
    the "show_immediately" argument should be set to DONT_SHOW_IMMEDIATELY (=FALSE). 
    gtk_widget_show_all () should be called then after all additional widgets have been added to the content area.

*/

GtkWidget *create_dialog (      GtkWidget **dialog, 
                          const gchar      *dialog_title, 
                          const gchar      *icon_name, 
                          const gchar      *label_txt, 
                          const gchar      *button_txt_1, 
                          const gchar      *button_txt_2, 
                          const gchar      *button_txt_3, 
                          const gboolean    show_immediately)
{
    GtkWidget *content_area, *label;
    g_autofree gchar *label_txt_with_addl_border = g_strdup_printf ("\n%s\n", label_txt);

    *dialog = gtk_dialog_new_with_buttons (NULL, GTK_WINDOW (ks.window), GTK_DIALOG_MODAL, 
                                           button_txt_1, 1, button_txt_2, 2, button_txt_3, 3,
                                           NULL);

    set_header_bar_for_dialog (*dialog, dialog_title);

    content_area = gtk_dialog_get_content_area (GTK_DIALOG (*dialog));
    gtk_container_set_border_width (GTK_CONTAINER (content_area), 10);
    gtk_container_add (GTK_CONTAINER (content_area), gtk_image_new_from_icon_name (icon_name, GTK_ICON_SIZE_DIALOG));
    label = new_label_with_formattings (label_txt_with_addl_border, TRUE);
    gtk_widget_set_size_request (label, 570, -1);
    gtk_container_add (GTK_CONTAINER (content_area), label);
    if (show_immediately) {
        gtk_widget_show_all (*dialog);
    }
    gtk_window_set_position (GTK_WINDOW (*dialog), GTK_WIN_POS_CENTER_ALWAYS);

    return content_area;
}

/*

    Creates a formatted header bar for a dialog window.

*/

void set_header_bar_for_dialog (      GtkWidget *dialog, 
                                const gchar     *dialog_title_txt)
{
    GtkWidget *header_bar = gtk_header_bar_new ();
    g_autoptr(GtkCssProvider) header_bar_provider = gtk_css_provider_new ();

    gtk_header_bar_set_title (GTK_HEADER_BAR (header_bar), dialog_title_txt);
    gtk_window_set_titlebar (GTK_WINDOW (dialog), header_bar);

    gtk_css_provider_load_from_data (header_bar_provider, 
                                     "headerbar { min-height:0px;"
                                     "            padding-top:0px;"
                                     "            padding-bottom:0px; }",
                                     -1, NULL);
    gtk_style_context_add_provider (gtk_widget_get_style_context (header_bar), 
                                    GTK_STYLE_PROVIDER (header_bar_provider), 
                                    GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
}

/*

    Shows a simple dialog for noticing purposes.

*/

void show_message_dialog (const gchar *dialog_title, 
                          const gchar *icon_name, 
                          const gchar *label_txt, 
                          const gchar *button_txt)
{
    GtkWidget *dialog;

    create_dialog (&dialog, dialog_title, icon_name, label_txt, button_txt, NULL, NULL, SHOW_IMMEDIATELY);

    gtk_dialog_run (GTK_DIALOG (dialog));
    gtk_widget_destroy (dialog);
}

/* 

    Creates a file dialog for opening a new and saving an existing menu.

*/

void create_file_dialog (      GtkWidget **dialog, 
                         const gboolean    open)
{
    g_autofree gchar *menu_folder_str = (ks.filename) ? g_path_get_dirname (ks.filename) : g_get_current_dir ();
    g_autoptr(GFile) menu_folder = g_file_new_for_path (menu_folder_str);
    GtkFileFilter *file_filter;

    *dialog = gtk_file_chooser_dialog_new ((open) ? _("Open Openbox Menu File") : _("Save Openbox Menu File As…"), 
                                           GTK_WINDOW (ks.window),
                                           (open) ? GTK_FILE_CHOOSER_ACTION_OPEN : GTK_FILE_CHOOSER_ACTION_SAVE, 
                                           C_("Cancel|File Dialogue", "_Cancel"), GTK_RESPONSE_CANCEL,
                                           (open) ? _("_Open") : _("_Save"), GTK_RESPONSE_ACCEPT,
                                           NULL);

    if (g_file_query_exists (menu_folder, NULL)) {
        gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (*dialog), menu_folder_str);
    }

    file_filter = gtk_file_filter_new ();
    gtk_file_filter_set_name (file_filter, _("Openbox Menu Files"));
    gtk_file_filter_add_pattern (file_filter, "*.xml");
    gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (*dialog), file_filter);
}

/*

    Determines if a dark theme or a theme with a background/gradient is active

*/

void determine_theme_type (void)
{
    GtkStyleContext *window_context = gtk_widget_get_style_context (ks.window);
    g_autoptr(cairo_pattern_t) background_image = NULL; // Default

    gtk_style_context_get (window_context, GTK_STATE_FLAG_NORMAL, GTK_STYLE_PROPERTY_BACKGROUND_IMAGE, &background_image, NULL);

    const gboolean at_startup = (ks.red_hue == NULL);
          gchar *old_color = ks.red_hue;

    if (!background_image) {
        g_autoptr(GdkRGBA) fg_color, bg_color;

        gtk_style_context_get (window_context, GTK_STATE_FLAG_NORMAL, 
                               GTK_STYLE_PROPERTY_COLOR, &fg_color, 
                               GTK_STYLE_PROPERTY_BACKGROUND_COLOR, &bg_color, 
                               NULL);

        if (fg_color->red + fg_color->green + fg_color->blue < bg_color->red + bg_color->green + bg_color->blue) {
            ks.red_hue = "darkred";
            ks.dark_theme = FALSE;
        }
        else {
            ks.red_hue = "salmon";
            ks.dark_theme = TRUE;
        }

        ks.bg_image_or_gradient = FALSE;
    }
    else {
        ks.bg_image_or_gradient = TRUE;
        ks.red_hue = "red";
    }

    if (at_startup || !STREQ (ks.red_hue, old_color)) {
        ks.blue_hue = (ks.bg_image_or_gradient) ? "blue" : ((!ks.dark_theme) ? "darkblue" : "lightblue");

        g_string_printf (ks.label_field_txt, _(" Label (<span foreground='%s'>*</span>): "), ks.red_hue);
        if (G_UNLIKELY (g_slist_find_custom (ks.menu_ids, "", (GCompareFunc) strcmp))) {
            g_string_printf (ks.menu_ID_field_txt, _(" Menu ID (<span foreground='%s'>*</span>): "), ks.blue_hue);
        }
        else {
            g_string_assign (ks.menu_ID_field_txt, _(" Menu ID: "));
        }
        g_string_printf (ks.execute_field_txt, " Execute (<span foreground='%s'>*</span>): ", ks.red_hue);
        g_string_printf (ks.prompt_field_txt, " Prompt (<span foreground='%s'>*</span>): ", ks.red_hue);
        g_string_printf (ks.command_field_txt, " Command (<span foreground='%s'>*</span>): ", ks.red_hue);
        g_string_printf (ks.mandatory_label_txt, "(<span foreground='%s'>*</span>) = %s", ks.red_hue, _("mandatory"));
        g_string_printf (ks.mandatory_empty_string_txt, "(<span foreground='%s'>*</span>) = %s; "
                                                        "(<span foreground='%s'>*</span>) = %s",
                                                        ks.red_hue, _("mandatory"), ks.blue_hue, _("mand. as empty-string menu ID exists"));
        g_string_printf (ks.double_menu_id_label_txt, " <span foreground='%s'>!!</span> %s <span foreground='%s'>!!</span> ", 
                                                      ks.red_hue,  _("Menu ID already exists "), ks.red_hue);
    }

    if (!at_startup && !STREQ (ks.red_hue, old_color)) {
        gtk_label_set_markup (GTK_LABEL (ks.entry_labels[MENU_ELEMENT_OR_VALUE_ENTRY]), ks.label_field_txt->str);
        gtk_label_set_markup (GTK_LABEL (ks.entry_labels[MENU_ID_ENTRY]), ks.menu_ID_field_txt->str);
        gtk_label_set_markup (GTK_LABEL (ks.entry_labels[EXECUTE_ENTRY]), ks.execute_field_txt->str);
        if (!gtk_widget_get_visible (ks.enter_values_box)) {
            gtk_label_set_markup (GTK_LABEL (ks.options_labels[PROMPT]), ks.prompt_field_txt->str);
        }
        gtk_label_set_markup (GTK_LABEL (ks.options_labels[COMMAND]), ks.command_field_txt->str);
        gtk_label_set_markup (GTK_LABEL (ks.mandatory), 
                              (G_UNLIKELY (g_slist_find_custom (ks.menu_ids, "", (GCompareFunc) strcmp))) ? 
                              ks.mandatory_empty_string_txt->str : ks.mandatory_label_txt->str);
        gtk_label_set_markup (GTK_LABEL (ks.double_menu_id_label), ks.double_menu_id_label_txt->str);

        for (guint8 snotify_opts_cnt = 0; snotify_opts_cnt < NUMBER_OF_STARTUPNOTIFY_OPTS; ++snotify_opts_cnt) {
            if (gtk_widget_get_visible (ks.suboptions_labels[snotify_opts_cnt]) && 
                strstr (gtk_label_get_text (GTK_LABEL (ks.suboptions_labels[snotify_opts_cnt])), "*")) {
                g_autofree gchar *suboptions_label_txt = g_strdup_printf ("%s (<span foreground='%s'>*</span>): ", 
                                                                          ks.startupnotify_displayed_txts[snotify_opts_cnt], 
                                                                          ks.red_hue);

                gtk_label_set_markup (GTK_LABEL (ks.suboptions_labels[snotify_opts_cnt]), suboptions_label_txt);
            }
        }

        gtk_widget_queue_draw (ks.treeview); // Redraw treeview
    }
}

/* 

    Creates icon images for the case of a broken path or an invalid image file.

*/

void create_invalid_icon_imgs (void)
{
    for (guint8 invalid_icon_img_cnt = 0; invalid_icon_img_cnt < NUMBER_OF_INVALID_ICON_IMGS; ++invalid_icon_img_cnt) {
        // This becomes only true if the font size or icon theme have already been changed during the runtime of this program.
        if (G_UNLIKELY (ks.invalid_icon_imgs[invalid_icon_img_cnt])) {
            g_object_unref (ks.invalid_icon_imgs[invalid_icon_img_cnt]);
        }

        const gchar *icon_name = (invalid_icon_img_cnt == INVALID_PATH_ICON) ? "dialog-question" : "image-missing";
        g_autoptr(GdkPixbuf) invalid_icon_img_pixbuf_dialog_size = gtk_icon_theme_load_icon (gtk_icon_theme_get_default (), 
                                                                                             icon_name, 48, 
                                                                                             GTK_ICON_LOOKUP_FORCE_SIZE, 
                                                                                             NULL);

        ks.invalid_icon_imgs[invalid_icon_img_cnt] = gdk_pixbuf_scale_simple (invalid_icon_img_pixbuf_dialog_size, 
                                                                              ks.font_size + 10, ks.font_size + 10, 
                                                                              GDK_INTERP_BILINEAR);
    }
}

/* 

    Checks expansion statuses of nodes to determine 
    - how to set the sensivity of the application menu items and toolbar buttons for the expansion and collapse of all nodes.

    In this case this function has been called after a selection.
    It is only necessary to check whether there is at least one expanded and collapsed node, 
    regardless of their position inside the treeview.
    at_least_one_is_expanded and at_least_one_is_collapsed are set to TRUE, if the respective conditions apply.

    - which expansion and collapse options to add to the context menu 
    (expand recursively, expand only immediate children and collapse).

    In this case this function has been called after one or more rows, all of them containing at least one child, 
    were rightlicked.
    It has to be checked whether an immediate child of a row is expanded. If this applies to one of the selected rows 
    or at least one of the selected rows is currently not expanded, the option to expand only the immediate children is 
    added later to the context menu.
    The second check is for whether there is at least one collapsed node, since if one is collapsed it should 
    be possible to recursively expand all nodes. To determine whether the collapse option should be added it is 
    sufficient to check if the root node is expanded, which is not done here.
    at_least_one_imd_ch_is_collapsed and at_least_one_is_collapsed are set to TRUE, if the respective conditions apply.

*/

gboolean check_expansion_statuses_of_nodes (GtkTreeModel *foreach_or_filter_model, 
                                            GtkTreePath  *foreach_or_filter_path, 
                                            GtkTreeIter  *foreach_or_filter_iter, 
                                            gpointer      expansion_statuses_of_nodes_ptr)
{
    b_ExpansionStatuses *expansion_statuses_of_nodes = (b_ExpansionStatuses *) expansion_statuses_of_nodes_ptr;
    g_autoptr(GtkTreePath) model_path = NULL; // Default

    if (gtk_tree_model_iter_has_child (foreach_or_filter_model, foreach_or_filter_iter)) {
        if (ks.ts_model != foreach_or_filter_model) { // = filter, called from create_context_menu ()
            // The path of the model, not filter model, is needed to check whether the row is expanded.
            model_path = gtk_tree_model_filter_convert_path_to_child_path ((GtkTreeModelFilter *) foreach_or_filter_model, 
                                                                           foreach_or_filter_path);
        }

        if (gtk_tree_view_row_expanded (GTK_TREE_VIEW (ks.treeview), (model_path) ? model_path : foreach_or_filter_path)) {
            if (!model_path) { // = called from row_selected ()
                expansion_statuses_of_nodes->at_least_one_is_expanded = TRUE;
            }
            else if (gtk_tree_path_get_depth (foreach_or_filter_path) == 1) {
                expansion_statuses_of_nodes->at_least_one_imd_ch_is_expanded = TRUE;
            }
        }
        else {
            expansion_statuses_of_nodes->at_least_one_is_collapsed = TRUE;
        }
    }

    // Iterate only as long as not for all queried statuses a positive match has been found.
    return (((model_path) ? expansion_statuses_of_nodes->at_least_one_imd_ch_is_expanded : 
                            expansion_statuses_of_nodes->at_least_one_is_expanded) && 
                            expansion_statuses_of_nodes->at_least_one_is_collapsed);
}

/* 

    Checks for existing options of an Execute action or startupnotify option.

*/

void check_for_existing_options (      GtkTreeIter  *parent, 
                                 const guint8        number_of_opts, 
                                 const gchar       **options_array, 
                                       gboolean     *opts_exist)
{
    GtkTreeIter iter_loop;

    for (gint ch_cnt = 0; ch_cnt < gtk_tree_model_iter_n_children (ks.ts_model, parent); ++ch_cnt) {
        g_autofree gchar *menu_element_txt_loop;

        gtk_tree_model_iter_nth_child (ks.ts_model, &iter_loop, parent, ch_cnt);
        gtk_tree_model_get (ks.ts_model, &iter_loop, TS_MENU_ELEMENT, &menu_element_txt_loop, -1);

        for (guint8 opts_cnt = 0; opts_cnt < number_of_opts; ++opts_cnt) {
            if (STREQ (menu_element_txt_loop, options_array[opts_cnt])) {
                opts_exist[opts_cnt] = TRUE;
            }
        }
    }
}

/* 

    Looks for invisible descendants of a row.

*/

gboolean check_if_invisible_descendant_exists (              GtkTreeModel *filter_model, 
                                               G_GNUC_UNUSED GtkTreePath  *filter_path,
                                                             GtkTreeIter  *filter_iter, 
                                                             gboolean     *at_least_one_descendant_is_invisible)
{
    g_autofree gchar *menu_element_txt_loop, 
                     *type_txt_loop;

    gtk_tree_model_get (filter_model, filter_iter, 
                        TS_MENU_ELEMENT, &menu_element_txt_loop, 
                        TS_TYPE, &type_txt_loop, 
                        -1);

    *at_least_one_descendant_is_invisible = !menu_element_txt_loop && !STREQ (type_txt_loop, "separator");

    // Stop iterating if an invisible descendant has been found.
    return *at_least_one_descendant_is_invisible;
}

/* 

    Looks for invisible ancestors of a given path and
    returns the status of the elements visibility column for the first found one.

*/

guint8 check_if_invisible_ancestor_exists (GtkTreeModel *local_model, 
                                           GtkTreePath  *local_path)
{
    if (gtk_tree_path_get_depth (local_path) == 1) {
        return NONE_OR_VISIBLE_ANCESTOR;
    }

    guint8 visibility_of_ancestor;

    g_autoptr(GtkTreePath) path_loop = gtk_tree_path_copy (local_path);

    do {
        g_autofree gchar *element_ancestor_visibility_txt_loop;
        GtkTreeIter iter_loop;

        gtk_tree_path_up (path_loop);
        gtk_tree_model_get_iter (local_model, &iter_loop, path_loop);
        gtk_tree_model_get (local_model, &iter_loop, TS_ELEMENT_VISIBILITY, &element_ancestor_visibility_txt_loop, -1);

        if (G_UNLIKELY (element_ancestor_visibility_txt_loop && 
                        g_str_has_prefix (element_ancestor_visibility_txt_loop, "invisible"))) {
            visibility_of_ancestor = (g_str_has_suffix  (element_ancestor_visibility_txt_loop, "orphaned menu")) ? 
                                      INVISIBLE_ORPHANED_ANCESTOR : INVISIBLE_ANCESTOR;

            return visibility_of_ancestor;
        }
    } while (gtk_tree_path_get_depth (path_loop) > 1);

    return NONE_OR_VISIBLE_ANCESTOR;
}

/* 

    Adds a row that contains an icon to a list.

*/

gboolean add_icon_occurrence_to_list (G_GNUC_UNUSED GtkTreeModel *foreach_model, 
                                                    GtkTreePath  *foreach_path,
                                                    GtkTreeIter  *foreach_iter,
                                      G_GNUC_UNUSED gpointer      user_data)
{
    g_autoptr(GdkPixbuf) icon;

    gtk_tree_model_get (ks.ts_model, foreach_iter, TS_ICON_IMG, &icon, -1);
    if (icon) {
        ks.rows_with_icons = g_slist_prepend (ks.rows_with_icons, gtk_tree_path_copy (foreach_path));
    }

    return CONTINUE_ITERATING;
}

/* 

    (De)activates all other column check buttons if "All columns" is (un)selected. 

*/

void find_in_columns_management (const gboolean    origin_is_fr, 
                                       GtkWidget **in_columns, 
                                       GtkWidget  *in_all_columns, 
                                       gulong     *handler_ids, 
                                 const gchar      *column_check_button_clicked)
{
    const gboolean all_columns_check_button_clicked = STREQ (column_check_button_clicked, "all");
    const gboolean marking_active = gtk_style_context_has_class (gtk_widget_get_style_context (in_all_columns), 
                                                                 "user_intervention_requested");

    if (marking_active || all_columns_check_button_clicked) {
        const gboolean find_in_all_activated = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (in_all_columns));

        for (guint8 columns_cnt = 0; columns_cnt < COL_ELEMENT_VISIBILITY; ++columns_cnt) {
            if (origin_is_fr && columns_cnt == COL_TYPE) {
                continue;
            }
            if (all_columns_check_button_clicked) {
                g_signal_handler_block (in_columns[columns_cnt], handler_ids[columns_cnt]);
                gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (in_columns[columns_cnt]), find_in_all_activated);
                g_signal_handler_unblock (in_columns[columns_cnt], handler_ids[columns_cnt]);
                gtk_widget_set_sensitive (in_columns[columns_cnt], !find_in_all_activated);
            }
            if (marking_active) {
                gtk_style_context_remove_class (gtk_widget_get_style_context (in_columns[columns_cnt]), 
                                                "user_intervention_requested");
            }
        }
        if (marking_active) {
            gtk_style_context_remove_class (gtk_widget_get_style_context (in_all_columns), "user_intervention_requested");
        }
    }
}

/*

    If regular expressions are activated, check if the regular expression is valid. If not, an error message will be displayed.

*/

gboolean check_if_regex_is_valid (GtkWidget *regex_checkbox, GtkWidget *entry_field)
{
    if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (regex_checkbox))) {
        g_autoptr(GError) error = NULL;
        g_autoptr(GRegex) regex = g_regex_new (gtk_entry_get_text (GTK_ENTRY (entry_field)), 0, 0, &error);

        if (G_UNLIKELY (error)) {
            g_autofree gchar *err_msg = g_strdup_printf (_("<b>The regular expression that has been entered is invalid.\n"
                                                           "Error:</b> %s"), error->message);

            show_errmsg (err_msg);           

            return FALSE;
        }
    }

    return TRUE;
}

/* 

    Gives widget a red border (entry field) or background color (checkbox) to indicate 
    that a mandatory input or setting is missing or there is something wrong with existing data. 

*/

void visually_indicate_request_for_user_intervention (GtkWidget      *widget, 
                                                      GtkCssProvider *css_provider)
{
    GtkStyleContext *context = gtk_widget_get_style_context (widget);

    if (!gtk_style_context_has_class (context, "user_intervention_requested")) {
        gtk_style_context_add_class (context, "user_intervention_requested");
        gtk_css_provider_load_from_data (css_provider, (GTK_IS_ENTRY (widget)) ? 
        ".user_intervention_requested { border:darkred solid 2px; }" : 
        ".user_intervention_requested { color:white; background:darkred; }", 
        -1, NULL);
    }
}

/*

    Checks if Kickshaw is installed somewhere and returns the command suiting for a restart.

*/

const gchar *installed_or_local (void)
{
    g_auto(GStrv) paths = g_strsplit (g_getenv ("PATH"), ":", -1);
    guint paths_idx = 0;
    struct stat sb;
    const gchar *command = "./kickshaw"; // Default

    while (paths[paths_idx]) {
        g_autofree gchar *path_with_iconv = g_strconcat (paths[paths_idx], "/kickshaw", NULL);

        if (stat (path_with_iconv, &sb) == 0) {
            command = "kickshaw";

            break;
        }

        ++paths_idx;
    }

    return command;
}
