#include <u.h>
#include <libc.h>
#include <draw.h>
#include <thread.h>
#include <cursor.h>
#include <mouse.h>
#include <keyboard.h>
#include <frame.h>
#include <fcall.h>
#include "dat.h"
#include "fns.h"

char Eperm[] = "permission denied";
char Eexist[] = "file does not exist";
char Enotdir[] = "not a directory";
char Ebadfcall[] = "bad fcall type";
char Eoffset[] = "illegal offset";
char Enomem[] = "out of memory";

int	messagesize = 8192+IOHDRSZ;	/* good start */

Dirtab dirtab[]=
{
	{ ".",			QTDIR,	Qdir,			0500|DMDIR },
	{ "screen",		QTFILE,	Qscreen,		0400 },
	{ "snarf",		QTFILE,	Qsnarf,		0600 },
	{ "wctl",		QTFILE,	Qwctl,		0600 },
	{ "kbdtap",	QTFILE,	Qtap,	0660 },
	{ "wsys",		QTDIR,	Qwsys,		0500|DMDIR },

	{ "cons",		QTFILE,	Qcons,		0600 },
	{ "cursor",		QTFILE,	Qcursor,		0600 },
	{ "consctl",	QTFILE,	Qconsctl,		0200 },
	{ "winid",		QTFILE,	Qwinid,		0400 },
	{ "winname",	QTFILE,	Qwinname,	0400 },
	{ "label",		QTFILE,	Qlabel,		0600 },
	{ "kbd",	QTFILE,	Qkbd,		0600 },
	{ "mouse",	QTFILE,	Qmouse,		0600 },
	{ "text",		QTFILE,	Qtext,		0600 },
	{ "wdir",		QTFILE,	Qwdir,		0600 },
	{ "window",	QTFILE,	Qwindow,		0400 },
	{ nil, }
};

static uint		getclock(void);
static void		filsysproc(void*);
static Fid*		newfid(Filsys*, int);
static int		dostat(Filsys*, int, Dirtab*, uchar*, int, uint);

int	clockfd;
int	firstmessage = 1;

char	srvpipe[64];
char	srvwctl[64];

static	Xfid*	filsysflush(Filsys*, Xfid*, Fid*);
static	Xfid*	filsysversion(Filsys*, Xfid*, Fid*);
static	Xfid*	filsysauth(Filsys*, Xfid*, Fid*);
static	Xfid*	filsysattach(Filsys*, Xfid*, Fid*);
static	Xfid*	filsyswalk(Filsys*, Xfid*, Fid*);
static	Xfid*	filsysopen(Filsys*, Xfid*, Fid*);
static	Xfid*	filsyscreate(Filsys*, Xfid*, Fid*);
static	Xfid*	filsysread(Filsys*, Xfid*, Fid*);
static	Xfid*	filsyswrite(Filsys*, Xfid*, Fid*);
static	Xfid*	filsysclunk(Filsys*, Xfid*, Fid*);
static	Xfid*	filsysremove(Filsys*, Xfid*, Fid*);
static	Xfid*	filsysstat(Filsys*, Xfid*, Fid*);
static	Xfid*	filsyswstat(Filsys*, Xfid*, Fid*);

Xfid* 	(*fcall[Tmax])(Filsys*, Xfid*, Fid*) =
{
	[Tflush]	= filsysflush,
	[Tversion]	= filsysversion,
	[Tauth]	= filsysauth,
	[Tattach]	= filsysattach,
	[Twalk]	= filsyswalk,
	[Topen]	= filsysopen,
	[Tcreate]	= filsyscreate,
	[Tread]	= filsysread,
	[Twrite]	= filsyswrite,
	[Tclunk]	= filsysclunk,
	[Tremove]= filsysremove,
	[Tstat]	= filsysstat,
	[Twstat]	= filsyswstat,
};

void
post(char *name, char *envname, int srvfd)
{
	int fd;
	char buf[32];

	fd = create(name, OWRITE|ORCLOSE|OCEXEC, 0600);
	if(fd < 0)
		error(name);
	snprint(buf, sizeof(buf), "%d", srvfd);
	if(write(fd, buf, strlen(buf)) != strlen(buf))
		error("srv write");
	putenv(envname, name);
}

/*
 * Build pipe with OCEXEC set on second fd.
 * Can't put it on both because we want to post one in /srv.
 */
int
cexecpipe(int *p0, int *p1)
{
	/* pipe the hard way to get close on exec */
	if(bind("#|", "/mnt/temp", MREPL) == -1)
		return -1;
	*p0 = open("/mnt/temp/data", ORDWR);
	*p1 = open("/mnt/temp/data1", ORDWR|OCEXEC);
	unmount(nil, "/mnt/temp");
	if(*p0<0 || *p1<0)
		return -1;
	return 0;
}

Filsys*
filsysinit(Channel *cxfidalloc)
{
	Filsys *fs;

	fs = emalloc(sizeof(Filsys));
	if(cexecpipe(&fs->cfd, &fs->sfd) < 0)
		goto Rescue;
	fmtinstall('F', fcallfmt);
	clockfd = open("/dev/time", OREAD|OCEXEC);
	fs->user = getuser();
	fs->csyncflush = chancreate(sizeof(int), 0);
	if(fs->csyncflush == nil)
		error("chancreate syncflush");
	fs->cxfidalloc = cxfidalloc;

	proccreate(filsysproc, fs, 10000);
	snprint(srvpipe, sizeof(srvpipe), "/srv/rio.%s.%lud", fs->user, (ulong)getpid());
	post(srvpipe, "wsys", fs->cfd);

	return fs;

Rescue:
	free(fs);
	return nil;
}

static
void
filsysproc(void *arg)
{
	int n;
	Xfid *x;
	Fid *f;
	Fcall t;
	uchar *buf;
	Filsys *fs;

	threadsetname("FILSYSPROC");
	fs = arg;
	fs->pid = getpid();
	x = nil;
	for(;;){
		buf = malloc(messagesize+UTFmax);	/* UTFmax for appending partial rune in xfidwrite */
		if(buf == nil)
			error(Enomem);
		n = read9pmsg(fs->sfd, buf, messagesize);
		if(n <= 0){
			yield();	/* if threadexitsall'ing, will not return */
			fprint(2, "rio: %d: read9pmsg: %d %r\n", getpid(), n);
			errorshouldabort = 0;
			error("eof or i/o error on server channel");
		}
		if(x == nil){
			send(fs->cxfidalloc, nil);
			recv(fs->cxfidalloc, &x);
			x->fs = fs;
		}
		x->buf = buf;
		if(convM2S(buf, n, x) != n)
			error("convert error in convM2S");
		if(debug)
			fprint(2, "rio:<-%F\n", &x->Fcall);
		if(fcall[x->type] == nil)
			x = filsysrespond(fs, x, &t, Ebadfcall);
		else{
			if(x->type==Tversion || x->type==Tauth)
				f = nil;
			else
				f = newfid(fs, x->fid);
			x->f = f;
			x  = (*fcall[x->type])(fs, x, f);
		}
		firstmessage = 0;
	}
}

/*
 * Called only from a different FD group
 */
int
filsysmount(Filsys *fs, int id)
{
	char buf[32];

	close(fs->sfd);	/* close server end so mount won't hang if exiting */
	snprint(buf, sizeof buf, "%d", id);
	if(mount(fs->cfd, -1, "/mnt/wsys", MREPL, buf) == -1){
		fprint(2, "mount failed: %r\n");
		return -1;
	}
	if(bind("/mnt/wsys", "/dev", MBEFORE) == -1){
		fprint(2, "bind failed: %r\n");
		return -1;
	}
	return 0;
}

Xfid*
filsysrespond(Filsys *fs, Xfid *x, Fcall *t, char *err)
{
	int n;

	if(err){
		t->type = Rerror;
		t->ename = err;
	}else
		t->type = x->type+1;
	t->fid = x->fid;
	t->tag = x->tag;
	if(x->buf == nil)
		error("no buffer in respond");
	n = convS2M(t, x->buf, messagesize);
	if(n <= 0)
		error("convert error in convS2M");
	if(write(fs->sfd, x->buf, n) != n)
		error("write error in respond");
	if(debug)
		fprint(2, "rio:->%F\n", t);
	free(x->buf);
	x->buf = nil;
	x->flushtag = -1;
	return x;
}

void
filsyscancel(Xfid *x)
{
	if(x->buf){
		free(x->buf);
		x->buf = nil;
	}
}

static
Xfid*
filsysversion(Filsys *fs, Xfid *x, Fid*)
{
	Fcall t;

	if(!firstmessage)
		return filsysrespond(x->fs, x, &t, "version request not first message");
	if(x->msize < 256)
		return filsysrespond(x->fs, x, &t, "version: message size too small");
	messagesize = x->msize;
	t.msize = messagesize;
	t.version = "9P2000";
	if(strncmp(x->version, "9P", 2) != 0)
		t.version = "unknown";
	return filsysrespond(fs, x, &t, nil);
}

static
Xfid*
filsysauth(Filsys *fs, Xfid *x, Fid*)
{
	Fcall t;

	return filsysrespond(fs, x, &t, "rio: authentication not required");
}

static
Xfid*
filsysflush(Filsys *fs, Xfid *x, Fid*)
{
	sendp(x->c, xfidflush);

	/*
	 * flushes need to be replied in order. xfidflush() will
	 * awaken us when the flush has been queued.
	 */
	recv(fs->csyncflush, nil);

	return nil;
}

static
Xfid*
filsysattach(Filsys *, Xfid *x, Fid *f)
{
	Fcall t;

	if(strcmp(x->uname, x->fs->user) != 0)
		return filsysrespond(x->fs, x, &t, Eperm);
	f->busy = TRUE;
	f->open = FALSE;
	f->qid.path = Qdir;
	f->qid.type = QTDIR;
	f->qid.vers = 0;
	f->dir = dirtab;
	f->nrpart = 0;
	sendp(x->c, xfidattach);
	return nil;
}

static
int
numeric(char *s)
{
	for(; *s!='\0'; s++)
		if(*s<'0' || '9'<*s)
			return 0;
	return 1;
}

static
int
skipdir(char *name)
{
	/* don't serve these if it's provided in the environment */
	if(snarffd>=0 && strcmp(name, "snarf")==0)
		return 1;
	if(gotscreen && strcmp(name, "screen")==0)
		return 1;
	if(!servekbd && strcmp(name, "kbd")==0)
		return 1;
	return 0;
}

static
Xfid*
filsyswalk(Filsys *fs, Xfid *x, Fid *f)
{
	Fcall t;
	Fid *nf;
	int i, id;
	uchar type;
	ulong path;
	Dirtab *d, *dir;
	Window *w;
	char *err;
	Qid qid;

	if(f->open)
		return filsysrespond(fs, x, &t, "walk of open file");
	nf = nil;
	if(x->fid  != x->newfid){
		/* BUG: check exists */
		nf = newfid(fs, x->newfid);
		if(nf->busy)
			return filsysrespond(fs, x, &t, "clone to busy fid");
		nf->busy = TRUE;
		nf->open = FALSE;
		nf->dir = f->dir;
		nf->qid = f->qid;
		nf->w = f->w;
		if(f->w != nil)
			incref(f->w);
		nf->nrpart = 0;	/* not open, so must be zero */
		f = nf;	/* walk f */
	}

	t.nwqid = 0;
	err = nil;

	/* update f->qid, f->dir only if walk completes */
	qid = f->qid;
	dir = f->dir;

	if(x->nwname > 0){
		for(i=0; i<x->nwname; i++){
			if((qid.type & QTDIR) == 0){
				err = Enotdir;
				break;
			}
			if(strcmp(x->wname[i], "..") == 0){
				type = QTDIR;
				path = Qdir;
				dir = dirtab;
				if(FILE(qid) == Qwsysdir)
					path = Qwsys;
				id = 0;
    Accept:
				if(i == MAXWELEM){
					err = "name too long";
					break;
				}
				qid.type = type;
				qid.vers = 0;
				qid.path = QID(id, path);
				t.wqid[t.nwqid++] = qid;
				continue;
			}

			if(qid.path == Qwsys){
				/* is it a numeric name? */
				if(!numeric(x->wname[i]))
					break;
				/* yes: it's a directory */
				id = atoi(x->wname[i]);
				qlock(&all);
				w = wlookid(id);
				if(w == nil){
					qunlock(&all);
					break;
				}
				path = Qwsysdir;
				type = QTDIR;
				qunlock(&all);
				incref(w);
				if(f->w)
					sendp(winclosechan, f->w);
				f->w = w;
				dir = dirtab;
				goto Accept;
			}
			if(skipdir(x->wname[i]))
				break;
			id = WIN(f->qid);
			d = dirtab;
			d++;	/* skip '.' */
			for(; d->name; d++)
				if(strcmp(x->wname[i], d->name) == 0){
					if(f->w == nil && d->qid >= Qglobal)
						break;
					path = d->qid;
					type = d->type;
					dir = d;
					goto Accept;
				}

			break;	/* file not found */
		}

		if(i==0 && err==nil)
			err = Eexist;
	}

	if(err!=nil || t.nwqid<x->nwname){
		if(nf){
			if(nf->w)
				sendp(winclosechan, nf->w);
			nf->open = FALSE;
			nf->busy = FALSE;
		}
	}else if(t.nwqid == x->nwname){
		f->dir = dir;
		f->qid = qid;
	}

	return filsysrespond(fs, x, &t, err);
}

static
Xfid*
filsysopen(Filsys *fs, Xfid *x, Fid *f)
{
	Fcall t;
	int m;

	/* can't truncate anything but Qtext, so just disregard */
	if(FILE(f->qid) != Qtext)
		x->mode &= ~OTRUNC;
	x->mode &= ~OCEXEC;
	/* can't execute or remove anything */
	if(x->mode==OEXEC || (x->mode&ORCLOSE))
		goto Deny;
	switch(x->mode & ~OTRUNC){
	default:
		goto Deny;
	case OREAD:
		m = 0400;
		break;
	case OWRITE:
		m = 0200;
		break;
	case ORDWR:
		m = 0600;
		break;
	}
	if(((f->dir->perm&~(DMDIR|DMAPPEND))&m) != m)
		goto Deny;

	sendp(x->c, xfidopen);
	return nil;

    Deny:
	return filsysrespond(fs, x, &t, Eperm);
}

static
Xfid*
filsyscreate(Filsys *fs, Xfid *x, Fid*)
{
	Fcall t;

	return filsysrespond(fs, x, &t, Eperm);
}

static
int
idcmp(void *a, void *b)
{
	return *(int*)a - *(int*)b;
}

static
Xfid*
filsysread(Filsys *fs, Xfid *x, Fid *f)
{
	Fcall t;
	uchar *b;
	int i, n, o, e, len, j, k, *ids;
	Dirtab *d, dt;
	uint clock;
	char buf[32];

	if((f->qid.type & QTDIR) == 0){
		sendp(x->c, xfidread);
		return nil;
	}
	o = x->offset;
	e = x->offset+x->count;
	clock = getclock();
	b = malloc(messagesize-IOHDRSZ);	/* avoid memset of emalloc */
	if(b == nil)
		return filsysrespond(fs, x, &t, Enomem);
	n = 0;
	switch(FILE(f->qid)){
	case Qdir:
	case Qwsysdir:
		d = dirtab;
		d++;	/* first entry is '.' */
		for(i=0; d->name!=nil && i<e; d++){
			if(skipdir(d->name))
				continue;
			if(f->w == nil && d->qid >= Qglobal)
				continue;
			len = dostat(fs, WIN(x->f->qid), d, b+n, x->count-n, clock);
			if(len <= BIT16SZ)
				break;
			if(i >= o)
				n += len;
			i += len;
		}
		break;
	case Qwsys:
		qlock(&all);
		ids = emalloc(nwindow*sizeof(int));
		for(j=0; j<nwindow; j++)
			ids[j] = window[j]->id;
		qunlock(&all);
		qsort(ids, nwindow, sizeof ids[0], idcmp);
		dt.name = buf;
		for(i=0, j=0; j<nwindow && i<e; i+=len){
			k = ids[j];
			sprint(dt.name, "%d", k);
			dt.qid = QID(k, Qdir);
			dt.type = QTDIR;
			dt.perm = DMDIR|0700;
			len = dostat(fs, k, &dt, b+n, x->count-n, clock);
			if(len == 0)
				break;
			if(i >= o)
				n += len;
			j++;
		}
		free(ids);
		break;
	}
	t.data = (char*)b;
	t.count = n;
	filsysrespond(fs, x, &t, nil);
	free(b);
	return x;
}

static
Xfid*
filsyswrite(Filsys*, Xfid *x, Fid*)
{
	sendp(x->c, xfidwrite);
	return nil;
}

static
Xfid*
filsysclunk(Filsys *fs, Xfid *x, Fid *f)
{
	Fcall t;

	if(f->open){
		f->busy = FALSE;
		f->open = FALSE;
		sendp(x->c, xfidclose);
		return nil;
	}
	if(f->w)
		sendp(winclosechan, f->w);
	f->busy = FALSE;
	f->open = FALSE;
	return filsysrespond(fs, x, &t, nil);
}

static
Xfid*
filsysremove(Filsys *fs, Xfid *x, Fid*)
{
	Fcall t;

	return filsysrespond(fs, x, &t, Eperm);
}

static
Xfid*
filsysstat(Filsys *fs, Xfid *x, Fid *f)
{
	Fcall t;

	t.stat = emalloc(messagesize-IOHDRSZ);
	t.nstat = dostat(fs, WIN(x->f->qid), f->dir, t.stat, messagesize-IOHDRSZ, getclock());
	x = filsysrespond(fs, x, &t, nil);
	free(t.stat);
	return x;
}

static
Xfid*
filsyswstat(Filsys *fs, Xfid *x, Fid*)
{
	Fcall t;

	return filsysrespond(fs, x, &t, Eperm);
}

static
Fid*
newfid(Filsys *fs, int fid)
{
	Fid *f, *ff, **fh;

	ff = nil;
	fh = &fs->fids[fid&(Nhash-1)];
	for(f=*fh; f; f=f->next)
		if(f->fid == fid)
			return f;
		else if(ff==nil && f->busy==FALSE)
			ff = f;
	if(ff){
		ff->fid = fid;
		return ff;
	}
	f = emalloc(sizeof *f);
	f->fid = fid;
	f->next = *fh;
	*fh = f;
	return f;
}

static
uint
getclock(void)
{
	char buf[32];

	seek(clockfd, 0, 0);
	read(clockfd, buf, sizeof buf);
	return atoi(buf);
}

static
int
dostat(Filsys *fs, int id, Dirtab *dir, uchar *buf, int nbuf, uint clock)
{
	Dir d;

	d.qid.path = QID(id, dir->qid);
	if(dir->qid == Qsnarf)
		d.qid.vers = snarfversion;
	else
		d.qid.vers = 0;
	d.qid.type = dir->type;
	d.mode = dir->perm;
	d.length = 0;	/* would be nice to do better */
	d.name = dir->name;
	d.uid = fs->user;
	d.gid = fs->user;
	d.muid = fs->user;
	d.atime = clock;
	d.mtime = clock;
	return convD2M(&d, buf, nbuf);
}
