/*
 * Copyright (C) 2004 John Ellis
 * Copyright (C) 2008 - 2016 The Geeqie Team
 *
 * Author: John Ellis
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include "ui-bookmark.h"

#include <algorithm>
#include <array>
#include <cstdlib>
#include <cstring>
#include <string>

#include <gdk-pixbuf/gdk-pixbuf.h>
#include <gdk/gdk.h>
#include <glib-object.h>
#include <pango/pango.h>

#include "compat.h"
#include "dnd.h"
#include "history-list.h"
#include "intl.h"
#include "layout.h"
#include "main-defines.h"
#include "misc.h"
#include "pixbuf-util.h"
#include "ui-file-chooser.h"
#include "ui-fileops.h"
#include "ui-menu.h"
#include "ui-misc.h"
#include "ui-tabcomp.h"
#include "ui-utildlg.h"

/*
 *-----------------------------------------------------------------------------
 * bookmarks
 *-----------------------------------------------------------------------------
 */

#define BOOKMARK_DATA_KEY "bookmarkdata"
#define MARKER_PATH "[path]"
#define MARKER_ICON "[icon]"

namespace
{

struct BookButtonData
{
	std::string text;
	std::string name;
	std::string path;
	std::string icon;
};

struct BookMarkData
{
	GtkWidget *widget;
	GtkWidget *box;
	std::string key;

	BookmarkSelectFunc select_func;
	BookmarkDropFunc drop_func;

	gboolean no_defaults;
	gboolean editable;
	gboolean only_directories;

	GtkWidget *active_button;
};

struct BookmarkDropData
{
	GtkWidget *list;
	gchar *path;
};

struct BookPropData
{
	GtkWidget *name_entry;
	GtkWidget *path_entry;
	GtkWidget *icon_entry;

	std::string key;
	std::string text;
};

enum {
	TARGET_URI_LIST,
	TARGET_X_URL,
	TARGET_TEXT_PLAIN
};

std::vector<BookMarkData *> bookmark_widget_list;
std::vector<std::pair<std::string, std::string>> bookmark_default_list;

const gchar *bookmark_icon(const gchar *path)
{
	if (!isfile(path)) return nullptr;

	g_autofree gchar *real_path = realpath(path, nullptr);

	return strstr(real_path, get_collections_dir()) ? PIXBUF_INLINE_COLLECTION : GQ_ICON_FILE;
}

} // namespace

static void bookmark_populate_all(const std::string &key);


static BookButtonData *bookmark_from_string(const gchar *text)
{
	if (!text)
		{
		auto *b = new BookButtonData();
		b->name = _("New Bookmark");
		b->path = homedir();
		return b;
		}

	const gchar *path_ptr = strstr(text, MARKER_PATH);
	const gchar *icon_ptr = strstr(text, MARKER_ICON);

	if (path_ptr && icon_ptr && icon_ptr < path_ptr)
		{
		log_printf("warning, bookmark icon must be after path\n");
		return nullptr;
		}

	auto *b = new BookButtonData();
	b->text = text;

	if (path_ptr)
		{
		static const size_t marker_path_len = strlen(MARKER_PATH);
		gint l;

		l = path_ptr - text;
		b->name.assign(text, l);
		path_ptr += marker_path_len;
		if (icon_ptr)
			{
			l = icon_ptr - path_ptr;
			b->path.assign(path_ptr, l);
			}
		else
			{
			b->path = path_ptr;
			}
		}
	else
		{
		b->name = text;
		}

	if (icon_ptr)
		{
		static const size_t marker_icon_len = strlen(MARKER_ICON);
		b->icon = icon_ptr + marker_icon_len;
		}

	return b;
}

static gchar *bookmark_string(const gchar *name, const gchar *path, const gchar *icon)
{
	if (!name) name = _("New Bookmark");

	if (icon && icon[0] != '\0')
		{
		return g_strdup_printf("%s" MARKER_PATH "%s" MARKER_ICON "%s", name, path, icon);
		}

	return g_strdup_printf("%s" MARKER_PATH "%s", name, path);
}

static void bookmark_select_cb(GtkWidget *button, gpointer data)
{
	auto bm = static_cast<BookMarkData *>(data);
	BookButtonData *b;

	b = static_cast<BookButtonData *>(g_object_get_data(G_OBJECT(button), "bookbuttondata"));
	if (!b) return;

	if (bm->select_func) bm->select_func(b->path.c_str());
}

static void bookmark_drop_data_free(BookmarkDropData *drop_data)
{
	g_object_unref(drop_data->list);
	g_free(drop_data->path);
	g_free(drop_data);
}

static void bookmark_dnd_file_received(GdkDrop *drop, GList *list, gpointer data)
{
	auto *drop_data = static_cast<BookmarkDropData *>(data);
	auto *bm = static_cast<BookMarkData *>(g_object_get_data(G_OBJECT(drop_data->list), BOOKMARK_DATA_KEY));
	auto action = GDK_ACTION_NONE;

	if (bm && bm->drop_func && list)
		{
		bm->drop_func(drop_data->path, list);
		action = GDK_ACTION_COPY;
		}

	gdk_drop_finish(drop, action);
	bookmark_drop_data_free(drop_data);
}

static gboolean bookmark_dnd_drop(GtkDropTargetAsync *target, GdkDrop *drop, gdouble, gdouble, gpointer data)
{
	auto *bm = static_cast<BookMarkData *>(data);
	GtkWidget *button = gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(target));
	auto *b = static_cast<BookButtonData *>(g_object_get_data(G_OBJECT(button), "bookbuttondata"));

	if (!bm || !bm->drop_func || !b) return FALSE;

	auto *drop_data = g_new(BookmarkDropData, 1);
	drop_data->list = GTK_WIDGET(g_object_ref(bm->widget));
	drop_data->path = g_strdup(b->path.c_str());

	dnd_read_file_list_async(drop, bookmark_dnd_file_received, drop_data);

	return TRUE;
}

static void bookmark_dnd_init(BookMarkData *bm, GtkWidget *button)
{
	if (!bm->drop_func) return;

	static const char *mime_types[] = {"text/uri-list"};
	GdkContentFormats *formats = gdk_content_formats_new(mime_types, G_N_ELEMENTS(mime_types));
	GtkDropTargetAsync *drop_target = gtk_drop_target_async_new(formats, static_cast<GdkDragAction>(GDK_ACTION_COPY | GDK_ACTION_MOVE));
	g_signal_connect(drop_target, "drop", G_CALLBACK(bookmark_dnd_drop), bm);
	gtk_widget_add_controller(button, GTK_EVENT_CONTROLLER(drop_target));
}

static void bookmark_edit_ok_cb(GenericDialog *, gpointer data)
{
	auto p = static_cast<BookPropData *>(data);

	const char *name = gtk_editable_get_text(GTK_EDITABLE(p->name_entry));
	g_autofree gchar *path = remove_trailing_slash(gtk_editable_get_text(GTK_EDITABLE(p->path_entry)));
	const char *icon = gtk_editable_get_text(GTK_EDITABLE(p->icon_entry));

	g_autofree gchar *new_string = bookmark_string(name, path, icon);

	if (!p->text.empty())
		{
		history_list_item_change(p->key.c_str(), p->text.c_str(), new_string);
		}
	else
		{
		history_list_add_to_key(p->key.c_str(), new_string, 0);
		}

	if (path && path[0] != '\0') tab_completion_append_to_history(p->path_entry, path);
	if (icon && icon[0] != '\0') tab_completion_append_to_history(p->icon_entry, icon);

	bookmark_populate_all(p->key);
}

static void bookmark_edit(const std::string &key, const BookButtonData *bb, GtkWidget *parent)
{
	GtkWidget *table;

	auto *p = new BookPropData();
	p->key = key;
	p->text = bb->text;

	GenericDialog *gd = generic_dialog_new(_("Edit Bookmark"), "bookmark_edit",
	                                       parent, TRUE,
	                                       generic_dialog_dummy_cb, p);
	g_object_set_data_full(G_OBJECT(gd->dialog), "bookpropdata", p, delete_cb<BookPropData>);

	generic_dialog_add_message(gd, nullptr, _("Edit Bookmark"), nullptr, FALSE);

	generic_dialog_add_button(gd, GQ_ICON_OK, "OK",
				  bookmark_edit_ok_cb, TRUE);

	table = pref_table_new(gd->vbox, 3, 2, FALSE, TRUE);
	pref_table_label(table, 0, 0, _("Name:"), GTK_ALIGN_END);

	p->name_entry = gtk_entry_new();
	gtk_widget_set_size_request(p->name_entry, 300, -1);
	gq_gtk_entry_set_text(GTK_ENTRY(p->name_entry), bb->name.c_str());
	gtk_grid_attach(GTK_GRID(table), p->name_entry, 1, 0, 1, 1);
	generic_dialog_attach_default(gd, p->name_entry);
	gtk_widget_show(p->name_entry);

	pref_table_label(table, 0, 1, _("Path:"), GTK_ALIGN_END);

	p->path_entry = tab_completion_new_with_history(nullptr, bb->path.c_str(), "bookmark_path", -1);
	tab_completion_add_select_button(p->path_entry, nullptr, TRUE, nullptr, nullptr, nullptr);
	gtk_grid_attach(GTK_GRID(table), tab_completion_get_box(p->path_entry), 1, 1, 1, 1);
	generic_dialog_attach_default(gd, p->path_entry);

	pref_table_label(table, 0, 2, _("Icon:"), GTK_ALIGN_END);

	p->icon_entry = tab_completion_new_with_history(nullptr, bb->icon.c_str(), "bookmark_icons", -1);
	tab_completion_add_select_button(p->icon_entry, _("Select icon"), FALSE, nullptr, nullptr, nullptr);
	gtk_grid_attach(GTK_GRID(table), tab_completion_get_box(p->icon_entry), 1, 2, 1, 1);
	generic_dialog_attach_default(gd, p->icon_entry);

	gtk_widget_show(gd->dialog);
}

static void bookmark_move(BookMarkData *bm, GtkWidget *button, gint direction)
{
	if (!bm->editable) return;

	auto *b = static_cast<BookButtonData *>(g_object_get_data(G_OBJECT(button), "bookbuttondata"));
	if (!b) return;

	int p = -1;
	int i = 0;
	for (GtkWidget *child = gtk_widget_get_first_child(bm->box);
	    child;
	    child = gtk_widget_get_next_sibling(child), ++i)
		{
		if (child == button)
			{
			p = i;
			break;
			}
		}

	if (p < 0 || p + direction < 0) return;

	std::string key_holder = bm->key;
	bm->key = "_TEMPHOLDER";
	history_list_item_move(key_holder.c_str(), b->text.c_str(), -direction);
	bookmark_populate_all(key_holder);
	bm->key = key_holder;

	gq_gtk_box_reorder_child(GTK_BOX(bm->box), button, p + direction);
}

static void bookmark_menu_prop_cb(GSimpleAction *, GVariant *, gpointer data)
{
	auto bm = static_cast<BookMarkData *>(data);

	if (!bm->active_button) return;

	auto *b = static_cast<BookButtonData *>(g_object_get_data(G_OBJECT(bm->active_button), "bookbuttondata"));
	if (!b) return;

	bookmark_edit(bm->key, b, bm->active_button);
}

template<gint direction>
static void bookmark_menu_move_cb(GSimpleAction *, GVariant *, gpointer data)
{
	auto *bm = static_cast<BookMarkData *>(data);

	if (!bm->active_button) return;

	bookmark_move(bm, bm->active_button, direction);
}

static void bookmark_menu_remove_cb(GSimpleAction *, GVariant *, gpointer data)
{
	auto bm = static_cast<BookMarkData *>(data);

	if (!bm->active_button) return;

	auto *b = static_cast<BookButtonData *>(g_object_get_data(G_OBJECT(bm->active_button), "bookbuttondata"));
	if (!b) return;

	history_list_item_remove(bm->key.c_str(), b->text.c_str());
	bookmark_populate_all(bm->key);
}

static void bookmark_menu_append_item(GMenu *menu, const gchar *label, const gchar *icon_name, const gchar *action_name)
{
	g_autoptr(GMenuItem) item = g_menu_item_new(label, action_name);
	g_autoptr(GIcon) icon = g_themed_icon_new(icon_name);

	g_menu_item_set_icon(item, icon);
	g_menu_append_item(menu, item);
}

static void bookmark_menu_popup(BookMarkData *bm, GtkWidget *button)
{
	g_autoptr(GSimpleActionGroup) action_group = g_simple_action_group_new();
	g_autoptr(GMenu) menu = g_menu_new();

	bm->active_button = button;

	g_autoptr(GSimpleAction) properties_action = g_simple_action_new("properties", nullptr);
	g_autoptr(GSimpleAction) move_up_action = g_simple_action_new("move-up", nullptr);
	g_autoptr(GSimpleAction) move_down_action = g_simple_action_new("move-down", nullptr);
	g_autoptr(GSimpleAction) remove_action = g_simple_action_new("remove", nullptr);

	for (GSimpleAction *action : {properties_action, move_up_action, move_down_action, remove_action})
		{
		g_simple_action_set_enabled(action, bm->editable);
		}

	g_signal_connect(properties_action, "activate", G_CALLBACK(bookmark_menu_prop_cb), bm);
	g_signal_connect(move_up_action, "activate", G_CALLBACK(bookmark_menu_move_cb<-1>), bm);
	g_signal_connect(move_down_action, "activate", G_CALLBACK(bookmark_menu_move_cb<1>), bm);
	g_signal_connect(remove_action, "activate", G_CALLBACK(bookmark_menu_remove_cb), bm);

	g_action_map_add_action(G_ACTION_MAP(action_group), G_ACTION(properties_action));
	g_action_map_add_action(G_ACTION_MAP(action_group), G_ACTION(move_up_action));
	g_action_map_add_action(G_ACTION_MAP(action_group), G_ACTION(move_down_action));
	g_action_map_add_action(G_ACTION_MAP(action_group), G_ACTION(remove_action));

	bookmark_menu_append_item(menu, _("_Properties…"), PIXBUF_INLINE_ICON_PROPERTIES, "bookmark.properties");
	bookmark_menu_append_item(menu, _("Move _up"), GQ_ICON_GO_UP, "bookmark.move-up");
	bookmark_menu_append_item(menu, _("Move _down"), GQ_ICON_GO_DOWN, "bookmark.move-down");
	bookmark_menu_append_item(menu, _("_Remove"), GQ_ICON_REMOVE, "bookmark.remove");

	GtkWidget *popover = gtk_popover_menu_new_from_model(G_MENU_MODEL(menu));
	gtk_widget_insert_action_group(popover, "bookmark", G_ACTION_GROUP(action_group));
	popover_set_parent(popover, button);
	popover_popup(popover);
}

static gboolean bookmark_press_common(GtkWidget *button, guint button_id, gpointer data)
{
	auto bm = static_cast<BookMarkData *>(data);

	if (button_id != GDK_BUTTON_SECONDARY) return FALSE;

	bookmark_menu_popup(bm, button);

	return TRUE;
}

static void bookmark_gesture_press_cb(GtkGestureClick *gesture, gint, gdouble, gdouble, gpointer data)
{
	GtkWidget *button = gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(gesture));
	bookmark_press_common(button, gtk_gesture_single_get_current_button(GTK_GESTURE_SINGLE(gesture)), data);
}

static gboolean bookmark_keypress_cb(GtkEventControllerKey *controller, guint keyval, guint, GdkModifierType state, gpointer data)
{
	GtkWidget *button = gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(controller));
	auto bm = static_cast<BookMarkData *>(data);

	switch (keyval)
		{
		case GDK_KEY_F10:
			if (!(state & GDK_CONTROL_MASK)) return FALSE;
			/* fall through */
		case GDK_KEY_Menu:
			bookmark_menu_popup(bm, button);
			return TRUE;
			break;
		case GDK_KEY_Up:
			if (state & GDK_SHIFT_MASK)
				{
				bookmark_move(bm, button, -1);
				return TRUE;
				}
			break;
		case GDK_KEY_Down:
			if (state & GDK_SHIFT_MASK)
				{
				bookmark_move(bm, button, 1);
				return TRUE;
				}
			break;
		default:
			break;
		}

	return FALSE;
}

static gboolean bookmark_tooltip_cb(GtkWidget *button, gint, gint, gboolean,
                                    GtkTooltip *, gpointer user_data)
{
	auto *b = static_cast<BookButtonData *>(user_data);

	gtk_widget_set_tooltip_text(button, b->path.c_str());

	return FALSE;
}

static void bookmark_add_button(BookMarkData *bm, const gchar *text)
{
	BookButtonData *b = bookmark_from_string(text);
	if (!b) return;

	if (b->name == ".")
		{
		b->path = history_list_find_last_path_by_key("path_list");

		g_autofree gchar *buf = bookmark_string(".", b->path.c_str(), b->icon.c_str());
		history_list_item_change("bookmarks", b->text.c_str(), buf);
		b->text = buf;
		}

	GtkWidget *button = gtk_button_new();
	gtk_widget_add_css_class(button, "flat");
	gq_gtk_box_pack_start(GTK_BOX(bm->box), button, FALSE, FALSE, 0);
	gtk_widget_show(button);

	g_object_set_data_full(G_OBJECT(button), "bookbuttondata", b, delete_cb<BookButtonData>);

	GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PREF_PAD_BUTTON_GAP);
	gq_gtk_container_add(button, box);
	gtk_widget_show(box);

	GtkWidget *image;
	if (!b->icon.empty())
		{
		g_autoptr(GdkPixbuf) pixbuf = nullptr;

		if (isfile(b->icon.c_str()))
			{
			g_autofree gchar *iconl = path_from_utf8(b->icon.c_str());
			pixbuf = gdk_pixbuf_new_from_file(iconl, nullptr);
			}
		else
			{
			constexpr gint size = 16;

			pixbuf = gq_gtk_icon_theme_load_icon_copy(gq_icon_theme_get_default(), b->icon.c_str(), size, GTK_ICON_LOOKUP_NONE);
			}

		if (pixbuf)
			{
			constexpr gint w = 16;
			constexpr gint h = 16;

			g_autoptr(GdkPixbuf) scaled = gdk_pixbuf_scale_simple(pixbuf, w, h, GDK_INTERP_BILINEAR);
			image = gtk_image_new_from_pixbuf(scaled);
			}
		else
			{
			image = gtk_image_new_from_icon_name(GQ_ICON_DIRECTORY);
			}
		}
	else
		{
		image = gtk_image_new_from_icon_name(GQ_ICON_DIRECTORY);
		}
	gq_gtk_box_pack_start(GTK_BOX(box), image, FALSE, FALSE, 0);
	gtk_widget_show(image);

	pref_label_new(box, b->name.c_str());

	g_signal_connect(G_OBJECT(button), "clicked",
	                 G_CALLBACK(bookmark_select_cb), bm);
	bookmark_dnd_init(bm, button);

	GtkGesture *gesture = gtk_gesture_click_new();
	gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(gesture), GDK_BUTTON_SECONDARY);
	g_signal_connect(gesture, "pressed", G_CALLBACK(bookmark_gesture_press_cb), bm);
	gtk_widget_add_controller(button, GTK_EVENT_CONTROLLER(gesture));
	GtkEventController *controller = gtk_event_controller_key_new();
	g_signal_connect(controller, "key-pressed", G_CALLBACK(bookmark_keypress_cb), bm);
	gtk_widget_add_controller(button, controller);

	gtk_widget_set_has_tooltip(button, TRUE);
	g_signal_connect(G_OBJECT(button), "query-tooltip", G_CALLBACK(bookmark_tooltip_cb), b);
}

static void bookmark_populate(BookMarkData *bm)
{
	while (GtkWidget *child = gtk_widget_get_first_child(bm->box))
		{
		gtk_box_remove(GTK_BOX(bm->box), child);
		}

	if (!bm->no_defaults && !history_list_find_by_key(bm->key.c_str()))
		{
		if (bookmark_default_list.empty())
			{
			g_autofree gchar *home_buf = bookmark_string(_("Home"), homedir(), nullptr);
			history_list_add_to_key(bm->key.c_str(), home_buf, 0);

			if (bm->key != "shortcuts")
				{
				g_autofree gchar *buf = bookmark_string(".", history_list_find_last_path_by_key("path_list"), nullptr);
				history_list_add_to_key(bm->key.c_str(), buf, 0);
				}

			const gchar *path = get_desktop_dir();
			if (isname(path))
				{
				g_autofree gchar *buf = bookmark_string(_("Desktop"), path, nullptr);
				history_list_add_to_key(bm->key.c_str(), buf, 0);
				}
			}

		for (const auto &[name, default_path] : bookmark_default_list)
			{
			const gchar *path = default_path.c_str();

			if (name == ".")
				{
				if (bm->key == "shortcuts") continue;

				path = history_list_find_last_path_by_key("path_list");
				}

			g_autofree gchar *buf = bookmark_string(name.c_str(), path, nullptr);
			history_list_add_to_key(bm->key.c_str(), buf, 0);
			}
		}

	const HistoryList *history_list = history_list_find_by_key(bm->key.c_str());
	if (!history_list) return;

	for (auto work = history_list->crbegin(); work != history_list->crend(); ++work)
		{
		bookmark_add_button(bm, work->c_str());
		}
}

static void bookmark_populate_all(const std::string &key)
{
	for (BookMarkData *bm : bookmark_widget_list)
		{
		if (bm->key != key) continue;

		bookmark_populate(bm);
		}
}

static void bookmark_data_destroy(gpointer data)
{
	auto *bm = static_cast<BookMarkData *>(data);

	if (auto it = std::find(bookmark_widget_list.cbegin(), bookmark_widget_list.cend(), bm);
	    it != bookmark_widget_list.cend())
		{
		bookmark_widget_list.erase(it);
		}

	delete bm;
}

GtkWidget *bookmark_list_new(const gchar *key, const BookmarkSelectFunc &select_func)
{
	auto *bm = new BookMarkData();
	bm->key = key ? key : "bookmarks";
	bm->select_func = select_func;
	bm->drop_func = nullptr;
	bm->no_defaults = FALSE;
	bm->editable = TRUE;
	bm->only_directories = FALSE;

	GtkWidget *scrolled = gtk_scrolled_window_new();

	g_autoptr(PangoLayout) layout = gtk_widget_create_pango_layout(scrolled, "reasonable width");
	GqSize size;
	pango_layout_get_pixel_size(layout, &size.width, &size.height);
	gtk_scrolled_window_set_min_content_width(GTK_SCROLLED_WINDOW(scrolled), size.width);

	gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);

	bm->box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
	gq_gtk_container_add(scrolled, bm->box);
	gtk_widget_show(bm->box);

	bookmark_populate(bm);

	g_object_set_data_full(G_OBJECT(bm->box), BOOKMARK_DATA_KEY, bm, bookmark_data_destroy);
	g_object_set_data(G_OBJECT(scrolled), BOOKMARK_DATA_KEY, bm);
	bm->widget = scrolled;

	bookmark_widget_list.push_back(bm);

	return scrolled;
}

void bookmark_list_set_key(GtkWidget *list, const gchar *key)
{
	BookMarkData *bm;

	if (!list || !key) return;

	bm = static_cast<BookMarkData *>(g_object_get_data(G_OBJECT(list), BOOKMARK_DATA_KEY));
	if (!bm || bm->key == key) return;

	bm->key = key;

	bookmark_populate(bm);
}

void bookmark_list_set_no_defaults(GtkWidget *list, gboolean no_defaults)
{
	BookMarkData *bm;

	bm = static_cast<BookMarkData *>(g_object_get_data(G_OBJECT(list), BOOKMARK_DATA_KEY));
	if (!bm) return;

	bm->no_defaults = no_defaults;
}

void bookmark_list_set_editable(GtkWidget *list, gboolean editable)
{
	BookMarkData *bm;

	bm = static_cast<BookMarkData *>(g_object_get_data(G_OBJECT(list), BOOKMARK_DATA_KEY));
	if (!bm) return;

	bm->editable = editable;
}

void bookmark_list_set_only_directories(GtkWidget *list, gboolean only_directories)
{
	BookMarkData *bm;

	bm = static_cast<BookMarkData *>(g_object_get_data(G_OBJECT(list), BOOKMARK_DATA_KEY));
	if (!bm) return;

	bm->only_directories = only_directories;
}

void bookmark_list_set_drop_func(GtkWidget *list, const BookmarkDropFunc &drop_func)
{
	BookMarkData *bm;

	bm = static_cast<BookMarkData *>(g_object_get_data(G_OBJECT(list), BOOKMARK_DATA_KEY));
	if (!bm) return;

	bm->drop_func = drop_func;
	bookmark_populate(bm);
}

void bookmark_list_add(GtkWidget *list, const gchar *name, const gchar *path)
{
	BookMarkData *bm;

	bm = static_cast<BookMarkData *>(g_object_get_data(G_OBJECT(list), BOOKMARK_DATA_KEY));
	if (!bm) return;

	g_autofree gchar *buf = bookmark_string(name, path, bookmark_icon(path));
	history_list_add_to_key(bm->key.c_str(), buf, 0);

	bookmark_populate_all(bm->key);
}

/**
 * @brief Allows apps to set up the defaults
 */
static void bookmark_add_default(const gchar *name, const gchar *path)
{
	if (!name || !path) return;

	bookmark_default_list.emplace_back(name, path);
}

void bookmark_setup_default()
{
	g_autofree gchar *current_path = get_current_dir();
	bookmark_add_default(".", current_path);
	bookmark_add_default(_("Home"), homedir());
	bookmark_add_default(_("Desktop"), get_desktop_dir());
	bookmark_add_default(_("Collections"), get_collections_dir());
}

struct BookmarkAliasData
{
	GtkWidget *entry;
	GtkWidget *list;
	gchar *selected_dir;
};

static void bookmark_alias_data_free(gpointer data)
{
	auto *bad = static_cast<BookmarkAliasData *>(data);
	g_free(bad->selected_dir);
	g_free(bad);
}

static void bookmark_alias_ok_cb(GenericDialog *, gpointer data)
{
	auto *bad = static_cast<BookmarkAliasData *>(data);
	const char *name = gtk_editable_get_text(GTK_EDITABLE(bad->entry));
	gboolean empty_name = (g_strcmp0(name, "") == 0);

	bookmark_list_add(bad->list, empty_name ? filename_from_path(bad->selected_dir) : name, bad->selected_dir);
}

static void bookmark_prompt_for_alias(GtkWidget *list, const gchar *selected_dir)
{
	auto *bad = g_new0(BookmarkAliasData, 1);
	bad->list = list;
	bad->selected_dir = g_strdup(selected_dir);

	GenericDialog *gd = generic_dialog_new(_("Add Bookmark"), "bookmark_add_alias", widget_get_toplevel(list), TRUE, nullptr, bad);
	g_object_set_data_full(G_OBJECT(gd->dialog), "bookmark_alias_data", bad, bookmark_alias_data_free);

	generic_dialog_add_message(gd, nullptr, _("Optional name"), nullptr, FALSE);
	generic_dialog_add_button(gd, GQ_ICON_OK, _("OK"), bookmark_alias_ok_cb, TRUE);

	GtkWidget *entry = gtk_entry_new();
	gtk_entry_set_placeholder_text(GTK_ENTRY(entry), _("Optional name…"));
	gtk_widget_set_tooltip_text(entry, _("Optional alias name for the shortcut.\nThis may be amended or added from the Sort Manager pane.\nIf none given, the basename of the folder is used"));
	gq_gtk_box_pack_start(GTK_BOX(gd->vbox), entry, FALSE, FALSE, 0);
	gtk_widget_show(entry);
	generic_dialog_attach_default(gd, entry);

	bad->entry = entry;
	gtk_widget_show(gd->dialog);
}

static void bookmark_add_response_cb(GFile *file, gpointer data)
{
	if (!file)
		{
		return;
		}

	g_autofree gchar *selected_dir = g_file_get_path(file);
	auto *list = static_cast<GtkWidget *>(data);
	bookmark_prompt_for_alias(list, selected_dir);
}

void bookmark_add_dialog(const gchar *title, GtkWidget *list)
{
	FileDialogData fdd{};

	fdd.action = FileDialogAction::SELECT_FOLDER;
	fdd.accept_text = _("Open");
	fdd.callback = bookmark_add_response_cb;
	fdd.data = list;
	fdd.filename = layout_get_path(get_current_layout());
	fdd.title = title;

	file_dialog_show(fdd);
}

/*
 *-----------------------------------------------------------------------------
 * combo with history key
 *-----------------------------------------------------------------------------
 */

struct HistoryComboData
{
	GtkWidget *combo;
	GtkWidget *entry;
	std::string history_key;
	gint history_levels;
};

/* if text is NULL, entry is set to the most recent item */
GtkWidget *history_combo_new(GtkWidget **entry, const gchar *text,
                             std::string history_key, gint max_levels)
{
	auto *hc = new HistoryComboData();
	hc->history_key = std::move(history_key);
	hc->history_levels = max_levels;

	hc->combo = gtk_combo_box_text_new_with_entry();

	hc->entry = gtk_combo_box_get_child(GTK_COMBO_BOX(hc->combo));
	if (!GTK_IS_ENTRY(hc->entry))
		{
		delete hc;
		return nullptr;
		}

	g_object_set_data_full(G_OBJECT(hc->combo), "history_combo_data", hc, delete_cb<HistoryComboData>);
	g_object_set_data(G_OBJECT(hc->entry), "history_combo_data", hc);

	gint n = 0;
	const HistoryList *history_list = history_list_find_by_key(hc->history_key.c_str());
	if (history_list)
		{
		n = history_list->size();
		for (const std::string &item : *history_list)
			{
			gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(hc->combo), item.c_str());
			}
		}

	if (text)
		{
		gq_gtk_entry_set_text(GTK_ENTRY(hc->entry), text);
		}
	else if (n > 0)
		{
		gtk_combo_box_set_active(GTK_COMBO_BOX(hc->combo), 0);
		}

	if (entry) *entry = hc->entry;
	return hc->combo;
}

/* if text is NULL, current entry text is used
 * widget can be the combo or entry widget
 */
void history_combo_append_history(GtkWidget *widget, const gchar *text)
{
	HistoryComboData *hc;

	hc = static_cast<HistoryComboData *>(g_object_get_data(G_OBJECT(widget), "history_combo_data"));
	if (!hc)
		{
		log_printf("widget is not a history combo\n");
		return;
		}

	g_autofree gchar *new_text = g_strdup(text ? text : gtk_editable_get_text(GTK_EDITABLE(hc->entry)));

	if (new_text && new_text[0] != '\0')
		{
		GtkTreeModel *store;

		history_list_add_to_key(hc->history_key.c_str(), new_text, hc->history_levels);

		gtk_combo_box_set_active(GTK_COMBO_BOX(hc->combo), -1);

		store = gtk_combo_box_get_model(GTK_COMBO_BOX(hc->combo));
		gtk_list_store_clear(GTK_LIST_STORE(store));

		const HistoryList *history_list = history_list_find_by_key(hc->history_key.c_str());
		if (!history_list) return;

		for (const std::string &item : *history_list)
			{
			gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(hc->combo), item.c_str());
			}
		}
}
/* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */
