#include <u.h>
#include <libc.h>
#include <draw.h>
#include <plumb.h>
#include <regexp.h>
#include <bio.h>
#include "faces.h"

enum	/* number of deleted faces to cache */
{
	Nsave	= 20,
};

static Facefile	*facefiles;
static int		nsaved;
static char	*facedom;
static char *homeface;

/*
 * Loading the files is slow enough on a dial-up line to be worth this trouble
 */
typedef struct Readcache	Readcache;
struct Readcache {
	char *file;
	char *data;
	long mtime;
	long rdtime;
	Readcache *next;
};

static Readcache *rcache;

ulong
dirlen(char *s)
{
	Dir *d;
	ulong len;

	d = dirstat(s);
	if(d == nil)
		return 0;
	len = d->length;
	free(d);
	return len;
}

ulong
dirmtime(char *s)
{
	Dir *d;
	ulong t;

	d = dirstat(s);
	if(d == nil)
		return 0;
	t = d->mtime;
	free(d);
	return t;
}

static char*
doreadfile(char *s)
{
	char *p;
	int fd, n;
	ulong len;

	len = dirlen(s);
	if(len == 0)
		return nil;

	p = malloc(len+1);
	if(p == nil)
		return nil;

	if((fd = open(s, OREAD)) < 0
	|| (n = readn(fd, p, len)) < 0) {
		close(fd);
		free(p);
		return nil;
	}

	p[n] = '\0';
	return p;
}

static char*
readfile(char *s)
{
	Readcache *r, **l;
	char *p;
	ulong mtime;

	for(l=&rcache, r=*l; r; l=&r->next, r=*l) {
		if(strcmp(r->file, s) != 0)
			continue;

		/*
		 * if it's less than 30 seconds since we read it, or it 
		 * hasn't changed, send back our copy
		 */
		if(time(0) - r->rdtime < 30)
			return strdup(r->data);
		if(dirmtime(s) == r->mtime) {
			r->rdtime = time(0);
			return strdup(r->data);
		}

		/* out of date, remove this and fall out of loop */
		*l = r->next;
		free(r->file);
		free(r->data);
		free(r);
		break;
	}

	/* add to cache */
	mtime = dirmtime(s);
	if(mtime == 0)
		return nil;

	if((p = doreadfile(s)) == nil)
		return nil;

	r = malloc(sizeof(*r));
	if(r == nil)
		return nil;
	r->mtime = mtime;
	r->file = estrdup(s);
	r->data = p;
	r->rdtime = time(0);
	r->next = rcache;
	rcache = r;
	return strdup(r->data);
}

static char*
translatedomain(char *dom, char *list)
{
	static char buf[200];
	char *p, *ep, *q, *nextp, *file;
	char *bbuf, *ebuf;
	Reprog *exp;

	if(dom == nil || *dom == 0)
		return nil;

	if(list == nil || (file = readfile(list)) == nil)
		return dom;

	for(p=file; p; p=nextp) {
		if(nextp = strchr(p, '\n'))
			*nextp++ = '\0';

		if(*p == '#' || (q = strpbrk(p, " \t")) == nil || q-p > sizeof(buf)-2)
			continue;

		bbuf = buf+1;
		ebuf = buf+(1+(q-p));
		strncpy(bbuf, p, ebuf-bbuf);
		*ebuf = 0;
		if(*bbuf != '^')
			*--bbuf = '^';
		if(ebuf[-1] != '$') {
			*ebuf++ = '$';
			*ebuf = 0;
		}

		if((exp = regcomp(bbuf)) == nil){
			fprint(2, "bad regexp in machinelist: %s\n", bbuf);
			killall("regexp");
		}

		if(regexec(exp, dom, 0, 0)){
			free(exp);
			ep = p+strlen(p);
			q += strspn(q, " \t");
			if(ep-q+2 > sizeof buf) {
				fprint(2, "huge replacement in machinelist: %.*s\n", utfnlen(q, ep-q), q);
				exits("bad big replacement");
			}
			strncpy(buf, q, ep-q);
			ebuf = buf+(ep-q);
			*ebuf = 0;
			while(ebuf > buf && (ebuf[-1] == ' ' || ebuf[-1] == '\t'))
				*--ebuf = 0;
			free(file);
			return buf;
		}
		free(exp);
	}
	free(file);

	return dom;
}

static char*
tryfindpicture(char *dom, char *user, char *dir, char *dict)
{
	static char buf[1024];
	char *file, *p, *nextp, *q;
	
	if((file = readfile(dict)) == nil)
		return nil;

	snprint(buf, sizeof buf, "%s/%s", dom, user);

	for(p=file; p; p=nextp){
		if(nextp = strchr(p, '\n'))
			*nextp++ = '\0';

		if(*p == '#' || (q = strpbrk(p, " \t")) == nil)
			continue;
		*q++ = 0;

		if(strcmp(buf, p) == 0){
			q += strspn(q, " \t");
			snprint(buf, sizeof buf, "%s/%s", dir, q);
			q = buf+strlen(buf);
			while(q > buf && (q[-1] == ' ' || q[-1] == '\t'))
				*--q = 0;
			free(file);
			return estrdup(buf);
		}
	}
	free(file);
	return nil;
}

static char*
estrstrdup(char *a, char *b)
{
	char *t;
	
	t = emalloc(strlen(a)+strlen(b)+1);
	strcpy(t, a);
	strcat(t, b);
	return t;
}

static char*
tryfindfiledir(char *dom, char *user, char *dir)
{
	char *dict, *ndir, *x;
	int fd;
	int i, n;
	Dir *d;
	
	/*
	 * If this directory has a .machinelist, use it.
	 */
	x = estrstrdup(dir, "/.machinelist");
	dom = estrdup(translatedomain(dom, x));
	free(x);

	/*
	 * If this directory has a .dict, use it.
	 */
	dict = estrstrdup(dir, "/.dict");
	if(access(dict, AEXIST) >= 0){
		x = tryfindpicture(dom, user, dir, dict);
		free(dict);
		free(dom);
		return x;
	}
	free(dict);
	
	/*
	 * If not, recurse into subdirectories.
	 * Ignore 512x512 directories.
	 * Save 48x48 directories for later.
	 */
	if((fd = open(dir, OREAD)) < 0)
		return nil;
	while((n = dirread(fd, &d)) > 0){
		for(i=0; i<n; i++){
			if((d[i].mode&DMDIR)
			&& strncmp(d[i].name, "512x", 4) != 0
			&& strncmp(d[i].name, "48x48x", 6) != 0){
				ndir = emalloc(strlen(dir)+1+strlen(d[i].name)+1);
				strcpy(ndir, dir);
				strcat(ndir, "/");
				strcat(ndir, d[i].name);
				if((x = tryfindfiledir(dom, user, ndir)) != nil){
					free(ndir);
					free(d);
					close(fd);
					free(dom);
					return x;
				}
			}
		}
		free(d);
	}
	close(fd);
	
	/*
	 * Handle 48x48 directories in the right order.
	 */
	ndir = estrstrdup(dir, "/48x48x8");
	for(i=8; i>0; i>>=1){
		ndir[strlen(ndir)-1] = i+'0';
		if(access(ndir, AEXIST) >= 0 && (x = tryfindfiledir(dom, user, ndir)) != nil){
			free(ndir);
			free(dom);
			return x;
		}
	}
	free(ndir);
	free(dom);
	return nil;
}

static char*
tryfindfile(char *dom, char *user)
{
	char *p;

	while(dom && *dom){
		if(homeface && (p = tryfindfiledir(dom, user, homeface)) != nil)
			return p;
		if((p = tryfindfiledir(dom, user, "/lib/face")) != nil)
			return p;
		if((dom = strchr(dom, '.')) == nil)
			break;
		dom++;
	}
	return nil;
}

char*
findfile(Face *f, char *dom, char *user)
{
	char *p;

	if(facedom == nil){
		facedom = getenv("facedom");
		if(facedom == nil)
			facedom = DEFAULT;
	}
	if(dom == nil)
		dom = facedom;
	if(homeface == nil)
		homeface = smprint("%s/lib/face", getenv("home"));

	f->unknown = 0;
	if((p = tryfindfile(dom, user)) != nil)
		return p;
	f->unknown = 1;
	p = tryfindfile(dom, "unknown");
	if(p != nil || strcmp(dom, facedom) == 0)
		return p;
	return tryfindfile("unknown", "unknown");
}

static
void
clearsaved(void)
{
	Facefile *f, *next, **lf;

	lf = &facefiles;
	for(f=facefiles; f!=nil; f=next){
		next = f->next;
		if(f->ref > 0){
			*lf = f;
			lf = &(f->next);
			continue;
		}
		if(f->image != display->black && f->image != display->white)
			freeimage(f->image);
		free(f->file);
		free(f);
	}
	*lf = nil;
	nsaved = 0;
}

void
freefacefile(Facefile *f)
{
	if(f==nil || f->ref-->1)
		return;
	if(++nsaved > Nsave)
		clearsaved();
}	

static Image*
myallocimage(ulong chan)
{
	Image *img;
	img = allocimage(display, Rect(0,0,Facesize,Facesize), chan, 0, DNofill);
	if(img == nil){
		clearsaved();
		img = allocimage(display, Rect(0,0,Facesize,Facesize), chan, 0, DNofill);
		if(img == nil)
			return nil;
	}
	return img;
}
		

static Image*
readbit(int fd, ulong chan)
{
	char buf[4096], hx[4], *p;
	uchar data[Facesize*Facesize];	/* more than enough */
	int nhx, i, n, ndata, nbit;
	Image *img;

	n = readn(fd, buf, sizeof buf);
	if(n <= 0)
		return nil;
	if(n >= sizeof buf)
		n = sizeof(buf)-1;
	buf[n] = '\0';

	n = 0;
	nhx = 0;
	nbit = chantodepth(chan);
	ndata = (Facesize*Facesize*nbit)/8;
	p = buf;
	while(n < ndata) {
		p = strpbrk(p+1, "0123456789abcdefABCDEF");
		if(p == nil)
			break;
		if(p[0] == '0' && p[1] == 'x')
			continue;

		hx[nhx] = *p;
		if(++nhx == 2) {
			hx[nhx] = 0;
			i = strtoul(hx, 0, 16);
			data[n++] = i;
			nhx = 0;
		}
	}
	if(n < ndata)
		return allocimage(display, Rect(0,0,Facesize,Facesize), CMAP8, 0, 0x88888888);

	img = myallocimage(chan);
	if(img == nil)
		return nil;
	loadimage(img, img->r, data, ndata);
	return img;
}

static Facefile*
readface(char *fn)
{
	int x, y, fd;
	uchar bits;
	uchar *p;
	Image *mask;
	Image *face;
	char buf[16];
	uchar data[Facesize*Facesize];
	uchar mdata[(Facesize*Facesize)/8];
	Facefile *f;
	Dir *d;

	for(f=facefiles; f!=nil; f=f->next){
		if(strcmp(fn, f->file) == 0){
			if(f->image == nil)
				break;
			if(time(0) - f->rdtime >= 30) {
				if(dirmtime(fn) != f->mtime){
					f = nil;
					break;
				}
				f->rdtime = time(0);
			}
			f->ref++;
			return f;
		}
	}

	if((fd = open(fn, OREAD)) < 0)
		return nil;

	if(readn(fd, buf, sizeof buf) != sizeof buf){
		close(fd);
		return nil;
	}

	seek(fd, 0, 0);

	mask = nil;
	if(buf[0] == '0' && buf[1] == 'x'){
		/* greyscale faces are just masks that we draw black through! */
		if(buf[2+8] == ',')	/* ldepth 1 */
			mask = readbit(fd, GREY2);
		else
			mask = readbit(fd, GREY1);
		face = display->black;
	}else{
		face = readimage(display, fd, 0);
		if(face == nil)
			goto Done;
		else if(face->chan == GREY4 || face->chan == GREY8){	/* greyscale: use inversion as mask */
			mask = myallocimage(face->chan);
			/* okay if mask is nil: that will copy the image white background and all */
			if(mask == nil)
				goto Done;

			/* invert greyscale image */
			draw(mask, mask->r, display->white, nil, ZP);
			gendraw(mask, mask->r, display->black, ZP, face, face->r.min);
			freeimage(face);
			face = display->black;
		}else if(face->depth == 8){	/* snarf the bytes back and do a fill. */
			mask = myallocimage(GREY1);
			if(mask == nil)
				goto Done;
			if(unloadimage(face, face->r, data, Facesize*Facesize) != Facesize*Facesize){	
				freeimage(mask);
				goto Done;
			}
			bits = 0;
			p = mdata;
			for(y=0; y<Facesize; y++){
				for(x=0; x<Facesize; x++){	
					bits <<= 1;
					if(data[Facesize*y+x] != 0xFF)
						bits |= 1;
					if((x&7) == 7)
						*p++ = bits&0xFF;
				}
			}
			if(loadimage(mask, mask->r, mdata, sizeof mdata) != sizeof mdata){
				freeimage(mask);
				goto Done;
			}
		}
	}

Done:
	/* always add at beginning of list, so updated files don't collide in cache */
	if(f == nil){
		f = emalloc(sizeof(Facefile));
		f->file = estrdup(fn);
		d = dirfstat(fd);
		if(d != nil){
			f->mtime = d->mtime;
			free(d);
		}
		f->next = facefiles;
		facefiles = f;
	}
	f->ref++;
	f->image = face;
	f->mask = mask;
	f->rdtime = time(0);
	close(fd);
	return f;
}

void
findbit(Face *f)
{
	char *fn;

	fn = findfile(f, f->str[Sdomain], f->str[Suser]);
	if(fn) {
		if(strstr(fn, "unknown"))
			f->unknown = 1;
		f->file = readface(fn);
	}
	if(f->file){
		f->bit = f->file->image;
		f->mask = f->file->mask;
	}else{
		/* if returns nil, this is still ok: draw(nil) works */
		f->bit = allocimage(display, Rect(0,0,1,1), CMAP8, 1, DYellow);
		replclipr(f->bit, 1, Rect(0, 0, Facesize, Facesize));
		f->mask = nil;
	}
}
