/* vim: ts=4 sw=4
 */
/*
 * zenity-tree-column-view.c
 *
 * Copyright © 2023 Logan Rathbone
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#include "zenity-tree-column-view.h"
#include <config.h>

#define UI_FILE RESOURCE_BASE_PATH "/zenity-tree-column-view.ui"

/* ZenityTreeItem */

enum zenity_tree_item_prop_enum
{
	TEXT = 1,
	CHILD,
	N_PROPERTIES_ZENITY_TREE_ITEM
};

static GParamSpec *zenity_tree_item_properties[N_PROPERTIES_ZENITY_TREE_ITEM];

struct _ZenityTreeItem
{
	GObject parent_instance;
	GtkWidget *child;
	char *text;
};

G_DEFINE_TYPE (ZenityTreeItem, zenity_tree_item, G_TYPE_OBJECT)

static void
zenity_tree_item_set_property (GObject *object,
		guint property_id,
		const GValue *value,
		GParamSpec *pspec)
{
	ZenityTreeItem *self = ZENITY_TREE_ITEM(object);

	switch (property_id)
	{
		case TEXT:
			zenity_tree_item_set_text (self, g_value_get_string (value));
			break;

		case CHILD:
			zenity_tree_item_set_child (self, GTK_WIDGET(g_value_get_object (value)));
			break;

		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
			break;
	}
}

static void
zenity_tree_item_get_property (GObject *object,
		guint property_id,
		GValue *value,
		GParamSpec *pspec)
{
	ZenityTreeItem *self = ZENITY_TREE_ITEM(object);

	switch (property_id)
	{
		case TEXT:
			g_value_set_string (value, zenity_tree_item_get_text (self));
			break;

		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
			break;
	}
}
static void
zenity_tree_item_init (ZenityTreeItem *item)
{
}

static void
zenity_tree_item_dispose (GObject *object)
{
	ZenityTreeItem *self = ZENITY_TREE_ITEM(object);

	if (g_object_is_floating (self->child))
		g_warning ("%s: trying to dispose ZenityTreeItem before the widget has been parented. "
				"Likely a programmer error. A leak will likely result.", __func__);
	else
		g_clear_object (&self->child);

	G_OBJECT_CLASS(zenity_tree_item_parent_class)->dispose (object);
}

static void
zenity_tree_item_finalize (GObject *object)
{
	ZenityTreeItem *self = ZENITY_TREE_ITEM(object);

	g_free (self->text);

	G_OBJECT_CLASS(zenity_tree_item_parent_class)->finalize (object);
}

static void
zenity_tree_item_class_init (ZenityTreeItemClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS(klass);
	GParamFlags flags = G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT;

	object_class->dispose = zenity_tree_item_dispose;
	object_class->finalize = zenity_tree_item_finalize;
	object_class->set_property = zenity_tree_item_set_property;
	object_class->get_property = zenity_tree_item_get_property;

	zenity_tree_item_properties[TEXT] = g_param_spec_string ("text", NULL, NULL, NULL, flags);
	zenity_tree_item_properties[CHILD] = g_param_spec_object ("child", NULL, NULL, GTK_TYPE_WIDGET, flags);

	g_object_class_install_properties (object_class, N_PROPERTIES_ZENITY_TREE_ITEM, zenity_tree_item_properties);
}

GtkWidget *
zenity_tree_item_get_child (ZenityTreeItem *item)
{
	return item->child;
}

const char *
zenity_tree_item_get_text (ZenityTreeItem *item)
{
	return item->text;
}

void
zenity_tree_item_set_text (ZenityTreeItem *self, const char *text)
{
	g_clear_pointer (&self->text, g_free);
	self->text = g_strdup (text);

	g_object_notify_by_pspec (G_OBJECT(self), zenity_tree_item_properties[TEXT]);
}

void
zenity_tree_item_set_child (ZenityTreeItem *self, GtkWidget *child)
{
	if (self->child)
		g_object_unref (self->child);

	self->child = g_object_ref (child);

	g_object_notify_by_pspec (G_OBJECT(self), zenity_tree_item_properties[CHILD]);
}

ZenityTreeItem *
zenity_tree_item_new (const char *text, GtkWidget *child)
{
	ZenityTreeItem *item;

	g_return_val_if_fail (GTK_IS_WIDGET (child), NULL);

	item = g_object_new (ZENITY_TREE_TYPE_ITEM,
			"text", text,
			"child", child,
			NULL);

	return item;
}

/* ZenityTreeRow */

struct _ZenityTreeRow
{
	GObject parent_instance;
	GPtrArray *items;
};

G_DEFINE_TYPE (ZenityTreeRow, zenity_tree_row, G_TYPE_OBJECT)

static void
zenity_tree_row_init (ZenityTreeRow *self)
{
	self->items = g_ptr_array_new ();
}

static void
zenity_tree_row_class_init (ZenityTreeRowClass *klass)
{
}

ZenityTreeRow *
zenity_tree_row_new (void)
{
	return g_object_new (ZENITY_TREE_TYPE_ROW, NULL);
}

void
zenity_tree_row_add (ZenityTreeRow *row, ZenityTreeItem *item)
{
	g_ptr_array_add (row->items, item);
}

guint
zenity_tree_row_get_n_items (ZenityTreeRow *row)
{
	return row->items->len;
}

ZenityTreeItem *
zenity_tree_row_get_item (ZenityTreeRow *row, guint index)
{
	g_return_val_if_fail (index < row->items->len, NULL);

	return row->items->pdata[index];
}

/* ZenityTreeColumnView */

enum zenity_tree_column_view_signal_enum {
	ACTIVATED,
	LAST_SIGNAL
};

static guint zenity_tree_column_view_signals[LAST_SIGNAL];

enum zenity_tree_column_view_prop_enum
{
	MULTI = 1,
	LIST_TYPE,
	MODEL,
	N_PROPERTIES_ZENITY_TREE_COLUMN_VIEW
};

static GParamSpec *zenity_tree_column_view_properties[N_PROPERTIES_ZENITY_TREE_COLUMN_VIEW];

struct _ZenityTreeColumnView
{
	GtkWidget parent_instance;
	GtkWidget *scrolled_window;
	GtkColumnView *child_cv;
	gboolean multi;
	ZenityTreeListType list_type;
	GListModel *model;
	GtkStringFilter *filter;
	GtkCheckButton *initial_checkbutton_group;
};

G_DEFINE_TYPE (ZenityTreeColumnView, zenity_tree_column_view, GTK_TYPE_WIDGET)

/* Callback for the GClosure defined below. Just cram the text from all row
 * items into a single string; that way, if the string in the searchbar matches
 * any text in a given row, there will be a match, and only those rows will be
 * shown.
 */
static char *
eval_str (ZenityTreeRow *row)
{
	GString *gstring;

	g_return_val_if_fail (ZENITY_TREE_IS_ROW (row), NULL);

	gstring = g_string_new (NULL);

	for (guint i = 0; i < row->items->len; ++i)
	{
		ZenityTreeItem *item = row->items->pdata[i];
		g_string_append_printf (gstring, "%s ", item->text);
	}

	return g_string_free (gstring, FALSE);
}

void
zenity_tree_column_view_set_model (ZenityTreeColumnView *self, GListModel *model)
{
	GtkStringFilter *filter;
	GtkFilterListModel *filter_model;

	GtkExpression *expr;

	/* This tells the column view to use a callback with 'this' (ZenityTreeRow)
	 * as the instance and no other params or user_data, with a string retval.
	 */
	expr = gtk_cclosure_expression_new (G_TYPE_STRING,
			NULL,					/* GClosureMarshal marshal, */
			0, 						/* guint n_params, */
			NULL,					/* GtkExpression** params, */
			G_CALLBACK(eval_str),	/* GCallback callback_func, */
			NULL,					/* gpointer user_data, */
			NULL);					/* GClosureNotify user_destroy) */

	filter = gtk_string_filter_new (expr);
	filter_model = gtk_filter_list_model_new (model, GTK_FILTER(filter));

	self->model = model;
	self->filter = filter;

	if (self->multi)
	{
		gtk_column_view_set_model (self->child_cv,
				GTK_SELECTION_MODEL(gtk_multi_selection_new (G_LIST_MODEL(filter_model))));
	}
	else
	{
		gtk_column_view_set_model (self->child_cv,
				GTK_SELECTION_MODEL(gtk_single_selection_new (G_LIST_MODEL(filter_model))));
	}

	g_object_notify_by_pspec (G_OBJECT(self), zenity_tree_column_view_properties[MODEL]);
}

GListModel *
zenity_tree_column_view_get_model (ZenityTreeColumnView *self)
{
	return self->model;
}

static void
zenity_tree_column_view_emit_activated (ZenityTreeColumnView *self)
{
	g_signal_emit (self, zenity_tree_column_view_signals[ACTIVATED], 0);
}

static void
cv_check_or_radio_activated_cb (ZenityTreeColumnView *self, guint position, GtkColumnView *cv)
{
	GListModel *model = zenity_tree_column_view_get_model (self);
	ZenityTreeRow *row = g_list_model_get_item (model, position);
	ZenityTreeItem *item = zenity_tree_row_get_item (row, 0);
	GtkWidget *item_child = zenity_tree_item_get_child (item);
	GtkCheckButton *cb;

	if (! GTK_IS_CHECK_BUTTON (item_child))
		return;

	cb = GTK_CHECK_BUTTON(item_child);

	switch (self->list_type)
	{
		case ZENITY_TREE_LIST_CHECK:
			gtk_check_button_set_active (cb, !gtk_check_button_get_active (cb));
			break;

		case ZENITY_TREE_LIST_RADIO:
			gtk_check_button_set_active (cb, TRUE);
			break;

		default:
			g_warning ("%s: Programmer error: invalid list type.", __func__);
			break;
	}
}

void
zenity_tree_column_view_set_list_type (ZenityTreeColumnView *self, ZenityTreeListType type)
{
	self->list_type = type;

	g_signal_handlers_disconnect_by_func (self->child_cv, zenity_tree_column_view_emit_activated, self);

	switch (self->list_type)
	{
		case ZENITY_TREE_LIST_NONE:
		case ZENITY_TREE_LIST_IMAGE:
			gtk_column_view_set_single_click_activate (self->child_cv, FALSE);
			g_signal_connect_swapped (self->child_cv, "activate", G_CALLBACK(zenity_tree_column_view_emit_activated), self);
			break;

		case ZENITY_TREE_LIST_RADIO:
		case ZENITY_TREE_LIST_CHECK:
			gtk_column_view_set_single_click_activate (self->child_cv, TRUE);
			g_signal_connect_swapped (self->child_cv, "activate", G_CALLBACK(cv_check_or_radio_activated_cb), self);
			break;

		default:
			g_warning ("%s: Invalid ZenityTreeListType provided.", __func__);
			break;
	}

	g_object_notify_by_pspec (G_OBJECT(self), zenity_tree_column_view_properties[LIST_TYPE]);
}

ZenityTreeListType
zenity_tree_column_view_get_list_type (ZenityTreeColumnView *self)
{
	return self->list_type;
}

gboolean
zenity_tree_column_view_get_multi (ZenityTreeColumnView *self)
{
	return self->multi;
}

static void
zenity_tree_column_view_set_property (GObject *object,
		guint property_id,
		const GValue *value,
		GParamSpec *pspec)
{
	ZenityTreeColumnView *self = ZENITY_TREE_COLUMN_VIEW(object);

	switch (property_id)
	{
		case MULTI:
			self->multi = g_value_get_boolean (value);
			break;

		case LIST_TYPE:
			zenity_tree_column_view_set_list_type (self, g_value_get_enum (value));
			break;

		case MODEL:
			zenity_tree_column_view_set_model (self, G_LIST_MODEL(g_value_get_object (value)));
			break;

		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
			break;
	}
}

static void
zenity_tree_column_view_get_property (GObject *object,
		guint property_id,
		GValue *value,
		GParamSpec *pspec)
{
	ZenityTreeColumnView *self = ZENITY_TREE_COLUMN_VIEW(object);

	switch (property_id)
	{
		case MULTI:
			g_value_set_boolean (value, self->multi);
			break;

		case LIST_TYPE:
			g_value_set_enum (value, zenity_tree_column_view_get_list_type (self));
			break;

		case MODEL:
			g_value_set_object (value, G_OBJECT(zenity_tree_column_view_get_model (self)));
			break;

		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
			break;
	}
}

static void
zenity_tree_column_view_dispose (GObject *object)
{
	ZenityTreeColumnView *self = ZENITY_TREE_COLUMN_VIEW(object);

	g_clear_pointer (&self->scrolled_window, gtk_widget_unparent);

	G_OBJECT_CLASS(zenity_tree_column_view_parent_class)->dispose (object);
}

static void
zenity_tree_column_view_init (ZenityTreeColumnView *self)
{
	GtkWidget *widget = GTK_WIDGET(self);

	gtk_widget_init_template (widget);

	gtk_widget_set_hexpand (widget, TRUE);
	gtk_widget_set_vexpand (widget, TRUE);
}

static void
zenity_tree_column_view_class_init (ZenityTreeColumnViewClass *klass)
{
	GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
	GObjectClass *object_class = G_OBJECT_CLASS(klass);
	GParamFlags flags = G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT;

	object_class->dispose = zenity_tree_column_view_dispose;
	object_class->set_property = zenity_tree_column_view_set_property;
	object_class->get_property = zenity_tree_column_view_get_property;

	zenity_tree_column_view_properties[MULTI] = g_param_spec_boolean ("multi", NULL, NULL,
			FALSE, flags);
	zenity_tree_column_view_properties[LIST_TYPE] = g_param_spec_enum ("list-type", NULL, NULL,
			ZENITY_TYPE_TREE_LIST_TYPE, ZENITY_TREE_LIST_NONE, flags);
	zenity_tree_column_view_properties[MODEL] = g_param_spec_object ("model", NULL, NULL,
			G_TYPE_LIST_MODEL, flags);

	g_object_class_install_properties (object_class, N_PROPERTIES_ZENITY_TREE_COLUMN_VIEW, zenity_tree_column_view_properties);

	zenity_tree_column_view_signals[ACTIVATED] = g_signal_new_class_handler ("activated",
			G_OBJECT_CLASS_TYPE (object_class),
			G_SIGNAL_RUN_LAST,
		/* no default C function */
			NULL,
		/* defaults for accumulator, marshaller &c. */
			NULL, NULL, NULL,	
		/* No return type or params. */
			G_TYPE_NONE, 0);

	gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);

	gtk_widget_class_set_template_from_resource (widget_class, UI_FILE);

	gtk_widget_class_bind_template_child (widget_class, ZenityTreeColumnView, scrolled_window);
	gtk_widget_class_bind_template_child (widget_class, ZenityTreeColumnView, child_cv);
}

#if 0
static GtkWidget *
zenity_tree_column_view_new (GListModel *model)
{
	g_return_val_if_fail (G_IS_LIST_MODEL (model), NULL);

	return g_object_new (ZENITY_TREE_TYPE_COLUMN_VIEW,
			"model", model,
			NULL);
}
#endif

static void
editable_notify_text_cb (GtkEditable *editable, GParamSpec *pspec, ZenityTreeItem *item)
{
	const char *str = gtk_editable_get_text (editable);
	zenity_tree_item_set_text (item, str);
}

static void
factory_bind_cb (ZenityTreeColumnView *self,
		GtkListItem *list_item,
		GtkSignalListItemFactory *factory)
{
	ZenityTreeRow *row = gtk_list_item_get_item (list_item);
	int col_index = GPOINTER_TO_INT (g_object_get_data (G_OBJECT(factory), "col_index"));
	ZenityTreeItem *item;
	GtkWidget *item_child;
	const char *item_text;

	item = zenity_tree_row_get_item (row, col_index);
	item_child = zenity_tree_item_get_child (item);
	item_text = zenity_tree_item_get_text (item);
	gtk_list_item_set_child (list_item, item_child);

	gtk_widget_set_halign (item_child, GTK_ALIGN_START);

	if (GTK_IS_EDITABLE (item_child))	/* handle first to capture anything editable */
	{
		gtk_editable_set_text (GTK_EDITABLE(item_child), item_text);
		g_signal_connect (item_child, "notify::text", G_CALLBACK(editable_notify_text_cb), item);
	}
	else if (GTK_IS_LABEL (item_child))
	{
		gtk_label_set_text (GTK_LABEL(item_child), item_text);
	}
	else if (GTK_IS_CHECK_BUTTON (item_child) && item_text)
	{
		gboolean initialized = FALSE;
		gboolean checked = FALSE;

		if (g_object_get_data (G_OBJECT(item_child), "initialized"))
			initialized = TRUE;

		if (! initialized)
		{
			/* Radio-button-ize our check buttons if radio list requested
			*/
			if (self->list_type == ZENITY_TREE_LIST_RADIO)
			{
				if (!self->initial_checkbutton_group)
				{
					self->initial_checkbutton_group = GTK_CHECK_BUTTON(item_child);
				}

				/* Annoying - if you try to add checkbtn to its own group, gtk
				 * spews errors instead of just returning silently.
				 */
				if (GTK_CHECK_BUTTON(item_child) != self->initial_checkbutton_group)	
					gtk_check_button_set_group (GTK_CHECK_BUTTON(item_child), self->initial_checkbutton_group);
			}

			if (g_ascii_strcasecmp (item_text, "true") == 0)
				checked = TRUE;

			gtk_check_button_set_active (GTK_CHECK_BUTTON(item_child), checked);

			g_object_set_data (G_OBJECT(item_child), "initialized", GINT_TO_POINTER(TRUE));
		}
	}
	else if (GTK_IS_IMAGE (item_child) && item_text)
	{
		gtk_image_set_from_file (GTK_IMAGE(item_child), item_text);
	}
	else
	{
		g_warning ("%s: Widget type of child not implemented.", __func__);
	}
}

void
zenity_tree_column_view_add_column (ZenityTreeColumnView *self, const char *col_name)
{
	int new_col_index;
	GtkListItemFactory *factory;
	GtkColumnViewColumn *column;

	new_col_index = zenity_tree_column_view_get_n_columns (self);

	factory = gtk_signal_list_item_factory_new ();
	g_object_set_data (G_OBJECT(factory), "col_index", GINT_TO_POINTER(new_col_index));
	g_signal_connect_swapped (factory, "bind", G_CALLBACK (factory_bind_cb), self);

	/* nb: seems the signals for the factory need to be setup first *before* creating the column. */

	column = gtk_column_view_column_new (col_name, factory);
	
	if (new_col_index == 0 &&
			(self->list_type == ZENITY_TREE_LIST_CHECK || self->list_type == ZENITY_TREE_LIST_RADIO))
	{
		gtk_column_view_column_set_resizable (column, FALSE);
	}
	else
	{
		gtk_column_view_column_set_resizable (column, TRUE);
		gtk_column_view_column_set_expand (column, TRUE);
	}

	gtk_column_view_append_column (self->child_cv, column);
}

void
zenity_tree_column_view_foreach_item (ZenityTreeColumnView *self, GFunc func, gpointer user_data)
{
	GListModel *model = zenity_tree_column_view_get_model (self);

	for (guint i = 0; i < g_list_model_get_n_items (model); ++i)
	{
		ZenityTreeRow *row = g_list_model_get_item (model, i);

		for (guint j = 0; j < zenity_tree_row_get_n_items (row); ++j)
		{
			ZenityTreeItem *item = zenity_tree_row_get_item (row, j);

			func (item, user_data);
		}
	}
}

void
zenity_tree_column_view_foreach_row (ZenityTreeColumnView *self, GFunc func, gpointer user_data)
{
	GListModel *model = zenity_tree_column_view_get_model (self);

	for (guint i = 0; i < g_list_model_get_n_items (model); ++i)
	{
		ZenityTreeRow *row = g_list_model_get_item (model, i);
		func (row, user_data);
	}
}

int
zenity_tree_column_view_get_n_columns (ZenityTreeColumnView *self)
{
	return g_list_model_get_n_items (gtk_column_view_get_columns (self->child_cv));
}

gboolean
zenity_tree_column_view_is_selected (ZenityTreeColumnView *self, guint pos)
{
	return gtk_selection_model_is_selected (gtk_column_view_get_model (self->child_cv), pos);
}

void
zenity_tree_column_view_set_search (ZenityTreeColumnView *self, const char *search_str)
{
	gtk_string_filter_set_search (self->filter, search_str);
}

void
zenity_tree_column_view_show_column (ZenityTreeColumnView *self, guint pos, gboolean show)
{
	GListModel *cols = gtk_column_view_get_columns (self->child_cv);
	GtkColumnViewColumn *col = g_list_model_get_item (cols, pos);

	gtk_column_view_column_set_visible (col, show);
}
