/* vim: set noet ts=8 sts=8 sw=8 : */

/**
 * \file tree.c
 *
 * Implement filesystem trees.
 */

#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <dirent.h>
#include <libgen.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <err.h>
#include <stdio.h>

#include "tree.h"
#include "exclude.h"

static void
set_default_stat(struct stat *s)
{
	time_t now = time(NULL);

	memset(s, 0, sizeof(struct stat));
	s->st_mode = 0555;
	s->st_atime = s->st_mtime = s->st_ctime = now;
}

void
iso_tree_add_child(struct iso_tree_node_dir *parent,
	               struct iso_tree_node *child)
{
	assert(parent && child);
	assert(!child->parent);
	
	parent->nchildren++;
	parent->children =
		realloc(parent->children, parent->nchildren * sizeof(void*));
	parent->children[parent->nchildren-1] = child;
	child->parent = parent;
}

struct iso_tree_node_dir*
iso_tree_new_root()
{
	struct iso_tree_node_dir *root;
	
	root = calloc(1, sizeof(struct iso_tree_node_dir));
	
	set_default_stat(&root->node.attrib);
	root->node.refcount = 1;
	root->node.attrib.st_mode = S_IFDIR | 0777;
	root->node.type = LIBISO_NODE_DIR;
	return root;
}

struct iso_tree_node*
iso_tree_add_file(struct iso_tree_node_dir *parent, const char *path)
{
	struct iso_tree_node_file *f;
	char *p;
	struct stat st;
	
	assert( parent && path);
	
	if (lstat(path, &st) == -1) {
		libisofs_errno = NO_FILE;
		return NULL;
	}
		
	if ( !S_ISREG(st.st_mode) ) {
		libisofs_errno = UNEXPECTED_FILE_TYPE;
		return NULL;
	}
	
	if ( access(path, R_OK) ) {
		libisofs_errno = NO_READ_ACCESS;
		return NULL;
	}
	
	f = calloc(1, sizeof(struct iso_tree_node_file));
	
	/* fill fields */
	f->node.refcount = 1;
	f->node.attrib = st;
	f->loc.path = strdup(path);
	f->node.type = LIBISO_NODE_FILE;
	
	p = strdup(path); /* because basename() might modify its arg */
	f->node.name = strdup( basename(p) );
	free(p);
	
	/* add to parent (this also sets f->node->parent) */
	iso_tree_add_child(parent, (struct iso_tree_node*) f);
	
	return (struct iso_tree_node*) f;
}

struct iso_tree_node*
iso_tree_add_symlink(struct iso_tree_node_dir *parent, 
					const char *name, const char *dest)
{
	struct iso_tree_node_symlink *link;
	
	assert( parent && name && dest);
	
	link = calloc(1, sizeof(struct iso_tree_node_symlink));
	
	/* fill fields */
	set_default_stat(&link->node.attrib);
	link->node.refcount = 1;
	link->node.attrib.st_mode |= S_IFLNK;
	link->node.name = strdup(name);
	link->node.type = LIBISO_NODE_SYMLINK;
	link->dest = strdup(dest);
	
	/* add to parent (this also sets link->node->parent) */
	iso_tree_add_child(parent, (struct iso_tree_node*) link);
	
	return (struct iso_tree_node*) link;
}

struct iso_tree_node_dir*
iso_tree_add_dir(struct iso_tree_node_dir *parent, 
				 const char *name)
{
	struct iso_tree_node_dir *dir;
	
	assert( parent && name );
	
	dir = calloc(1, sizeof(struct iso_tree_node_dir));
	
	dir->node.refcount = 1;
	dir->node.attrib = parent->node.attrib;
	dir->node.type = LIBISO_NODE_DIR;
	dir->node.name = strdup(name);
	
	iso_tree_add_child(parent, (struct iso_tree_node*) dir);
	return dir;
}

enum iso_tree_node_type 
iso_tree_node_get_type(struct iso_tree_node *node)
{
	assert(node);
	return node->type;
}

void
iso_tree_node_set_name(struct iso_tree_node *node, const char *name)
{
	free(node->name);
	node->name = strdup(name);
}

const char *
iso_tree_node_get_name(struct iso_tree_node *node)
{
	assert(node);
	return node->name;
}

void 
iso_tree_node_set_hidden(struct iso_tree_node *node, int hide_attrs) 
{
	assert(node);
	node->hide_flags = hide_attrs;
}

int 
iso_tree_node_is_hidden(struct iso_tree_node *node)
{
	assert(node);
	return node->hide_flags;
}

void 
iso_tree_node_set_gid(struct iso_tree_node *node, gid_t gid)
{
	assert(node);
	node->attrib.st_gid = gid;
}

gid_t 
iso_tree_node_get_gid(struct iso_tree_node *node)
{
	assert(node);
	return node->attrib.st_gid;
}

void 
iso_tree_node_set_uid(struct iso_tree_node *node, uid_t uid)
{
	assert(node);
	node->attrib.st_uid = uid;
}

uid_t 
iso_tree_node_get_uid(struct iso_tree_node *node)
{
	assert(node);
	return node->attrib.st_uid;
}

void 
iso_tree_node_set_permissions(struct iso_tree_node *node, mode_t mode)
{
	assert(node);
	node->attrib.st_mode = (node->attrib.st_mode & S_IFMT) |
	                       (mode & ~S_IFMT);
}

mode_t 
iso_tree_node_get_permissions(struct iso_tree_node *node)
{
	assert(node);
	return node->attrib.st_mode & ~S_IFMT;
}

off_t 
iso_tree_node_get_size(struct iso_tree_node *node)
{
	return node->attrib.st_size;
}

void 
iso_tree_node_set_mtime(struct iso_tree_node *node, time_t time)
{
	node->attrib.st_mtime = time;
}

time_t 
iso_tree_node_get_mtime(struct iso_tree_node *node)
{
	return node->attrib.st_mtime;
}

void 
iso_tree_node_set_atime(struct iso_tree_node *node, time_t time)
{
	node->attrib.st_atime = time;
}

time_t 
iso_tree_node_get_atime(struct iso_tree_node *node)
{
	return node->attrib.st_atime;
}

void 
iso_tree_node_set_ctime(struct iso_tree_node *node, time_t time)
{
	node->attrib.st_ctime = time;
}

time_t 
iso_tree_node_get_ctime(struct iso_tree_node *node)
{
	return node->attrib.st_ctime;
}

void 
iso_tree_node_set_sort_weight(struct iso_tree_node *node, int w)
{
	assert(node);
	if ( ISO_ISDIR(node) ) {
		size_t i;
		struct iso_tree_node_dir *dir;
		dir = (struct iso_tree_node_dir *) node;
		for (i=0; i < dir->nchildren; i++) {
			iso_tree_node_set_sort_weight(dir->children[i], w);
		}
	} else if ( ISO_ISREG(node) ) {
		struct iso_tree_node_file *file;
		file = (struct iso_tree_node_file *) node;
		file->sort_weight = w;
	}
}

void 
iso_tree_node_symlink_set_dest(struct iso_tree_node_symlink *node, 
                               const char *dest)
{
	assert(node && dest);
	free(node->dest);
	node->dest = strdup(dest);
}

const char *
iso_tree_node_symlink_get_dest(struct iso_tree_node_symlink *node)
{
	assert(node);
	return node->dest;
}

struct iso_tree_node*
iso_tree_add_node(struct iso_tree_node_dir *parent,
				  const char *path)
{
	struct stat st;
	struct iso_tree_node *node;
	
	assert( parent && path);
	
	if (lstat(path, &st) == -1) {
		libisofs_errno = NO_FILE;
		return NULL;
	}
	
	if ( access(path, R_OK) ) {
		libisofs_errno = NO_READ_ACCESS;
		return NULL;
	}
	
	switch (st.st_mode & S_IFMT) { 
		case S_IFREG:
			/* regular file */
			node = iso_tree_add_file(parent, path);
			break;
		case S_IFLNK:
			/* symlink */
			{
			char dest[PATH_MAX];
			char *p;
			int n;
			
			n = readlink(path, dest, PATH_MAX);
			if ( n == -1 ) {
				libisofs_errno = INTERNAL_ERROR;
				return NULL;
			}
			dest[n] = '\0';
			p = strdup(path); /* because basename() might modify its arg */
			node = iso_tree_add_symlink(parent, basename(p), dest);
			free(p);
			node->attrib = st;
			}
			break;
		case S_IFDIR:
			/* directory */
			{
			char *p;
			p = strdup(path); /* because basename() might modify its arg */
			node = (struct iso_tree_node*) iso_tree_add_dir(parent, basename(p));
			free(p);
			node->attrib = st;
			}
			break;
		default:
			libisofs_errno = UNEXPECTED_FILE_TYPE;
			node = NULL;
			break;
	}
	return node;
}

struct iso_tree_iter *
iso_tree_node_children(struct iso_tree_node_dir *dir)
{
	struct iso_tree_iter *iter;
	assert(dir);
	iter = malloc(sizeof(struct iso_tree_iter));
	iter->dir = dir;
	iter->index = -1;
	return iter;
}

struct iso_tree_node *
iso_tree_iter_next(struct iso_tree_iter *iter)
{
	assert(iter);
	if ( ++iter->index < iter->dir->nchildren )
		return iter->dir->children[iter->index];
	else
		return NULL;
}

int
iso_tree_iter_has_next(struct iso_tree_iter *iter)
{
	assert(iter);
	return iter->index + 1 < iter->dir->nchildren;
}

void 
iso_tree_iter_free(struct iso_tree_iter *iter)
{
	free(iter);
}

int 
iso_tree_node_take(struct iso_tree_node_dir *dir, struct iso_tree_node *node)
{
	int i;
	assert(dir && node);
	
	/* search for the node in the dir */
	for (i = 0; i < dir->nchildren; ++i) {
		if ( dir->children[i] == node )
			break;
	}
	
	if (i < dir->nchildren) {
		int j;
		for (j = i+1; j < dir->nchildren; ++j) {
			dir->children[j-1] = dir->children[j];
		}
		--dir->nchildren;
		dir->children = realloc(dir->children, dir->nchildren * sizeof(void*));
		node->parent = NULL;
		return 0;
	} else {
		/* the node doesn't exist on dir */
		return -1;
	}
}

int 
iso_tree_node_remove(struct iso_tree_node_dir *dir, struct iso_tree_node *node)
{
	int res;
	assert(dir && node);
	res = iso_tree_node_take(dir, node);
	if (!res)
		iso_tree_free(node);
	return res;
}

int 
iso_tree_node_take_iter(struct iso_tree_iter *iter)
{
	int j;
	struct iso_tree_node_dir *dir;
	struct iso_tree_node *node;
	
	assert(iter);
	
	dir = iter->dir;
	
	if (iter->index < 0)
		return -1; /* index before beginning */
	
	if (iter->index >= dir->nchildren)
		return -2; /* index after end */
	
	node = dir->children[iter->index];
	node->parent = NULL;
	for (j = iter->index+1; j < dir->nchildren; ++j) {
		dir->children[j-1] = dir->children[j];
	}
	--dir->nchildren;
	dir->children = realloc(dir->children, dir->nchildren * sizeof(void*));
	
	/* update iter index */
	--iter->index;
	return 0;
}

int 
iso_tree_node_remove_iter(struct iso_tree_iter *iter)
{
	int j;
	struct iso_tree_node_dir *dir;
	struct iso_tree_node *node;
	
	assert(iter);
	
	dir = iter->dir;
	
	if (iter->index < 0)
		return -1; /* index before beginning */
	
	if (iter->index >= dir->nchildren)
		return -2; /* index after end */
	
	node = dir->children[iter->index];
	for (j = iter->index+1; j < dir->nchildren; ++j) {
		dir->children[j-1] = dir->children[j];
	}
	--dir->nchildren;
	dir->children = realloc(dir->children, dir->nchildren * sizeof(void*));
	
	/* update iter index */
	--iter->index;
	
	/* and free node */
	node->parent = NULL;
	iso_tree_free(node);
	return 0;
}

struct iso_tree_node_dir *
iso_tree_node_get_parent(struct iso_tree_node *node)
{
	assert(node);
	return node->parent;
}

void 
iso_tree_node_ref(struct iso_tree_node *node)
{
	++node->refcount;
}

void
iso_tree_free(struct iso_tree_node *root)
{	
	if (!root)
		return;
	if (--root->refcount < 1) {
		if ( ISO_ISDIR(root) ) {
			size_t i;
			struct iso_tree_node_dir *dir;
			dir = (struct iso_tree_node_dir *) root;
			for (i=0; i < dir->nchildren; i++) {
				iso_tree_free(dir->children[i]);
			}
			free(dir->children);
		} else if ( ISO_ISLNK(root) ) {
			struct iso_tree_node_symlink *link;
			link = (struct iso_tree_node_symlink *) root;
			free(link->dest);
		} else if ( ISO_ISREG(root) ) {
			struct iso_tree_node_file *file;
			file = (struct iso_tree_node_file *) root;
			if (root->procedence == LIBISO_NEW)
				free(file->loc.path);
		} else if ( ISO_ISBOOT(root) ) {
			struct iso_tree_node_boot *boot;
			boot = (struct iso_tree_node_boot *) root;
			if (root->procedence == LIBISO_NEW && boot->img)
				free(boot->loc.path);
		}
		free(root->name);
		free(root);
	}
}

static void
iso_tree_radd_dir_aux(struct iso_tree_node_dir *parent, const char *path, 
                      struct iso_tree_radd_dir_behavior *behavior, 
                      struct iso_hash_table *excludes)
{
	struct iso_tree_node *new;
	DIR *dir;
	struct dirent *ent;

	dir = opendir(path);
	if (!dir) {
		warn("couldn't open directory %s: %s\n", path, strerror(errno));
		return;
	}

	while ((ent = readdir(dir))) {
		char child[strlen(ent->d_name) + strlen(path) + 2];

		if (behavior->stop_on_error & behavior->error)
			break;
			
		if (strcmp(ent->d_name, ".") == 0 ||
				strcmp(ent->d_name, "..") == 0)
			continue;

		//TODO check if path already finished in '/'
		sprintf(child, "%s/%s", path, ent->d_name);

		/* see if this child is excluded. */
		if (iso_exclude_lookup(excludes, child))
			continue;

		new = iso_tree_add_node(parent, child);
		if (!new || !ISO_ISDIR(new)) {
			if (!new) 
				behavior->error = 1;
			continue;
		}

		iso_tree_radd_dir_aux( (struct iso_tree_node_dir *) new, child, 
		                             behavior, excludes);
		
	}
	closedir(dir);

	return;
}

void 
iso_tree_radd_dir(struct iso_tree_node_dir *parent, const char *path, 
                  struct iso_tree_radd_dir_behavior *behavior)
{
	struct iso_tree_node_dir *dir;
	struct iso_hash_table table = { {0,}, 0};
	
	assert ( parent && path );
	
	behavior->error = 0;
	
	/* initialize exclude hash_table */
	if ( behavior->excludes ) {
		char *exclude;
		int i = 0; 
		while ( (exclude = behavior->excludes[i++]) ) {
			iso_exclude_add_path(&table, exclude);
		}  
	}
	
	/* recurse into dir */
	iso_tree_radd_dir_aux(parent, path, behavior, &table);
	
	/* clear hashtable */
	iso_exclude_empty(&table);
}

void
iso_tree_print(const struct iso_tree_node *root, int spaces)
{
	char sp[spaces+1];

	memset(sp, ' ', spaces);
	sp[spaces] = '\0';

	printf("%s%s\n", sp, root->name);
	
	if ( ISO_ISDIR(root) ) {
		size_t i;
		struct iso_tree_node_dir *dir;
		
		dir = (struct iso_tree_node_dir *) root;
		for (i=0; i < dir->nchildren; i++) {
			iso_tree_print(dir->children[i], spaces+2);
		}
	}
}

void
iso_tree_print_verbose(const struct iso_tree_node *root,
		       print_dir_callback dir,
		       print_file_callback file,
		       void *callback_data,
		       int spaces)
{

	(ISO_ISDIR(root) ? dir : file)
		(root, callback_data, spaces);
	
	if ( ISO_ISDIR(root) ) {
		size_t i;
		struct iso_tree_node_dir *dir_node;
		
		dir_node = (struct iso_tree_node_dir *) root;
		for (i=0; i < dir_node->nchildren; i++) {
			iso_tree_print_verbose(dir_node->children[i], dir,
					file, callback_data, spaces+2);
		}
	}
}