/**
 * GNT - The GLib Ncurses Toolkit
 *
 * GNT is the legal property of its developers, whose names are too numerous
 * to list here.  Please refer to the COPYRIGHT file distributed with this
 * source distribution.
 *
 * This library 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  02111-1301  USA
 */

#include <string.h>

#include "gntinternal.h"
#undef GNT_LOG_DOMAIN
#define GNT_LOG_DOMAIN "Bindable"

#include "gntbindable.h"
#include "gntstyle.h"
#include "gnt.h"
#include "gntutils.h"
#include "gnttextview.h"
#include "gnttree.h"
#include "gntbox.h"
#include "gntbutton.h"
#include "gntwindow.h"
#include "gntlabel.h"

static GObjectClass *parent_class = NULL;

static struct
{
	char * okeys;         /* Old keystrokes */
	char * keys;          /* New Keystrokes being bound to the action */
	GntBindableClass * klass; /* Class of the object that's getting keys rebound */
	char * name;          /* The name of the action */
	GList * params;       /* The list of paramaters */
} rebind_info;

static void
gnt_bindable_free_rebind_info(void)
{
	g_free(rebind_info.name);
	g_free(rebind_info.keys);
	g_free(rebind_info.okeys);
}

static void
gnt_bindable_rebinding_cancel(GntWidget *button, gpointer data)
{
	gnt_bindable_free_rebind_info();
	gnt_widget_destroy(GNT_WIDGET(data));
}

static void
gnt_bindable_rebinding_rebind(GntWidget *button, gpointer data)
{
	if (rebind_info.keys) {
		gnt_bindable_register_binding(rebind_info.klass,
				NULL,
				rebind_info.okeys,
				rebind_info.params);
		gnt_bindable_register_binding(rebind_info.klass,
				rebind_info.name,
				rebind_info.keys,
				rebind_info.params);
	}
	gnt_bindable_free_rebind_info();
	gnt_widget_destroy(GNT_WIDGET(data));
}

static gboolean
gnt_bindable_rebinding_grab_key(GntBindable *bindable, const char *text, gpointer data)
{
	GntTextView *textview = GNT_TEXT_VIEW(data);
	char *new_text;
	const char *tmp;

	if (text && *text) {
		/* Rebinding tab or enter for something is probably not that great an idea */
		if (!strcmp(text, GNT_KEY_CTRL_I) || !strcmp(text, GNT_KEY_ENTER)) {
			return FALSE;
		}

		tmp = gnt_key_lookup(text);
		new_text = g_strdup_printf("KEY: \"%s\"", tmp);
		gnt_text_view_clear(textview);
		gnt_text_view_append_text_with_flags(textview, new_text, GNT_TEXT_FLAG_NORMAL);
		g_free(new_text);

		g_free(rebind_info.keys);
		rebind_info.keys = g_strdup(text);

		return TRUE;
	}
	return FALSE;
}
static void
gnt_bindable_rebinding_activate(GntBindable *data, gpointer bindable)
{
	const char *widget_name = g_type_name(G_OBJECT_TYPE(bindable));
	char *keys;
	GntWidget *key_textview;
	GntWidget *label;
	GntWidget *bind_button, *cancel_button;
	GntWidget *button_box;
	GList *current_row_data;
	char *tmp;
	GntWidget *win = gnt_window_new();
	GntTree *tree = GNT_TREE(data);
	GntWidget *vbox = gnt_box_new(FALSE, TRUE);

	rebind_info.klass = GNT_BINDABLE_GET_CLASS(bindable);

	current_row_data = gnt_tree_get_selection_text_list(tree);
	rebind_info.name = g_strdup(g_list_nth_data(current_row_data, 1));

	keys = gnt_tree_get_selection_data(tree);
	rebind_info.okeys = g_strdup(gnt_key_translate(keys));

	rebind_info.params = NULL;

	g_list_foreach(current_row_data, (GFunc)g_free, NULL);
	g_list_free(current_row_data);

	gnt_box_set_alignment(GNT_BOX(vbox), GNT_ALIGN_MID);

	gnt_box_set_title(GNT_BOX(win), "Key Capture");

	tmp = g_strdup_printf("Type the new bindings for %s in a %s.", rebind_info.name, widget_name);
	label = gnt_label_new(tmp);
	g_free(tmp);
	gnt_box_add_widget(GNT_BOX(vbox), label);

	tmp = g_strdup_printf("KEY: \"%s\"", keys);
	key_textview = gnt_text_view_new();
	gnt_widget_set_size(key_textview, key_textview->priv.x, 2);
	gnt_text_view_append_text_with_flags(GNT_TEXT_VIEW(key_textview), tmp, GNT_TEXT_FLAG_NORMAL);
	g_free(tmp);
	gnt_widget_set_name(key_textview, "keystroke");
	gnt_box_add_widget(GNT_BOX(vbox), key_textview);

	g_signal_connect(G_OBJECT(win), "key_pressed", G_CALLBACK(gnt_bindable_rebinding_grab_key), key_textview);

	button_box = gnt_box_new(FALSE, FALSE);

	bind_button = gnt_button_new("BIND");
	gnt_widget_set_name(bind_button, "bind");
	gnt_box_add_widget(GNT_BOX(button_box), bind_button);

	cancel_button = gnt_button_new("Cancel");
	gnt_widget_set_name(cancel_button, "cancel");
	gnt_box_add_widget(GNT_BOX(button_box), cancel_button);

	g_signal_connect(G_OBJECT(bind_button), "activate", G_CALLBACK(gnt_bindable_rebinding_rebind), win);
	g_signal_connect(G_OBJECT(cancel_button), "activate", G_CALLBACK(gnt_bindable_rebinding_cancel), win);

	gnt_box_add_widget(GNT_BOX(vbox), button_box);

	gnt_box_add_widget(GNT_BOX(win), vbox);
	gnt_widget_show(win);
}

typedef struct
{
	GHashTable *hash;
	GntTree *tree;
} BindingView;

static void
add_binding(gpointer key, gpointer value, gpointer data)
{
	BindingView *bv = data;
	GntBindableActionParam *act = value;
	const char *name = g_hash_table_lookup(bv->hash, act->action);
	if (name && *name) {
		const char *k = gnt_key_lookup(key);
		if (!k)
			k = key;
		gnt_tree_add_row_after(bv->tree, (gpointer)k,
				gnt_tree_create_row(bv->tree, k, name), NULL, NULL);
	}
}

static void
add_action(gpointer key, gpointer value, gpointer data)
{
	BindingView *bv = data;
	g_hash_table_insert(bv->hash, value, key);
}

static void
gnt_bindable_class_init(GntBindableClass *klass)
{
	parent_class = g_type_class_peek_parent(klass);

	klass->actions = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
				(GDestroyNotify)gnt_bindable_action_free);
	klass->bindings = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
				(GDestroyNotify)gnt_bindable_action_param_free);

	gnt_style_read_actions(G_OBJECT_CLASS_TYPE(klass), GNT_BINDABLE_CLASS(klass));
	GNTDEBUG;
}

static gpointer
bindable_clone(GntBindableAction *action)
{
	GntBindableAction *ret = g_new0(GntBindableAction, 1);
	ret->name = g_strdup(action->name);
	ret->u = action->u;
	return ret;
}

static gpointer
binding_clone(GntBindableActionParam *param)
{
	GntBindableActionParam *p = g_new0(GntBindableActionParam, 1);
	p->list = g_list_copy(param->list);
	p->action = param->action;
	return p;
}

static void
duplicate_hashes(GntBindableClass *klass)
{
	/* Duplicate the bindings from parent class */
	if (klass->actions) {
		klass->actions = g_hash_table_duplicate(klass->actions, g_str_hash,
					g_str_equal, g_free, (GDestroyNotify)gnt_bindable_action_free,
					(GDupFunc)g_strdup, (GDupFunc)bindable_clone);
		klass->bindings = g_hash_table_duplicate(klass->bindings, g_str_hash,
					g_str_equal, g_free, (GDestroyNotify)gnt_bindable_action_param_free,
					(GDupFunc)g_strdup, (GDupFunc)binding_clone);
	} else {
		klass->actions = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
					(GDestroyNotify)gnt_bindable_action_free);
		klass->bindings = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
					(GDestroyNotify)gnt_bindable_action_param_free);
	}

	GNTDEBUG;
}

/******************************************************************************
 * GntBindable API
 *****************************************************************************/
GType
gnt_bindable_get_gtype(void)
{
	static GType type = 0;

	if (type == 0) {
		static const GTypeInfo info = {
			sizeof(GntBindableClass),
			(GBaseInitFunc)duplicate_hashes,	/* base_init		*/
			NULL,					/* base_finalize	*/
			(GClassInitFunc)gnt_bindable_class_init,
			NULL,
			NULL,					/* class_data		*/
			sizeof(GntBindable),
			0,						/* n_preallocs		*/
			NULL,					/* instance_init	*/
			NULL					/* value_table		*/
		};

		type = g_type_register_static(G_TYPE_OBJECT,
									  "GntBindable",
									  &info, G_TYPE_FLAG_ABSTRACT);
	}

	return type;
}

/**
 * Key Remaps
 */
const char *
gnt_bindable_remap_keys(GntBindable *bindable, const char *text)
{
	const char *remap = NULL;
	GType type = G_OBJECT_TYPE(bindable);
	GntBindableClass *klass = GNT_BINDABLE_CLASS(GNT_BINDABLE_GET_CLASS(bindable));

	if (klass->remaps == NULL)
	{
		klass->remaps = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
		gnt_styles_get_keyremaps(type, klass->remaps);
	}

	remap = g_hash_table_lookup(klass->remaps, text);

	return (remap ? remap : text);
}

/**
 * Actions and Bindings
 */
gboolean
gnt_bindable_perform_action_named(GntBindable *bindable, const char *name, ...)
{
	GntBindableClass *klass = GNT_BINDABLE_CLASS(GNT_BINDABLE_GET_CLASS(bindable));
	GList *list = NULL;
	va_list args;
	GntBindableAction *action;
	void *p;

	va_start(args, name);
	while ((p = va_arg(args, void *)) != NULL)
		list = g_list_append(list, p);
	va_end(args);

	action = g_hash_table_lookup(klass->actions, name);
	if (action && action->u.action) {
		return action->u.action(bindable, list);
	}
	return FALSE;
}

gboolean
gnt_bindable_perform_action_key(GntBindable *bindable, const char *keys)
{
	GntBindableClass *klass = GNT_BINDABLE_CLASS(GNT_BINDABLE_GET_CLASS(bindable));
	GntBindableActionParam *param = g_hash_table_lookup(klass->bindings, keys);

	if (param && param->action) {
		if (param->list)
			return param->action->u.action(bindable, param->list);
		else
			return param->action->u.action_noparam(bindable);
	}
	return FALSE;
}

gboolean
gnt_bindable_check_key(GntBindable *bindable, const char *keys) {
	GntBindableClass *klass = GNT_BINDABLE_CLASS(GNT_BINDABLE_GET_CLASS(bindable));
	GntBindableActionParam *param = g_hash_table_lookup(klass->bindings, keys);
	return (param && param->action);
}

static void
register_binding(GntBindableClass *klass, const char *name, const char *trigger, GList *list)
{
	GntBindableActionParam *param;
	GntBindableAction *action;

	if (name == NULL || *name == '\0') {
		g_hash_table_remove(klass->bindings, (char*)trigger);
		gnt_keys_del_combination(trigger);
		return;
	}

	action = g_hash_table_lookup(klass->actions, name);
	if (!action) {
		gnt_warning("Invalid action name %s for %s",
				name, g_type_name(G_OBJECT_CLASS_TYPE(klass)));
		if (list)
			g_list_free(list);
		return;
	}

	param = g_new0(GntBindableActionParam, 1);
	param->action = action;
	param->list = list;
	g_hash_table_replace(klass->bindings, g_strdup(trigger), param);
	gnt_keys_add_combination(trigger);
}

void gnt_bindable_register_binding(GntBindableClass *klass, const char *name,
			const char *trigger, ...)
{
	GList *list = NULL;
	va_list args;
	void *data;

	va_start(args, trigger);
	while ((data = va_arg(args, void *))) {
		list = g_list_append(list, data);
	}
	va_end(args);

	register_binding(klass, name, trigger, list);
}

void gnt_bindable_class_register_action(GntBindableClass *klass, const char *name,
			GntBindableActionCallback callback, const char *trigger, ...)
{
	void *data;
	va_list args;
	GntBindableAction *action = g_new0(GntBindableAction, 1);
	GList *list;

	action->name = g_strdup(name);
	action->u.action = callback;

	g_hash_table_replace(klass->actions, g_strdup(name), action);

	if (trigger && *trigger) {
		list = NULL;
		va_start(args, trigger);
		while ((data = va_arg(args, void *))) {
			list = g_list_append(list, data);
		}
		va_end(args);

		register_binding(klass, name, trigger, list);
	}
}

void gnt_bindable_action_free(GntBindableAction *action)
{
	g_free(action->name);
	g_free(action);
}

void gnt_bindable_action_param_free(GntBindableActionParam *param)
{
	g_list_free(param->list);   /* XXX: There may be a leak here for string parameters */
	g_free(param);
}

GntBindable * gnt_bindable_bindings_view(GntBindable *bind)
{
	GntBindable *tree = GNT_BINDABLE(gnt_tree_new_with_columns(2));
	GntBindableClass *klass = GNT_BINDABLE_CLASS(GNT_BINDABLE_GET_CLASS(bind));
	GHashTable *hash = g_hash_table_new(g_direct_hash, g_direct_equal);
	BindingView bv = {hash, GNT_TREE(tree)};

	gnt_tree_set_compare_func(bv.tree, (GCompareFunc)g_utf8_collate);
	g_hash_table_foreach(klass->actions, add_action, &bv);
	g_hash_table_foreach(klass->bindings, add_binding, &bv);
	if (GNT_TREE(tree)->list == NULL) {
		gnt_widget_destroy(GNT_WIDGET(tree));
		tree = NULL;
	} else
		gnt_tree_adjust_columns(bv.tree);
	g_hash_table_destroy(hash);

	return tree;
}

static void
reset_binding_window(GntBindableClass *window, gpointer k)
{
	GntBindableClass *klass = GNT_BINDABLE_CLASS(k);
	klass->help_window = NULL;
}

gboolean
gnt_bindable_build_help_window(GntBindable *bindable)
{
	GntWidget *tree;
	GntBindableClass *klass = GNT_BINDABLE_GET_CLASS(bindable);
	char *title;

	tree = GNT_WIDGET(gnt_bindable_bindings_view(bindable));

	klass->help_window = GNT_BINDABLE(gnt_window_new());
	title = g_strdup_printf("Bindings for %s", g_type_name(G_OBJECT_TYPE(bindable)));
	gnt_box_set_title(GNT_BOX(klass->help_window), title);
	if (tree) {
		g_signal_connect(G_OBJECT(tree), "activate", G_CALLBACK(gnt_bindable_rebinding_activate), bindable);
		gnt_box_add_widget(GNT_BOX(klass->help_window), tree);
	} else
		gnt_box_add_widget(GNT_BOX(klass->help_window), gnt_label_new("This widget has no customizable bindings."));

	g_signal_connect(G_OBJECT(klass->help_window), "destroy", G_CALLBACK(reset_binding_window), klass);
	gnt_widget_show(GNT_WIDGET(klass->help_window));
	g_free(title);

	return TRUE;
}

