#include <u.h>
#include <libc.h>
#include <ctype.h>
#include <fcall.h>
#include <thread.h>
#include <9p.h>
#include <bio.h>

enum{
	Nsig = 4,
	Nhdr = Nsig+4+4,
	Ndict = 4+4+8,
	Nname = 8,
	Nbuf = 8192,
	Maxsz = 0x7fffffff - Nhdr
};

enum{
	LTnil,
	LTreg,
	LTmap,
	LTmrk,
	LTend
};
typedef struct Lump Lump;
struct Lump{
	char name[Nname+1];
	u32int ofs;
	uchar *buf;
	ulong nbuf;
	int type;
	File *f;
	Lump *l;
	Lump *lp;
};
Lump l1 = {.l = &l1, .lp = &l1}, *lumps = &l1;

Biobuf *wad;
u32int nlmp;
File *ldir, *fsig, *fwad;
int rdonly, dirty;

Srv fs;

char *mapn[] = {
	"things", "linedefs", "sidedefs", "vertexes", "segs",
	"ssectors", "nodes", "sectors", "reject", "blockmap",
	"behavior"
};

void
strupr(char *s, char *p)
{
	char c;

	do{
		c = *p++;
		*s++ = toupper(c);
	}while(c != 0);
}

void
strlwr(char *s, char *p)
{
	char c;

	do{
		c = *p++;
		*s++ = tolower(c);
	}while(c != 0);
}

void
link(Lump *l, Lump *lp, int len)
{
	l->lp = lp;
	l->l = lp->l;
	lp->l->lp = l;
	lp->l = l;
	nlmp++;
	fwad->length += Ndict + len;
}

void
unlink(Lump *l)
{
	if(l->l == nil)
		return;
	l->lp->l = l->l;
	l->l->lp = l->lp;
	l->l = nil;
	nlmp--;
	fwad->length -= Ndict + (l->f != nil ? l->f->length : 0);
}

void
freelump(Lump *l)
{
	unlink(l);
	free(l->buf);
	free(l);
}

void
readlump(Lump *l, uchar *p, long n)
{
	if(n <= 0)
		return;
	Bseek(wad, l->ofs, 0);
	if(Bread(wad, p, n) != n)
		fprint(2, "readlump: short read: %r\n");
}

void
loadlump(File *f, ulong n)
{
	Lump *l;

	l = f->aux;
	if(f->length > n)
		n = f->length;
	l->buf = emalloc9p(n);
	l->nbuf = n;
	l->ofs = 0;
	readlump(l, l->buf, f->length);
}

Lump *
lastlump(Lump *lp)
{
	File *f, *dir;

	for(dir=lp->f, f=lp->l->f; lp->l!=lumps; lp=lp->l, f=lp->l->f)
		if(f->parent != dir && f->parent->parent != dir)
			break;
	if(lp->type == LTend && lp->f->parent == dir)
		lp = lp->lp;
	return lp;
}

int
nextmaplump(char *s)
{
	char **p;

	for(p=mapn; p<mapn+nelem(mapn); p++)
		if(strcmp(s, *p) == 0)
			return p-mapn;
	return -1;
}

Lump *
sortmap(Lump *lp, Lump *l)
{
	int ip, i;

	i = nextmaplump(l->f->name);
	for(; lp->l != lumps; lp=lp->l){
		ip = nextmaplump(lp->l->f->name);
		if(ip < 0 || ip > i)
			break;
	}
	return lp;
}

int
ismaplump(char *s)
{
	return nextmaplump(s) >= 0;
}

int
ismapname(char *s)
{
	if(strncmp(s, "map", 3) == 0)
		return isdigit(s[3]) && isdigit(s[4]);
	return s[0] == 'e' && isdigit(s[1])
		&& s[2] == 'm' && isdigit(s[3]);
}

int
ismarkname(char *s, char *m)
{
	char *p;

	p = strstr(s, m);
	if(p == nil || p[strlen(m)] != 0)
		return 0;
	if(p - s > 2)
		return 0;
	return 1;
}

int
validname(char *s, File *dir, int *type, int isnew, int isdir)
{
	int n;
	char c, *p;
	Lump *lp;

	*type = LTnil;
	n = strlen(s);
	if(n < 1 || n > sizeof(lp->name)-1){
		werrstr("invalid lump name");
		return 0;
	}
	for(p=s+n-1; c=*p, p-->=s;)
		if(!isprint(c) || isupper(c) || c == '/'){
			werrstr("invalid char %c in filename", c);
			return 0;
		}
	if(isnew && !ismaplump(s))
		for(lp=lumps->l; lp!=lumps; lp=lp->l)
			if(cistrcmp(s, lp->name) == 0){
				werrstr("duplicate non map lump");
				return 0;
			}
	*type = LTreg;
	lp = dir->aux;
	if(ismapname(s)){
		*type = LTmap;
		if(isnew && !isdir){
			werrstr("map marker not a directory");
			return 0;
		}else if(dir != fs.tree->root){
			werrstr("nested map directory");
			return 0;
		}
		return 1;
	}else if(ismarkname(s, "_end")){
		*type = LTend;
		if(dir == fs.tree->root || lp == nil || lp->type == LTmap){
			werrstr("orphaned end marker");
			return 0;
		}
		return 1;
	}else if(ismarkname(s, "_start")){
		*type = LTmrk;
		if(isnew){
			werrstr("not allowed");
			return 0;
		}
		goto mrkchk;
	}else if(isnew && isdir){
		*type = LTmrk;
		if(n > 2){
			werrstr("marker name too long");
			return 0;
		}
mrkchk:
		if(dir->parent != fs.tree->root){
			werrstr("start marker nested too deep");
			return 0;
		}else if(lp != nil && lp->type == LTmap){
			werrstr("start marker within map directory");
			return 0;
		}
		return 1;
	}else if(ismaplump(s) ^ (lp != nil && lp->type == LTmap)){
		werrstr("map lump outside of map directory");
		return 0;
	}
	return 1;
}

int
endldir(Lump *lp, Lump *le)
{
	char *s, name[sizeof lp->name];
	Lump *l;
	File *f;

	l = emalloc9p(sizeof *l);
	strcpy(l->name, lp->name);
	s = strrchr(l->name, '_');
	strcpy(s, "_END");
	strlwr(name, l->name);
	fprint(2, "adding end marker %s\n", l->name);
	if(!validname(name, lp->f, &l->type, 1, 0) || l->type != LTend)
		goto err;
	f = createfile(lp->f, name, nil, lp->f->mode & 0666, l);
	if(f == nil)
		goto err;
	closefile(f);
	l->f = f;
	link(l, le, 0);
	return 0;
err:
	free(l);
	return -1;
}

void
accessfile(File *f, int mode)
{
	f->atime = time(nil);
	if(mode & AWRITE){
		f->mtime = f->atime;
		f->qid.vers++;
		dirty = 1;
	}
}

void
fswstat(Req *r)
{
	int type;
	char *e;
	File *f, *fp;
	Lump *lp;

	e = "permission denied";
	if(rdonly)
		goto err;
	if(r->d.mode != ~0 || r->d.gid[0] != 0)
		goto err;
	f = r->fid->file;
	lp = f->aux;
	if(r->d.length != ~0 && r->d.length != f->length){
		if(f == fsig || f->mode & DMDIR)
			goto err;
		if(!hasperm(f, r->fid->uid, AWRITE))
			goto err;
		if(r->d.length < 0){
			e = "invalid file length";
			goto err;
		}
		if(fwad->length - f->length + r->d.length >= Maxsz){
			e = "lump size exceeds wad limit";
			goto err;
		}
	}
	if(r->d.name[0] != 0 && strcmp(r->d.name, f->name) != 0){
		fp = f->parent;
		if(fp == nil){
			e = "orphaned file";
			goto err;
		}
		if(!hasperm(fp, r->fid->uid, AWRITE))
			goto err;
		if(!validname(r->d.name, fp, &type, 1, f->mode & DMDIR)){
			responderror(r);
			return;
		}
		if(lp->type != type){
			e = "incompatible lump type";
			goto err;
		}
		incref(fp);
		fp = walkfile(fp, r->d.name);
		if(fp != nil){
			e = "file already exists";
			goto err;
		}
	}

	if(r->d.length != ~0 && r->d.length != f->length){
		if(lp->buf == nil)
			loadlump(f, r->d.length);
		fwad->length += r->d.length - f->length;
		f->length = r->d.length;
	}
	if(r->d.name[0] != 0 && strcmp(r->d.name, f->name) != 0){
		free(f->name);
		f->name = estrdup9p(r->d.name);
		strupr(lp->name, f->name);
		if(lp->type == LTmrk)
			strcat(lp->name, "_START");
	}
	accessfile(f, AWRITE);
	if(r->d.mtime != ~0)
		f->mtime = r->d.mtime;
	respond(r, nil);
	return;
err:
	respond(r, e);
}

void
fsremove(Req *r)
{
	File *f;
	Lump *lp;

	f = r->fid->file;
	lp = f->aux;
	if(f == fsig || f == fwad){
		respond(r, "not allowed");
		return;
	}else if(lp->l->f != nil && lp->l->f->parent == f){
		respond(r, "has children");
		return;
	}
	unlink(f->aux);
	dirty = 1;
	respond(r, nil);
}

char *
writesig(uchar *buf, char *s, vlong n)
{
	if(n > Nsig+1 || strncmp(s, "IWAD", Nsig) != 0 && strncmp(s, "PWAD", Nsig) != 0)
		return "invalid wad signature";
	memcpy(buf, s, Nsig);
	dirty = 1;
	return nil;
}

void
fswrite(Req *r)
{
	vlong n, m, ofs, end;
	File *f;
	Lump *l;

	f = r->fid->file;
	n = r->ifcall.count;
	ofs = r->ifcall.offset;
	if(f->mode & DMAPPEND)
		ofs = f->length;
	end = ofs + n;
	l = f->aux;
	if(f == fsig){
		respond(r, writesig(l->buf, r->ifcall.data, n));
		return;
	}
	if(l->buf == nil)
		loadlump(f, end + Nbuf);
	if(end > l->nbuf){
		m = l->nbuf + Nbuf > end ? l->nbuf + Nbuf : end;
		if(fwad->length - l->nbuf + m >= Maxsz){
			respond(r, "lump size exceeds wad limit");
			return;
		}
		l->buf = erealloc9p(l->buf, m);
		l->nbuf = m;
	}
	memcpy(l->buf + ofs, r->ifcall.data, n);
	m = end - f->length;
	if(m > 0){
		f->length += m;
		fwad->length += m;
	}
	accessfile(f, AWRITE);
	r->ofcall.count = n;
	respond(r, nil);
}

void
makewad(void)
{
	vlong n;
	uchar *p;
	u32int ofs;
	Lump *l, *lp;

	l = fwad->aux;
	free(l->buf);
	l->buf = emalloc9p(fwad->length);
	p = l->buf;
	lp = fsig->aux;
	memcpy(p, lp->buf, 4), p += 4;
	PBIT32(p, nlmp);
	p += 8;
	for(lp=lumps->l; lp!=lumps; p+=n, lp=lp->l){
		n = lp->f->length;
		if(lp->buf != nil)
			memcpy(p, lp->buf, n);
		else
			readlump(lp, p, n);
	}
	PBIT32(l->buf + 8, p - l->buf);
	ofs = Nhdr;
	for(lp=lumps->l; lp!=lumps; ofs+=n, lp=lp->l){
		n = lp->f->length;
		PBIT32(p, ofs);
		p += 4;
		PBIT32(p, n);
		p += 4;
		memcpy(p, lp->name, 8), p += 8;
	}
	dirty = 0;
}

void
fsread(Req *r)
{
	vlong n, ofs, end;
	File *f;
	Lump *l;

	f = r->fid->file;
	l = f->aux;
	ofs = r->ifcall.offset + l->ofs;
	end = l->ofs + f->length;
	n = r->ifcall.count;
	if(ofs + n >= end)
		n = end - ofs;
	if(n <= 0){
		r->ofcall.count = 0;
		respond(r, nil);
		return;
	}
	if(f == fwad && dirty)
		makewad();
	if(l->buf != nil)
		memcpy(r->ofcall.data, l->buf+ofs, n);
	else{
		Bseek(wad, ofs, 0);
		n = Bread(wad, r->ofcall.data, n);
		if(n < 0){
			responderror(r);
			return;
		}
	}
	accessfile(f, AREAD);
	r->ofcall.count = n;
	respond(r, nil);
}

int
addlump(Lump *l, File *dir)
{
	Lump *lp;

	lp = lumps->lp;
	if(dir != fs.tree->root){
		lp = dir->aux;
		lp = lp->type == LTmap ? sortmap(lp, l) : lastlump(lp);
	}
	if(l->type == LTend && lp->l->type == LTend && lp->l->f->parent == dir){
		werrstr("an end marker already exists");
		return -1;
	}
	link(l, lp, 0);
	if(l->type == LTmrk){
		strcat(l->name, "_START");
		if(endldir(l, l) < 0)
			return -1;
	}else if(l->type == LTreg){
		l->buf = emalloc9p(Nbuf);
		l->nbuf = Nbuf;
	}
	dirty = 1;
	return 0;
}

Lump *
createlump(char *s, File *dir, int ismark)
{
	int type;
	Lump *l;

	if(!validname(s, dir, &type, 1, ismark))
		return nil;
	l = emalloc9p(sizeof *l);
	l->type = type;
	strupr(l->name, s);
	return l;
}

void
fscreate(Req *r)
{
	int p;
	File *f;
	Lump *l;

	f = r->fid->file;
	p = r->ifcall.perm;
	if(p & DMDIR)
		p = p & ~0777 | p & f->mode & 0777;
	else
		p = p & ~0666 | p & f->mode & 0666;
	l = createlump(r->ifcall.name, f, p & DMDIR);
	if(l == nil)
		goto err;
	f = createfile(f, r->ifcall.name, r->fid->uid, p, l);
	if(f == nil){
		free(l);
		goto err;
	}
	l->f = f;
	if(addlump(l, r->fid->file) < 0){
		removefile(f);
		goto err;
	}
	r->fid->file = f;
	r->ofcall.qid = f->qid;
	respond(r, nil);
	return;
err:
	responderror(r);
}

void
fsopen(Req *r)
{
	File *f;

	f = r->fid->file;
	if((f->mode & DMAPPEND) == 0 && (r->ifcall.mode & OTRUNC) != 0
	&& f != fsig){
		fwad->length -= f->length;
		f->length = 0;
		dirty = 1;
	}
	respond(r, nil);
}

void
fsdestroyfile(File *f)
{
	freelump(f->aux);
}

Srv fs = {
	.open = fsopen,
	.create = fscreate,
	.read = fsread,
	.write = fswrite,
	.remove = fsremove,
	.wstat = fswstat
};

int
get32(Biobuf *bf, u32int *v)
{
	int n;
	uchar u[4];

	n = Bread(bf, u, sizeof u);
	if(n != sizeof u)
		return -1;
	*v = GBIT32(u);
	return 0;
}

File *
replacefile(File *dir, char *fname, int mode, Lump *l)
{
	File *f;

	incref(dir);
	f = walkfile(dir, fname);
	if(f == nil)
		return nil;
	if(removefile(f) < 0)
		return nil;
	f = createfile(dir, fname, nil, mode, l);
	return f;
}

void
addsigfile(char *sig)
{
	int n;
	Lump *l;
	File *f;

	n = strlen(sig) + 1;
	l = emalloc9p(sizeof *l);
	l->buf = (uchar *)estrdup9p(sig);
	l->buf[n-1] = '\n';
	f = createfile(fs.tree->root, "SIG", nil, rdonly ? 0444 : 0666, l);
	if(f == nil)
		sysfatal("addsigfile: %r");
	else{
		fsig = f;
		f->length = n;
	}
}

void
addwadfile(void)
{
	Lump *l;
	File *f;

	l = emalloc9p(sizeof *l);
	f = createfile(fs.tree->root, "WAD", nil, 0444, l);
	if(f == nil)
		sysfatal("addwadfile: %r");
	else{
		fwad = f;
		f->length = Nhdr;
	}
	dirty++;

}

void
checkends(void)
{
	Lump *lp;

	if(ldir == fs.tree->root)
		return;
	lp = ldir->aux;
	if(lp->type != LTmap && endldir(lp, lastlump(lp)) < 0)
		fprint(2, "checkends: %r\n");
	ldir = ldir->parent;
	checkends();
}

int
addfile(Lump *l, u32int *len, int mode)
{
	int err;
	char fname[sizeof l->name], *s;
	Lump *lp;
	File *f;

	*len = 0;
	if(get32(wad, &l->ofs) < 0 || get32(wad, len) < 0)
		return -1;
	if(Bread(wad, l->name, sizeof(l->name)-1) != sizeof(l->name)-1)
		return -1;
	strlwr(fname, l->name);

	lp = ldir->aux;
	err = !validname(fname, ldir, &l->type, 0, 0);
	switch(l->type){
	case LTmap:
		closefile(ldir);
		ldir = fs.tree->root;
		if(err && lp != nil && lp->type != LTmap){
			fprint(2, "addfile %s ofs=%#ux len=%#ux: %r\n", l->name, l->ofs, *len);
			if(endldir(lp, lastlump(lp)) < 0)
				fprint(2, "endldir: %r\n");
		}
		mode |= DMDIR|0111;
		*len = 0;
		break;
	case LTmrk:
		if(err){
			if(lp != nil && lp->type == LTmap){
				closefile(ldir);
				ldir = fs.tree->root;
			}else{
				fprint(2, "addfile %s ofs=%#ux len=%#ux: %r\n", l->name, l->ofs, *len);
				if(endldir(lp, lastlump(lp)) < 0)
					return -1;
				ldir = ldir->parent;
			}
		}
		s = strrchr(fname, '_');
		*s = 0;
		mode |= DMDIR|0111;
		*len = 0;
		break;
	case LTend:
		if(err){
			ldir = ldir->parent;
			return -1;
		}
		*len = 0;
		break;
	case LTreg:
		if(err){
			if(ismaplump(fname))
				fprint(2, "addfile %s ofs=%#ux len=%#ux: %r\n", l->name, l->ofs, *len);
			else
				ldir = fs.tree->root;
		}
		break;
	default:
		return -1;
	}

	f = createfile(ldir, fname, nil, mode, l);
	if(f == nil){
		fprint(2, "createfile %s: %r\n", l->name);
		if(mode & DMDIR)
			return -1;
		f = replacefile(ldir, fname, mode, l);
		if(f == nil)
			return -1;
	}
	if(mode & DMDIR)
		ldir = f;
	else if(l->type == LTend)
		ldir = ldir->parent;
	else
		closefile(f);
	f->length = *len;
	l->f = f;
	return 0;
}

void
parsewad(void)
{
	int n, ne, mode;
	u32int len;
	Lump *l;

	mode = rdonly ? 0444 : 0666;
	ldir = fs.tree->root;
	for(n=0, ne=nlmp, nlmp=0; n<ne; n++){
		l = emalloc9p(sizeof *l);
		if(addfile(l, &len, mode) < 0){
			fprint(2, "addfile %s ofs=%#ux len=%#ux: %r\n", l->name, l->ofs, len);
			free(l);
			continue;
		}
		link(l, lumps->lp, len);
	}
	checkends();
}

void
wadinfo(char *sig)
{
	int n;
	u32int dictofs;

	n = Bread(wad, sig, Nsig);
	if(n != Nsig)
		sysfatal("readwad: short read: %r");
	sig[4] = 0;
	if(strcmp(sig, "IWAD") != 0 && strcmp(sig, "PWAD") != 0)
		sysfatal("invalid wad signature");
	if(get32(wad, &nlmp) < 0 || get32(wad, &dictofs) < 0)
		sysfatal("wadinfo: %r");
	Bseek(wad, dictofs, 0);
}

void
usage(void)
{
	fprint(2, "usage: %s [-Dr] [-m mtpt] [-S srvname] [wad]\n", argv0);
	exits("usage");
}

void
main(int argc, char **argv)
{
	int fl, p;
	char *mtpt, *srvname, sig[Nsig+1] = "PWAD";

	mtpt = "/mnt/wad";
	srvname = nil;
	fl = MREPL|MCREATE;
	p = DMDIR|0777;
	ARGBEGIN{
	case 'D': chatty9p++; break;
	case 'S': srvname = EARGF(usage()); break;
	case 'm': mtpt = EARGF(usage()); break;
	case 'r': rdonly++; p &= ~0222; fl &= ~MCREATE; break;
	default: usage();
	}ARGEND
	if(*argv != nil){
		wad = Bopen(*argv, OREAD);
		if(wad == nil)
			sysfatal("Bopen: %r");
		wadinfo(sig);
	}
	fs.tree = alloctree(nil, nil, p, fsdestroyfile);
	addsigfile(sig);
	addwadfile();
	parsewad();
	postmountsrv(&fs, srvname, mtpt, fl);
	exits(nil);
}
