// SPDX-License-Identifier: GPL-2.0-or-later
/**
 * @file
 * Dialog for adding a live path effect.
 *
 * 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 <cstddef>
#include <cstring>
#include <ctime>
#include <gdk/gdkkeysyms.h>
#include <giomm/action.h>
#include <giomm/application.h>
#include <giomm/file.h>
#include <giomm/fileinfo.h>
#include <glib/gi18n.h>
#include <glibconfig.h>
#include <glibmm/convert.h>
#include <glibmm/date.h>
#include <glibmm/error.h>
#include <glibmm/i18n.h>
#include <glibmm/markup.h>
#include <glibmm/ustring.h>
#include <gtkmm/application.h>
#include <gtkmm/box.h>
#include <gtkmm/enums.h>
#include <gtkmm/eventbox.h>
#include <gtkmm/label.h>
#include <gtkmm/messagedialog.h>
#include <gtkmm/recentinfo.h>
#include <iostream>
#include <iterator>
#include <memory>
#include <optional>
#include <ostream>
#include <sigc++/adaptors/bind.h>
#include <sigc++/functors/mem_fun.h>
#include <string>

#include "actions/actions-extra-data.h"
#include "file.h"
#include "gc-anchored.h"
#include "include/glibmm_version.h"
#include "inkscape-application.h"
#include "inkscape-window.h"
#include "inkscape.h"
#include "io/resource.h"
#include "message-context.h"
#include "message-stack.h"
#include "object/uri.h"
#include "preferences.h"
#include "ui/interface.h"
#include "xml/repr.h"

namespace Inkscape {
class MessageStack;
namespace UI {
namespace Dialog {

namespace {
template <typename T>
void debug_print(T variable)
{
    std::cerr << variable << std::endl;
}
} // namespace

// constructor
CommandPalette::CommandPalette()
{
    // setup _builder
    {
        auto gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "command-palette-main.glade");
        try {
            _builder = Gtk::Builder::create_from_file(gladefile);
        } catch (const Glib::Error &ex) {
            g_warning("Glade file loading failed for command palette dialog");
            return;
        }
    }

    // Setup Base UI Components
    _builder->get_widget("CPBase", _CPBase);
    _builder->get_widget("CPHeader", _CPHeader);
    _builder->get_widget("CPListBase", _CPListBase);

    _builder->get_widget("CPSearchBar", _CPSearchBar);
    _builder->get_widget("CPFilter", _CPFilter);

    _builder->get_widget("CPSuggestions", _CPSuggestions);
    _builder->get_widget("CPHistory", _CPHistory);

    _builder->get_widget("CPSuggestionsScroll", _CPSuggestionsScroll);
    _builder->get_widget("CPHistoryScroll", _CPHistoryScroll);

    _CPBase->add_events(Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK |
                        Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK | Gdk::KEY_PRESS_MASK);

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

    auto esc_func = sigc::mem_fun(*this, &CommandPalette::on_key_press_cpfilter_escape);
    _CPFilter->signal_key_press_event().connect(esc_func, false);
    _CPSuggestions->signal_key_release_event().connect(esc_func, false);
    _CPHistory->signal_key_press_event().connect(esc_func, false);
    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");

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

                valid_file = valid_file and recent_file->exists();

                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->hide();

    // 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)
{
    static const auto gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "command-palette-operation.glade");
    Glib::RefPtr<Gtk::Builder> operation_builder;
    try {
        operation_builder = Gtk::Builder::create_from_file(gladefile);
    } catch (const Glib::Error &ex) {
        g_warning("Glade file loading failed for Command Palette operation dialog");
    }

    // declaring required widgets pointers
    Gtk::EventBox *CPOperation;
    Gtk::Box *CPSynapseBox;

    Gtk::Label  *CPGroup;
    Gtk::Label  *CPName;
    Gtk::Label  *CPShortcut;
    Gtk::Button *CPActionFullButton;
    Gtk::Label  *CPActionFullLabel;
    Gtk::Label  *CPDescription;

    // Reading widgets
    operation_builder->get_widget("CPOperation", CPOperation);
    operation_builder->get_widget("CPSynapseBox", CPSynapseBox);

    operation_builder->get_widget("CPGroup", CPGroup);
    operation_builder->get_widget("CPName", CPName);
    operation_builder->get_widget("CPShortcut", CPShortcut);
    operation_builder->get_widget("CPActionFullButton", CPActionFullButton);
    operation_builder->get_widget("CPActionFullLabel", CPActionFullLabel);
    operation_builder->get_widget("CPDescription", CPDescription);

    const auto file = Gio::File::create_for_path(path);
    if (file->query_exists()) {
        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->hide();

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

        {
            Glib::DateTime mod_time;
#if GLIBMM_CHECK_VERSION(2, 62, 0)
            mod_time = file->query_info()->get_modification_date_time();
            // Using this to reduce instead of ActionFullName widget because fullname is searched
#else
            mod_time.create_now_local(file->query_info()->modification_time());
#endif
            CPShortcut->set_text(mod_time.format("%d %b %R"));
        }
        // 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 InkActionExtraData &action_data = app->get_action_extra_data();
    static const bool show_full_action_name =
        Inkscape::Preferences::get()->getBool("/options/commandpalette/showfullactionname/value");
    static const auto gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "command-palette-operation.glade");

    Glib::RefPtr<Gtk::Builder> operation_builder;
    try {
        operation_builder = Gtk::Builder::create_from_file(gladefile);
    } catch (const Glib::Error &ex) {
        g_warning("Glade file loading failed for Command Palette operation dialog");
        return false;
    }

    // declaring required widgets pointers
    Gtk::EventBox *CPOperation;
    Gtk::Box *CPSynapseBox;

    Gtk::Label *CPGroup;
    Gtk::Label *CPName;
    Gtk::Label *CPShortcut;
    Gtk::Label *CPDescription;
    Gtk::Button *CPActionFullButton;
    Gtk::Label *CPActionFullLabel;

    // Reading widgets
    operation_builder->get_widget("CPOperation", CPOperation);
    operation_builder->get_widget("CPSynapseBox", CPSynapseBox);

    operation_builder->get_widget("CPGroup", CPGroup);
    operation_builder->get_widget("CPName", CPName);
    operation_builder->get_widget("CPShortcut", CPShortcut);
    operation_builder->get_widget("CPActionFullButton", CPActionFullButton);
    operation_builder->get_widget("CPActionFullLabel", CPActionFullLabel);
    operation_builder->get_widget("CPDescription", CPDescription);

    CPGroup->set_text(action_data.get_section_for_action(Glib::ustring(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->hide();
        } else {
            CPActionFullButton->signal_clicked().connect(
                sigc::bind<Glib::ustring>(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::stringstream ss;
        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);
            ss << label.raw() << ' ';
        }
        std::string accel_label = ss.str();

        if (not accel_label.empty()) {
            accel_label.pop_back();
            CPShortcut->set_text(accel_label);
        } else {
            CPShortcut->set_no_show_all();
            CPShortcut->hide();
        }
    }

    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()
{
    _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;
}

bool CommandPalette::on_key_press_cpfilter_escape(GdkEventKey *evt)
{
    if (evt->keyval == GDK_KEY_Escape || evt->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
}

bool CommandPalette::on_key_press_cpfilter_search_mode(GdkEventKey *evt)
{
    auto key = evt->keyval;
    if (key == GDK_KEY_Return or key == GDK_KEY_Linefeed) {
        if (auto selected_row = _CPSuggestions->get_selected_row(); selected_row) {
            selected_row->activate();
        }
        return true;
    } else if (key == GDK_KEY_Up) {
        if (!_CPHistory->get_children().empty()) {
            set_mode(CPMode::HISTORY);
            return true;
        }
    } else if (key == GDK_KEY_Down) {
        if (!_CPSuggestions->get_children().empty()) {
             _CPSuggestions->unselect_all();
        }
    }
    return false;
}

bool CommandPalette::on_key_press_cpfilter_history_mode(GdkEventKey *evt)
{
    if (evt->keyval == GDK_KEY_BackSpace) {
        return true;
    }
    return false;
}

/**
 * Executes action when enter pressed
 */
bool CommandPalette::on_key_press_cpfilter_input_mode(GdkEventKey *evt, const ActionPtrName &action_ptr_name)
{
    switch (evt->keyval) {
        case GDK_KEY_Return:
            [[fallthrough]];
        case GDK_KEY_Linefeed:
            execute_action(action_ptr_name, _CPFilter->get_text());
            close();
            return true;
    }
    return false;
}

void CommandPalette::hide_suggestions()
{
    _CPBase->set_size_request(-1, 10);
    _CPListBase->hide();
}
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
    const 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(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)
{
    static auto prefs = Inkscape::Preferences::get();

    bool write_to_history = true;

    // if the last element in CPHistory is already this, don't update history file
    if (not _CPHistory->get_children().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) {
        prefs->setBool("/options/onimport", true);
        file_import(SP_ACTIVE_DOCUMENT, uri, nullptr);
        prefs->setBool("/options/onimport", true);

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

        close();
        return true;
    }

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

    close();
    return true;
} // namespace Dialog

/**
 * 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)
{
    // 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);

        _cpfilter_key_press_connection = _CPFilter->signal_key_press_event().connect(
            sigc::bind<ActionPtrName>(sigc::mem_fun(*this, &CommandPalette::on_key_press_cpfilter_input_mode),
                                      action_ptr_name),
            false);

        // 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 = "bool";
                break;
            case TypeOfVariant::INT:
                type_string = "integer";
                break;
            case TypeOfVariant::DOUBLE:
                type_string = "double";
                break;
            case TypeOfVariant::STRING:
                type_string = "string";
                break;
            case TypeOfVariant::TUPLE_DD:
                type_string = "pair of doubles";
                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.length()) {
            _CPFilter->set_placeholder_text(action_hint);
            _CPFilter->set_tooltip_text(action_hint);
        } else {
            _CPFilter->set_placeholder_text("Enter a " + type_string + "...");
            _CPFilter->set_tooltip_text("Enter a " + type_string + "...");
        }


        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) {
        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_LETTET_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_LETTET_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_LETTET_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 (const auto &character : search_string_character) {
        auto [alphabet, occurrence] = character;
        for (int i = 0; i < subject_string.length() && occurrence; i++) {
            if (subject_string[i] == alphabet) {
                if (i == 0)
                    fuzzy_cost += FIRST_LETTET_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) {
        if (fuzzy_search(CPName->get_text(), _search_text)) {
            add_color(CPName, _search_text, CPName->get_text());
            return fuzzy_points(CPName->get_text(), _search_text);
        }

        if (fuzzy_search(CPName->get_tooltip_text(), _search_text)) {
            add_color(CPName, _search_text, CPName->get_tooltip_text(), true);
            return fuzzy_points(CPName->get_tooltip_text(), _search_text);
        }

        if (fuzzy_tolerance_search(CPName->get_text(), _search_text)) {
            add_color(CPName, _search_text, CPName->get_text());
            return fuzzy_tolerance_points(CPName->get_text(), _search_text);
        }

        if (fuzzy_tolerance_search(CPName->get_tooltip_text(), _search_text)) {
            add_color(CPName, _search_text, CPName->get_tooltip_text(), true);
            return fuzzy_tolerance_points(CPName->get_tooltip_text(), _search_text);
        }
    }
    if (CPDescription && normal_search(CPDescription->get_text(), _search_text)) {
        add_color_description(CPDescription, _search_text);
        return fuzzy_points(CPDescription->get_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;
}

/**
 * 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)
{
    // 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);

    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) {
        if (fuzzy_search(cp_name_1->get_text(), _search_text)) {
            text_len_1 = cp_name_1->get_text().length();
            fuzzy_points_count_1 = fuzzy_points(cp_name_1->get_text(), _search_text);
        }
        if (fuzzy_search(cp_name_2->get_text(), _search_text)) {
            text_len_2 = cp_name_2->get_text().length();
            fuzzy_points_count_2 = fuzzy_points(cp_name_2->get_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(cp_name_1->get_text(), _search_text)) {
            text_len_1 = cp_name_1->get_text().length();
            fuzzy_points_count_1 = fuzzy_tolerance_points(cp_name_1->get_text(), _search_text);
        }
        if (fuzzy_tolerance_search(cp_name_2->get_text(), _search_text)) {
            text_len_2 = cp_name_2->get_text().length();
            fuzzy_points_count_2 = fuzzy_tolerance_points(cp_name_2->get_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_search(cp_name_1->get_tooltip_text(), _search_text)) {
            text_len_1 = cp_name_1->get_tooltip_text().length();
            fuzzy_points_count_1 = fuzzy_points(cp_name_1->get_tooltip_text(), _search_text) + TOOLTIP_PENALTY;
        }
        if (fuzzy_search(cp_name_2->get_tooltip_text(), _search_text)) {
            text_len_2 = cp_name_2->get_tooltip_text().length();
            fuzzy_points_count_2 = fuzzy_points(cp_name_2->get_tooltip_text(), _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(cp_name_1->get_tooltip_text(), _search_text)) {
            text_len_1 = cp_name_1->get_tooltip_text().length();
            fuzzy_points_count_1 = fuzzy_tolerance_points(cp_name_1->get_tooltip_text(), _search_text) +
                                   TOOLTIP_PENALTY; // Adding a constant integer to decrease the prefrence
        }
        if (fuzzy_tolerance_search(cp_name_2->get_tooltip_text(), _search_text)) {
            text_len_2 = cp_name_2->get_tooltip_text().length();
            fuzzy_points_count_2 = fuzzy_tolerance_points(cp_name_2->get_tooltip_text(), _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;
        }
    }

    if (cp_description_1 && normal_search(cp_description_1->get_text(), _search_text)) {
        text_len_1 = cp_description_1->get_text().length();
        fuzzy_points_count_1 = fuzzy_points(cp_description_1->get_text(), _search_text) +
                               DESCRIPTION_PENALTY; // Adding a constant integer to decrease the prefrence
    }
    if (cp_description_2 && normal_search(cp_description_2->get_text(), _search_text)) {
        text_len_2 = cp_description_2->get_text().length();
        fuzzy_points_count_2 = fuzzy_points(cp_description_2->get_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;
}

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

            _CPFilter->set_text("");
            _CPFilter->set_icon_from_icon_name("edit-find-symbolic");
            _CPFilter->set_placeholder_text("Search operation...");
            _CPFilter->set_tooltip_text("Search operation...");
            show_suggestions();

            // Show Suggestions instead of history
            _CPHistoryScroll->set_no_show_all();
            _CPHistoryScroll->hide();

            _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_key_press_connection.disconnect();

            _cpfilter_search_connection =
                _CPFilter->signal_search_changed().connect(sigc::mem_fun(*this, &CommandPalette::on_search));
            _cpfilter_key_press_connection = _CPFilter->signal_key_press_event().connect(
                sigc::mem_fun(*this, &CommandPalette::on_key_press_cpfilter_search_mode), false);

            _search_text = "";
            _CPSuggestions->invalidate_filter();
            break;

        case CPMode::INPUT:
            if (_mode == CPMode::INPUT) {
                return;
            }
            _cpfilter_search_connection.disconnect();
            _cpfilter_key_press_connection.disconnect();

            hide_suggestions();
            _CPFilter->set_text("");
            _CPFilter->grab_focus();

            _CPFilter->set_icon_from_icon_name("input-keyboard");
            _CPFilter->set_placeholder_text("Enter action argument");
            _CPFilter->set_tooltip_text("Enter action argument");

            break;

        case CPMode::SHELL:
            if (_mode == CPMode::SHELL) {
                return;
            }

            hide_suggestions();
            _CPFilter->set_icon_from_icon_name("gtk-search");
            _cpfilter_search_connection.disconnect();
            _cpfilter_key_press_connection.disconnect();

            break;

        case CPMode::HISTORY:
            if (_mode == CPMode::HISTORY) {
                return;
            }

            if (_CPHistory->get_children().empty()) {
                return;
            }

            // Show history instead of suggestions
            _CPSuggestionsScroll->set_no_show_all();
            _CPHistoryScroll->set_no_show_all(false);

            _CPSuggestionsScroll->hide();
            _CPHistoryScroll->show_all();

            _CPFilter->set_icon_from_icon_name("format-justify-fill");
            _CPFilter->set_icon_tooltip_text(N_("History mode"));
            _cpfilter_search_connection.disconnect();
            _cpfilter_key_press_connection.disconnect();

            _cpfilter_key_press_connection = _CPFilter->signal_key_press_event().connect(
                sigc::mem_fun(*this, &CommandPalette::on_key_press_cpfilter_history_mode), false);

            _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
                const auto last_row = _CPHistory->get_row_at_index(_CPHistory->get_children().size() - 1);
                _CPHistory->select_row(*last_row);
                last_row->grab_focus();
            }

            {
                // FIXME: scroll to bottom
                const auto adjustment = _CPHistoryScroll->get_vadjustment();
                adjustment->set_value(adjustment->get_upper());
            }

            break;
    }
    _mode = mode;
}

/**
 * Calls actions with parameters
 */
CommandPalette::ActionPtrName CommandPalette::get_action_ptr_name(const 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();
    auto action_domain_string = full_action_name.substr(0, full_action_name.find('.')); // app, win, doc
    auto action_name = full_action_name.substr(full_action_name.find('.') + 1);

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

    return {action_ptr, 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);
    }
    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();
            break;
    }
    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 event_box = dynamic_cast<Gtk::EventBox *>(child->get_child());
    if (event_box) {
        // 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 CPSynapseBox = dynamic_cast<Gtk::Box *>(event_box->get_child());
        if (CPSynapseBox) {
            auto synapse_children = CPSynapseBox->get_children();
            auto CPNameBox = dynamic_cast<Gtk::Box *>(synapse_children[0]);
            if (CPNameBox) {
                auto name_children = CPNameBox->get_children();
                auto CPName = dynamic_cast<Gtk::Label *>(name_children[0]);
                auto CPDescription = dynamic_cast<Gtk::Label *>(name_children[1]);
                return std::pair(CPName, CPDescription);
            }
        }
    }
    return std::pair(nullptr, nullptr);
}

Gtk::Label *CommandPalette::get_full_action_name(Gtk::ListBoxRow *child)
{
    auto event_box = dynamic_cast<Gtk::EventBox *>(child->get_child());
    if (event_box) {
        auto CPSynapseBox = dynamic_cast<Gtk::Box *>(event_box->get_child());
        if (CPSynapseBox) {
            auto synapse_children = CPSynapseBox->get_children();
            auto CPActionFullButton = dynamic_cast<Gtk::Button *>(synapse_children[1]);
            if (CPActionFullButton) {
                auto synapse_button = CPActionFullButton->get_children();
                auto CPSinapseButtonBox = dynamic_cast<Gtk::Box *>(synapse_button[0]);
                if (CPSinapseButtonBox) {
                    auto synapse_button_content = CPSinapseButtonBox->get_children();
                    return dynamic_cast<Gtk::Label *>(synapse_button_content[1]);
                }
            }
        }
    }

    return nullptr;
}

void CommandPalette::load_app_actions()
{
    auto gapp = InkscapeApplication::instance()->gtk_app();
    std::vector<ActionPtrName> all_actions_info;

    std::vector<Glib::ustring> actions = gapp->list_actions();
    for (const auto &action : actions) {
        generate_action_operation(get_action_ptr_name("app." + action), true);
    }
}

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

        if (auto document = window->get_document(); document) {
            auto map = document->getActionGroup();
            if (map) {
                std::vector<Glib::ustring> actions = map->list_actions();
                for (auto action : actions) {
                    generate_action_operation(get_action_ptr_name("doc." + 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 Dialog
} // namespace UI
} // namespace Inkscape

/*
  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 :
