/*
 * Tree Store
 *
 * Contains a storage of the file system tree representation
 *
   Copyright (C) 1994, 1995, 1996, 1997 The Free Software Foundation

   Written: 1994, 1996 Janne Kukonlehto
            1997 Norbert Warmuth
            1996, 1999 Miguel de Icaza
	    
   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., 675 Mass Ave, Cambridge, MA 02139, USA.

   This module has been converted to be a widget.

   The program load and saves the tree each time the tree widget is
   created and destroyed.  This is required for the future vfs layer,
   it will be possible to have tree views over virtual file systems.

   */
#include <config.h>
#include <sys/types.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <fcntl.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <errno.h>
#include <dirent.h>
#include <stdio.h>
#include <string.h>
#include "global.h"
#include "treestore.h"
#include "../vfs/vfs.h"
#ifdef NEEDS_IO_H
#   include <io.h>
#endif
#include "profile.h"
#include "setup.h"

#define TREE_SIGNATURE "Midnight Commander TreeStore v 2.0"

static TreeStore ts;

void (*tree_store_dirty_notify)(int state) = NULL;

void
tree_store_dirty (int state)
{
	ts.dirty = state;

	if (tree_store_dirty_notify)
		(*tree_store_dirty_notify)(state);
}

/* Returns number of common characters */
static int str_common (char *s1, char *s2)
{
    int result = 0;

    while (*s1++ == *s2++)
	result++;
    return result;
}

/* The directory names are arranged in a single linked list in the same
   order as they are displayed. When the tree is displayed the expected
   order is like this:
        /
        /bin
        /etc
        /etc/X11
        /etc/rc.d
        /etc.old/X11
        /etc.old/rc.d
        /usr

   i.e. the required collating sequence when comparing two directory names is 
        '\0' < PATH_SEP < all-other-characters-in-encoding-order

    Since strcmp doesn't fulfil this requirement we use pathcmp when
    inserting directory names into the list. The meaning of the return value 
    of pathcmp and strcmp are the same (an integer less than, equal to, or 
    greater than zero if p1 is found to be less than, to match, or be greater 
    than p2.
 */
static int
pathcmp (const char *p1, const char *p2)
{
    for ( ;*p1 == *p2; p1++, p2++)
        if (*p1 == '\0' )
    	    return 0;
	    
    if (*p1 == '\0')
        return -1;
    if (*p2 == '\0')
        return 1;
    if (*p1 == PATH_SEP)
        return -1;
    if (*p2 == PATH_SEP)
        return 1;
    return (*p1 - *p2);
}

/* Searches for specified directory */
tree_entry *
tree_store_whereis (char *name)
{
	tree_entry *current = ts.tree_first;
	int flag = -1;
	
	while (current && (flag = pathcmp (current->name, name)) < 0)
		current = current->next;
	
	if (flag == 0)
		return current;
	else
		return NULL;
}

TreeStore *
tree_store_init (void)
{
	ts.tree_first = 0;
	ts.tree_last = 0;
	ts.refcount++;
	ts.check_name = NULL;
	
	return &ts;
}

void
tree_store_destroy (void)
{
	tree_entry *current, *old;

	ts.refcount--;
	
	current = ts.tree_first;
	while (current){
		old = current;
		current = current->next;
		g_free (old->name);
		g_free (old);
	}

	ts.tree_first = NULL;
	ts.tree_last = NULL;
	ts.loaded = FALSE;
}

static char *
decode (char *buffer)
{
	char *res = g_strdup (buffer);
	char *p, *q;
	
	for (p = q = res; *p; p++, q++){
		if (*p == '\n'){
			*q = 0;
			return res;
		}

		if (*p != '\\'){
			*q = *p;
			continue;
		}
		
		p++;
		
		switch (*p){
		case 'n':
			*q = '\n';
			break;
		case '\\':
			*q = '\\';
			break;
		}
	}
	*q = *p;

	return res;
}

int
tree_store_load (char *name)
{
	FILE *file;
	char buffer [MC_MAXPATHLEN + 20], oldname[MC_MAXPATHLEN];
	char *different;
	int len, common;
	int do_load;
	
	g_return_val_if_fail (name != NULL, TRUE);

	if (ts.loaded)
		return TRUE;
	
	file = fopen (name, "r");

	if (file){
		fgets (buffer, sizeof (buffer), file);
	
		if (strncmp (buffer, TREE_SIGNATURE, strlen (TREE_SIGNATURE)) != 0){
			fclose (file);
			do_load = FALSE;
		} else
			do_load = TRUE;
	} else
		do_load = FALSE;

	if (do_load){
		ts.loaded = TRUE;

		/* File open -> read contents */
		oldname [0] = 0;
		while (fgets (buffer, MC_MAXPATHLEN, file)){
			tree_entry *e;
			int scanned;
			char *name;

			/* Skip invalid records */
			if ((buffer [0] != '0' && buffer [0] != '1'))
				continue;
			
			if (buffer [1] != ':')
				continue;

			scanned = buffer [0] == '1';

			name = decode (buffer+2);

			len = strlen (name);
#ifdef OS2_NT
			/* .ado: Drives for NT and OS/2 */
			if ((len > 2)         && 
			    isalpha(name[0]) && 
			    (name[1] == ':') && 
			    (name[2] == '\\')) {
				tree_store_add_entry (name);
				strcpy (oldname, name);
			} else
#endif
				/* UNIX Version */
				if (name [0] != PATH_SEP){
					/* Clear-text decompression */
					char *s = strtok (name, " ");
					
					if (s){
						common = atoi (s);
						different = strtok (NULL, "");
						if (different){
							strcpy (oldname + common, different);
							e = tree_store_add_entry (oldname);
							e->scanned = scanned;
						}
					}
				} else {
					e = tree_store_add_entry (name);
					e->scanned = scanned;
					strcpy (oldname, name);
				}
			g_free (name);
		}
		fclose (file);
	}

	/* Nothing loaded, we add some standard directories */
	if (!ts.tree_first){
		tree_store_add_entry (PATH_SEP_STR);
		tree_store_rescan (PATH_SEP_STR);
		ts.loaded = TRUE;
	}
	
	return TRUE;
}

static char *
encode (char *string)
{
	int special_chars;
	char *p, *q;
	char *res;

	for (special_chars = 0, p = string; *p; p++){
		if (*p == '\n' || *p == '\\')
			special_chars++;
	}
	
	res = g_malloc (p - string + special_chars + 1);
	for (p = string, q = res; *p; p++, q++){
		if (*p != '\n' && *p != '\\'){
			*q = *p;
			continue;
		}
		
		*q++ = '\\';
		
		switch (*p){
		case '\n':
			*q = 'n';
			break;
			
		case '\\':
			*q = '\\';
			break;
		}
	}
	*q = 0;
	return res;
}

int
tree_store_save (char *name)
{
	tree_entry *current;
	FILE *file;

	file = fopen (name, "w");
	if (!file)
		return errno;

	fprintf (file, "%s\n", TREE_SIGNATURE);
	
	current = ts.tree_first;
	while (current){
		int i, common;

		/* Clear-text compression */
		if (current->prev && (common = str_common (current->prev->name, current->name)) > 2){
			char *encoded = encode (current->name + common);
			
			i = fprintf (file, "%d:%d %s\n", current->scanned, common, encoded);
			g_free (encoded);
		} else {
			char *encoded = encode (current->name);
			
			i = fprintf (file, "%d:%s\n", current->scanned, encoded);
			g_free (encoded);
		}
		
		if (i == EOF){
			fprintf (stderr, _("Can't write to the %s file:\n%s\n"), name,
				 unix_error_string (errno));
			break;
		}
		current = current->next;
	}
	tree_store_dirty (FALSE);
	fclose (file);

	return 0;
}

tree_entry *
tree_store_add_entry (char *name)
{
	int flag = -1;
	tree_entry *current = ts.tree_first;
	tree_entry *old = NULL;
	tree_entry *new;
	int i, len;
	int submask = 0;

	if (ts.tree_last && ts.tree_last->next)
		abort ();

	/* Search for the correct place */
	while (current && (flag = pathcmp (current->name, name)) < 0){
		old = current;
		current = current->next;
	}
	
	if (flag == 0)
		return current; /* Already in the list */

	/* Not in the list -> add it */
	new = g_new0 (tree_entry, 1);
	if (!current){
		/* Append to the end of the list */
		if (!ts.tree_first){
			/* Empty list */
			ts.tree_first = new;
			new->prev = NULL;
		} else {
			old->next = new;
			new->prev = old;
		}
		new->next = NULL;
		ts.tree_last = new;
	} else {
		/* Insert in to the middle of the list */
		new->prev = old;
		if (old){
			/* Yes, in the middle */
			new->next = old->next;
			old->next = new;
		} else {
			/* Nope, in the beginning of the list */
			new->next = ts.tree_first;
			ts.tree_first = new;
		}
		new->next->prev = new;
	}

	/* Calculate attributes */
	new->name = g_strdup (name);
	len = strlen (new->name);
	new->sublevel = 0;
	for (i = 0; i < len; i++)
		if (new->name [i] == PATH_SEP){
			new->sublevel++;
			new->subname = new->name + i + 1;
		}
	if (new->next)
		submask = new->next->submask;
	else
		submask = 0;
	submask |= 1 << new->sublevel;
	submask &= (2 << new->sublevel) - 1;
	new->submask = submask;
	new->mark = 0;
	
	/* Correct the submasks of the previous entries */
	current = new->prev;
	while (current && current->sublevel > new->sublevel){
		current->submask |= 1 << new->sublevel;
		current = current->prev;
	}
	
	/* The entry has now been added */
	
	if (new->sublevel > 1){
		/* Let's check if the parent directory is in the tree */
		char *parent = g_strdup (new->name);
		int i;
		
		for (i = strlen (parent) - 1; i > 1; i--){
			if (parent [i] == PATH_SEP){
				parent [i] = 0;
				tree_store_add_entry (parent);
				break;
			}
		}
		g_free (parent);
	}

	tree_store_dirty (TRUE);
	return new;
}

tree_entry *
remove_entry (tree_entry *entry)
{
	tree_entry *current = entry->prev;
	long submask = 0;
	tree_entry *ret = NULL;

	tree_store_notify_remove (entry);

	/* Correct the submasks of the previous entries */
	if (entry->next)
		submask = entry->next->submask;
	while (current && current->sublevel > entry->sublevel){
		submask |= 1 << current->sublevel;
		submask &= (2 << current->sublevel) - 1;
		current->submask = submask;
		current = current->prev;
	}

	/* Unlink the entry from the list */
	if (entry->prev)
		entry->prev->next = entry->next;
	else
		ts.tree_first = entry->next;

	if (entry->next)
		entry->next->prev = entry->prev;
	else
		ts.tree_last = entry->prev;

	/* Free the memory used by the entry */
	g_free (entry->name);
	g_free (entry);

	return ret;
}

void
tree_store_remove_entry (char *name)
{
	tree_entry *current, *base, *old;
	int len, base_sublevel;
	
	g_return_if_fail (name != NULL);
	g_return_if_fail (ts.check_name != NULL);
	
	/* Miguel Ugly hack */
	if (name [0] == PATH_SEP && name [1] == 0)
		return;
	/* Miguel Ugly hack end */
    
	base = tree_store_whereis (name);
	if (!base)
		return;	/* Doesn't exist */

	if (ts.check_name [0] == PATH_SEP && ts.check_name [1] == 0)
		base_sublevel = base->sublevel;
	else
		base_sublevel = base->sublevel + 1;

	len = strlen (base->name);
	current = base->next;
	while (current
	       && strncmp (current->name, base->name, len) == 0
	       && (current->name[len] == '\0' || current->name[len] == PATH_SEP)) {
		old = current;
		current = current->next;
		remove_entry (old);
	}
	remove_entry (base);
	tree_store_dirty (TRUE);

	return;
}

/* This subdirectory exists -> clear deletion mark */
void
tree_store_mark_checked (const char *subname)
{
	char *name;
	tree_entry *current, *base;
	int flag = 1, len;
	if (!ts.loaded)
		return;

	if (ts.check_name == NULL)
		return;
	
	/* Calculate the full name of the subdirectory */
	if (subname [0] == '.' &&
	    (subname [1] == 0 || (subname [1] == '.' && subname [2] == 0)))
		return;
	if (ts.check_name [0] == PATH_SEP && ts.check_name [1] == 0)
		name = g_strconcat (PATH_SEP_STR, subname, NULL);
	else
		name = concat_dir_and_file (ts.check_name, subname);
	
	/* Search for the subdirectory */
	current = ts.check_start;
	while (current && (flag = pathcmp (current->name, name)) < 0)
		current = current->next;

	if (flag != 0){
		/* Doesn't exist -> add it */
		current = tree_store_add_entry (name);
		ts.add_queue = g_list_prepend (ts.add_queue, g_strdup (name));
	}
	g_free (name);

	/* Clear the deletion mark from the subdirectory and its children */
	base = current;
	if (base){
		len = strlen (base->name);
		base->mark = 0;
		current = base->next;
		while (current
		       && strncmp (current->name, base->name, len) == 0
		       && (current->name[len] == '\0' || current->name[len] == PATH_SEP || len == 1)){
			current->mark = 0;
			current = current->next;
		}
	}
}

/* Mark the subdirectories of the current directory for delete */
tree_entry *
tree_store_start_check (char *path)
{
	tree_entry *current, *retval;
	int len;

	if (!ts.loaded)
		return NULL;

	g_return_val_if_fail (ts.check_name == NULL, NULL);
	ts.check_start = NULL;

	tree_store_set_freeze (TRUE);
	
	/* Search for the start of subdirectories */
	current = tree_store_whereis (path);
	if (!current){
		struct stat s;

		if (mc_stat (path, &s) == -1)
			return NULL;

		if (!S_ISDIR (s.st_mode))
			return NULL;

		current = tree_store_add_entry (path);
		ts.check_name = g_strdup (path);

		return current;
	}

	ts.check_name = g_strdup (path);
	
	retval = current;

	/* Mark old subdirectories for delete */
	ts.check_start = current->next;
	len = strlen (ts.check_name);
	
	current = ts.check_start;
	while (current
	       && strncmp (current->name, ts.check_name, len) == 0
	       && (current->name[len] == '\0' || current->name[len] == PATH_SEP || len == 1)){
		current->mark = 1;
		current = current->next;
	}

	return retval;
}

tree_entry *
tree_store_start_check_cwd (void)
{
	char buffer [MC_MAXPATHLEN];
	
	mc_get_current_wd (buffer, MC_MAXPATHLEN);
	return tree_store_start_check (buffer);
}

/* Delete subdirectories which still have the deletion mark */
void
tree_store_end_check (void)
{
	tree_entry *current, *old;
	int len;
	GList *the_queue, *l;
	
	if (!ts.loaded)
		return;

	g_return_if_fail (ts.check_name != NULL);
	
	/* Check delete marks and delete if found */
	len = strlen (ts.check_name);
	
	current = ts.check_start;
	while (current
	       && strncmp (current->name, ts.check_name, len) == 0
	       && (current->name[len] == '\0' || current->name[len] == PATH_SEP || len == 1)){
		old = current;
		current = current->next;
		if (old->mark)
			remove_entry (old);
	}

	/* get the stuff in the scan order */
	ts.add_queue = g_list_reverse (ts.add_queue);
	the_queue = ts.add_queue;
	ts.add_queue = NULL;
	g_free (ts.check_name);
	ts.check_name = NULL;
	
	for (l = the_queue; l; l = l->next){
		tree_store_notify_add (l->data);
		g_free (l->data);
	}
	
	g_list_free (the_queue);

	tree_store_set_freeze (FALSE);
}

static void
process_special_dirs (GList **special_dirs, char *file)
{
	char *token;
	char *buffer = g_malloc (4096);
	char *s;
	
	GetPrivateProfileString ("Special dirs", "list",
				 "", buffer, 4096, file);
	s = buffer;
	while ((token = strtok (s, ",")) != NULL){
		*special_dirs = g_list_prepend (*special_dirs, g_strdup (token));
			s = NULL;
	}
}

gboolean
should_skip_directory (char *dir)
{
	static GList *special_dirs;
	GList *l;
	static int loaded;
	
	if (loaded == 0){
		loaded = 1;
		setup_init ();
		process_special_dirs (&special_dirs, profile_name);
		process_special_dirs (&special_dirs, CONFDIR "mc.global");
	}

	for (l = special_dirs; l; l = l->next){
		if (strcmp (dir, l->data) == 0)
			return TRUE;
	}
	return FALSE;
}

tree_entry *
tree_store_rescan (char *dir)
{
	DIR *dirp;
	struct dirent *dp;
	struct stat buf;
	tree_entry *entry;

	if (should_skip_directory (dir)){
		entry = tree_store_add_entry (dir);
		entry->scanned = 1;
		
		return entry;
	}

	entry = tree_store_start_check (dir);

	if (!entry)
		return NULL;

	dirp = mc_opendir (dir);
	if (dirp){
		for (dp = mc_readdir (dirp); dp; dp = mc_readdir (dirp)){
			char *full_name;

			if ((dp->d_name [0] == '.' && dp->d_name [1] == 0)
			    || (dp->d_name [1] == '.' && dp->d_name [2] == 0))
				continue;

			full_name = concat_dir_and_file (dir, dp->d_name);
			if (mc_lstat (full_name, &buf) != -1){
				if (S_ISDIR (buf.st_mode))
					tree_store_mark_checked (dp->d_name);
			}
			g_free (full_name);
		}
		mc_closedir (dirp);
	}
	tree_store_end_check ();
	entry->scanned = 1;

	return entry;
}      

static Hook *remove_entry_hooks;
static Hook *add_entry_hooks;
static Hook *freeze_hooks;

void
tree_store_add_entry_remove_hook (tree_store_remove_fn callback, void *data)
{
	add_hook (&remove_entry_hooks, (void (*)(void *))callback, data);
}

void
tree_store_remove_entry_remove_hook (tree_store_remove_fn callback)
{
	delete_hook (&remove_entry_hooks, (void (*)(void *))callback);
}

void
tree_store_notify_remove (tree_entry *entry)
{
	Hook *p = remove_entry_hooks;
	tree_store_remove_fn r;

	
	while (p){
		r = (tree_store_remove_fn) p->hook_fn;
		r (entry, p->hook_data);
		p = p->next;
	}
}
void
tree_store_add_entry_add_hook (tree_store_add_fn callback, void *data)
{
	add_hook (&add_entry_hooks, (void (*)(void *))callback, data);
}

void
tree_store_remove_entry_add_hook (tree_store_add_fn callback)
{
	delete_hook (&add_entry_hooks, (void (*)(void *))callback);
}

void
tree_store_notify_add (char *directory)
{
	Hook *p = add_entry_hooks;
	tree_store_add_fn r;
	
	while (p) {
		r = (tree_store_add_fn) p->hook_fn;
		r (directory, p->hook_data);
		p = p->next;
	}
}

void
tree_store_add_freeze_hook (tree_freeze_fn callback, void *data)
{
	add_hook (&freeze_hooks, (void (*)(void *)) callback, data);
}

void
tree_store_remove_freeze_hook (tree_freeze_fn callback)
{
	delete_hook (&freeze_hooks, (void (*)(void *))callback);
}

void
tree_store_set_freeze (int freeze)
{
	Hook *p = freeze_hooks;
	tree_freeze_fn f;
	
	while (p) {
		f = (tree_freeze_fn) p->hook_fn;
		f (freeze, p->hook_data);
		p = p->next;
	}
}

tree_scan *
tree_store_opendir (char *path)
{
	tree_entry *entry;
	tree_scan *scan;

	entry = tree_store_whereis (path);
	if (!entry || (entry && !entry->scanned)) {
		entry = tree_store_rescan (path);

		if (!entry)
			return NULL;
	}

	if (entry->next == NULL)
		return NULL;

	scan = g_new (tree_scan, 1);
	scan->base = entry;
	scan->current = entry->next;
	scan->sublevel = entry->next->sublevel;

	scan->base_dir_len = strlen (path);
	return scan;
}

tree_entry *
tree_store_readdir (tree_scan *scan)
{
	tree_entry *entry;
	int len;
	
	g_assert (scan != NULL);

	len = scan->base_dir_len;
	entry = scan->current;
	while (entry &&
	    (strncmp (entry->name, scan->base->name, len) == 0) &&
	    (entry->name [len] == 0 || entry->name [len] == PATH_SEP || len == 1)){

		if (entry->sublevel == scan->sublevel){
			scan->current = entry->next;
			return entry;
		}
		entry = entry->next;
	}

	return NULL;
}

void
tree_store_closedir (tree_scan *scanner)
{
	g_assert (scanner != NULL);
	
	g_free (scanner);
}
