/*
   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 the Find grid, or hides it (which also resets all its input fields and settings).

*/

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);
    }
}

/*

    Updates the sensitivity of the Find grid’s forward and back buttons based on the current selection.

*/

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);
}

/* 

    Toggles all other column check buttons when "All columns" is (un)checked. 
    Updates search results whenever the selected columns or criteria 
    ("match case" and "regular expression") change.

*/

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 matching rows so it can be rebuilt 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 containing a match in any selected column to the list of occurrences.

*/

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)) {
		    // Tree row references are not used here; the list is rebuilt whenever the tree store changes.
            ks.rows_with_found_occurrences = g_list_prepend (ks.rows_with_found_occurrences, gtk_tree_path_copy (local_path));
            break;
        }
    }

    return CONTINUE_ITERATING;
}

/* 

    Builds a list of all rows containing at least one cell that matches the search term.

    The global list ks.rows_with_found_occurrences is returned so the caller 
    can check immediately whether it contains any matches.

*/

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);
}

/*

    Builds the final search string according to the "Regular expression" and "Whole word" options.

*/

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 whether the specified column of a row contains the search term.

*/

static gboolean check_for_match (      GtkTreeIter *local_iter, 
                                 const guint8       column_number)
{
    gboolean match_found = FALSE; // Default value.
    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 found:
    - Expand the parent if it is collapsed.
    - Show the column if it is currently hidden.

*/

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 for 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 value.

    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);
        /*
            No horizontal scrolling to a specific GtkTreeViewColumn (NULL).  
            Alignment arguments (row_align, col_align) are unused (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 navigation between 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 is needed here; the list is guaranteed to be non-NULL.
    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 called directly here; model argument is unused and 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);
    /*
        No horizontal scrolling to a specific GtkTreeViewColumn (NULL).  
        Alignment arguments (row_align, col_align) are unused (FALSE).
    */
    gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (ks.treeview), path_of_occurrence, NULL, FALSE, 0, 0);
}
