// SPDX-License-Identifier: GPL-2.0-or-later
/** \file
 * CommandPalette: Class providing Command Palette feature
 */
/* Author:
 * Abhay Raj Singh <abhayonlyone@gmail.com>
 *
 * Copyright (C) 2020 Authors
 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
 */

#include "command-palette.h"

#include <cassert>
#include <iostream>
#include <map>
#include <stdexcept>
#include <string_view>
#include <sigc++/adaptors/bind.h>
#include <sigc++/functors/mem_fun.h>

#include <glibmm/error.h>
#include <glibmm/i18n.h>
#include <glibmm/main.h>
#include <glibmm/variant.h>
#include <giomm/action.h>
#include <gtkmm/adjustment.h>
#include <gtkmm/box.h>
#include <gtkmm/builder.h>
#include <gtkmm/button.h>
#include <gtkmm/label.h>
#include <gtkmm/listbox.h>
#include <gtkmm/listboxrow.h>
#include <gtkmm/recentmanager.h>
#include <gtkmm/scrolledwindow.h>
#include <gtkmm/searchbar.h>
#include <gtkmm/searchentry.h>
#include <gtkmm/window.h>

#include "desktop.h"
#include "document.h"
#include "file.h"
#include "inkscape.h"
#include "message-stack.h"
#include "preferences.h"
#include "selection.h"
#include "include/glibmm_version.h"
#include "inkscape-application.h"
#include "inkscape-window.h"
#include "io/resource.h"
#include "ui/builder-utils.h"
#include "ui/controller.h"
#include "ui/util.h"
#include "util/callback-converter.h"

using Inkscape::UI::create_builder;
using Inkscape::UI::get_widget;

namespace Inkscape::UI::Dialog {

CommandPalette::CommandPalette()
    : _builder(create_builder("command-palette-main.glade"))
    , _CPBase              (get_widget<Gtk::Box>(_builder, "CPBase"))
    , _CPListBase          (get_widget<Gtk::Box>(_builder, "CPListBase"))
    , _CPFilter            (get_widget<Gtk::SearchEntry>(_builder, "CPFilter"))
    , _CPSuggestions       (get_widget<Gtk::ListBox>(_builder, "CPSuggestions"))
    , _CPHistory           (get_widget<Gtk::ListBox>(_builder, "CPHistory"))
    , _CPSuggestionsScroll (get_widget<Gtk::ScrolledWindow>(_builder, "CPSuggestionsScroll"))
    , _CPHistoryScroll     (get_widget<Gtk::ScrolledWindow>(_builder, "CPHistoryScroll"))
{
    // TODO: Move to a test program.
    test_sort();

    // TODO: Customise on user language RTL, LTR or better user preference
    _CPBase.set_halign(Gtk::ALIGN_CENTER);
    _CPBase.set_valign(Gtk::ALIGN_START);

    // Close the CommandPalette when the toplevel Window receives an Escape key press.
    // & also when the focused widget of said window is no longer a descendent of the Palette.
    /* Iʼve done it like so because changing ::key-*-events to GtkEventControllerKey
     * resulted in a LOT of weird stuff happening, and essential presses being lost.
     * and even before changing to a controller, itʼs nice to have 1 handler, not 3!
     * Itʼd probably make sense to move this to the main window when thereʼs time */
    // TODO: GTK4: can maybe move this back to self once Windows donʼt intercept/forward/etc key events
    Controller::add_key_on_window<&CommandPalette::on_window_key_pressed>(_CPBase, *this,
                                                                          Gtk::PHASE_CAPTURE);
    Controller::add_focus_on_window(_CPBase, sigc::mem_fun(*this, &CommandPalette::on_window_focus));

    _CPFilter.signal_activate().connect(sigc::mem_fun(*this, &CommandPalette::on_activate_cpfilter));
    _CPFilter.signal_focus   ().connect(sigc::mem_fun(*this, &CommandPalette::on_focus_cpfilter   ));

    set_mode(CPMode::SEARCH);

    _CPSuggestions.set_activate_on_single_click();
    _CPSuggestions.set_selection_mode(Gtk::SELECTION_SINGLE);

    // Setup operations [actions, extensions]
    {
        // setup recent files
        {
            //TODO: refactor this ==============================
            // this code is repeated in menubar.cpp
            auto recent_manager = Gtk::RecentManager::get_default();
            auto recent_files = recent_manager->get_items(); // all recent files not necessarily inkscape only
            int max_files = Inkscape::Preferences::get()->getInt("/options/maxrecentdocuments/value");
            Glib::ustring const prgname = g_get_prgname();

            for (auto const &recent_file : recent_files) {
                // check if given was generated by inkscape
                bool valid_file = recent_file->has_application(prgname) or
                                  recent_file->has_application("org.inkscape.Inkscape") or
                                  recent_file->has_application("inkscape") or
                                  recent_file->has_application("inkscape.exe");

                if (not valid_file) {
                    continue;
                }

                if (max_files-- <= 0) {
                    break;
                }

                append_recent_file_operation(recent_file->get_uri_display(), true,
                                             false); // open - second param true to append in _CPSuggestions
                append_recent_file_operation(recent_file->get_uri_display(), true,
                                             true); // import - last param true for import operation
            }
            // ==================================================
        }
    }

    // History management
    {
        const auto history = _history_xml.get_operation_history();

        for (const auto &page : history) {
            // second params false to append in history
            switch (page.history_type) {
                case HistoryType::ACTION:
                    generate_action_operation(get_action_ptr_name(page.data), false);
                    break;
                case HistoryType::IMPORT_FILE:
                    append_recent_file_operation(page.data, false, true);
                    break;
                case HistoryType::OPEN_FILE:
                    append_recent_file_operation(page.data, false, false);
                    break;
                default:
                    continue;
            }
        }
    }
    // for `enter to execute` feature
    _CPSuggestions.signal_row_activated().connect(sigc::mem_fun(*this, &CommandPalette::on_row_activated));
}

void CommandPalette::open()
{
    if (not _win_doc_actions_loaded) {
        // loading actions can be very slow
        load_app_actions();
        // win doc don't exist at construction so loading at first time opening Command Palette
        load_win_doc_actions();
        _win_doc_actions_loaded = true;
    }

    _CPBase.show_all();
    _CPFilter.grab_focus();
    _is_open = true;

}

void CommandPalette::close()
{
    _CPBase.set_visible(false);

    // Reset filtering - show all suggestions
    _CPFilter.set_text("");
    _CPSuggestions.invalidate_filter();

    set_mode(CPMode::SEARCH);

    _is_open = false;
}

void CommandPalette::toggle()
{
    if (not _is_open) {
        open();
        return;
    }
    close();
}

void CommandPalette::append_recent_file_operation(const Glib::ustring &path, bool is_suggestion, bool is_import)
{
    auto  operation_builder = create_builder("command-palette-operation.glade");
    auto &CPOperation        (get_widget<Gtk::Box>   (operation_builder, "CPOperation"));
    auto &CPGroup            (get_widget<Gtk::Label> (operation_builder, "CPGroup"));
    auto &CPName             (get_widget<Gtk::Label> (operation_builder, "CPName"));
    auto &CPShortcut         (get_widget<Gtk::Label> (operation_builder, "CPShortcut"));
    auto &CPActionFullButton (get_widget<Gtk::Button>(operation_builder, "CPActionFullButton"));
    auto &CPActionFullLabel  (get_widget<Gtk::Label> (operation_builder, "CPActionFullLabel"));
    auto &CPDescription      (get_widget<Gtk::Label> (operation_builder, "CPDescription"));

    const auto file = Gio::File::create_for_path(path);
    if (true) {
        const Glib::ustring file_name = file->get_basename();

        if (is_import) {
            // Used for Activate row signal of listbox and not
            CPGroup.set_text("import");
            CPActionFullLabel.set_text("import"); // For filtering only

        } else {
            CPGroup.set_text("open");
            CPActionFullLabel.set_text("open"); // For filtering only
        }

        // Hide for recent_file, not required
        CPActionFullButton.set_no_show_all();
        CPActionFullButton.set_visible(false);

        CPName.set_text((is_import ? _("Import") : _("Open")) + (": " + file_name));
        CPName.set_tooltip_text((is_import ? ("Import") : ("Open")) + (": " + file_name)); // Tooltip_text are not translatable
        CPDescription.set_text(path);
        CPDescription.set_tooltip_text(path);

        // Add to suggestions
        if (is_suggestion) {
            _CPSuggestions.append(CPOperation);
        } else {
            _CPHistory.append(CPOperation);
        }
    }
}

bool CommandPalette::generate_action_operation(const ActionPtrName &action_ptr_name, bool is_suggestion)
{
    static const auto app = InkscapeApplication::instance();
    static const auto gapp = app->gtk_app();
    static const InkActionExtraData &action_data = app->get_action_extra_data();
    static const bool show_full_action_name =
        Inkscape::Preferences::get()->getBool("/options/commandpalette/showfullactionname/value");

    auto  operation_builder  = create_builder("command-palette-operation.glade");
    auto &CPOperation        (get_widget<Gtk::Box>   (operation_builder, "CPOperation"));
    auto &CPGroup            (get_widget<Gtk::Label> (operation_builder, "CPGroup"));
    auto &CPName             (get_widget<Gtk::Label> (operation_builder, "CPName"));
    auto &CPShortcut         (get_widget<Gtk::Label> (operation_builder, "CPShortcut"));
    auto &CPActionFullButton (get_widget<Gtk::Button>(operation_builder, "CPActionFullButton"));
    auto &CPActionFullLabel  (get_widget<Gtk::Label> (operation_builder, "CPActionFullLabel"));
    auto &CPDescription      (get_widget<Gtk::Label> (operation_builder, "CPDescription"));

    CPGroup.set_text(action_data.get_section_for_action(action_ptr_name.second));

    // Setting CPName
    {
        auto name = action_data.get_label_for_action(action_ptr_name.second);
        auto untranslated_name = action_data.get_label_for_action(action_ptr_name.second, false);
        if (name.empty()) {
            // If action doesn't have a label, set the name = full action name
            name = action_ptr_name.second;
            untranslated_name = action_ptr_name.second;
        }

        CPName.set_text(name);
        CPName.set_tooltip_text(untranslated_name);
    }

    CPActionFullLabel.set_text(action_ptr_name.second);

        if (not show_full_action_name) {
        CPActionFullButton.set_no_show_all();
        CPActionFullButton.set_visible(false);
        } else {
        CPActionFullButton.signal_clicked().connect(
            sigc::bind(sigc::mem_fun(*this, &CommandPalette::on_action_fullname_clicked),
                                          action_ptr_name.second),
                false);
        }

    {
        std::vector<Glib::ustring> accels = gapp->get_accels_for_action(action_ptr_name.second);
        std::string accel_label;
        for (const auto &accel : accels) {
            guint key = 0;
            Gdk::ModifierType mods;
            Gtk::AccelGroup::parse(accel, key, mods);
            Glib::ustring label = Gtk::AccelGroup::get_label(key, mods);
            accel_label.append(label.raw()).append(1, ' ');
        }

        if (not accel_label.empty()) {
            accel_label.pop_back();
            CPShortcut.set_text(accel_label);
        } else {
            CPShortcut.set_no_show_all();
            CPShortcut.set_visible(false);
        }
    }

    CPDescription.set_text(action_data.get_tooltip_for_action(action_ptr_name.second));
    CPDescription.set_tooltip_text(action_data.get_tooltip_for_action(action_ptr_name.second, false));

    // Add to suggestions
    if (is_suggestion) {
        _CPSuggestions.append(CPOperation);
    } else {
        _CPHistory.append(CPOperation);
    }

    return true;
}

void CommandPalette::on_search()
{
    // TODO: Why is this done here? It seems very wasteful! But I didnʼt get anything else to work,
    //       for example by setting it elsewhere and then only invalidating it here. Ponder more...
    //       Although in saying that, TODO: GTK4: Consider porting this whole thing to GtkListView.
    _CPSuggestions.unset_sort_func();
    _CPSuggestions.set_sort_func(sigc::mem_fun(*this, &CommandPalette::on_sort));

    _search_text = _CPFilter.get_text();

    _CPSuggestions.invalidate_filter(); // Remove old filter constraint and apply new one

    if (auto top_row = _CPSuggestions.get_row_at_y(0); top_row) {
        _CPSuggestions.select_row(*top_row); // select top row
    }

    _CPSuggestionsScroll.get_vadjustment()->set_value(0);
}

bool CommandPalette::on_filter_full_action_name(Gtk::ListBoxRow *child)
{
    if (auto CPActionFullLabel = get_full_action_name(child);
        CPActionFullLabel and _search_text == CPActionFullLabel->get_text()) {
        return true;
    }
    return false;
}

bool CommandPalette::on_filter_recent_file(Gtk::ListBoxRow *child, bool const is_import)
{
    auto CPActionFullLabel = get_full_action_name(child);
    if (is_import) {
        if (CPActionFullLabel and CPActionFullLabel->get_text() == "import") {
            auto [CPName, CPDescription] = get_name_desc(child);
            if (CPDescription && CPDescription->get_text() == _search_text) {
                return true;
            }
        }
        return false;
    }
    if (CPActionFullLabel and CPActionFullLabel->get_text() == "open") {
        auto [CPName, CPDescription] = get_name_desc(child);
        if (CPDescription && CPDescription->get_text() == _search_text) {
            return true;
        }
    }
    return false;
}

gboolean CommandPalette::on_window_key_pressed(GtkEventControllerKey const * /*controller*/,
                                           unsigned const keyval, unsigned /*keycode*/,
                                           GdkModifierType /*state*/)
{
    g_return_val_if_fail(_is_open, false);

    if (keyval == GDK_KEY_Escape || keyval == GDK_KEY_question) {
        close();
        return true; // stop propagation of key press, not needed anymore
    }

    return false; // Pass the key event which are not used
}

void CommandPalette::on_window_focus(Gtk::Widget const * const focus)
{
    // TODO: GTK4: EventControllerFocus.property_contains_focus() should make this slightly nicer?
    if (focus && !is_descendant_of(*focus, _CPBase)) {
        close();
    }
}

void CommandPalette::on_activate_cpfilter()
{
    if (_mode == CPMode::SEARCH) {
        if (auto selected_row = _CPSuggestions.get_selected_row(); selected_row) {
            selected_row->activate();
        }
    } else if (_mode == CPMode::INPUT) {
        execute_action(_ask_action_ptr_name.value(), _CPFilter.get_text());
        _ask_action_ptr_name.reset();
        close();
    }
}

bool CommandPalette::on_focus_cpfilter(Gtk::DirectionType const direction)
{
    if (_mode != CPMode::SEARCH) return false;

    if (direction == Gtk::DIR_UP) {
        set_mode(CPMode::HISTORY);
        return true;
    }

    if (direction == Gtk::DIR_DOWN) {
        // Unselect so we go to 1st row
        _CPSuggestions.unselect_all();
}

    return false;
}

void CommandPalette::hide_suggestions()
{
    _CPBase.set_size_request(-1, 10);
    _CPListBase.set_visible(false);
}

void CommandPalette::show_suggestions()
{
    _CPBase.set_size_request(-1, _max_height_requestable);
    _CPListBase.show_all();
}

void CommandPalette::on_action_fullname_clicked(const Glib::ustring &action_fullname)
{
    static auto clipboard = Gtk::Clipboard::get();
    clipboard->set_text(action_fullname);
    clipboard->store();
}

void CommandPalette::on_row_activated(Gtk::ListBoxRow *activated_row)
{
    // this is set to import/export or full action name
    auto full_action_name = get_full_action_name(activated_row)->get_label();
    if (full_action_name == "import" or full_action_name == "open") {
        const auto [name, description] = get_name_desc(activated_row);
        operate_recent_file(description->get_text(), full_action_name == "import");
    } else {
        ask_action_parameter(get_action_ptr_name(std::move(full_action_name)));
        // this is an action
    }
}

void CommandPalette::on_history_selection_changed(Gtk::ListBoxRow *lb)
{
    // set the search box text to current selection
    if (const auto name_label = get_name_desc(lb).first; name_label) {
        _CPFilter.set_text(name_label->get_text());
    }
}

bool CommandPalette::operate_recent_file(Glib::ustring const &uri, bool const import)
{
    bool write_to_history = true;

    // if the last element in CPHistory is already this, don't update history file
    if (not UI::get_children(_CPHistory).empty()) {
        if (const auto last_operation = _history_xml.get_last_operation(); last_operation.has_value()) {
            if (uri == last_operation->data) {
                bool last_operation_was_import = last_operation->history_type == HistoryType::IMPORT_FILE;
                // As previous uri is verfied to be the same as current uri we can write to history if current and
                // previous operation are not the same.
                // For example: if we want to import and previous operation was import (with same uri) we should not
                // write ot history, similarly if current is open and previous was open to then dont WTH.
                // But in case previous operation was open and current is import and vice-versa we should write to
                // history.
                if (not(import xor last_operation_was_import)) {
                    write_to_history = false;
                }
            }
        }
    }

    if (import) {
        file_import(SP_ACTIVE_DOCUMENT, uri, nullptr);

        if (write_to_history) {
            _history_xml.add_import(uri);
        }

        close();
        return true;
    }

    // open
    {
        get_action_ptr_name("app.file-open-window").first->activate(uri);
        if (write_to_history) {
            _history_xml.add_open(uri);
        }
    }

    close();
    return true;
}

static void set_hint_texts(Gtk::Entry &entry, Glib::ustring const &text)
{
    entry.set_placeholder_text(text);
    entry.set_tooltip_text    (text);
}

/**
 * Maybe replaced by: Temporary arrangement may be replaced by snippets
 * This can help us provide parameters for multiple argument function
 * whose actions take a string as param
 */
bool CommandPalette::ask_action_parameter(const ActionPtrName &action_ptr_name)
{
    _ask_action_ptr_name.emplace(action_ptr_name);

    // Avoid writing same last action again
    // TODO: Merge the if else parts
    if (const auto last_of_history = _history_xml.get_last_operation(); last_of_history.has_value()) {
        // operation history is not empty
        const auto last_full_action_name = last_of_history->data;
        if (last_full_action_name != action_ptr_name.second) {
            // last action is not the same so write this one
            _history_xml.add_action(action_ptr_name.second);   // to history file
            generate_action_operation(action_ptr_name, false); // to _CPHistory
        }
    } else {
        // History is empty so no need to check
        _history_xml.add_action(action_ptr_name.second);   // to history file
        generate_action_operation(action_ptr_name, false); // to _CPHistory
    }

    // Checking if action has handleable parameter type
    TypeOfVariant action_param_type = get_action_variant_type(action_ptr_name.first);
    if (action_param_type == TypeOfVariant::UNKNOWN) {
        std::cerr << "CommandPalette::ask_action_parameter: unhandled action value type (Unknown Type) "
                  << action_ptr_name.second.raw() << std::endl;
        return false;
    }

    if (action_param_type != TypeOfVariant::NONE) {
        set_mode(CPMode::INPUT);

        // get type string NOTE: Temporary should be replaced by adding some data to InkActionExtraDataj
        Glib::ustring type_string;
        switch (action_param_type) {
            case TypeOfVariant::BOOL:
                type_string = _("boolean");
                break;
            case TypeOfVariant::INT:
                type_string = _("whole number");
                break;
            case TypeOfVariant::DOUBLE:
                type_string = _("decimal number");
                break;
            case TypeOfVariant::STRING:
                type_string = _("text string");
                break;
            case TypeOfVariant::TUPLE_DD:
                type_string = _("pair of decimal numbers");
                break;
            default:
                break;
        }

        const auto app = InkscapeApplication::instance();
        InkActionHintData &action_hint_data = app->get_action_hint_data();
        auto action_hint = action_hint_data.get_tooltip_hint_for_action(action_ptr_name.second, false);

        // Indicate user about what to enter FIXME Dialog generation
        if (action_hint.empty()) {
            /* Translators: %1 will be replaced with the type of parameter
             *              expected by the action, e.g., “whole number”. */
            action_hint = Glib::ustring::compose(_("Enter a %1..."), type_string);
        }
        set_hint_texts(_CPFilter, action_hint);

        return true;
    }

    execute_action(action_ptr_name, "");
    close();

    return true;
}

/**
 * Color removal
 */
void CommandPalette::remove_color(Gtk::Label *label, const Glib::ustring &subject, bool tooltip)
{
    /* if (tooltip) {
        label->set_tooltip_text(subject);
    } else if (label->get_use_markup()) {
        label->set_text(subject);
    } */
}

/**
 * Color addition
 */
Glib::ustring make_bold(const Glib::ustring &search)
{
    // TODO: Add a CSS class that changes the color of the search
    return "<span weight=\"bold\">" + search + "</span>";
}

void CommandPalette::add_color(Gtk::Label *label, const Glib::ustring &search, const Glib::ustring &subject, bool tooltip)
{
    //is no working on master fill all chars so I comment to speedup
    /* Glib::ustring text = "";
    Glib::ustring subject_string = subject.lowercase();
    Glib::ustring search_string = search.lowercase();
    int j = 0;

    if (search_string.length() > 7) {
        for (gunichar i : search_string) {
            if (i == ' ') {
                continue;
            }
            while (j < subject_string.length()) {
                if (i == subject_string[j]) {
                    text += make_bold(Glib::Markup::escape_text(subject.substr(j, 1)));
                    j++;
                    break;
                } else {
                    text += Glib::Markup::escape_text(subject.substr(j, 1));
                }
                j++;
            }
        }
        if (j < subject.length()) {
            text += Glib::Markup::escape_text(subject.substr(j));
        }
    } else {
        std::map<gunichar, int> search_string_character;

        for (const auto &character : search_string) {
            search_string_character[character]++;
        }

        int subject_length = subject_string.length();

        for (int i = 0; i < subject_length; i++) {
            if (search_string_character[subject_string[i]]--) {
                text += make_bold(Glib::Markup::escape_text(subject.substr(i, 1)));
            } else {
                text += Glib::Markup::escape_text(subject.substr(i, 1));
            }
        }
    }

    if (tooltip) {
        label->set_tooltip_markup(text);
    } else {
        label->set_markup(text);
    } */
}

/**
 * Color addition for description text
 * Coloring complete consecutive search text in the description text
 */
void CommandPalette::add_color_description(Gtk::Label *label, const Glib::ustring &search)
{
   /*  Glib::ustring subject = label->get_text();

    Glib::ustring const subject_normalize = subject.lowercase().normalize();
    Glib::ustring const search_normalize = search.lowercase().normalize();

    auto const position = subject_normalize.find(search_normalize);
    auto const search_length = search_normalize.size();

    subject = Glib::Markup::escape_text(subject.substr(0, position)) +
              make_bold(Glib::Markup::escape_text(subject.substr(position, search_length))) +
              Glib::Markup::escape_text(subject.substr(position + search_length));

    label->set_markup(subject); */
}

/**
 * The Searching algorithm consists of fuzzy search and fuzzy points.
 *
 * Ever search of the label can contain up to three subjects to search
 * CPName text,CPName tooltip text,CPDescription text
 *
 * Fuzzy search searches the search text in these subjects and returns a boolean
 * Searching of a search text as a subsequence of the subject
 *
 * Fuzzy points give an integer of a particular search text concerning a particular subject.
 * Less the fuzzy point more is the precedence.
 *
 * Special case for CPDescription text search by searching text as a substring of the subject
 *
 * TODO: Adding more conditions in fuzzy points and fuzzy search for creating better user experience
 */

bool CommandPalette::fuzzy_tolerance_search(const Glib::ustring &subject, const Glib::ustring &search)
{
    Glib::ustring subject_string = subject.lowercase();
    Glib::ustring search_string = search.lowercase();
    std::map<gunichar, int> subject_string_character, search_string_character;
    for (const auto &character : subject_string) {
        subject_string_character[character]++;
    }
    for (const auto &character : search_string) {
        search_string_character[character]++;
    }
    for (const auto &character : search_string_character) {
        const auto &[alphabet, occurrence] = character;
        if (subject_string_character[alphabet] < occurrence) {
            return false;
        }
    }
    return true;
}

bool CommandPalette::fuzzy_search(const Glib::ustring &subject, const Glib::ustring &search)
{
    Glib::ustring subject_string = subject.lowercase();
    Glib::ustring search_string = search.lowercase();

    for (int j = 0, i = 0; i < search_string.length(); i++) {
        bool alphabet_present = false;

        while (j < subject_string.length()) {
            if (search_string[i] == subject_string[j]) {
                alphabet_present = true;
                j++;
                break;
            }
            j++;
        }

        if (!alphabet_present) {
            return false; // If not present
        }
    }

    return true;
}

/**
 * Searching the full search_text in the subject string
 * used for CPDescription text
 */
bool CommandPalette::normal_search(const Glib::ustring &subject, const Glib::ustring &search)
{
    if (subject.lowercase().find(search.lowercase()) != -1) {
        return true;
    }
    return false;
}

/**
 * Calculates the fuzzy_point
 */
int CommandPalette::fuzzy_points(const Glib::ustring &subject, const Glib::ustring &search)
{
    int fuzzy_cost = 100; // Taking initial fuzzy_cost as 100

    constexpr int SEQUENTIAL_BONUS = -15;      // bonus for adjacent matches
    constexpr int SEPARATOR_BONUS = -30;       // bonus if search occurs after a separator
    constexpr int CAMEL_BONUS = -30;           // bonus if search is uppercase and subject is lower
    constexpr int FIRST_LETTER_BONUS = -15;    // bonus if the first letter is matched
    constexpr int LEADING_LETTER_PENALTY = +5; // penalty applied for every letter in subject before the first match
    constexpr int MAX_LEADING_LETTER_PENALTY = +15; // maximum penalty for leading letters
    constexpr int UNMATCHED_LETTER_PENALTY = +1;    // penalty for every letter that doesn't matter

    Glib::ustring subject_string = subject.lowercase();
    Glib::ustring search_string = search.lowercase();

    bool sequential_compare = false;
    bool leading_letter = true;
    int total_leading_letter_penalty = 0;
    int j = 0, i = 0;

    while (i < search_string.length() && j < subject_string.length()) {
        if (search_string[i] != subject_string[j]) {
            j++;
            sequential_compare = false;
            fuzzy_cost += UNMATCHED_LETTER_PENALTY;

            if (leading_letter) {
                if (total_leading_letter_penalty < MAX_LEADING_LETTER_PENALTY) {
                    fuzzy_cost += LEADING_LETTER_PENALTY;
                    total_leading_letter_penalty += LEADING_LETTER_PENALTY;
                }
            }

            continue;
        }

        if (search_string[i] == subject_string[j]) {
            leading_letter = false;

            if (j > 0 && subject_string[j - 1] == ' ') {
                fuzzy_cost += SEPARATOR_BONUS;
            }

            if (i == 0 && j == 0) {
                fuzzy_cost += FIRST_LETTER_BONUS;
            }

            if (search[i] == subject_string[j]) {
                fuzzy_cost += CAMEL_BONUS;
            }

            if (sequential_compare) {
                fuzzy_cost += SEQUENTIAL_BONUS;
            }

            sequential_compare = true;
            i++;
        }
    }

    return fuzzy_cost;
}

int CommandPalette::fuzzy_tolerance_points(const Glib::ustring &subject, const Glib::ustring &search)
{
    int fuzzy_cost = 200;                   // Taking initial fuzzy_cost as 200
    constexpr int FIRST_LETTER_BONUS = -15; // bonus if the first letter is matched

    Glib::ustring subject_string = subject.lowercase();
    Glib::ustring search_string = search.lowercase();
    std::map<gunichar, int> search_string_character;

    for (const auto &character : search_string) {
        search_string_character[character]++;
    }

    for (auto [alphabet, occurrence] : search_string_character) {
        for (int i = 0; i < subject_string.length() && occurrence; i++) {
            if (subject_string[i] == alphabet) {
                if (i == 0)
                    fuzzy_cost += FIRST_LETTER_BONUS;
                fuzzy_cost += i;
                occurrence--;
            }
        }
    }

    return fuzzy_cost;
}

int CommandPalette::on_filter_general(Gtk::ListBoxRow *child)
{
    auto [CPName, CPDescription] = get_name_desc(child);

    /*
    if (CPName) {
        remove_color(CPName, CPName->get_text());
        remove_color(CPName, CPName->get_tooltip_text(), true);
    }
    if (CPDescription) {
        remove_color(CPDescription, CPDescription->get_text());
    }
    */

    if (_search_text.empty()) {
        return 1;
    } // Every operation is visible if search text is empty

    if (CPName) {
        auto const &name_text = CPName->get_text();

        if (fuzzy_search(name_text, _search_text)) {
            // add_color(CPName, _search_text, name_text);
            return fuzzy_points(name_text, _search_text);
        }

        auto const &name_tooltip = CPName->get_tooltip_text();

        if (fuzzy_search(name_tooltip, _search_text)) {
            // add_color(CPName, _search_text, name_tooltip, true);
            return fuzzy_points(name_tooltip, _search_text);
        }

        if (fuzzy_tolerance_search(name_text, _search_text)) {
            // add_color(CPName, _search_text, name_text);
            return fuzzy_tolerance_points(name_text, _search_text);
        }

        if (fuzzy_tolerance_search(name_tooltip, _search_text)) {
            // add_color(CPName, _search_text, name_tooltip, true);
            return fuzzy_tolerance_points(name_tooltip, _search_text);
        }
    }

    if (CPDescription) {
        auto const &description_text = CPDescription->get_text();
        if (normal_search(description_text, _search_text)) {
            // add_color_description(CPDescription, _search_text);
            return fuzzy_points(description_text, _search_text);
        }
    }

    return 0;
}

int CommandPalette::fuzzy_points_compare(int fuzzy_points_count_1, int fuzzy_points_count_2, int text_len_1,
                                         int text_len_2)
{
    if (fuzzy_points_count_1 && fuzzy_points_count_2) {
        if (fuzzy_points_count_1 < fuzzy_points_count_2) {
            return -1;
        } else if (fuzzy_points_count_1 == fuzzy_points_count_2) {
            if (text_len_1 > text_len_2) {
                return 1;
            } else {
                return -1;
            }
        } else {
            return 1;
        }
    }

    if (fuzzy_points_count_1 == 0 && fuzzy_points_count_2) {
        return 1;
    }
    if (fuzzy_points_count_2 == 0 && fuzzy_points_count_1) {
        return -1;
    }

    return 0;
}

// TODO: Move to a test program.
void CommandPalette::test_sort()
{
    // tests for fuzzy_search
    assert(fuzzy_search("Export background", "ebo") == true);
    assert(fuzzy_search("Query y", "qyy") == true);
    assert(fuzzy_search("window close", "qt") == false);

    // tests for fuzzy_points
    assert(fuzzy_points("Export background", "ebo") == -22);
    assert(fuzzy_points("Query y", "qyy") == -16);
    assert(fuzzy_points("window close", "wc") == 2);

    // tests for fuzzy_tolerance_search
    assert(fuzzy_tolerance_search("object to path", "ebo") == true);
    assert(fuzzy_tolerance_search("execute verb", "qyy") == false);
    assert(fuzzy_tolerance_search("color mode", "moco") == true);

    // tests for fuzzy_tolerance_points
    assert(fuzzy_tolerance_points("object to path", "ebo") == 189);
    assert(fuzzy_tolerance_points("execute verb", "vec") == 196);
    assert(fuzzy_tolerance_points("color mode", "moco") == 195);
}

/**
 * compare different rows for order of display
 * priority of comparison
 * 1) CPName->get_text()
 * 2) CPName->get_tooltip_text()
 * 3) CPDescription->get_text()
 */
int CommandPalette::on_sort(Gtk::ListBoxRow *row1, Gtk::ListBoxRow *row2)
{
    if (_search_text.empty()) {
        return -1;
    } // No change in the order

    auto [cp_name_1, cp_description_1] = get_name_desc(row1);
    auto [cp_name_2, cp_description_2] = get_name_desc(row2);

    int fuzzy_points_count_1 = 0, fuzzy_points_count_2 = 0;
    int text_len_1 = 0, text_len_2 = 0;
    int points_compare = 0;

    constexpr int TOOLTIP_PENALTY = 100;
    constexpr int DESCRIPTION_PENALTY = 500;

    if (cp_name_1 && cp_name_2) {
        auto const &name_1_text = cp_name_1->get_text();
        auto const &name_2_text = cp_name_2->get_text();

        if (fuzzy_search(name_1_text, _search_text)) {
            text_len_1 = name_1_text.length();
            fuzzy_points_count_1 = fuzzy_points(name_1_text, _search_text);
        }
        if (fuzzy_search(name_2_text, _search_text)) {
            text_len_2 = name_2_text.length();
            fuzzy_points_count_2 = fuzzy_points(name_2_text, _search_text);
        }

        points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2);
        if (points_compare != 0) {
            return points_compare;
        }

        if (fuzzy_tolerance_search(name_1_text, _search_text)) {
            text_len_1 = name_1_text.length();
            fuzzy_points_count_1 = fuzzy_tolerance_points(name_1_text, _search_text);
        }
        if (fuzzy_tolerance_search(name_2_text, _search_text)) {
            text_len_2 = name_2_text.length();
            fuzzy_points_count_2 = fuzzy_tolerance_points(name_2_text, _search_text);
        }

        points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2);
        if (points_compare != 0) {
            return points_compare;
        }

        auto const &name_1_tooltip = cp_name_1->get_tooltip_text();
        auto const &name_2_tooltip = cp_name_2->get_tooltip_text();

        if (fuzzy_search(name_1_tooltip, _search_text)) {
            text_len_1 = name_1_tooltip.length();
            fuzzy_points_count_1 = fuzzy_points(name_1_tooltip, _search_text) + TOOLTIP_PENALTY;
        }
        if (fuzzy_search(name_2_tooltip, _search_text)) {
            text_len_2 = name_2_tooltip.length();
            fuzzy_points_count_2 = fuzzy_points(name_2_tooltip, _search_text) + TOOLTIP_PENALTY;
        }

        points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2);
        if (points_compare != 0) {
            return points_compare;
        }

        if (fuzzy_tolerance_search(name_1_tooltip, _search_text)) {
            text_len_1 = name_1_tooltip.length();
            fuzzy_points_count_1 = fuzzy_tolerance_points(name_1_tooltip, _search_text) +
                                   TOOLTIP_PENALTY; // Adding a constant integer to decrease the prefrence
        }
        if (fuzzy_tolerance_search(name_2_tooltip, _search_text)) {
            text_len_2 = name_2_tooltip.length();
            fuzzy_points_count_2 = fuzzy_tolerance_points(name_2_tooltip, _search_text) +
                                   TOOLTIP_PENALTY; // Adding a constant integer to decrease the prefrence
        }
        points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2);
        if (points_compare != 0) {
            return points_compare;
        }
    }

    auto const &description_1_text = cp_description_1->get_text();
    auto const &description_2_text = cp_description_2->get_text();

    if (cp_description_1 && normal_search(description_1_text, _search_text)) {
        text_len_1 = description_1_text.length();
        fuzzy_points_count_1 = fuzzy_points(description_1_text, _search_text) +
                               DESCRIPTION_PENALTY; // Adding a constant integer to decrease the prefrence
    }
    if (cp_description_2 && normal_search(description_2_text, _search_text)) {
        text_len_2 = description_2_text.length();
        fuzzy_points_count_2 = fuzzy_points(description_2_text, _search_text) +
                               DESCRIPTION_PENALTY; // Adding a constant integer to decrease the prefrence
    }

    points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2);
    if (points_compare != 0) {
        return points_compare;
    }
    return 0;
}

// Widget.set_sensitive() made the cursor vanish, so… TODO: GTK4: Check if fixed
static void set_sensitive(Gtk::Entry &entry, bool const sensitive)
{
    entry.set_editable(sensitive);
    entry.set_icon_activatable(sensitive, Gtk::ENTRY_ICON_PRIMARY  );
    entry.set_icon_activatable(sensitive, Gtk::ENTRY_ICON_SECONDARY);
}

void CommandPalette::set_mode(CPMode mode)
{
    if (_mode == mode) {
                return;
            }

    switch (mode) {
        case CPMode::SEARCH:
            set_sensitive(_CPFilter, true);
            _CPFilter.set_text("");
            _CPFilter.set_icon_from_icon_name("edit-find-symbolic");
            set_hint_texts(_CPFilter, _("Enter search term to search for a command"));

            show_suggestions();

            // Show Suggestions instead of history
            _CPHistoryScroll.set_no_show_all();
            _CPHistoryScroll.set_visible(false);

            _CPSuggestionsScroll.set_no_show_all(false);
            _CPSuggestionsScroll.show_all();

            _CPSuggestions.unset_filter_func();
            _CPSuggestions.set_filter_func(sigc::mem_fun(*this, &CommandPalette::on_filter_general));

            _cpfilter_search_connection.disconnect(); // to be sure

            _cpfilter_search_connection =
                _CPFilter.signal_search_changed().connect(sigc::mem_fun(*this, &CommandPalette::on_search));

            _search_text = "";
            _CPSuggestions.invalidate_filter();

            break;

        case CPMode::INPUT:
            _cpfilter_search_connection.disconnect();

            hide_suggestions();

            set_sensitive(_CPFilter, true);
            _CPFilter.set_text("");
            _CPFilter.grab_focus();
            _CPFilter.set_icon_from_icon_name("input-keyboard");
            set_hint_texts(_CPFilter, _("Enter action argument"));

            break;

        case CPMode::SHELL:
            hide_suggestions();

            set_sensitive(_CPFilter, true);
            _CPFilter.set_icon_from_icon_name("gtk-search");

            _cpfilter_search_connection.disconnect();

            break;

        case CPMode::HISTORY: {
            auto const children = UI::get_children(_CPHistory);
            if (children.empty()) {
                return;
            }

            // Show history instead of suggestions
            _CPSuggestionsScroll.set_no_show_all();
            _CPHistoryScroll.set_no_show_all(false);
            _CPSuggestionsScroll.set_visible(false);
            _CPHistoryScroll.show_all();

            set_sensitive(_CPFilter, false);
            _CPFilter.set_icon_from_icon_name("format-justify-fill");
            set_hint_texts(_CPFilter, _("History mode"));

            _cpfilter_search_connection.disconnect();

            _CPHistory.signal_row_selected().connect(
                sigc::mem_fun(*this, &CommandPalette::on_history_selection_changed));
            _CPHistory.signal_row_activated().connect(sigc::mem_fun(*this, &CommandPalette::on_row_activated));

            {
                // select last row
                auto const last_row = _CPHistory.get_row_at_index(children.size() - 1);
                _CPHistory.select_row(*last_row);
                last_row->grab_focus();
            }

            Glib::signal_idle().connect_once([this]
            {
                const auto adjustment = _CPHistoryScroll.get_vadjustment();
                adjustment->set_value(adjustment->get_upper());
            });
        }
            }

    _mode = mode;
}

/**
 * Calls actions with parameters
 */
CommandPalette::ActionPtrName CommandPalette::get_action_ptr_name(Glib::ustring full_action_name)
{
    static auto gapp = InkscapeApplication::instance()->gtk_app();
    // TODO: Optimisation: only try to assign if null, make static
    const auto win = InkscapeApplication::instance()->get_active_window();
    const auto doc = InkscapeApplication::instance()->get_active_document();
    const auto dot = full_action_name.find('.');
    const auto action_domain = std::string_view{full_action_name.c_str(), dot}; // app, win, doc
    const auto action_name = full_action_name.substr(dot + 1);

    ActionPtr action_ptr;
    if (action_domain == "app") {
        action_ptr = gapp->lookup_action(action_name);
    } else if (win && action_domain == "win") {
        action_ptr = win->lookup_action(action_name);
    } else if (doc && action_domain == "doc") {
        if (const auto map = doc->getActionGroup(); map) {
            action_ptr = map->lookup_action(action_name);
        }
    }

    return {std::move(action_ptr), std::move(full_action_name)};
}

bool CommandPalette::execute_action(const ActionPtrName &action_ptr_name, const Glib::ustring &value)
{
    if (not value.empty()) {
        _history_xml.add_action_parameter(action_ptr_name.second, value);
    }

    const auto &[action_ptr, action_name] = action_ptr_name;

    switch (get_action_variant_type(action_ptr)) {
        case TypeOfVariant::BOOL:
            if (value == "1" || value == "t" || value == "true" || value.empty()) {
                action_ptr->activate(Glib::Variant<bool>::create(true));
            } else if (value == "0" || value == "f" || value == "false") {
                action_ptr->activate(Glib::Variant<bool>::create(false));
            } else {
                std::cerr << "CommandPalette::execute_action: Invalid boolean value: " << action_name.raw() << ":" << value
                          << std::endl;
            }
            break;

        case TypeOfVariant::INT:
            try {
                action_ptr->activate(Glib::Variant<int>::create(std::stoi(value)));
            } catch (...) {
                if (SPDesktop *dt = SP_ACTIVE_DESKTOP; dt) {
                    dt->messageStack()->flash(ERROR_MESSAGE, _("Invalid input! Enter an integer number."));
                }
            }
            break;

        case TypeOfVariant::DOUBLE:
            try {
                action_ptr->activate(Glib::Variant<double>::create(std::stod(value)));
            } catch (...) {
                if (SPDesktop *dt = SP_ACTIVE_DESKTOP; dt) {
                    dt->messageStack()->flash(ERROR_MESSAGE, _("Invalid input! Enter a decimal number."));
                }
            }
            break;

        case TypeOfVariant::STRING:
            action_ptr->activate(Glib::Variant<Glib::ustring>::create(value));
            break;

        case TypeOfVariant::TUPLE_DD:
            try {
                double d0 = 0;
                double d1 = 0;
                std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("\\s*,\\s*", value);

                try {
                    if (tokens.size() != 2) {
                        throw std::invalid_argument("requires two numbers");
                    }
                } catch (...) {
                    throw;
                }

                try {
                    d0 = std::stod(tokens[0]);
                    d1 = std::stod(tokens[1]);
                } catch (...) {
                    throw;
                }

                auto variant = Glib::Variant<std::tuple<double, double>>::create(std::tuple<double, double>(d0, d1));
                action_ptr->activate(variant);
            } catch (...) {
                if (SPDesktop *dt = SP_ACTIVE_DESKTOP; dt) {
                    dt->messageStack()->flash(ERROR_MESSAGE, _("Invalid input! Enter two comma separated numbers."));
                }
            }
            break;

        case TypeOfVariant::UNKNOWN:
            std::cerr << "CommandPalette::execute_action: unhandled action value type (Unknown Type) " << action_name.raw()
                      << std::endl;
            break;

        case TypeOfVariant::NONE:
        default:
            action_ptr->activate();
    }
    return false;
}

TypeOfVariant CommandPalette::get_action_variant_type(const ActionPtr &action_ptr)
{
    const GVariantType *gtype = g_action_get_parameter_type(action_ptr->gobj());
    if (gtype) {
        Glib::VariantType type = action_ptr->get_parameter_type();
        if (type.get_string() == "b") {
            return TypeOfVariant::BOOL;
        } else if (type.get_string() == "i") {
            return TypeOfVariant::INT;
        } else if (type.get_string() == "d") {
            return TypeOfVariant::DOUBLE;
        } else if (type.get_string() == "s") {
            return TypeOfVariant::STRING;
        } else if (type.get_string() == "(dd)") {
            return TypeOfVariant::TUPLE_DD;
        } else {
            std::cerr << "CommandPalette::get_action_variant_type: unknown variant type: " << type.get_string() << std::endl;
            return TypeOfVariant::UNKNOWN;
        }
    }
    // With value.
    return TypeOfVariant::NONE;
}

std::pair<Gtk::Label *, Gtk::Label *> CommandPalette::get_name_desc(Gtk::ListBoxRow *child)
{
    auto box = dynamic_cast<Gtk::Box *>(child->get_child());
    if (box && (box->get_name() == "CPOperation")) {
        // NOTE: These variables have same name as in the glade file command-palette-operation.glade
        // FIXME: When structure of Gladefile of CPOperation changes, refactor this
        auto const box_children = UI::get_children(*box);
        auto CPNameBox = dynamic_cast<Gtk::Box *>(box_children.at(0));
            if (CPNameBox) {
            auto const name_children = UI::get_children(*CPNameBox);
            auto CPName = dynamic_cast<Gtk::Label *>(name_children.at(0));
            auto CPDescription = dynamic_cast<Gtk::Label *>(name_children.at(1));
                return std::pair(CPName, CPDescription);
            }
        }
    return std::pair(nullptr, nullptr);
}

Gtk::Label *CommandPalette::get_full_action_name(Gtk::ListBoxRow *child)
{
    auto box = dynamic_cast<Gtk::Box *>(child->get_child());
    if (box && (box->get_name() == "CPOperation")) {
        auto const box_children = UI::get_children(*box);
        auto CPActionFullButton = dynamic_cast<Gtk::Button *>(box_children.at(1));
            if (CPActionFullButton) {
            auto const synapse_button = UI::get_children(*CPActionFullButton);
            auto CPSynapseButtonBox = dynamic_cast<Gtk::Box *>(synapse_button.at(0));
            if (CPSynapseButtonBox) {
                auto const synapse_button_content = UI::get_children(*CPSynapseButtonBox);
                return dynamic_cast<Gtk::Label *>(synapse_button_content.at(1));
                }
            }
        }
    return nullptr;
}

void CommandPalette::load_app_actions()
{
    auto gapp = InkscapeApplication::instance()->gtk_app();
    for (auto &&action : gapp->list_actions()) {
        generate_action_operation(get_action_ptr_name("app." + std::move(action)), true);
    }
}

void CommandPalette::load_win_doc_actions()
{
    if (auto window = InkscapeApplication::instance()->get_active_window(); window) {
        for (auto &&action : window->list_actions()) {
            generate_action_operation(get_action_ptr_name("win." + std::move(action)), true);
        }

        if (auto document = window->get_document(); document) {
            auto map = document->getActionGroup();
            if (map) {
                for (auto &&action : map->list_actions()) {
                    generate_action_operation(get_action_ptr_name("doc." + std::move(action)), true);
                }
            } else {
                std::cerr << "CommandPalette::load_win_doc_actions: No document map!" << std::endl;
            }
        }
    }
}

Gtk::Box &CommandPalette::get_base_widget()
{
    return _CPBase;
}

// CPHistoryXML ---------------------------------------------------------------
CPHistoryXML::CPHistoryXML()
    : _file_path(IO::Resource::profile_path("cphistory.xml"))
{
    _xml_doc = sp_repr_read_file(_file_path.c_str(), nullptr);
    if (not _xml_doc) {
        _xml_doc = sp_repr_document_new("cphistory");

        /* STRUCTURE EXAMPLE ------------------ Illustration 1
        <cphistory>
            <operations>
                <action> full.action_name </action>
                <import> uri </import>
                <export> uri </export>
            </operations>
            <params>
                <action name="app.transfor-rotate">
                    <param> 30 </param>
                    <param> 23.5 </param>
                </action>
            </params>
        </cphistory>
        */

        // Just a pointer, we don't own it, don't free/release/delete
        auto root = _xml_doc->root();

        // add operation history in this element
        auto operations = _xml_doc->createElement("operations");
        root->appendChild(operations);

        // add param history in this element
        auto params = _xml_doc->createElement("params");
        root->appendChild(params);

        // This was created by allocated
        Inkscape::GC::release(operations);
        Inkscape::GC::release(params);

        // only save if created new
        save();
    }

    // Only two children :) check and ensure Illustration 1
    _operations = _xml_doc->root()->firstChild();
    _params = _xml_doc->root()->lastChild();
}

CPHistoryXML::~CPHistoryXML()
{
    Inkscape::GC::release(_xml_doc);
}
void CPHistoryXML::add_action(const std::string &full_action_name)
{
    add_operation(HistoryType::ACTION, full_action_name);
}

void CPHistoryXML::add_import(const std::string &uri)
{
    add_operation(HistoryType::IMPORT_FILE, uri);
}
void CPHistoryXML::add_open(const std::string &uri)
{
    add_operation(HistoryType::OPEN_FILE, uri);
}

void CPHistoryXML::add_action_parameter(const std::string &full_action_name, const std::string &param)
{
    /* Creates
     *  <params>
     * +1 <action name="full.action-name">
     * +    <param>30</param>
     * +    <param>60</param>
     * +    <param>90</param>
     * +1 <action name="full.action-name">
     *   <params>
     *
     * + : generally creates
     * +1: creates once
     */
    const auto parameter_node = _xml_doc->createElement("param");
    const auto parameter_text = _xml_doc->createTextNode(param.c_str());

    parameter_node->appendChild(parameter_text);
    Inkscape::GC::release(parameter_text);

    for (auto action_iter = _params->firstChild(); action_iter; action_iter = action_iter->next()) {
        // If this action's node already exists
        if (full_action_name == action_iter->attribute("name")) {
            // If the last parameter was the same don't do anything, inner text is also a node hence 2 times last
            // child
            if (action_iter->lastChild()->lastChild() && action_iter->lastChild()->lastChild()->content() == param) {
                Inkscape::GC::release(parameter_node);
                return;
            }

            // If last current than parameter is different, add current
            action_iter->appendChild(parameter_node);
            Inkscape::GC::release(parameter_node);

            save();
            return;
        }
    }

    // only encountered when the actions element doesn't already exists,so we create that action's element
    const auto action_node = _xml_doc->createElement("action");
    action_node->setAttribute("name", full_action_name.c_str());
    action_node->appendChild(parameter_node);

    _params->appendChild(action_node);
    save();

    Inkscape::GC::release(action_node);
    Inkscape::GC::release(parameter_node);
}

std::optional<History> CPHistoryXML::get_last_operation()
{
    auto last_child = _operations->lastChild();
    if (last_child) {
        if (const auto operation_type = _get_operation_type(last_child); operation_type.has_value()) {
            // inner text is a text Node thus last child
            return History{*operation_type, last_child->lastChild()->content()};
        }
    }
    return std::nullopt;
}
std::vector<History> CPHistoryXML::get_operation_history() const
{
    // TODO: add max items in history
    std::vector<History> history;
    for (auto operation_iter = _operations->firstChild(); operation_iter; operation_iter = operation_iter->next()) {
        if (const auto operation_type = _get_operation_type(operation_iter); operation_type.has_value()) {
            history.emplace_back(*operation_type, operation_iter->firstChild()->content());
        }
    }
    return history;
}

std::vector<std::string> CPHistoryXML::get_action_parameter_history(const std::string &full_action_name) const
{
    std::vector<std::string> params;
    for (auto action_iter = _params->firstChild(); action_iter; action_iter = action_iter->prev()) {
        // If this action's node already exists
        if (full_action_name == action_iter->attribute("name")) {
            // lastChild and prev for LIFO order
            for (auto param_iter = _params->lastChild(); param_iter; param_iter = param_iter->prev()) {
                params.emplace_back(param_iter->content());
            }
            return params;
        }
    }
    // action not used previously so no params;
    return {};
}

void CPHistoryXML::save() const
{
    sp_repr_save_file(_xml_doc, _file_path.c_str());
}

void CPHistoryXML::add_operation(const HistoryType history_type, const std::string &data)
{
    std::string operation_type_name;
    switch (history_type) {
        // see Illustration 1
        case HistoryType::ACTION:
            operation_type_name = "action";
            break;
        case HistoryType::IMPORT_FILE:
            operation_type_name = "import";
            break;
        case HistoryType::OPEN_FILE:
            operation_type_name = "open";
            break;
        default:
            return;
    }
    auto operation_to_add = _xml_doc->createElement(operation_type_name.c_str()); // action, import, open
    auto operation_data = _xml_doc->createTextNode(data.c_str());
    operation_data->setContent(data.c_str());

    operation_to_add->appendChild(operation_data);
    _operations->appendChild(operation_to_add);

    Inkscape::GC::release(operation_data);
    Inkscape::GC::release(operation_to_add);

    save();
}

std::optional<HistoryType> CPHistoryXML::_get_operation_type(Inkscape::XML::Node *operation)
{
    const std::string operation_type_name = operation->name();

    if (operation_type_name == "action") {
        return HistoryType::ACTION;
    } else if (operation_type_name == "import") {
        return HistoryType::IMPORT_FILE;
    } else if (operation_type_name == "open") {
        return HistoryType::OPEN_FILE;
    } else {
        return std::nullopt;
        // unknown HistoryType
    }
}

} // namespace Inkscape::UI::Dialog

/*
  Local Variables:
  mode:c++
  c-file-style:"stroustrup"
  c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
  indent-tabs-mode:nil
  fill-column:99
  End:
*/
// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
