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

#include "usb.h"

enum
{
	Qroot,
	Qstore,
	Qobj,
	Qthumb,
};

enum {
	/* flags */
	DataSend			=	0x00010000,
	DataRecv			=	0x00020000,
	OutParam			=	0x00040000,

	/* rpc codes */
	OpenSession		=	0x1002,
	CloseSession		=	0x1003,
	GetStorageIds		=	0x1004,
	GetStorageInfo		=	0x1005,
	GetObjectHandles	=	0x1007,
	GetObjectInfo		=	0x1008,
	GetObject			=	0x1009,
	GetThumb		=	0x100A,
	DeleteObject		=	0x100B,
	GetPartialObject	=	0x101B,

	Maxio			=	0x2000,
};

typedef struct Ptprpc Ptprpc;
typedef struct Node Node;

struct Ptprpc
{
	uchar	length[4];
	uchar	type[2];
	uchar	code[2];
	uchar	transid[4];
	uchar	d[1012];
};

struct Node
{
	Dir		d;

	Node	*parent;
	Node	*next;
	Node	*child;

	int		store;
	int		handle;
	int		format;

	void		*data;
	int		ndata;
};

enum {
	In,
	Out,
	Int,
	Setup,
};

static Dev *usbep[Setup+1];

static int debug = 0;
static ulong time0;
static int sessionId;
static int transId;

static Node **nodes;
static int nnodes;

static Channel *iochan;

char Eperm[] = "permission denied";
char Einterrupt[] = "interrupted";

#define PATH(type, n)		((uvlong)(type)|((uvlong)(n)<<4))
#define TYPE(path)			((int)((path)&0xF))
#define NUM(path)			((int)((path)>>4))

static void
hexdump(char *prefix, uchar *p, int n)
{
	char *s;
	int i;
	int m;

	m = 12;
	s = emalloc9p(1+((n+1)*((m*6)+7)));
	s[0] = '\0';
	for(i=0; i<n; i++){
		int printable;
		char x[8];
		if((i % m)==0){
			sprint(x, "\n%.4x: ", i);
			strcat(s, x);
		}
		printable = (p[i] >= 32 && p[i]<=127);
		sprint(x, "%.2x %c  ", (int)p[i],  printable ? p[i] : '.');
		strcat(s, x);
	}
	fprint(2, "%20-s: %6d bytes %s\n", prefix, n, s);
	free(s);
}

static int
wasinterrupt(void)
{
	char err[ERRMAX];

	rerrstr(err, sizeof(err));
	if(strstr(err, Einterrupt) || strstr(err, "timed out")){
		werrstr(Einterrupt);
		return 1;
	}
	return 0;
}

static char *
ptperrstr(int code)
{
	static char *a[] = {
		"undefined",
		nil ,
		"general error" ,
		"session not open" ,
		 "invalid transaction id" ,
		 "operation not supported" ,
		 "parameter not supported" ,
		 "incomplete transfer" ,
		 "invalid storage id" ,
		 "invalid object handle" ,
		 "device prop not supported" ,
		 "invalid object format code" ,
		 "storage full" ,
		 "object write protected" ,
		 "store read only" ,
		 "access denied" ,
		 "no thumbnail present" ,
		 "self test failed" ,
		 "partial deletion" ,
		 "store not available" ,
		 "specification by format unsupported" ,
		 "no valid object info" ,
		 "invalid code format" ,
		 "unknown vendor code",
		"capture already terminated",
		"device busy",
		"invalid parent object",
		"invalid device prop format",
		"invalid device prop value",
		"invalid parameter",
		"session already opend",
		"transaction canceld",
		"specification of destination unsupported"
	};

	code -= 0x2000;
	if(code < 0)
		return nil;
	if(code >= nelem(a))
		return "invalid error number";
	return a[code];
}

static int
ptpcheckerr(Ptprpc *rpc, int type, int transid, int length)
{
	char *s;

	if(length < 4+2+2+4){
		werrstr("short response: %d < %d", length, 4+2+2+4);
		return 1;
	}
	if(GET4(rpc->length) < length){
		werrstr("unexpected response length 0x%x < 0x%x", GET4(rpc->length), length);
		return 1;
	}
	if(GET4(rpc->transid) != transid){
		werrstr("unexpected transaction id 0x%x != 0x%x", GET4(rpc->transid), transid);
		return 1;
	}
	if(GET2(rpc->type) != type){
		werrstr("unexpected response type 0x%x != 0x%x", GET2(rpc->type), type);
		return 1;
	}
	if(s = ptperrstr(GET2(rpc->code))){
		werrstr("%s", s);
		return -GET2(rpc->code);
	}
	return 0;
}

static int
vptprpc(Ioproc *io, int code, int flags, va_list a)
{
	Ptprpc rpc;
	int np, n, t, i, l;
	uchar *b, *p, *e;

	np = flags & 0xF;
	n = 4+2+2+4+4*np;
	t = transId++;

	PUT4(rpc.length, n);
	PUT2(rpc.type, 1);
	PUT2(rpc.code, code);
	PUT4(rpc.transid, t);

	for(i=0; i<np; i++){
		int x = va_arg(a, int);
		PUT4(rpc.d + i*4, x);
	}
	if(debug)
		hexdump("req>", (uchar*)&rpc, n);
	werrstr("");
	if(iowrite(io, usbep[Out]->dfd, &rpc, n) != n){
		wasinterrupt();
		return -1;
	}

	if(flags & DataSend){
		void *sdata;
		int sdatalen;

		sdata = va_arg(a, void*);
		sdatalen = va_arg(a, int);

		b = (uchar*)sdata;
		p = b;
		e = b + sdatalen;

		l = 4+2+2+4+sdatalen;
		PUT4(rpc.length, l);
		PUT2(rpc.type, 2);

		if((n = sdatalen) > sizeof(rpc.d))
			n = sizeof(rpc.d);
		memmove(rpc.d, p, n);
		p += n;
		n += (4+2+2+4);

		if(debug)
			hexdump("data>", (uchar*)&rpc, n);
		if(iowrite(io, usbep[Out]->dfd, &rpc, n) != n){
			wasinterrupt();
			return -1;
		}
		while(p < e){
			n = e - p;
			if(n > Maxio)
				n = Maxio;
			if((n = iowrite(io, usbep[Out]->dfd, p, n)) < 0){
				wasinterrupt();
				return -1;
			}
			p += n;
		}
	}

	if(flags & DataRecv){
		void **prdata;
		int *prdatalen;

		prdata = va_arg(a, void**);
		prdatalen = va_arg(a, int*);

		*prdata = nil;
		*prdatalen = 0;

		while((n = ioread(io, usbep[In]->dfd, &rpc, usbep[In]->maxpkt)) <= 0){
			if(n < 0){
				wasinterrupt();
				return -1;
			}
		}
		if((l = ptpcheckerr(&rpc, 2, t, n)) < 0)
			return -1;
		if(l && GET2(rpc.type) == 3)
			goto Resp1;

		l = GET4(rpc.length);
		l -= (4+2+2+4);
		n -= (4+2+2+4);
	
		b = emalloc9p(l);
		p = b;
		e = b+l;
		if(n <= l){
			if(debug)
				hexdump("data<", rpc.d, n);
			memmove(p, rpc.d, n);
			p += n;
			while(p < e){
				n = e-p;
				if(n > Maxio)
					n = Maxio;
				while((n = ioread(io, usbep[In]->dfd, p, n)) <= 0){
					if(n < 0){
						wasinterrupt();
						free(b);
						return -1;
					}
				}
				if(debug)
					hexdump("data<", p, n);
				p += n;
			}
			*prdata = b;
			*prdatalen = e-b;
		} else {
			if(debug)
				hexdump("data<", rpc.d, l);
			memmove(p, rpc.d, l);
			*prdata = b;
			*prdatalen = e-b;

			n -= l;
			memmove(&rpc, rpc.d+l, n);
			goto Resp1;
		}
	}

	while((n = ioread(io, usbep[In]->dfd, &rpc, usbep[In]->maxpkt)) <= 0){
		if(n < 0){
			wasinterrupt();
			return -1;
		}
	}
Resp1:
	if(debug)
		hexdump("resp<", (uchar*)&rpc, n);
	if(ptpcheckerr(&rpc, 3, t, n))
		return -1;
	if(flags & OutParam){
		int *pp;

		for(i=0; i<5; i++){
			if((pp = va_arg(a, int*)) == nil)
				break;
			*pp = GET4(rpc.d + i*4);
		}
	}
	return 0;
}

static int
ptprpc(Req *r, int code, int flags, ...)
{
	va_list va;
	Channel *c;
	Ioproc *io;
	Alt a[3];
	char *m;
	int i;

	i = -1;
	c = nil;
	io = nil;
	m = Einterrupt;
	a[0].op = CHANRCV;
	a[0].c = iochan;
	a[0].v = &io;
	if(r){
		c = chancreate(sizeof(char*), 0);
		a[1].op = CHANRCV;
		a[1].c = c;
		a[1].v = &m;
		a[2].op = CHANEND;
		r->aux = c;
		srvrelease(r->srv);
	} else
		a[1].op = CHANEND;
	if(alt(a) == 0){
		va_start(va, flags);
		i = vptprpc(io, code, flags, va);
		va_end(va);
		if(i < 0 && debug)
			fprint(2, "rpc: %r\n");
	} else
		werrstr("%s", m);
	if(r){
		srvacquire(r->srv);
		r->aux = nil;
	}
	if(io)
		sendp(iochan, io);
	if(c)
		chanfree(c);
	return i;
}

static int*
ptparray4(uchar *d, uchar *e)
{
	int *a, i, n;

	if(d + 4 > e)
		return nil;
	n = GET4(d);
	d += 4;
	if(d + n*4 > e)
		return nil;
	a = emalloc9p((1+n) * sizeof(int));
	a[0] = n;
	for(i=0; i<n; i++){
		a[i+1] = GET4(d);
		d += 4;
	}
	return a;
}

static char*
ptpstring2(uchar *d, uchar *e)
{
	int n, i;
	char *s, *p;

	if(d+1 > e)
		return nil;
	n = *d;
	d++;
	if(d + n*2 > e)
		return nil;
	p = s = emalloc9p((n+1)*UTFmax);
	for(i=0; i<n; i++){
		Rune r;

		r = GET2(d);
		d += 2;
		if(r == 0)
			break;
		p += runetochar(p, &r);
	}
	*p = 0;
	return s;
}

static void
cleardir(Dir *d)
{
	free(d->name);
	free(d->uid);
	free(d->gid);
	free(d->muid);
	memset(d, 0, sizeof(*d));
}

static void
copydir(Dir *d, Dir *s)
{
	memmove(d, s, sizeof(*d));
	if(d->name)
		d->name = estrdup9p(d->name);
	if(d->uid)
		d->uid = estrdup9p(d->uid);
	if(d->gid)
		d->gid = estrdup9p(d->gid);
	if(d->muid)
		d->muid = estrdup9p(d->muid);
}

static Node*
cachednode(uvlong path, Node ***pf)
{
	Node *x;
	int i;

	if(pf)
		*pf = nil;
	for(i=0; i<nnodes; i++){
		if((x = nodes[i]) == nil){
			if(pf)
				*pf = &nodes[i];
			continue;
		}
		if(x->d.qid.path == path)
			return x;
	}
	return nil;
}

static Node*
getnode(Req *r, uvlong path)
{
	Node *x, *y, **f;
	uchar *p;
	int np;
	char *s;

	if(x = cachednode(path, &f))
		return x;

	y = nil;
	x = emalloc9p(sizeof(*x));
	memset(x, 0, sizeof(*x));
	x->d.qid.path = path;
	x->d.uid = estrdup9p("ptp");
	x->d.gid = estrdup9p("usb");
	x->d.atime = x->d.mtime = time0;

	p = nil;
	np = 0;
	switch(TYPE(path)){
	case Qroot:
		x->d.qid.type = QTDIR;
		x->d.mode = DMDIR|0777;
		x->d.name = estrdup9p("/");
		goto Addnode;

	case Qstore:
		x->store = NUM(path);
		x->handle = 0xffffffff;
		x->d.qid.type = QTDIR;
		x->d.mode = DMDIR|0777;

		if(ptprpc(r, GetStorageInfo, 1|DataRecv, NUM(path), &p, &np) < 0)
			break;
		if(debug)
			hexdump("storageinfo", p, np);
		if(np < 26){
			werrstr("bad storageinfo");
			break;
		}

		if((x->d.name = ptpstring2(p+26, p+np)) == nil){
			werrstr("bad storageinfo");
			break;
		}

		free(p);
		goto Addnode;

	case Qobj:
	case Qthumb:
		if(ptprpc(r, GetObjectInfo, 1|DataRecv, NUM(path), &p, &np) < 0)
			break;
		if(debug)
			hexdump("objectinfo", p, np);
		if(np < 52){
			werrstr("bad objectinfo");
			break;
		}

		/*
		 * another proc might've come in and done it for us,
		 * so check the cache again.
		 */
		if(y = cachednode(path, &f))
			break;

		if((x->d.name = ptpstring2(p+52, p+np)) == nil){
			werrstr("bad objectinfo");
			break;
		}
		x->handle = NUM(path);
		x->store = GET4(p);
		x->format = GET2(p+4);
		if(x->format == 0x3001){
			x->d.qid.type = QTDIR;
			x->d.mode = DMDIR|0777;
		} else {
			x->d.mode = 0666;
			if(TYPE(path) == Qthumb){
				char *t;

				t = emalloc9p(8 + strlen(x->d.name));
				sprint(t, "thumb_%s", x->d.name);
				free(x->d.name);
				x->d.name = t;

				x->d.length = GET4(p+14);
			} else {
				x->d.length = GET4(p+8);
			}
		}
		if(s = ptpstring2(p+(53+p[52]*2), p+np)){
			if(strlen(s) >= 15){
				Tm t;

				// 0123 45 67 8 9A BC DF
				// 2008 12 26 T 00 21 18
				memset(&t, 0, sizeof(t));

				s[0x10] = 0;
				t.sec = atoi(s+0xD);
				s[0xD] = 0;
				t.min = atoi(s+0xB);
				s[0xB] = 0;
				t.hour = atoi(s+0x9);
				s[0x8] = 0;
				t.mday = atoi(s+0x6);
				s[0x6] = 0;
				t.mon = atoi(s+0x4) - 1;
				s[0x4] = 0;
				t.year = atoi(s) - 1900;

				x->d.atime = x->d.mtime = tm2sec(&t);
			}
			free(s);
		}
		free(p);
	Addnode:
		if(f == nil){
			if(nnodes % 64 == 0)
				nodes = erealloc9p(nodes, sizeof(nodes[0]) * (nnodes + 64));
			f = &nodes[nnodes++];
		}
		return *f = x;
	}

	cleardir(&x->d);
	free(x);
	free(p);
	return y;
}

static void
freenode(Node *nod)
{
	int i;

	/* remove the node from the tree */
	for(i=0; i<nnodes; i++){
		if(nod == nodes[i]){
			nodes[i] = nil;
			break;
		}
	}
	cleardir(&nod->d);
	free(nod->data);
	free(nod);
}

static int
readchilds(Req *r, Node *nod)
{
	int e, i;
	int *a;
	uchar *p;
	int np;
	Node *x, **xx;

	e = 0;
	switch(TYPE(nod->d.qid.path)){
	case Qroot:
		if(ptprpc(r, GetStorageIds, 0|DataRecv, &p, &np) < 0)
			return -1;
		a = ptparray4(p, p+np);
		free(p);
		xx = &nod->child;
		*xx = nil;
		for(i=0; a && i<a[0]; i++){
			if((x = getnode(r, PATH(Qstore, a[i+1]))) == nil){
				e = -1;
				break;
			}
			x->parent = nod;
			*xx = x;
			xx = &x->next;
			*xx = nil;
		}		
		free(a);
		break;

	case Qstore:
	case Qobj:
		if(ptprpc(r, GetObjectHandles, 3|DataRecv, nod->store, 0, nod->handle, &p, &np) < 0)
			return -1;
		a = ptparray4(p, p+np);
		free(p);
		xx = &nod->child;
		*xx = nil;
		for(i=0; a && i<a[0]; i++){
			if((x = getnode(r, PATH(Qobj, a[i+1]))) == nil){
				e = -1;
				break;
			}
			x->parent = nod;
			*xx = x;
			xx = &x->next;
			*xx = nil;

			/* skip thumb when not image format */
			if((x->format & 0xFF00) != 0x3800)
				continue;
			if((x = getnode(r, PATH(Qthumb, a[i+1]))) == nil){
				e = -1;
				break;
			}
			x->parent = nod;
			*xx = x;
			xx = &x->next;
			*xx = nil;
		}
		free(a);
		break;
	}

	return e;
}

static void
fsattach(Req *r)
{
	if(r->ifcall.aname && r->ifcall.aname[0]){
		respond(r, "invalid attach specifier");
		return;
	}
	r->fid->qid.path = PATH(Qroot, 0);
	r->fid->qid.type = QTDIR;
	r->fid->qid.vers = 0;
	r->ofcall.qid = r->fid->qid;
	respond(r, nil);
}

static void
fsstat(Req *r)
{
	Node *nod;

	if((nod = getnode(r, r->fid->qid.path)) == nil){
		responderror(r);
		return;
	}
	copydir(&r->d, &nod->d);
	respond(r, nil);
}

static int
nodegen(int i, Dir *d, void *aux)
{
	Node *nod = aux;

	for(nod=nod->child; nod && i; nod=nod->next, i--)
		;
	if(i==0 && nod){
		copydir(d, &nod->d);
		return 0;
	}
	return -1;
}

static char*
fswalk1(Req *r, char *name, Qid *qid)
{
	static char buf[ERRMAX];
	uvlong path;
	Node *nod;
	Fid *fid;

	fid = r->newfid;
	path = fid->qid.path;
	if(!(fid->qid.type&QTDIR))
		return "walk in non-directory";
	if(nod = getnode(r, path)){
		if(strcmp(name, "..") == 0){
			if(nod = nod->parent)
				*qid = nod->d.qid;
			return nil;
		}
		if(readchilds(r, nod) == 0){
			for(nod=nod->child; nod; nod=nod->next){
				if(strcmp(nod->d.name, name) == 0){
					*qid = nod->d.qid;
					return nil;
				}
			}
			return "directory entry not found";
		}
	}
	rerrstr(buf, sizeof(buf));
	return buf;
}

static char*
oldwalk1(Fid *fid, char *name, void *arg)
{
	Qid qid;
	char *e;
	Req *r;

	r = arg;
	assert(fid == r->newfid);
	if(e = fswalk1(r, name, &qid))
		return e;
	fid->qid = qid;
	return nil;
}

static char*
oldclone(Fid *, Fid *, void *)
{
	return nil;
}

static void
fswalk(Req *r)
{
	walkandclone(r, oldwalk1, oldclone, r);
}

static void
fsread(Req *r)
{
	uvlong path;
	Node *nod;
	uchar *p;
	int np;

	np = 0;
	p = nil;
	path = r->fid->qid.path;
	if(nod = getnode(r, path)){
		switch(TYPE(path)){
		case Qroot:
		case Qstore:
		case Qobj:
			if(nod->d.qid.type & QTDIR){
				if(readchilds(r, nod) < 0)
					break;
				dirread9p(r, nodegen, nod);
				respond(r, nil);
				return;
			}
			if(nod->data == nil){
				int offset, count, pos;

				offset = r->ifcall.offset;
				if(offset >= nod->d.length){
					r->ofcall.count = 0;
					respond(r, nil);
					return;
				}
				/* are these people stupid? */
				pos = (offset == 0) ? 1 : 2;
				count = r->ifcall.count;
				if((count + offset) > nod->d.length){
					count = nod->d.length - offset;
					pos = 3;
				}
				if(!ptprpc(r, GetPartialObject, 4|DataRecv, 
					nod->handle, offset, count, pos, &p, &np)){
					if(np <= count){
						memmove(r->ofcall.data, p, np);
						r->ofcall.count = np;
						respond(r, nil);
						free(p);
						return;
					}
				}
				free(p);
			}
			/* no break */
		case Qthumb:
			if(nod->data == nil){
				np = 0;
				p = nil;
				if(ptprpc(r, TYPE(path)==Qthumb ? GetThumb : GetObject,
					1|DataRecv, nod->handle, &p, &np) < 0){
					free(p);
					break;
				}
				nod->data = p;
				nod->ndata = np;
			}
			readbuf(r, nod->data, nod->ndata);
			respond(r, nil);
			return;
		}
	}
	responderror(r);
}

static void
fsremove(Req *r)
{
	Node *nod;
	uvlong path;

	path = r->fid->qid.path;
	if(nod = getnode(r, path)){
		switch(TYPE(path)){
		default:
			werrstr(Eperm);
			break;
		case Qobj:
			if(ptprpc(r, DeleteObject, 2, nod->handle, 0) < 0)
				break;
			/* no break */
		case Qthumb:
			if(nod = cachednode(path, nil))
				freenode(nod);
			respond(r, nil);
			return;
		}
	}
	responderror(r);
}

static void
fsopen(Req *r)
{
	if(r->ifcall.mode != OREAD){
		respond(r, Eperm);
		return;
	}
	respond(r, nil);
}

static void
fsflush(Req *r)
{
	Channel *c;

	if(c = r->oldreq->aux)
		nbsendp(c, Einterrupt);
	respond(r, nil);
}

static void
fsdestroyfid(Fid *fid)
{
	Node *nod;
	uvlong path;

	path = fid->qid.path;
	switch(TYPE(path)){
	case Qobj:
	case Qthumb:
		if(nod = cachednode(path, nil)){
			free(nod->data);
			nod->data = nil;
			nod->ndata = 0;
		}
		break;
	}
}

static void
fsend(Srv *)
{
	ptprpc(nil, CloseSession, 0);
	closeioproc(recvp(iochan));
}

static int
findendpoints(Dev *d, Ep **epin, Ep **epout, Ep **epint)
{
	int i;
	Ep *ep;
	Usbdev *ud;

	ud = d->usb;
	*epin = *epout = *epint = nil;
	for(i=0; i<nelem(ud->ep); i++){
		if((ep = ud->ep[i]) == nil)
			continue;
		if(ep->type == Eintr && *epint == nil)
			*epint = ep;
		if(ep->type != Ebulk)
			continue;
		if(ep->dir == Eboth || ep->dir == Ein)
			if(*epin == nil)
				*epin =  ep;
		if(ep->dir == Eboth || ep->dir == Eout)
			if(*epout == nil)
				*epout = ep;
	}
	if(*epin != nil && *epout != nil)
		return 0;
	return -1;
}

Srv fs = 
{
	.attach = fsattach,
	.destroyfid = fsdestroyfid,
	.walk = fswalk,
	.open = fsopen,
	.read = fsread,
	.remove = fsremove,
	.stat = fsstat,
	.flush = fsflush,
	.end = fsend,
};

static void
usage(void)
{
	fprint(2, "usage: %s [-dD] devid\n", argv0);
	threadexits("usage");
}

void
threadmain(int argc, char **argv)
{
	char name[64], desc[64];
	Ep *epin, *epout, *epint;
	Dev *d;

	ARGBEGIN {
	case 'd':
		debug++;
		break;
	case 'D':
		chatty9p++;
		break;
	default:
		usage();
	} ARGEND;

	if(argc == 0)
		usage();
	if((d = getdev(*argv)) == nil)
		sysfatal("opendev: %r");
	if(findendpoints(d, &epin, &epout, &epint)  < 0)
		sysfatal("findendpoints: %r");

	usbep[In] = openep(usbep[Setup] = d, epin);
	if(epin == epout){
		incref(usbep[In]);
		usbep[Out] = usbep[In];
		opendevdata(usbep[In], ORDWR);
	} else {
		usbep[Out] = openep(d, epout);
		opendevdata(usbep[In], OREAD);
		opendevdata(usbep[Out], OWRITE);
	}
	if(usbep[In]->dfd < 0 || usbep[Out]->dfd < 0)
		sysfatal("open endpoints: %r");
	if(usbep[In]->maxpkt < 12 || usbep[In]->maxpkt > sizeof(Ptprpc))
		sysfatal("bad packet size: %d", usbep[In]->maxpkt);
	iochan = chancreate(sizeof(Ioproc*), 1);
	sendp(iochan, ioproc());

	sessionId = getpid();
	transId   = 0;
	if(ptprpc(nil, OpenSession, 1, sessionId) < 0)
		sysfatal("open session: %r");

	time0 = time(0);

	snprint(name, sizeof name, "sdU%s", d->hname);
	snprint(desc, sizeof desc, "%d.ptp", d->id);
	threadpostsharesrv(&fs, nil, name, desc);

	threadexits(0);
}
