#include <9pm/u.h>
#include <9pm/libc.h>
#include <9pm/ns.h>
#include <9pm/devip.h>

enum
{
	Qtopdir,
	Qprotodir,
	Qclonus,
	Qconvdir,
	Qdata,
	Qctl,
	Qstatus,
	Qremote,
	Qlocal,
	Qlisten,

	MAXPROTO = 4
};
#define TYPE(x) 	((ulong)(x).path & 0xf)
#define CONV(x) 	(((ulong)(x).path >> 4)&0xfff)
#define PROTO(x) 	(((ulong)(x).path >> 16)&0xff)
#define QID(p, c, y) 	(((p)<<16) | ((c)<<4) | (y))

static	int	np;
static	Proto	proto[MAXPROTO];
static	int	eipconv(va_list*, Fconv*);
static	Conv*	cloneproto(Proto*, char*);
static	void	setladdr(Conv*);
static	ulong parseip(char*, char*);

#define DIRQID(q, p) mkqid(q, p, 0, QTDIR)
#define FILEQID(q, p) mkqid(q, p, 0, QTFILE)

static int
ipgen(Chan *c, char *un0, Dirtab *d, int nd, int s, Dir *dp)
{
	Qid q;
	Conv *cv;
	char name[16], *p;

	USED(un0);
	USED(d);
	USED(nd);

	switch(TYPE(c->qid)) {
	case Qtopdir:
		if(s == DEVDOTDOT){
			DIRQID(&q, QID(0,0,Qtopdir));
			devdir(c, q, "#I", 0, pm_conf.eve, DMDIR|0555, dp);
			return 1;
		}

		if(s >= np)
			return -1;
		DIRQID(&q, QID(s, 0, Qprotodir));
		devdir(c, q, proto[s].name, 0, pm_conf.eve, DMDIR|0555, dp);
		return 1;

	case Qprotodir:
		if(s == DEVDOTDOT){
			DIRQID(&q, QID(0,0,Qtopdir));
			devdir(c, q, "#I", 0, pm_conf.eve, DMDIR|0555, dp);
			return 1;
		}
		if(s < proto[PROTO(c->qid)].nc) {
			cv = proto[PROTO(c->qid)].conv[s];
			sprint(name, "%d", s);
			DIRQID(&q, QID(PROTO(c->qid), s, Qconvdir));
			devdir(c, q, name, 0, cv->owner, DMDIR|0555, dp);
			return 1;
		}
		if(s > proto[PROTO(c->qid)].nc)
			return -1;
		FILEQID(&q, QID(PROTO(c->qid), 0, Qclonus));
		devdir(c, q, "clone", 0, pm_conf.eve, 0555, dp);
		return 1;

	case Qclonus:
		devdir(c, c->qid, "clone", 0, pm_conf.eve, 0555, dp);
		return 1;

	case Qconvdir:
		if(s == DEVDOTDOT){
			DIRQID(&q, QID(PROTO(c->qid), 0, Qprotodir));
			devdir(c, q, proto[PROTO(c->qid)].name, 0, pm_conf.eve, DMDIR|0555, dp);
			return 1;
		}
		cv = proto[PROTO(c->qid)].conv[CONV(c->qid)];
		switch(s) {
		default:
			return -1;
		case 0:
			FILEQID(&q, QID(PROTO(c->qid), CONV(c->qid), Qdata));
			devdir(c, q, "data", 0, cv->owner, cv->perm, dp);
			return 1;
		case 1:
			FILEQID(&q, QID(PROTO(c->qid), CONV(c->qid), Qctl));
			devdir(c, q, "ctl", 0, cv->owner, cv->perm, dp);
			return 1;
		case 2:
			FILEQID(&q, QID(PROTO(c->qid), CONV(c->qid), Qstatus));
			p = "status";
			break;
		case 3:
			FILEQID(&q, QID(PROTO(c->qid), CONV(c->qid), Qremote));
			p = "remote";
			break;
		case 4:
			FILEQID(&q, QID(PROTO(c->qid), CONV(c->qid), Qlocal));
			p = "local";
			break;
		case 5:
			FILEQID(&q, QID(PROTO(c->qid), CONV(c->qid), Qlisten));
			p = "listen";
			break;
		}
		devdir(c, q, p, 0, cv->owner, 0444, dp);
		return 1;

	case Qdata:
		p = "data";
		goto Genperm;

	case Qctl:
		p = "ctl";

	Genperm:
		cv = proto[PROTO(c->qid)].conv[CONV(c->qid)];
		devdir(c, c->qid, p, 0, cv->owner, cv->perm, dp);
		return 1;

	case Qstatus:
		p = "remote";
		goto Gen444;

	case Qremote:
		p = "status";
		goto Gen444;

	case Qlocal:
		p = "local";
		goto Gen444;

	case Qlisten:
		p = "listen";

	Gen444:
		cv = proto[PROTO(c->qid)].conv[CONV(c->qid)];
		devdir(c, c->qid, p, 0, cv->owner, cv->perm, dp);
		return 1;
	}
	return -1;
}

static void
installipproto(Proto *xp)
{
	int l;
	Proto *p;

	if(np >= MAXPROTO) {
		print("no %s: increase MAXPROTO", xp->name);
		return;
	}

	p = &proto[np];
	p->name = xp->name;
	p->read = xp->read;
	p->write = xp->write;
	p->clone = xp->clone;
	p->connect = xp->connect;
	p->announce = xp->announce;
	p->listen = xp->listen;
	p->close = xp->close;
	p->maxconv = xp->maxconv;

	DIRQID(&p->qid, QID(np, 0, Qprotodir));
	p->x = np++;
	l = sizeof(Conv*)*(p->maxconv+1);
	p->conv = smalloc(l);
}

static void
ipreset(void)
{
	fmtinstall('i', eipconv);
	fmtinstall('I', eipconv);
	fmtinstall('E', eipconv);

	ipinit(installipproto);
}

static Chan*
ipattach(char *spec)
{
	Chan *c;

	c = devattach('I', spec);
	DIRQID(&c->qid, QID(0, 0, Qtopdir));
	return c;
}

static Walkqid*
ipwalk(Chan *c, Chan *nc, char **name, int nname)
{
	return devwalk(c, nc, name, nname, 0, 0, ipgen);
}

static int
ipstat(Chan *c, uchar *db, int n)
{
	return devstat(c, db, n, 0, 0, ipgen);
}

static void
closeconv(Conv *cc)
{
	lock(&cc->r.l);
	if(--cc->r.ref > 0){
		unlock(&cc->r.l);
		return;
	}
	unlock(&cc->r.l);
	cc->owner = pm_conf.eve;
	cc->perm = 0666;
	cc->state = "Closed";
	cc->laddr = 0;
	cc->raddr = 0;
	cc->lport = 0;
	cc->rport = 0;
	if(cc->p->close == nil)
		abort();
	cc->p->close(cc);
}

static Chan*
ipopen(Chan *c, int omode)
{
	Proto *p;
	int perm;
	Conv *cv, *lcv;

/* BUG: can't we use devopen here? */
	omode &= 3;
	switch(omode) {
	default:
		error(Eperm);
	case OREAD:
		perm = 4;
		break;
	case OWRITE:
		perm = 2;
		break;
	case ORDWR:
		perm = 6;
		break;
	}

	switch(TYPE(c->qid)) {
	default:
		panic("ipopen");
	case Qtopdir:
	case Qprotodir:
	case Qconvdir:
	case Qstatus:
	case Qremote:
	case Qlocal:
		if(omode != OREAD)
			error(Eperm);
		break;
	case Qclonus:
		p = &proto[PROTO(c->qid)];
		cv = cloneproto(p, pm_conf.eve);
		if(cv == 0)
			error(Enodev);
		if(waserror()){
			closeconv(cv);
			nexterror();
		}
		p->clone(cv);
		poperror();
		c->qid.path = QID(p->x, cv->x, Qctl);
		c->qid.vers = 0;
		break;
	case Qdata:
	case Qctl:
		p = &proto[PROTO(c->qid)];
		lock(&p->l);
		cv = p->conv[CONV(c->qid)];
		lock(&cv->r.l);
		if((perm & (cv->perm>>6)) != perm) {
			if(strcmp(pm_conf.eve, cv->owner) != 0 ||
		 	  (perm & cv->perm) != perm) {
				unlock(&cv->r.l);
				unlock(&p->l);
				error(Eperm);
			}
		}
		cv->r.ref++;
		if(cv->r.ref == 1) {
			cv->owner = pm_conf.eve;
			cv->perm = 0660;
		}
		unlock(&cv->r.l);
		unlock(&p->l);
		break;
	case Qlisten:
		p = &proto[PROTO(c->qid)];
		lcv = p->conv[CONV(c->qid)];
		cv = cloneproto(p, pm_conf.eve);
		if(cv == 0)
			error(Enodev);
		if(waserror()){
			closeconv(cv);
			nexterror();
		}
		p->listen(cv, lcv);
		poperror();
		cv->state = "Established";
		c->qid.path = QID(p->x, cv->x, Qctl);
		break;
	}
	c->mode = openmode(omode);
	c->flag |= COPEN;
	c->offset = 0;
	return c;
}

static void
ipclose(Chan *c)
{
	Proto *x;
	Conv *cc;

	switch(TYPE(c->qid)) {
	case Qdata:
	case Qctl:
		if((c->flag & COPEN) == 0)
			break;
		x = &proto[PROTO(c->qid)];
		cc = x->conv[CONV(c->qid)];
		closeconv(cc);
		break;
	}
}

static long
ipread(Chan *ch, void *a, long n, vlong offset)
{
	Conv *c;
	Proto *x;
	uchar ip[4];
	char buf[128], *p;

	p = a;
	switch(TYPE(ch->qid)) {
	default:
		error(Eperm);
	case Qprotodir:
	case Qtopdir:
	case Qconvdir:
		return devdirread(ch, a, n, 0, 0, ipgen);
	case Qctl:
		sprint(buf, "%lud", CONV(ch->qid));
		return readstr(offset, p, n, buf);
	case Qremote:
		c = proto[PROTO(ch->qid)].conv[CONV(ch->qid)];
		hnputl(ip, c->raddr);
		sprint(buf, "%I!%d\n", ip, c->rport);
		return readstr(offset, p, n, buf);
	case Qlocal:
		c = proto[PROTO(ch->qid)].conv[CONV(ch->qid)];
		hnputl(ip, c->laddr);
		sprint(buf, "%I!%d\n", ip, c->lport);
		return readstr(offset, p, n, buf);
	case Qstatus:
		x = &proto[PROTO(ch->qid)];
		c = x->conv[CONV(ch->qid)];
		sprint(buf, "%s/%d %ld %s \n",
			c->p->name, c->x, c->r.ref, c->state);
		return readstr(offset, p, n, buf);
	case Qdata:
		x = &proto[PROTO(ch->qid)];
		c = x->conv[CONV(ch->qid)];
		return x->read(c, a, n);
	}
}

static void
setladdrport(Conv *c, char *str)
{
	char *p, addr[4];

	p = strchr(str, '!');
	if(p == 0) {
		p = str;
		c->laddr = 0;
	}
	else {
		*p++ = 0;
		parseip(addr, str);
		c->laddr = nhgetl((uchar*)addr);
	}
	if(*p == '*')
		c->lport = 0;
	else
		c->lport = atoi(p);
}

static char*
setraddrport(Conv *c, char *str)
{
	char *p, addr[4];

	p = strchr(str, '!');
	if(p == 0)
		return "malformed address";
	*p++ = 0;
	parseip(addr, str);
	c->raddr = nhgetl((uchar*)addr);
	c->rport = atoi(p);
	p = strchr(p, '!');
	if(p) {
		if(strcmp(p, "!r") == 0)
			c->restricted = 1;
	}
	return 0;
}

long
ipwrite(Chan *ch, void *a, long n, vlong offset)
{
	Conv *c;
	Proto *x;
	int r, nf;
	char *p, *fields[3], buf[128];

	USED(offset);

	switch(TYPE(ch->qid)) {
	default:
		error(Eperm);
	case Qctl:
		x = &proto[PROTO(ch->qid)];
		c = x->conv[CONV(ch->qid)];
		if(n > sizeof(buf)-1)
			n = sizeof(buf)-1;
		memmove(buf, a, n);
		buf[n] = '\0';

		nf = getfields(buf, fields, 3, 1, " ");
		if(strcmp(fields[0], "connect") == 0){
			switch(nf) {
			default:
				error("bad args to connect");
			case 2:
				p = setraddrport(c, fields[1]);
				if(p != 0)
					error(p);
				break;
			case 3:
				p = setraddrport(c, fields[1]);
				if(p != 0)
					error(p);
				c->lport = atoi(fields[2]);
				break;
			}
			x->connect(c);
			c->state = "Established";
			return n;
		}
		if(strcmp(fields[0], "announce") == 0) {
			switch(nf){
			default:
				error("bad args to announce");
			case 2:
				setladdrport(c, fields[1]);
				break;
			}
			x->announce(c);
			c->state = "Announced";
			return n;
		}
		if(strcmp(fields[0], "bind") == 0){
			switch(nf){
			default:
				error("bad args to bind");
			case 2:
				c->lport = atoi(fields[1]);
				break;
			}
//			setlport(c);
			return n;
		}
		error("bad control message");
	case Qdata:
		x = &proto[PROTO(ch->qid)];
		c = x->conv[CONV(ch->qid)];
		r = x->write(c, a, n);
		return r;
	}
	return n;
}

static Conv*
cloneproto(Proto *p, char *user)
{
	Conv *c, **pp, **ep;

	c = 0;
	lock(&p->l);
	if(waserror()) {
		unlock(&p->l);
		nexterror();
	}
	ep = &p->conv[p->maxconv];
	for(pp = p->conv; pp < ep; pp++) {
		c = *pp;
		if(c == 0) {
			c = smalloc(sizeof(Conv));
			if(c == 0)
				error(Enomem);
			lock(&c->r.l);
			c->r.ref = 1;
			c->p = p;
			c->x = pp - p->conv;
			p->nc++;
			*pp = c;
			break;
		}
		lock(&c->r.l);
		if(c->r.ref == 0) {
			c->r.ref++;
			break;
		}
		unlock(&c->r.l);
	}
	if(pp >= ep) {
		unlock(&p->l);
		poperror();
		return 0;
	}

	c->owner = user;
	c->perm = 0660;
	c->state = "Closed";
	c->restricted = 0;
	c->laddr = 0;
	c->raddr = 0;
	c->lport = 0;
	c->rport = 0;

	unlock(&c->r.l);
	unlock(&p->l);
	poperror();
	return c;
}

static int
eipconv(va_list *v, Fconv *f)
{
	static char buf[64];
	static char *efmt = "%.2lux%.2lux%.2lux%.2lux%.2lux%.2lux";
	static char *ifmt = "%d.%d.%d.%d";
	uchar *p, ip[4];

	switch(f->chr) {
	case 'E':		/* Ethernet address */
		p = va_arg(*v, uchar*);
		sprint(buf, efmt, p[0], p[1], p[2], p[3], p[4], p[5]);
		break;
	case 'I':		/* Ip address */
		p = va_arg(*v, uchar*);
		sprint(buf, ifmt, p[0], p[1], p[2], p[3]);
		break;
	case 'i':
		hnputl(ip, va_arg(*v, ulong));
		sprint(buf, ifmt, ip[0], ip[1], ip[2], ip[3]);
		break;
	default:
		strcpy(buf, "(eipconv)");
	}
	strconv(buf, f);
	return 0;
}

void
hnputl(void *p, ulong v)
{
	unsigned char *a;

	a = p;
	a[0] = v>>24;
	a[1] = v>>16;
	a[2] = v>>8;
	a[3] = v;
}

void
hnputs(void *p, ushort v)
{
	unsigned char *a;

	a = p;
	a[0] = v>>8;
	a[1] = v;
}

ulong
nhgetl(void *p)
{
	unsigned char *a;
	a = p;
	return (a[0]<<24)|(a[1]<<16)|(a[2]<<8)|(a[3]<<0);
}

ushort
nhgets(void *p)
{
	unsigned char *a;
	a = p;
	return (a[0]<<8)|(a[1]<<0);
}

#define CLASS(p) ((*(unsigned char*)(p))>>6)

static ulong
parseip(char *to, char *from)
{
	int i;
	char *p;

	p = from;
	memset(to, 0, 4);
	for(i = 0; i < 4 && *p; i++){
		to[i] = strtoul(p, &p, 0);
		if(*p == '.')
			p++;
	}
	switch(CLASS(to)){
	case 0:	/* class A - 1 byte net */
	case 1:
		if(i == 3){
			to[3] = to[2];
			to[2] = to[1];
			to[1] = 0;
		} else if (i == 2){
			to[3] = to[1];
			to[1] = 0;
		}
		break;
	case 2:	/* class B - 2 byte net */
		if(i == 3){
			to[3] = to[2];
			to[2] = 0;
		}
		break;
	}
	return nhgetl(to);
}

Dev devip = {
	'I',
	"ip",

	ipreset,
	ipattach,
	ipwalk,
	ipstat,
	ipopen,
	devcreate,
	ipclose,
	ipread,
	devbread,
	ipwrite,
	devbwrite,
	devremove,
	devwstat,
};

