#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){
               free(dom);
               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(ndir);
                       }
               }
               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){
               if((p = getenv("home")) != nil){
                       homeface = smprint("%s/lib/face", p);
                       free(p);
               }
       }

       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;
       }
       free(fn);
}