/*
 * dml - Dialog Markup Language
 *
 * $Id: fileselector.c,v 1.3 2002/07/29 13:07:05 malekith Exp $
 * Author: Michal Moskal <malekith@pld-linux.org>
 * include COPYING-GNU
 */

#include "pi.h"
#include "ml.h"
#include <string.h>
#include <sys/stat.h>
#include <dirent.h>
#include <stdlib.h>

enum {
	pi_ft_broken,
	pi_ft_regular,
	pi_ft_dir,
	pi_ft_dev,
};

struct pi_fileselector_entry {
	struct pi_fileselector_entry *left, *right;
	struct pi_fileselector_entry *children, *parent;
	int lev, no;

	int type;
	char *name;
};

static void xkill(struct pi_fileselector_entry *root)
{
	if (root == 0)
		return;
		
	xkill(root->left);
	xkill(root->right);
	xkill(root->children);
	xfree(root->name);
	xfree(root);
}

static struct pi_fileselector_entry *add_entry(
			struct pi_fileselector_entry *root,
			const char *dir, const char *file)
{
	char *n;
	struct stat s;
	int e;

	if (root) {
		if (strcmp(file, root->name) < 0) {
			root->left = add_entry(root->left, dir, file);
			root->left->parent = root;
		} else {
			root->right = add_entry(root->right, dir, file);
			root->right->parent = root;
		}
		return root;
	}
	
	n = xmalloc(strlen(dir) + strlen(file) + 1);
	strcpy(n, dir);
	strcat(n, file);
	
	memset(&s, 0, sizeof(s));
	e = stat(n, &s);

	root = xmalloc(sizeof(struct pi_fileselector_entry));
	memset(root, 0, sizeof(struct pi_fileselector_entry));
	
	pi_setstr(&root->name, file);
	if (e)
		root->type = pi_ft_broken;
	else if (S_ISDIR(s.st_mode))
		root->type = pi_ft_dir;
	else if (S_ISCHR(s.st_mode) || S_ISBLK(s.st_mode))
		root->type = pi_ft_dev;
	else
		root->type = pi_ft_regular;

	return root;
}

static char *full_name(struct pi_fileselector_entry *dir)
{
	struct dbuf *db;
	struct pi_fileselector_entry *e;
	
	for (e = dir; e->parent; e = e->parent)
		/* nothing */ ;

	db = db_new();
	
	for (e = e->children; 
	     e && e->children && e != dir; 
	     e = e->children) {
		db_addc(db, '/');
		db_add(db, e->name, strlen(e->name));
	}

	db_addc(db, '/');
	db_add(db, dir->name, strlen(dir->name));

	if (dir->type == pi_ft_dir)
		db_addc(db, '/');
	db_addc(db, 0);

	return db_finish(db, 0);
}

static void read_children(struct pi_fileselector_entry *dir)
{
	char *name;
	DIR *d;
	struct dirent *de;

	name = full_name(dir);
	
	d = opendir(name);
	
	if (d == 0) {
		xfree(name);
		return;
	}

	while ((de = readdir(d))) {
		if (strcmp(de->d_name, ".") == 0 ||
		    strcmp(de->d_name, "..") == 0) 
			continue;
		dir->children = add_entry(dir->children, name, de->d_name);
		dir->children->parent = dir;
	}

	closedir(d);
	xfree(name);
}

static void recompute_numbers(struct pi_fileselector_entry *dir, 
			      int lev, int *no)
{
	if (dir == 0)
		return;

	recompute_numbers(dir->left, lev, no);
	dir->no = (*no)++;
	dir->lev = lev;
	recompute_numbers(dir->right, lev, no);
	recompute_numbers(dir->children, lev + 1, no);
}

static void xopen(struct pi_fileselector *self,
		  struct pi_fileselector_entry *dir)
{
	struct pi_fileselector_entry *e;

	xkill(dir->children);
	xkill(dir->left);
	xkill(dir->right);

	dir->children = dir->left = dir->right = 0;

	if (dir->parent && dir->parent->children != dir) {
		if (dir->parent->right == dir)
			dir->parent->right = 0;
		else if (dir->parent->left == dir)
			dir->parent->left = 0;
		else
			abort();

		for (e = dir->parent; 
		     e->parent->children != e; 
		     e = e->parent)
			/* nothing */ ;
		
		e = e->parent;
		xkill(e->children);
		e->children = dir;
		dir->parent = e;
	}

	/* now, as we cleaned up the tree, we need own children */
	read_children(dir);

	self->nlines = 0;
	recompute_numbers(self->root, 0, &self->nlines);
	self->hl = dir->no;
}

static void set_root(struct pi_fileselector *self, const char *dir)
{
	char *full, *work;
	int i, k;
	struct pi_fileselector_entry *ent, *root = 0;

	while (*dir == '/')
		dir++;
		
	full = xmalloc(strlen(dir) + 2);

	strcpy(full, "/");
	strcat(full, dir);
	work = 0;
	pi_setstr(&work, full);

	root = ent = add_entry(0, "/", "");
	
	for (i = 1; work[i]; i = k) {
		k = i;
		while (work[k] && work[k] != '/')
			k++;
		work[k] = 0;
		full[i] = 0;
		ent->children = add_entry(0, full, work + i);
		ent->children->parent = ent;
		ent = ent->children;
		work[k] = full[k];
		full[i] = work[i];
		while (work[k] == '/')
			k++;
	}

	xfree(work);
	xfree(full);

	self->root = root;
	xopen(self, ent);
}

static struct pi_fileselector_entry *find_entry(
				struct pi_fileselector_entry *root, 
				int no)
{
	if (root == 0 || root->no == no)
		return root;
		
	if (root->children)
		return find_entry(root->children, no);

	return find_entry(no < root->no ? root->left : root->right, no);
}

void pi_fileselector_draw(struct pi_fileselector *self)
{
	int c1, c2;
	int x, y, i;
	struct pi_fileselector_entry *e;

	pi_locate((struct pi_object*)self, &x, &y);

	if (self->hl < self->pos.h)
		self->top = 0;
	else if (self->hl < self->top)
		self->top = self->hl;

	for (i = self->top; i < self->top + self->pos.h; i++) {
		pi_goto(x, y++);
		if (i == self->hl) {
			if (pi_is_active((struct pi_object*)self)) {
				c1 = pi_color_fileselector_hl_active_1;
				c2 = pi_color_fileselector_hl_active_2;
			} else {
				c1 = pi_color_fileselector_hl_1;
				c2 = pi_color_fileselector_hl_2;
			}
		} else {
			c1 = pi_color_fileselector_1;
			c2 = pi_color_fileselector_2;
		}

		if (i < self->nlines) {
			e = find_entry(self->root, i);
			if (e->lev > self->pos.w - 4)
				pi_addstr_fill_alt("", self->pos.w - 2, c1, c2);
			else {
				pi_color(c2);
				pi_addstrn_fill("", e->lev * 2);
				switch (e->type) {
				case pi_ft_dir: pi_putch('/'); break;
				case pi_ft_dev: pi_putch('%'); break;
				case pi_ft_regular: pi_putch(' '); break;
				default: pi_putch('?'); break;
				}
				pi_color(c1);
				pi_addstrn_fill(e->name, 
					self->pos.w - 3 - e->lev * 2);
			}
		} else {
			pi_addstr_fill_alt("_~", self->pos.w - 2, c1, c2); 
		}

		if (i == self->hl)
			pi_putch('<');
		else
			pi_putch(' ');
			
		if (self->nlines <= self->pos.h)
			pi_putch(' ');
		else 
		pi_scrollbar(i, self->top, self->pos.h, self->nlines, 
			pi_is_active((struct pi_object*)self) ?
					pi_color_afileselector_scrollbar :
					pi_color_fileselector_scrollbar,
			pi_is_active((struct pi_object*)self) ?
					pi_color_afileselector_scrollbar_marked :
					pi_color_fileselector_scrollbar_marked);
	}
}

void pi_fileselector_kill(struct pi_fileselector *self)
{
	xkill(self->root);
	self->root = 0;
}

void pi_fileselector_set_root(struct pi_fileselector *self, const char *root)
{
	pi_fileselector_kill(self);
	set_root(self, root);
	xfree(self->full_name);
	self->full_name = 0;
}

void pi_fileselector_key(struct pi_fileselector *self, int key)
{
	int n;
	
	switch (key) {
	case pi_key_up:
	case '-':
		if (self->hl)
			self->hl--;
		if (self->hl < self->top)
			self->top = self->hl;
		break;
	case pi_key_down:
	case '+':
		if (self->hl < self->nlines - 1)
			self->hl++;
		if (self->hl - self->top >= self->pos.h)
			self->top = self->hl - self->pos.h + 1;
		break;

	case pi_key_pgup:
		n = self->pos.h;
		while (n--)
			pi_fileselector_key(self, pi_key_up);
		break;
		
	case pi_key_pgdn:
		n = self->pos.h;
		while (n--)
			pi_fileselector_key(self, pi_key_down);
		break;
		
	case '\n':
	case ' ':
		if (find_entry(self->root, self->hl)->type == pi_ft_dir)
			xopen(self, find_entry(self->root, self->hl));
		break;
		
	default:
		pi_object_key((struct pi_object*)self, key);
		break;
	}
	
	pi_draw((struct pi_object*)self);
}

const char *pi_fileselector_fetch(struct pi_fileselector *self)
{
	xfree(self->full_name);
	self->full_name = full_name(find_entry(self->root, self->hl));
	return self->full_name;
}

static struct pi_funcs fileselector_funcs = {
	1,
	(pi_handler_void)pi_fileselector_draw,
	(pi_handler_int)pi_object_cc,
	(pi_handler_int)pi_fileselector_key,
	(pi_handler_void)pi_fileselector_kill,
	(pi_handler_rstring)pi_fileselector_fetch,
};

struct pi_fileselector *pi_new_fileselector(int size)
{
	struct pi_fileselector *self;

	if (size == 0)
		size = sizeof(struct pi_fileselector);
	self = (struct pi_fileselector*)pi_new_object(size);
	self->f = &fileselector_funcs;
	pi_fileselector_set_root(self, "/");

	return self;
}

char *pi_select_file(const char *def, int flags, const char *title)
{
	struct pi_window *win;
	struct pi_fileselector *f;
	struct pi_object *back;
	const char *b;
	char *s;
	struct stat buf;

	back = _pi_root_object->active;
	
	win = pi_new_window(0);
	f = pi_new_fileselector(0);
	pi_fileselector_set_root(f, def);
	pi_add_child((struct pi_object*)win, (struct pi_object*)f);
	pi_setpos((struct pi_object*)f, 2, 1, 70, 18);
	pi_setpos((struct pi_object*)win, 0, 0, 74, 22);
	pi_center(win);
	pi_add_button(win, 34, 20, "_Ok", 1);
	pi_setstr(&win->title, title);

	for (;;) {
		win->modal_result = 0;
		pi_exec(win);
		if (win->modal_result != 1)
			continue;
		b = f->f->fetch((struct pi_object*)f);
#define TEST(cond, flag)			\
	if (cond) {				\
		if (flags & (flag))		\
			break;			\
		else				\
			continue;		\
	}

		TEST((stat(b, &buf)),
			PI_FSF_ALLOW_BROKEN);
		TEST((S_ISBLK(buf.st_mode) && S_ISCHR(buf.st_mode)),
			PI_FSF_ALLOW_DEV);
		TEST((S_ISDIR(buf.st_mode)),
			PI_FSF_ALLOW_DIR);
		TEST((S_ISREG(buf.st_mode)),
			PI_FSF_ALLOW_REG);
		if (flags & PI_FSF_ALLOW_OTHER)
			break;
	}

	s = strdup(b);
	pi_kill((struct pi_object*)win);
	
	_pi_root_object->active = back;
	pi_repaint();

	return s;
}
