#include <u.h>
#include <libc.h>
#include <bio.h>
#include <auth.h>
#include <fcall.h>
#include <disk.h>

enum {
       LEN     = 8*1024,
       HUNKS   = 128,
};

typedef struct File File;
struct File{
       char    *new;
       char    *elem;
       char    *old;
       char    *uid;
       char    *gid;
       ulong   mode;
};

typedef void Mkfserr(char*, void*);
typedef void Mkfsenum(char*, char*, Dir*, void*);

typedef struct Name Name;
struct Name {
       int n;
       char *s;
};

typedef struct Mkaux Mkaux;
struct Mkaux {
       Mkfserr *warn;
       Mkfsenum *mkenum;
       char *root;
       char *proto;
       jmp_buf jmp;
       Biobuf *b;

       Name oldfile;
       Name fullname;
       int     lineno;
       int     indent;

       void *a;
};

static void domkfs(Mkaux *mkaux, File *me, int level);

static int      copyfile(Mkaux*, File*, Dir*, int);
static void     freefile(File*);
static File*    getfile(Mkaux*, File*);
static char*    getmode(Mkaux*, char*, ulong*);
static char*    getname(Mkaux*, char*, char**);
static char*    getpath(Mkaux*, char*);
static int      mkfile(Mkaux*, File*);
static char*    mkpath(Mkaux*, char*, char*);
static void     mktree(Mkaux*, File*, int);
static void     setnames(Mkaux*, File*);
static void     skipdir(Mkaux*);
static void     warn(Mkaux*, char *, ...);

//static void
//mprint(char *new, char *old, Dir *d, void*)
//{
//      print("%s %s %D\n", new, old, d);
//}

int
rdproto(char *proto, char *root, Mkfsenum *mkenum, Mkfserr *mkerr, void *a)
{
       Mkaux mx, *m;
       File file;
       int rv;

       m = &mx;
       memset(&mx, 0, sizeof mx);
       if(root == nil)
               root = "/";

       m->root = root;
       m->warn = mkerr;
       m->mkenum = mkenum;
       m->a = a;
       m->proto = proto;
       m->lineno = 0;
       m->indent = 0;
       if((m->b = Bopen(proto, OREAD)) == nil) {
               werrstr("open %q: %r", proto);
               return -1;
       }

       memset(&file, 0, sizeof file);
       file.new = "";
       file.old = nil;

       rv = 0;
       if(setjmp(m->jmp) == 0)
               domkfs(m, &file, -1);
       else
               rv = -1;
       free(m->oldfile.s);
       free(m->fullname.s);
       return rv;
}

static void*
emalloc(Mkaux *mkaux, ulong n)
{
       void *v;

       v = malloc(n);
       if(v == nil)
               longjmp(mkaux->jmp, 1); /* memory leak */
       memset(v, 0, n);
       return v;
}

static char*
estrdup(Mkaux *mkaux, char *s)
{
       s = strdup(s);
       if(s == nil)
               longjmp(mkaux->jmp, 1); /* memory leak */
       return s;
}

static void
domkfs(Mkaux *mkaux, File *me, int level)
{
       File *child;
       int rec;

       child = getfile(mkaux, me);
       if(!child)
               return;
       if((child->elem[0] == '+' || child->elem[0] == '*') && child->elem[1] == '\0'){
               rec = child->elem[0] == '+';
               free(child->new);
               child->new = estrdup(mkaux, me->new);
               setnames(mkaux, child);
               mktree(mkaux, child, rec);
               freefile(child);
               child = getfile(mkaux, me);
       }
       while(child && mkaux->indent > level){
               if(mkfile(mkaux, child))
                       domkfs(mkaux, child, mkaux->indent);
               freefile(child);
               child = getfile(mkaux, me);
       }
       if(child){
               freefile(child);
               Bseek(mkaux->b, -Blinelen(mkaux->b), 1);
               mkaux->lineno--;
       }
}

static void
mktree(Mkaux *mkaux, File *me, int rec)
{
       File child;
       Dir *d;
       int i, n, fd;

       fd = open(mkaux->oldfile.s, OREAD);
       if(fd < 0){
               warn(mkaux, "can't open %q: %r", mkaux->oldfile.s);
               return;
       }

       child = *me;
       while((n = dirread(fd, &d)) > 0){
               for(i = 0; i < n; i++){
                       child.new = mkpath(mkaux, me->new, d[i].name);
                       if(me->old)
                               child.old = mkpath(mkaux, me->old, d[i].name);
                       child.elem = d[i].name;
                       setnames(mkaux, &child);
                       if(
                       if(copyfile(mkaux, &child, &d[i], 1) && rec)
                               mktree(mkaux, &child, rec);
                       free(child.new);
                       if(child.old)
                               free(child.old);
               }
       }
       close(fd);
}

static int
mkfile(Mkaux *mkaux, File *f)
{
       Dir *d;

       if((d = dirstat(mkaux->oldfile.s)) == nil){
               warn(mkaux, "can't stat file %q: %r", mkaux->oldfile.s);
               skipdir(mkaux);
               return 0;
       }
       return copyfile(mkaux, f, d, 0);
}

enum {
       SLOP = 30
};

static void
setname(Mkaux *mkaux, Name *name, char *s1, char *s2)
{
       int l;

       l = strlen(s1)+strlen(s2)+1;
       if(name->n < l) {
               free(name->s);
               name->s = emalloc(mkaux, l+SLOP);
               name->n = l+SLOP;
       }
       snprint(name->s, name->n, "%s%s%s", s1, s1[0]==0 || s1[strlen(s1)-1]!='/' ? "/" : "", s2);
}

static int
copyfile(Mkaux *mkaux, File *f, Dir *d, int permonly)
{
       Dir *nd;
       ulong xmode;
       char *p;

       setname(mkaux, &mkaux->fullname, mkaux->root, f->old ? f->old : f->new);
       /*
        * Extra stat here is inefficient but accounts for binds.
        */
       if((nd = dirstat(mkaux->fullname.s)) != nil)
               d = nd;

       d->name = f->elem;
       if(d->type != 'M'){
               d->uid = "sys";
               d->gid = "sys";
               xmode = (d->mode >> 6) & 7;
               d->mode |= xmode | (xmode << 3);
       }
       if(strcmp(f->uid, "-") != 0)
               d->uid = f->uid;
       if(strcmp(f->gid, "-") != 0)
               d->gid = f->gid;
       if(f->mode != ~0){
               if(permonly)
                       d->mode = (d->mode & ~0666) | (f->mode & 0666);
               else if((d->mode&DMDIR) != (f->mode&DMDIR))
                       warn(mkaux, "inconsistent mode for %s", f->new);
               else
                       d->mode = f->mode;
       }

       if(p = strrchr(f->new, '/'))
               d->name = p+1;
       else
               d->name = f->new;

       mkaux->mkenum(f->new, mkaux->fullname.s, d, mkaux->a);
       xmode = d->mode;
       free(nd);
       return (xmode&DMDIR) != 0;
}

static char *
mkpath(Mkaux *mkaux, char *prefix, char *elem)
{
       char *p;
       int n;

       n = strlen(prefix) + strlen(elem) + 2;
       p = emalloc(mkaux, n);
       strcpy(p, prefix);
       strcat(p, "/");
       strcat(p, elem);
       return p;
}

static void
setnames(Mkaux *mkaux, File *f)
{

       if(f->old){
               if(f->old[0] == '/')
                       setname(mkaux, &mkaux->oldfile, f->old, "");
               else
                       setname(mkaux, &mkaux->oldfile, mkaux->root, f->old);
       } else
               setname(mkaux, &mkaux->oldfile, mkaux->root, f->new);
}

static void
freefile(File *f)
{
       if(f->old)
               free(f->old);
       if(f->new)
               free(f->new);
       free(f);
}

/*
* skip all files in the proto that
* could be in the current dir
*/
static void
skipdir(Mkaux *mkaux)
{
       char *p, c;
       int level;

       if(mkaux->indent < 0)
               return;
       level = mkaux->indent;
       for(;;){
               mkaux->indent = 0;
               p = Brdline(mkaux->b, '\n');
               mkaux->lineno++;
               if(!p){
                       mkaux->indent = -1;
                       return;
               }
               while((c = *p++) != '\n')
                       if(c == ' ')
                               mkaux->indent++;
                       else if(c == '\t')
                               mkaux->indent += 8;
                       else
                               break;
               if(mkaux->indent <= level){
                       Bseek(mkaux->b, -Blinelen(mkaux->b), 1);
                       mkaux->lineno--;
                       return;
               }
       }
}

static File*
getfile(Mkaux *mkaux, File *old)
{
       File *f;
       char *elem;
       char *p;
       int c;

       if(mkaux->indent < 0)
               return 0;
loop:
       mkaux->indent = 0;
       p = Brdline(mkaux->b, '\n');
       mkaux->lineno++;
       if(!p){
               mkaux->indent = -1;
               return 0;
       }
       while((c = *p++) != '\n')
               if(c == ' ')
                       mkaux->indent++;
               else if(c == '\t')
                       mkaux->indent += 8;
               else
                       break;
       if(c == '\n' || c == '#')
               goto loop;
       p--;
       f = emalloc(mkaux, sizeof *f);
       p = getname(mkaux, p, &elem);
       if(p == nil)
               return nil;

       f->new = mkpath(mkaux, old->new, elem);
       free(elem);
       f->elem = utfrrune(f->new, L'/') + 1;
       p = getmode(mkaux, p, &f->mode);
       p = getname(mkaux, p, &f->uid); /* LEAK */
       if(p == nil)
               return nil;

       if(!*f->uid)
               strcpy(f->uid, "-");
       p = getname(mkaux, p, &f->gid); /* LEAK */
       if(p == nil)
               return nil;

       if(!*f->gid)
               strcpy(f->gid, "-");
       f->old = getpath(mkaux, p);
       if(f->old && strcmp(f->old, "-") == 0){
               free(f->old);
               f->old = 0;
       }
       setnames(mkaux, f);

       return f;
}

static char*
getpath(Mkaux *mkaux, char *p)
{
       char *q, *new;
       int c, n;

       while((c = *p) == ' ' || c == '\t')
               p++;
       q = p;
       while((c = *q) != '\n' && c != ' ' && c != '\t')
               q++;
       if(q == p)
               return 0;
       n = q - p;
       new = emalloc(mkaux, n + 1);
       memcpy(new, p, n);
       new[n] = 0;
       return new;
}

static char*
getname(Mkaux *mkaux, char *p, char **buf)
{
       char *s, *start;
       int c;

       while((c = *p) == ' ' || c == '\t')
               p++;

       start = p;
       while((c = *p) != '\n' && c != ' ' && c != '\t')
               p++;

       *buf = malloc(p+1-start);
       if(*buf == nil)
               return nil;
       memmove(*buf, start, p-start);

       (*buf)[p-start] = '\0';

       if(**buf == '$'){
               s = getenv(*buf+1);
               if(s == 0){
                       warn(mkaux, "can't read environment variable %s", *buf+1);
                       skipdir(mkaux);
                       free(*buf);
                       return nil;
               }
               free(*buf);
               *buf = s;
       }
       return p;
}

static char*
getmode(Mkaux *mkaux, char *p, ulong *xmode)
{
       char *buf, *s;
       ulong m;

       *xmode = ~0;
       p = getname(mkaux, p, &buf);
       if(p == nil)
               return nil;

       s = buf;
       if(!*s || strcmp(s, "-") == 0)
               return p;
       m = 0;
       if(*s == 'd'){
               m |= DMDIR;
               s++;
       }
       if(*s == 'a'){
               m |= DMAPPEND;
               s++;
       }
       if(*s == 'l'){
               m |= DMEXCL;
               s++;
       }
       if(s[0] < '0' || s[0] > '7'
       || s[1] < '0' || s[1] > '7'
       || s[2] < '0' || s[2] > '7'
       || s[3]){
               warn(mkaux, "bad mode specification %s", buf);
               free(buf);
               return p;
       }
       *xmode = m | strtoul(s, 0, 8);
       free(buf);
       return p;
}

static void
warn(Mkaux *mkaux, char *fmt, ...)
{
       char buf[256];
       va_list va;

       va_start(va, fmt);
       vseprint(buf, buf+sizeof(buf), fmt, va);
       va_end(va);

       if(mkaux->warn)
               mkaux->warn(buf, mkaux->a);
       else
               fprint(2, "warning: %s\n", buf);
}