/*
** 1998-05-29 -	This command is simply essential. It selects all entries in the current
**		pane.
** 1998-06-04 -	Greatly expanded; now also includes commands to deselect all entries,
**		toggle selected/unselected, and a quite powerful regular expression
**		selection tool. :) Also added freezing/thawing of the dirpane clist
**		during execution of these routines; helps speed, improves looks.
** 1998-06-05 -	Added a glob->RE translator, triggered by a check box in the selreg
**		dialog. Simplifies life for casual users (like myself). Also added
**		a check box which allows the user to invert the RE matching set, i.e.
**		to act upon all entries that do NOT match the expression.
**		BUG: The matching should check for regexec() internal errors!
** 1998-08-02 -	Added (mysteriously missing) support for SM_TYPE_FILES in selection,
**		and also closing the dialog by return/enter (accepts) and escape (cancels).
** 1998-09-19 -	Replaced regexp routines with the POSIX ones in the C library, which I
**		just discovered. Better.
** 1999-03-04 -	Changes in how selections are handled through-out gentoo made some
**		changes in this module necessary. We're on GTK+ 1.2.0 now.
** 1999-05-06 -	Adapted for built-in command argument support. Really cool.
*/

#include "gentoo.h"

#include <stdlib.h>
#include <sys/types.h>
#include <regex.h>

#include <gdk/gdkkeysyms.h>

#include "dirpane.h"
#include "dialog.h"
#include "strutil.h"

#include "cmd_select.h"

/* ----------------------------------------------------------------------------------------- */

#define	SM_SET			(0)
#define	SM_SET_ALL		(0)
#define	SM_SET_SELECTED		(1)
#define	SM_SET_UNSELECTED	(2)

#define	SM_TYPE			(1)
#define	SM_TYPE_ALL		(0)
#define	SM_TYPE_DIRS		(1)
#define	SM_TYPE_FILES		(2)

#define	SM_ACTION		(2)
#define	SM_ACTION_SELECT	(0)
#define	SM_ACTION_UNSELECT	(1)
#define	SM_ACTION_TOGGLE	(2)

/* Selection parameters. Filled-in either by GUI, or by direct command arguments. */
typedef struct {
	GString		*re;		/* The actual regular expression string. */
	guint		set;		/* In which set of rows should we operate? */
	guint		type;		/* Do we operate only on files, dirs, or both? */
	guint		action;		/* What do we do with matching rows? */
	gboolean	glob;		/* Treat the RE as a glob pattern? */
	gboolean	invert;		/* Invert the matching sense? */
	gboolean	full;		/* Require RE to match entire row? */
	gboolean	nocase;		/* Ignore case differences? */
} SelParam;

static SelParam	old_sp = { NULL, SM_SET_ALL, SM_TYPE_ALL, SM_ACTION_SELECT, TRUE, FALSE, TRUE, FALSE };

/* ----------------------------------------------------------------------------------------- */

/* 1999-03-04 -	Select all rows in the currently active pane. Rewritten for the new cooler
**		selection management made possible by GTK+ 1.2.0.
*/
int cmd_selectall(MainInfo *min, DirPane *src, DirPane *dst, CmdArg *ca)
{
	dp_select_all(src);
	return 1;
}

/* ----------------------------------------------------------------------------------------- */

/* 1999-03-04 -	Unselect all rows of current pane. Rewritten. */
int cmd_selectnone(MainInfo *min, DirPane *src, DirPane *dst, CmdArg *ca)
{
	dp_unselect_all(src);
	return 1;
}

/* ----------------------------------------------------------------------------------------- */

/* 1998-06-04 -	Invert the selection set, making all currently selected files become
**		unselected, and all unselected become selected.
*/
int cmd_selecttoggle(MainInfo *min, DirPane *src, DirPane *dst, CmdArg *ca)
{
	guint	i;

	gtk_clist_freeze(src->list);
	for(i = 0; i < src->dir.num_lines; i++)
		dp_toggle(src, i);
	gtk_clist_thaw(src->list);
	return 1;
}

/* ----------------------------------------------------------------------------------------- */

/* 1999-05-06 -	Inspect which set (selected or unselected) the given row is, and determine
**		if it complies with the wanted one as specified in <sp>.
*/
static gboolean filter_set(DirPane *dp, guint row, SelParam *sp)
{
	gboolean	sel;

	if(sp->set == SM_SET_ALL)
		return TRUE;
	sel = dp_is_selected(dp, &dp->dir.line[row]);
	return (sp->set == SM_SET_SELECTED) ? sel : !sel;
}

/* 1999-05-06 -	Inspect type (dir or non-dir) given row has, and match against <sp>. */
static gboolean filter_type(DirPane *dp, guint row, SelParam *sp)
{
	gboolean	dir;

	if(sp->type == SM_TYPE_ALL)
		return TRUE;
	dir = S_ISDIR(DP_ROW_LSTAT(&dp->dir.line[row]).st_mode);

	return (sp->type == SM_TYPE_DIRS) ? dir : !dir;
}

/* 1999-05-06 -	Once we have all the selection's parameters neatly tucked into <sp>, perform the
**		actual operation.
*/
static void do_selectre(MainInfo *min, DirPane *dp, SelParam *sp)
{
	GString		*tmp;
	regex_t		re;
	gint		(*sel_action)(DirPane *dp, gint row) = NULL;

	if(sp->re == NULL)
		return;

	switch(sp->action)
	{
		case SM_ACTION_SELECT:
			sel_action = dp_select;
			break;
		case SM_ACTION_UNSELECT:
			sel_action = dp_unselect;
			break;
		case SM_ACTION_TOGGLE:
			sel_action = dp_toggle;
			break;
	}
	if((tmp = g_string_new(sp->re->str)) != NULL)
	{
		gint	reret;

		if(sp->glob)
			str_gstring_glob_to_re(tmp);
		if(sp->full)	/* Require RE to match entire name? */
		{
			g_string_prepend_c(tmp, '^');
			g_string_append_c(tmp, '$');
		}
		if((reret = regcomp(&re, tmp->str, REG_EXTENDED | (sp->nocase ? REG_ICASE : 0))) == 0)
		{
			int	matchok;
			guint	i;

			dp_freeze(dp);
			matchok = sp->invert ? REG_NOMATCH : 0;
			for(i = 0; i < dp->dir.num_lines; i++)
			{
				if(filter_set(dp, i, sp) && filter_type(dp, i, sp))
				{
					if(regexec(&re, DP_ROW_NAME(&dp->dir.line[i]), 0, NULL, 0) == matchok)
						sel_action(dp, i);
				}
			}
			dp_thaw(dp);
			regfree(&re);
		}
		else
		{
			gchar	ebuf[1024];

			regerror(reret, &re, ebuf, sizeof ebuf);
			dlg_dialog_error(ebuf);
		}
		g_string_free(tmp, TRUE);
	}
}

/* 1999-05-06 -	Build a mode selection menu, using items from <def>. Items are separated by vertical
**		bars. Each item will have it's user data set to the index of the item in question.
*/
static GtkWidget * build_mode_menu(const gchar *def)
{
	GtkWidget	*menu = NULL, *item;
	gchar		*temp, *iname, *iend;
	guint		minor;

	if((temp = g_strdup(def)) != NULL)
	{
		menu = gtk_menu_new();
		for(iname = temp, minor = 0; (*iname != '\0') && (iend = strchr(iname, '|')) != NULL; iname = iend + 1, minor++)
		{
			*iend = '\0';
			item = gtk_menu_item_new_with_label(iname);
			gtk_menu_append(GTK_MENU(menu), item);
			gtk_object_set_user_data(GTK_OBJECT(item), GUINT_TO_POINTER(minor));
			gtk_widget_show(item);
		}
		g_free(temp);
	}
	return menu;
}

/* 1999-05-06 -	Build option menus for mode selections. */
static void init_mode_menus(GtkWidget *vbox, GtkWidget *opt_menu[3], guint *history[3])
{
	const gchar	*mdef[] = {"All rows|Selected|Unselected|",
				  "All types|Directories only|Non-directories only|",
				  "Select|Unselect|Toggle|"},
			*mlabel[] = {"Set", "Type", "Action"};
	GtkWidget	*table, *label, *menu;
	guint		i;

	table = gtk_table_new(2, 3, FALSE);
	for(i = 0; i < sizeof mdef / sizeof mdef[0]; i++)
	{
		label = gtk_label_new(mlabel[i]);
		gtk_table_attach_defaults(GTK_TABLE(table), label, 0, 1, i, i + 1);
		gtk_widget_show(label);
		menu = build_mode_menu(mdef[i]);
		opt_menu[i] = gtk_option_menu_new();
		gtk_option_menu_set_menu(GTK_OPTION_MENU(opt_menu[i]), menu);
		gtk_option_menu_set_history(GTK_OPTION_MENU(opt_menu[i]), *history[i]);
		gtk_table_attach_defaults(GTK_TABLE(table), opt_menu[i], 1, 2, i, i+1);
		gtk_widget_show(opt_menu[i]);
	}
	gtk_box_pack_start(GTK_BOX(vbox), table, FALSE, FALSE, 0);
	gtk_widget_show(table);
}

/* 1998-06-04 -	Pop up a dialog window asking the user for a regular expression, and
**		then match only those entries whose names match. Very Opus.
** 1999-05-06 -	Basically rewritten, to accomodate command arguments in a nice way. Now
**		the GUI uses no external state; all values are extracted from the
**		widgets after the dialog is closed.
*/
int cmd_selectre(MainInfo *min, DirPane *src, DirPane *dst, CmdArg *ca)
{
	SelParam	sp;
	const gchar	*str;
	guint		res = 0;

	/* Always start from the previous set of parameters. */
	sp = old_sp;

	if((str = car_bareword_get(ca, 0)) != NULL)	/* Was the expression given on the command line? */
	{
		if(sp.re == NULL)
			sp.re = g_string_new(str);
		else
			g_string_assign(sp.re, str);
		sp.set    = car_keyword_get_enum(ca, "set", sp.set, "all rows", "selected", "unselected", NULL);
		sp.type   = car_keyword_get_enum(ca, "type", sp.type, "all types", "directories only", "non-directories only", NULL);
		sp.action = car_keyword_get_enum(ca, "action", sp.action, "select", "unselect", "toggle", NULL);
		sp.glob   = car_keyword_get_boolean(ca, "glob", sp.glob);
		sp.invert = car_keyword_get_boolean(ca, "invert", sp.invert);
		sp.full   = car_keyword_get_boolean(ca, "full", sp.full);
		sp.nocase = car_keyword_get_boolean(ca, "nocase", sp.nocase);
	}	
	else			/* Use interactive dialog to get options. */
	{
		GtkWidget	*vbox, *label, *opt_menu[3], *glob, *invert, *full, *nocase, *re;
		guint		*mptr[sizeof opt_menu / sizeof opt_menu[0]];
		Dialog		*dlg;

		mptr[0] = &sp.set;
		mptr[1] = &sp.type;
		mptr[2] = &sp.action;

		vbox = gtk_vbox_new(FALSE, 0);
		gtk_container_set_border_width(GTK_CONTAINER(vbox), 2);
		label = gtk_label_new("Regular Expression (RE) Selection");
		gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 3);
		gtk_widget_show(label);
		init_mode_menus(vbox, opt_menu, mptr);
		glob = gtk_check_button_new_with_label("Treat RE as Glob Pattern?");
		gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(glob), sp.glob);
		gtk_box_pack_start(GTK_BOX(vbox), glob, FALSE, FALSE, 0);
		gtk_widget_show(glob);
		invert = gtk_check_button_new_with_label("Invert RE Matching?");
		gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(invert), sp.invert);
		gtk_box_pack_start(GTK_BOX(vbox), invert, FALSE, FALSE, 0);
		gtk_widget_show(invert);
		full = gtk_check_button_new_with_label("Require Match on Full Name?");
		gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(full), sp.full);
		gtk_box_pack_start(GTK_BOX(vbox), full, FALSE, FALSE, 0);
		gtk_widget_show(full);
		nocase = gtk_check_button_new_with_label("Ignore Case Differences?");
		gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(nocase), sp.nocase);
		gtk_box_pack_start(GTK_BOX(vbox), nocase, FALSE, FALSE, 0);
		gtk_widget_show(nocase);

		re = gtk_entry_new();
		if(sp.re != NULL)
		{
			gtk_entry_set_text(GTK_ENTRY(re), sp.re->str);
			gtk_entry_select_region(GTK_ENTRY(re), 0, -1);
		}
		gtk_box_pack_start(GTK_BOX(vbox), re, FALSE, FALSE, 0);
		gtk_widget_show(re);
		dlg = dlg_dialog(vbox, "Select Using RE", "OK|Cancel", NULL, NULL);
		dlg_dialog_set_keep(dlg, TRUE);
		gtk_widget_grab_focus(re);
		res = dlg_dialog_wait(dlg);
		if(res == 0)		/* If user accepted, collect widget state into 'sp'. */
		{
			GtkWidget	*menu, *mi;
			guint		i;

			for(i = 0; i < sizeof mptr / sizeof mptr[0]; i++)
			{
				if((menu = gtk_option_menu_get_menu(GTK_OPTION_MENU(opt_menu[i]))) != NULL)
				{
					if((mi = gtk_menu_get_active(GTK_MENU(menu))) != NULL)
						*mptr[i] = GPOINTER_TO_UINT(gtk_object_get_user_data(GTK_OBJECT(mi)));
				}
			}
			sp.glob	  = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(glob));
			sp.invert = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(invert));
			sp.full   = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(full));
			sp.nocase = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(nocase));
			if(sp.re != NULL)
				g_string_assign(sp.re, gtk_entry_get_text(GTK_ENTRY(re)));
			else
				sp.re = g_string_new(gtk_entry_get_text(GTK_ENTRY(re)));
		}
		dlg_dialog_destroy(dlg);
	}
	if(res == 0)
	{
		do_selectre(min, src, &sp);
		old_sp = sp;		/* Make this search's parameter the defaults for the next. */
	}
	return 0;
}

/* ----------------------------------------------------------------------------------------- */

/* 1998-09-19 -	This real fun command extends the selection "across" the style tree; all
**		files having styles on the same level as the selected ones will be selected,
**		too. Very fun. Or perhaps somewhat silly? Those features are a-creepin'.
*/
int cmd_selectext(MainInfo *min, DirPane *src, DirPane *dst, CmdArg *ca)
{
	/* FIXME! Killed by style rewrite. */
#if 0
	guint	i;
	GList	*stl = NULL;
	GSList	*slist, *iter;

	slist = dp_get_selection(src);
	for(iter = slist; iter != NULL; iter = g_slist_next(iter))
	{
		if(g_list_find(stl, DP_SEL_TYPE(iter)->style->parent) == NULL)
			stl = g_list_append(stl, DP_SEL_TYPE(iter)->style->parent);
	}
	for(i = 0; i < src->dir.num_lines; i++)
	{
		if(g_list_find(stl, DP_ROW_TYPE(&src->dir.line[i])->style->parent) ||
		   g_list_find(stl, DP_ROW_TYPE(&src->dir.line[i])->style))
			dp_select(src, i);
	}
	dp_free_selection(slist);
	g_list_free(stl);
#endif
	return 1;
}

/* ----------------------------------------------------------------------------------------- */

/* 1999-03-15 -	Select all rows of <src> having the same type as any of the currently selected.
**		That is, extend the selection but not as much as the SelExt command.
*/
int cmd_selecttype(MainInfo *min, DirPane *src, DirPane *dst, CmdArg *ca)
{
	GSList	*slist, *iter;

	if((slist = dp_get_selection(src)) != NULL)
	{
		GSList	*tlist = NULL;
		guint	i;

		for(iter = slist; iter != NULL; iter = g_slist_next(iter))
			tlist = g_slist_prepend(tlist, DP_SEL_TYPE(iter));
		dp_free_selection(slist);

		for(i = 0; i < src->dir.num_lines; i++)
			if(g_slist_find(tlist, DP_ROW_TYPE(&src->dir.line[i])) != NULL)
				dp_select(src, i);
		g_slist_free(tlist);

	}
	return 1;
}

/* ----------------------------------------------------------------------------------------- */

/* 1998-09-20 -	The UnselectFirst command. In wrong module?
** 1998-12-16 -	This was freeze/thaw bracketed, for no good reason. Didn't work.
*/
int cmd_unselectfirst(MainInfo *min, DirPane *src, DirPane *dst, CmdArg *ca)
{
	GSList	*slist;

	if((slist = dp_get_selection(src)) != NULL)
	{
		dp_unselect(src, DP_SEL_INDEX(src, slist));
		dp_free_selection(slist);
	}
	return 1;
}
