/*
   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 "declarations_definitions_and_enumerations.h"
#include "find.h"

static gboolean add_occurrence_to_list (G_GNUC_UNUSED GtkTreeModel *local_model, 
                                                      GtkTreePath  *local_path, 
                                                      GtkTreeIter  *local_iter,
                                        G_GNUC_UNUSED gpointer      user_data);
static gboolean check_for_match (      GtkTreeIter *local_iter, 
                                 const guint8       column_number);
static inline void clear_list_of_rows_with_found_occurrences (void);
static gboolean ensure_visibility_of_match (G_GNUC_UNUSED GtkTreeModel *foreach_or_local_model,  
                                                          GtkTreePath  *foreach_or_local_path, 
                                                          GtkTreeIter  *foreach_or_local_iter,
                                            G_GNUC_UNUSED gpointer      user_data);

/* 

    Shows or hides (and resets the settings of the elements inside) the find grid.

*/

void show_or_hide_find_grid (void)
{
    if (gtk_widget_get_visible (ks.find_grid)) {
        gtk_widget_hide (ks.find_grid);
        g_string_assign (ks.search_term, "");
        clear_list_of_rows_with_found_occurrences ();
        gtk_entry_set_text (GTK_ENTRY (ks.find_entry), "");
        gtk_style_context_remove_class (gtk_widget_get_style_context (ks.find_entry), "user_intervention_requested");
        for (guint8 columns_cnt = 0; columns_cnt < COL_ELEMENT_VISIBILITY; ++columns_cnt) {
            gtk_style_context_remove_class (gtk_widget_get_style_context (ks.find_in_columns[columns_cnt]), 
                                            "user_intervention_requested");
        }
        gtk_style_context_remove_class (gtk_widget_get_style_context (ks.find_in_all_columns), "user_intervention_requested");
        gtk_widget_set_sensitive (ks.find_entry_buttons[BACK], FALSE);
        gtk_widget_set_sensitive (ks.find_entry_buttons[FORWARD], FALSE);
        gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (ks.find_in_all_columns), TRUE);
        gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (ks.special_options[MATCH_CASE]), FALSE);
        gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (ks.special_options[REGULAR_EXPRESSION]), TRUE);
        gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (ks.special_options[WHOLE_WORD]), FALSE);
        gtk_widget_queue_draw (GTK_WIDGET (ks.treeview)); // Force redrawing of treeview.
    }
    else {
        gtk_widget_show (ks.find_grid);
        gtk_widget_grab_focus (ks.find_entry);
    }
}

/*

    Sets the sensitivity of the forward and back buttons of the find grid according to the current tree path.

*/

void set_forward_and_back_buttons_of_find_grid (void)
{
    GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (ks.treeview));
    const gint number_of_selected_rows = gtk_tree_selection_count_selected_rows (selection);
    const gboolean sensitivity_off = (!ks.rows_with_found_occurrences || number_of_selected_rows != 1);
    g_autoptr(GtkTreePath) path = (!sensitivity_off) ? gtk_tree_model_get_path (ks.ts_model, &ks.iter) : NULL;

    gtk_widget_set_sensitive (ks.find_entry_buttons[BACK], 
                              !sensitivity_off && gtk_tree_path_compare (path, ks.rows_with_found_occurrences->data) > 0);
    gtk_widget_set_sensitive (ks.find_entry_buttons[FORWARD], 
                              !sensitivity_off && gtk_tree_path_compare (path, g_list_last (ks.rows_with_found_occurrences)->data) < 0);
}

/* 

    (De)activates all other column check buttons if "All columns" is (un)selected. 
    Search results are updated for any change of the chosen columns and criteria ("match case" and "regular expression").

*/

void find_buttons_management (const gchar *column_check_button_clicked)
{
    // TRUE if any find_in_columns check button or find_in_all_columns check button clicked.
    if (column_check_button_clicked) {
        find_in_columns_management (FALSE, ks.find_in_columns, ks.find_in_all_columns, 
                                    ks.handler_id_find_in_columns, column_check_button_clicked);
    }

    if (*ks.search_term->str) {
        const gboolean search_result_existed = (ks.rows_with_found_occurrences != NULL);

        if (create_list_of_rows_with_found_occurrences ()) {
            gtk_tree_model_foreach (ks.ts_model, (GtkTreeModelForeachFunc) ensure_visibility_of_match, NULL);

            if (!search_result_existed) {
                GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (ks.treeview));

                gtk_tree_selection_unselect_all (selection);
                gtk_tree_selection_select_path (selection, ks.rows_with_found_occurrences->data);
            }
        }

        set_forward_and_back_buttons_of_find_grid ();
    }

    gtk_widget_queue_draw (GTK_WIDGET (ks.treeview)); // Force redrawing of treeview (for highlighting of search results).
}

/* 

    Clears the list of rows so it can be rebuild later.

*/

static inline void clear_list_of_rows_with_found_occurrences (void) {
#if GLIB_CHECK_VERSION(2,64,0)
    g_clear_list (&ks.rows_with_found_occurrences, (GDestroyNotify) gtk_tree_path_free);
#else
    g_list_free_full (ks.rows_with_found_occurrences, (GDestroyNotify) gtk_tree_path_free);
    ks.rows_with_found_occurrences = NULL;
#endif
}

/* 

    Adds a row that contains a column matching the search term to a list.

*/

static gboolean add_occurrence_to_list (G_GNUC_UNUSED GtkTreeModel *local_model, 
                                                      GtkTreePath  *local_path, 
                                                      GtkTreeIter  *local_iter,
                                        G_GNUC_UNUSED gpointer      user_data)
{
    for (guint8 columns_cnt = 0; columns_cnt < COL_ELEMENT_VISIBILITY; ++columns_cnt) {
        if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ks.find_in_columns[columns_cnt])) && 
            check_for_match (local_iter, columns_cnt)) {
            // Row references are not used here, since the list is recreated everytime the treestore is changed.
            ks.rows_with_found_occurrences = g_list_prepend (ks.rows_with_found_occurrences, gtk_tree_path_copy (local_path));
            break;
        }
    }

    return CONTINUE_ITERATING;
}

/* 

    Creates a list of all rows that contain at least one cell with the search term.

    rows_with_found_occurrences is a global GList, the purpose of returning the pointer is that the function to 
    which is returned to can immediately check whether a list has been created or not, in the latter case the 
    return value is NULL.

*/

GList *create_list_of_rows_with_found_occurrences (void)
{
    clear_list_of_rows_with_found_occurrences ();
    gtk_tree_model_foreach (ks.ts_model, (GtkTreeModelForeachFunc) add_occurrence_to_list, NULL);

    return ks.rows_with_found_occurrences = g_list_reverse (ks.rows_with_found_occurrences);
}

/*

    Computes a final search string dependent on the search options "Regular expression" and "Whole word".

*/

gchar *compute_final_search_string (const gchar *string)
{
    g_autofree gchar *search_term_str_escaped = (gtk_toggle_button_get_active 
                                                    (GTK_TOGGLE_BUTTON (ks.special_options[REGULAR_EXPRESSION]))) ? 
                                                NULL : g_regex_escape_string (string, -1);
    const gboolean whole_word = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ks.special_options[WHOLE_WORD]));
    gchar *final_search_str = g_strconcat ((whole_word) ? "\\b(" : "", 
                                           (search_term_str_escaped) ? search_term_str_escaped : string, 
                                           (whole_word) ? ")\\b" : "", 
                                           NULL);

    return final_search_str;
}

/* 

    Checks for each column if it contains the search term.

*/

static gboolean check_for_match (      GtkTreeIter *local_iter, 
                                 const guint8       column_number)
{
    gboolean match_found = FALSE; // Default
    g_autofree gchar *current_column;

    gtk_tree_model_get (ks.ts_model, local_iter, column_number + TREEVIEW_COLUMN_OFFSET, &current_column, -1);

    if (current_column) {
        g_autofree gchar *final_search_str = compute_final_search_string (ks.search_term->str);

        if (g_regex_match_simple (final_search_str, current_column, 
                                  (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ks.special_options[MATCH_CASE]))) ? 
                                  0 : G_REGEX_CASELESS, 
                                  G_REGEX_MATCH_NOTEMPTY)) {
            match_found = TRUE;
        }
    }

    return match_found;
}

/* 

    If the search term is contained inside...
    ...a row whose parent is not expanded expand the latter.
    ...a column which is hidden show this column.

*/

static gboolean ensure_visibility_of_match (G_GNUC_UNUSED GtkTreeModel *foreach_or_local_model,  
                                                          GtkTreePath  *foreach_or_local_path, 
                                                          GtkTreeIter  *foreach_or_local_iter,
                                            G_GNUC_UNUSED gpointer      user_data)
{
    for (guint8 columns_cnt = 0; columns_cnt < COL_ELEMENT_VISIBILITY; ++columns_cnt) {
        if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ks.find_in_columns[columns_cnt])) && 
            check_for_match (foreach_or_local_iter, columns_cnt)) {
            if (gtk_tree_path_get_depth (foreach_or_local_path) > 1 && 
                !gtk_tree_view_row_expanded (GTK_TREE_VIEW (ks.treeview), foreach_or_local_path)) {
                gtk_tree_view_expand_to_path (GTK_TREE_VIEW (ks.treeview), foreach_or_local_path);
                gtk_tree_view_collapse_row (GTK_TREE_VIEW (ks.treeview), foreach_or_local_path);
            }
            if (!gtk_tree_view_column_get_visible (ks.columns[columns_cnt])) {
                guint8 menu_item_idx = (columns_cnt == COL_MENU_ID) ? M_SHOW_MENU_ID_COL : M_SHOW_EXECUTE_COL;

                if (ks.settings.show_menu_button) {
                    change_view_and_options_GMenu (ks.view_actions[menu_item_idx], g_variant_new_boolean (TRUE), NULL);
                }
                else {
                    gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (ks.MBar_view_menu_items[menu_item_idx]), TRUE);
                }
            }
        }
    }

    return FALSE;
}

/* 

    Runs a search on the entered search term.

*/

void run_search (void)
{
    if (G_UNLIKELY (!(check_if_regex_is_valid (ks.special_options[REGULAR_EXPRESSION], ks.find_entry)))) {
        return;
    }

    gboolean no_find_in_columns_buttons_clicked = TRUE; // Default

    guint8 columns_cnt;

    g_string_assign (ks.search_term, gtk_entry_get_text (GTK_ENTRY (ks.find_entry)));

    if (*ks.search_term->str) {
        gtk_style_context_remove_class (gtk_widget_get_style_context (ks.find_entry), "user_intervention_requested");
    }
    else {
        visually_indicate_request_for_user_intervention (ks.find_entry, ks.find_entry_css_provider);
    }

    for (columns_cnt = 0; columns_cnt < COL_ELEMENT_VISIBILITY; ++columns_cnt) {
        if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ks.find_in_columns[columns_cnt]))) {
            no_find_in_columns_buttons_clicked = FALSE;
            break;
        }
    }

    if (!(*ks.search_term->str) || no_find_in_columns_buttons_clicked) {
        if (no_find_in_columns_buttons_clicked) {
            for (columns_cnt = 0; columns_cnt < COL_ELEMENT_VISIBILITY; ++columns_cnt) {
                visually_indicate_request_for_user_intervention (ks.find_in_columns[columns_cnt], 
                                                                 ks.find_in_columns_css_providers[columns_cnt]);
            }
            visually_indicate_request_for_user_intervention (ks.find_in_all_columns, ks.find_in_all_columns_css_provider);
        }
        clear_list_of_rows_with_found_occurrences ();
    }
    else if (create_list_of_rows_with_found_occurrences ()) {
        GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (ks.treeview));

        gtk_tree_model_foreach (ks.ts_model, (GtkTreeModelForeachFunc) ensure_visibility_of_match, NULL);
        gtk_tree_selection_unselect_all (selection);
        gtk_tree_selection_select_path (selection, ks.rows_with_found_occurrences->data);
        /*
            There is no horizontical movement to a specific GtkTreeViewColumn; this is indicated by NULL.  
            Alignment arguments (row_align and col_align) aren't used; this is indicated by FALSE.
        */
        gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (ks.treeview), ks.rows_with_found_occurrences->data, NULL, FALSE, 0, 0);
    }

    set_forward_and_back_buttons_of_find_grid ();

    gtk_widget_queue_draw (GTK_WIDGET (ks.treeview)); // Force redrawing of treeview (for highlighting of search results).
}

/* 

    Enables the possibility to move between the found occurrences.

*/

void jump_to_previous_or_next_occurrence (gpointer direction_pointer)
{
    const gboolean forward = GPOINTER_TO_INT (direction_pointer);

    GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (ks.treeview));
    g_autoptr(GtkTreePath) path = gtk_tree_model_get_path (ks.ts_model, &ks.iter);
    GtkTreePath *path_of_occurrence;
    GtkTreeIter iter_of_occurrence;

    GList *rows_with_found_occurrences_loop;

    rows_with_found_occurrences_loop = (forward) ? ks.rows_with_found_occurrences : g_list_last (ks.rows_with_found_occurrences);
    // No for loop here since rows_with_found_occurrences_loop is supposed to be always != NULL. Checking for that would be superfluous.
    while (TRUE) {
        if ((forward) ? (gtk_tree_path_compare (path, rows_with_found_occurrences_loop->data) < 0) : 
            (gtk_tree_path_compare (path, rows_with_found_occurrences_loop->data) > 0)) {
            break;
        }
        rows_with_found_occurrences_loop = (forward) ? rows_with_found_occurrences_loop->next : rows_with_found_occurrences_loop->prev;
    }

    path_of_occurrence = rows_with_found_occurrences_loop->data;
    gtk_tree_model_get_iter (ks.ts_model, &iter_of_occurrence, path_of_occurrence);

    // Ensure_visibility_of_match is not called by gtk_tree_model_foreach; model is unused so this argument is set to NULL.
    ensure_visibility_of_match (NULL, path_of_occurrence, &iter_of_occurrence, NULL);

    gtk_tree_selection_unselect_all (selection);
    gtk_tree_selection_select_path (selection, path_of_occurrence);
    /*
        There is no horizontical movement to a specific GtkTreeViewColumn; this is indicated by NULL.  
        Alignment arguments (row_align and col_align) aren't used; this is indicated by FALSE.
    */
    gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (ks.treeview), path_of_occurrence, NULL, FALSE, 0, 0);
}
