/*
** 1998-11-26 -	A neat little module to manage directory histories in the panes. Initially, this
**		will just remember the actual paths, but future versions might include storing
**		info about selected files, sizes, and so on...
** 1998-12-06 -	Rewritten after a suggestion from Johan Hanson (<johan@tiq.com>), to simply hash on
**		the inode numbers rather than the file names. Should reduce time and memory complex-
**		ities by a whole lot.
** 1998-12-15 -	Now stores the vertical position in a relative way, rather than using absolute pixeö
**		coordinates. Not good, but better.
** 1998-12-23 -	Eh. Inode numbers are _not_ unique across devices (which might be why there's
**		a st_dev field in the stat structure). This of course makes them a bad choice
**		for unique file identifiers - when buffering a directory containing inodes
**		from various directories (such as /), things went really haywire.
*/

#include "gentoo.h"

#include <stdlib.h>

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

#include "dirhistory.h"

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

#define	HISTORY_MAX	(16)	/* Maximum number of items in history. Should be dynamic. */

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

typedef struct {			/* A history key. Just a (device,inode) pair. */
	dev_t	dev;
	ino_t	inode;
} HKey;

/* This is used to store the selected files in a pane, and nothing else. */
struct _DHSel {
	GHashTable	*hash;			/* It's a hash of HKeys, as above. For fast 'apply' operations. */
};

/* Info about a single remembered directory. The 'hist' field of dirpanes stores a linked
** list of these.
*/
typedef struct {
	gchar		path[PATH_MAX];		/* The path we are remembering. MUST BE FIRST IN STRUCTURE! */
	DHSel		*sel;			/* The selection last time we were here. */
	gfloat		vpos;			/* Vertical position, as a [0,1] fraction (0=top, 1=bottom). */
	gint		focus_row;		/* Row that had the keyboard-controlled focus. */
} DHData;

/* Here's a glib GMemChunk we use to allocate HKeys, in the hope of getting better efficiency
** than malloc() would give us. Note that there is just one such GMemChunk, from which we
** allocate HKeys for _all_ directories (DHDatas).
*/
static GMemChunk	*the_chunk = NULL;

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

static DHSel *	dirsel_set(DHSel *sel, DirPane *dp);

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

/* 1998-12-23 -	Set a history key according to information in <st>. */
static void hkey_set(HKey *hk, struct stat *st)
{
	if(hk != NULL && st != NULL)
	{
		hk->dev   = st->st_dev;
		hk->inode = st->st_ino;
	}
}

/* 1998-12-23 -	Create a new history key, initialized with information from <st>. */
static HKey * hkey_new(struct stat *st)
{
	HKey	*hk = NULL;

	if(the_chunk == NULL)
		the_chunk = g_mem_chunk_new("HKey", sizeof *hk, 1024, G_ALLOC_AND_FREE);
	if(the_chunk != NULL)
	{
		if((hk = g_mem_chunk_alloc(the_chunk)) != NULL)
			hkey_set(hk, st);
	}
	return hk;
}

/* 1998-12-23 -	Compare two history keys for equality. */
static gint hkey_equal(gconstpointer a, gconstpointer b)
{
	HKey	*ha = (HKey *) a, *hb = (HKey *) b;

	return (ha->dev == hb->dev) && (ha->inode == hb->inode);
}

/* 1998-12-23 -	Compute hash value from a history key. Nothing fancy. */
static guint hkey_hash(gconstpointer a)
{
	HKey	*ha = (HKey *) a;

	return ((guint) ha->dev ^ (guint) ha->inode);
}

/* 1998-12-23 -	A g_hash_table_foreach() callback that frees a hash key. Note that both <key>
**		and <value> point at the same HKey structure here.
*/
static void hkey_free(gpointer key, gpointer value, gpointer user)
{
	g_mem_chunk_free(the_chunk, key);
}

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

/* 1999-03-05 -	Create a new empty DHData structure. */
static DHData * dhdata_new(void)
{
	DHData	*data;

	data = g_malloc(sizeof *data);
	data->sel = NULL;
	data->vpos = 0.0f;
	data->focus_row = -1;

	return data;
}

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

/* 1998-11-28 -	The given pane <dp> is about to leave the directory named <path>. Now is
**		the time to store that path in the history for the pane, and also to store
**		information about which lines are selected.
** 1999-03-05 -	Rewritten to use the new dirsel primitives.
*/
void dph_leaving_dir(DirPane *dp, const gchar *path)
{
	GList	*iter;

	if(path == NULL || *path == '\0')	/* Ignore empty paths. */
		return;

	for(iter = dp->hist.history; iter != NULL; iter = g_list_next(iter))
	{
		if(strcmp(path, ((DHData *) iter->data)->path) == 0)
		{
			DHData	*data = (DHData *) iter->data;
			gint	vs;

			data->sel = dirsel_set(data->sel, dp);
			if((vs = dp->list->rows * dp->list->row_height) != 0)	/* Avoid division by zero. */
				data->vpos = (float) -dp->list->voffset / vs;
			else
				data->vpos = 0.0f;
			data->focus_row = dp->focus_row;
			return;
		}
	}
}

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

/* 1998-11-26 -	We're just entered <path> in <dp>. Check to see if we've been here before,
**		and if so make sure the selection is remembered. Real neat.
** 1998-12-13 -	Fixed a huge bug; pane vertical position was always set, even if we didn't
**		have any history data to set it from. Geez.
*/
void dph_entered_dir(DirPane *dp, const gchar *path)
{
	GList		*iter;
	DHData		*data = NULL;
	GtkAdjustment	*adj;

	dp_redisplay(dp);
	dp_freeze(dp);
	for(iter = dp->hist.history; iter != NULL; iter = g_list_next(iter))
	{
		if(strcmp(path, ((DHData *) iter->data)->path) == 0)
			break;
	}
	if(iter == NULL)
	{
		if(g_list_length(dp->hist.history) == HISTORY_MAX)
		{
			iter = g_list_last(dp->hist.history);
			data = iter->data;
		}
		else if((data = dhdata_new()) != NULL)
			dp->hist.history = g_list_prepend(dp->hist.history, data);
		str_strncpy(data->path, path, sizeof data->path);
	}
	if(iter != NULL)			/* Not "else", since it also moves tail items. */
	{
		data = iter->data;
		dp->hist.history = g_list_remove_link(dp->hist.history, iter);
		g_list_free(iter);
		dp->hist.history = g_list_prepend(dp->hist.history, data);
		dph_dirsel_apply(dp, data->sel);
	}
	gtk_combo_set_popdown_strings(GTK_COMBO(dp->path), dp->hist.history);
	dp_thaw(dp);
	if((data != NULL) && (adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(dp->scwin))) != NULL)
	{
		gfloat	nv = data->vpos * dp->list->rows * dp->list->row_height;

		if(nv < 0.0f)
			nv = 0.0f;
		else if(nv > adj->upper - adj->page_size)
			nv = adj->upper - adj->page_size;
		gtk_adjustment_set_value(adj, nv);
		dp->focus_row = data->focus_row;
		if(dp->focus_row != -1)
			dp_focus(dp, dp->focus_row);
	}
}

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

/* 1999-03-05 -	Create a new empty selection. */
static DHSel * dirsel_new(void)
{
	DHSel	*sel;

	sel = g_malloc(sizeof *sel);
	sel->hash = g_hash_table_new(hkey_hash, hkey_equal);

	return sel;
}

static gint dummy_func(gpointer a, gpointer b, gpointer c)
{
	return TRUE;
}

static void dirsel_clear(DHSel *sel)
{
	if(sel == NULL || sel->hash == NULL)
		return;

	g_hash_table_foreach_remove(sel->hash, dummy_func, NULL);
}

/* 1999-03-05 -	Given an <sel> structure, replace selection with that of <dp>. Returns the
**		new selection (or NULL if <dp> had no selected items). If <sel> is NULL on
**		entry, a new selection will be created and returned.
*/
static DHSel * dirsel_set(DHSel *sel, DirPane *dp)
{
	GSList	*slist;

	if((slist = dp_get_selection_full(dp)) != NULL)
	{
		HKey	*key;
		GSList	*iter;

		if(sel == NULL)
			sel = dirsel_new();
		else
			dirsel_clear(sel);
		
		for(iter = slist; iter != NULL; iter = g_slist_next(iter))
		{
			key = hkey_new(&DP_SEL_LSTAT(iter));
			g_hash_table_insert(sel->hash, key, key);	/* *Value* is returned on lookup!! */
		}
		dp_free_selection(slist);
		return sel;
	}
	return NULL;
}

/* 1999-03-05 -	This returns an opaque representation of all selected rows of <dp>. The
**		selection is not related to the order in which these rows are displayed
**		in the pane, so it's handy to use before e.g. resorting the pane.
**		Note that NULL is a valid representation if there is no selection.
*/
DHSel * dph_dirsel_create(DirPane *dp)
{
	return dirsel_set(NULL, dp);
}

/* 1999-03-05 -	Apply given given <sel> selection to <dp>, making those rows selected
**		again. <dp> need not be the same as when the selection was created,
**		and it need not have the same contents. This is not terribly efficient,
**		but I think it'll be OK.
*/
void dph_dirsel_apply(DirPane *dp, DHSel *sel)
{
	HKey	*key;
	guint	i;

	if(sel == NULL || sel->hash == NULL)
		return;

	key = hkey_new(NULL);
	for(i = 0; i < dp->dir.num_lines; i++)
	{
		hkey_set(key, &DP_ROW_LSTAT(&dp->dir.line[i]));
		if(g_hash_table_lookup(sel->hash, key))
			dp_select(dp, i);
	}
	hkey_free(key, NULL, NULL);
}

/* 1999-03-05 -	Destroy a selection, this is handy when you're done with it (like after
**		having applied it).
*/
void dph_dirsel_destroy(DHSel *sel)
{
	if(sel == NULL)
		return;

	if(sel->hash != NULL)
	{
		g_hash_table_foreach(sel->hash, hkey_free, NULL);
		g_hash_table_destroy(sel->hash);
	}
	g_free(sel);
}

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

/* 1998-12-16 -	Return the name of the previous directory shown in <dp> (before the current).
**		This is simply the second entry in the list. Because the history reorders itself
**		when changed, you cannot call this repeatedly to step backwards.
*/
char * dph_previous(DirPane *dp)
{
	GList	*item;

	if((item = g_list_nth(dp->hist.history, 1)) != NULL)
		return ((DHData *) item->data)->path;
	return NULL;
}
