#include <u.h>
#include <libc.h>
#include <bio.h>
#include <libsec.h>
#include <ctype.h>
#include "iso9660.h"

static void
md5cd(Cdimg *cd, ulong block, ulong length, uchar *digest)
{
	int n;
	uchar buf[Blocksize];
	DigestState *s;

	s = md5(nil, 0, nil, nil);
	while(length > 0) {
		n = length;
		if(n > Blocksize)
			n = Blocksize;

		Creadblock(cd, buf, block, n);

		md5(buf, n, nil, s);

		block++;
		length -= n;
	}
	md5(nil, 0, digest, s);
}

static Dumpdir*
mkdumpdir(char *name, uchar *md5, ulong block, ulong length)
{
	Dumpdir *d;

	assert(block != 0);

	d = emalloc(sizeof *d);
	d->name = name;
	memmove(d->md5, md5, sizeof d->md5);
	d->block = block;
	d->length = length;

	return d;
}

static Dumpdir**
ltreewalkmd5(Dumpdir **l, uchar *md5)
{
	int i;

	while(*l) {
		i = memcmp(md5, (*l)->md5, MD5dlen);
		if(i < 0)
			l = &(*l)->md5left;
		else if(i == 0)
			return l;
		else
			l = &(*l)->md5right;
	}
	return l;
}

static Dumpdir**
ltreewalkblock(Dumpdir **l, ulong block)
{
	while(*l) {
		if(block < (*l)->block)
			l = &(*l)->blockleft;
		else if(block == (*l)->block)
			return l;
		else
			l = &(*l)->blockright;
	}
	return l;
}

/*
 * Add a particular file to our binary tree.
 */
static void
addfile(Cdimg *cd, Dump *d, char *name, Direc *dir)
{
	uchar md5[MD5dlen];
	Dumpdir **lblock;

	assert((dir->mode & DMDIR) == 0);

	if(dir->length == 0)
		return;

	lblock = ltreewalkblock(&d->blockroot, dir->block);
	if(*lblock != nil) {
		if((*lblock)->length == dir->length)
			return;
		fprint(2, "block %lud length %lud %s %lud %s\n", dir->block, (*lblock)->length, (*lblock)->name,
			dir->length, dir->name);
		assert(0);
	}

	md5cd(cd, dir->block, dir->length, md5);
	if(chatty > 1)
		fprint(2, "note file %.16H %lud (%s)\n", md5, dir->length, dir->name);
	insertmd5(d, name, md5, dir->block, dir->length);
}

void
insertmd5(Dump *d, char *name, uchar *md5, ulong block, ulong length)
{
	Dumpdir **lmd5;
	Dumpdir **lblock;

	lblock = ltreewalkblock(&d->blockroot, block);
	if(*lblock != nil) {
		if((*lblock)->length == length)
			return;
		fprint(2, "block %lud length %lud %lud\n", block, (*lblock)->length, length);
		assert(0);
	}

	assert(length != 0);
	*lblock = mkdumpdir(name, md5, block, length);

	lmd5 = ltreewalkmd5(&d->md5root, md5);
	if(*lmd5 != nil)
		fprint(2, "warning: data duplicated on CD\n");
	else
		*lmd5 = *lblock;
}

/*
 * Fill all the children entries for a particular directory;
 * all we care about is block, length, and whether it is a directory.
 */
void
readkids(Cdimg *cd, Direc *dir, char *(*cvt)(uchar*, int))
{
	char *dot, *dotdot;
	int m, n;
	uchar buf[Blocksize], *ebuf, *p;
	ulong b, nb;
	Cdir *c;
	Direc dx;

	assert(dir->mode & DMDIR);

	dot = atom(".");
	dotdot = atom("..");
	ebuf = buf+Blocksize;
	nb = (dir->length+Blocksize-1) / Blocksize;

	n = 0;
	for(b=0; b<nb; b++) {
		Creadblock(cd, buf, dir->block + b, Blocksize);
		p = buf;
		while(p < ebuf) {
			c = (Cdir*)p;
			if(c->len == 0)
				break;
			if(p+c->len > ebuf)
				break;
			if(parsedir(cd, &dx, p, ebuf-p, cvt) == 0 && dx.name != dot && dx.name != dotdot)
				n++;
			p += c->len;
		}
	}

	m = (n+Ndirblock-1)/Ndirblock * Ndirblock;
	dir->child = emalloc(m*sizeof dir->child[0]);
	dir->nchild = n;

	n = 0;
	for(b=0; b<nb; b++) {
		assert(n <= dir->nchild);
		Creadblock(cd, buf, dir->block + b, Blocksize);
		p = buf;
		while(p < ebuf) {
			c = (Cdir*)p;
			if(c->len == 0)
				break;
			if(p+c->len > ebuf)
				break;
			if(parsedir(cd, &dx, p, ebuf-p, cvt) == 0 && dx.name != dot && dx.name != dotdot) {
				assert(n < dir->nchild);
				dir->child[n++] = dx;
			}
			p += c->len;
		}
	}
}

/*
 * Free the children.  Make sure their children are free too.
 */
void
freekids(Direc *dir)
{
	int i;

	for(i=0; i<dir->nchild; i++)
		assert(dir->child[i].nchild == 0);

	free(dir->child);
	dir->child = nil;
	dir->nchild = 0;
}

/*
 * Add a whole directory and all its children to our binary tree.
 */
static void
adddir(Cdimg *cd, Dump *d, Direc *dir)
{
	int i;

	readkids(cd, dir, isostring);
	for(i=0; i<dir->nchild; i++) {
		if(dir->child[i].mode & DMDIR)
			adddir(cd, d, &dir->child[i]);
		else
			addfile(cd, d, atom(dir->name), &dir->child[i]);
	}
	freekids(dir);
}

Dumpdir*
lookupmd5(Dump *d, uchar *md5)
{
	return *ltreewalkmd5(&d->md5root, md5);
}

void
adddirx(Cdimg *cd, Dump *d, Direc *dir, int lev)
{
	int i;
	Direc dd;

	if(lev == 2){
		dd = *dir;
		adddir(cd, d, &dd);
		return;
	}
	for(i=0; i<dir->nchild; i++)
		adddirx(cd, d, &dir->child[i], lev+1);
}

Dump*
dumpcd(Cdimg *cd, Direc *dir)
{
	Dump *d;

	d = emalloc(sizeof *d);
	d->cd = cd;
	adddirx(cd, d, dir, 0);
	return d;
}

/*
static ulong
minblock(Direc *root, int lev)
{
	int i;
	ulong m, n;

	m = root->block;
	for(i=0; i<root->nchild; i++) {
		n = minblock(&root->child[i], lev-1);
		if(m > n)
			m = n;
	}
	return m;
}
*/

void
copybutname(Direc *d, Direc *s)
{
	Direc x;

	x = *d;
	*d = *s;
	d->name = x.name;
	d->confname = x.confname;
}

Direc*
createdumpdir(Direc *root, XDir *dir, char *utfname)
{
	char *p;
	Direc *d;

	if(utfname[0]=='/')
		sysfatal("bad dump name '%s'", utfname);
	p = strchr(utfname, '/');
	if(p == nil || strchr(p+1, '/'))
		sysfatal("bad dump name '%s'", utfname);
	*p++ = '\0';
	if((d = walkdirec(root, utfname)) == nil)
		d = adddirec(root, utfname, dir);
	if(walkdirec(d, p))
		sysfatal("duplicate dump name '%s/%s'", utfname, p);
	d = adddirec(d, p, dir);
	return d;
}

static void
rmdirec(Direc *d, Direc *kid)
{
	Direc *ekid;

	ekid = d->child+d->nchild;
	assert(d->child <= kid && kid < ekid);
	if(ekid != kid+1)
		memmove(kid, kid+1, (ekid-(kid+1))*sizeof(*kid));
	d->nchild--;
}

void
rmdumpdir(Direc *root, char *utfname)
{
	char *p;
	Direc *d, *dd;

	if(utfname[0]=='/')
		sysfatal("bad dump name '%s'", utfname);
	p = strchr(utfname, '/');
	if(p == nil || strchr(p+1, '/'))
		sysfatal("bad dump name '%s'", utfname);
	*p++ = '\0';
	if((d = walkdirec(root, utfname)) == nil)
		sysfatal("cannot remove %s/%s: %s does not exist", utfname, p, utfname);
	p[-1] = '/';

	if((dd = walkdirec(d, p)) == nil)
		sysfatal("cannot remove %s: does not exist", utfname);

	rmdirec(d, dd);
	if(d->nchild == 0)
		rmdirec(root, d);
}

char*
adddumpdir(Direc *root, ulong now, XDir *dir)
{
	char buf[40], *p;
	int n;
	Direc *dday, *dyear;
	Tm tm;

	tm = *localtime(now);
	
	sprint(buf, "%d", tm.year+1900);
	if((dyear = walkdirec(root, buf)) == nil) {
		dyear = adddirec(root, buf, dir);
		assert(dyear != nil);
	}

	n = 0;
	sprint(buf, "%.2d%.2d", tm.mon+1, tm.mday);
	p = buf+strlen(buf);
	while(walkdirec(dyear, buf))
		sprint(p, "%d", ++n);

	dday = adddirec(dyear, buf, dir);
	assert(dday != nil);

	sprint(buf, "%s/%s", dyear->name, dday->name);
assert(walkdirec(root, buf)==dday);
	return atom(buf);
}

/*
 * The dump directory tree is inferred from a linked list of special blocks.
 * One block is written at the end of each dump.
 * The blocks have the form
 *
 * plan 9 dump cd
 * <dump-name> <dump-time> <next-block> <conform-block> <conform-length> \
 *	<iroot-block> <iroot-length> <jroot-block> <jroot-length>
 *
 * If only the first line is present, this is the end of the chain.
 */
static char magic[] = "plan 9 dump cd\n";
ulong
Cputdumpblock(Cdimg *cd)
{
	ulong x;

	Cwseek(cd, cd->nextblock*Blocksize);
	x = Cwoffset(cd);
	Cwrite(cd, magic, sizeof(magic)-1);
	Cpadblock(cd);
	return x/Blocksize;
}

int
hasdump(Cdimg *cd)
{
	int i;
	char buf[128];

	for(i=16; i<24; i++) {
		Creadblock(cd, buf, i, sizeof buf);
		if(memcmp(buf, magic, sizeof(magic)-1) == 0)
			return i;
	}
	return 0;
}
	
Direc
readdumpdirs(Cdimg *cd, XDir *dir, char *(*cvt)(uchar*, int))
{
	char buf[Blocksize];
	char *p, *q, *f[16];
	int i, nf;
	ulong db, t;
	Direc *nr, root;
	XDir xd;

	mkdirec(&root, dir);
	db = hasdump(cd);
	xd = *dir;
	for(;;){
		if(db == 0)
			sysfatal("error in dump blocks");

		Creadblock(cd, buf, db, sizeof buf);
		if(memcmp(buf, magic, sizeof(magic)-1) != 0)
			break;
		p = buf+sizeof(magic)-1;
		if(p[0] == '\0')
			break;
		if((q = strchr(p, '\n')) != nil)
			*q = '\0';

		nf = tokenize(p, f, nelem(f));
		i = 5;
		if(nf < i || (cvt==jolietstring && nf < i+2))
			sysfatal("error in dump block %lud: nf=%d; p='%s'", db, nf, p);
		nr = createdumpdir(&root, &xd, f[0]);
		t = strtoul(f[1], 0, 0);
		xd.mtime = xd.ctime = xd.atime = t;
		db = strtoul(f[2], 0, 0);
		if(cvt == jolietstring)
			i += 2;
		nr->block = strtoul(f[i], 0, 0);
		nr->length = strtoul(f[i+1], 0, 0);
	}
	cd->nulldump = db;
	return root;
}

extern void addtx(char*, char*);

static int
isalldigit(char *s)
{
	while(*s)
		if(!isdigit((uchar)*s++))
			return 0;
	return 1;
}

void
readdumpconform(Cdimg *cd)
{
	char buf[Blocksize];
	char *p, *q, *f[10];
	ulong cb, nc, m, db;
	int nf;

	db = hasdump(cd);
	assert(map==nil || map->nt == 0);

	for(;;){
		if(db == 0)
			sysfatal("error0 in dump blocks");

		Creadblock(cd, buf, db, sizeof buf);
		if(memcmp(buf, magic, sizeof(magic)-1) != 0)
			break;
		p = buf+sizeof(magic)-1;
		if(p[0] == '\0')
			break;
		if((q = strchr(p, '\n')) != nil)
			*q = '\0';

		nf = tokenize(p, f, nelem(f));
		if(nf < 5)
			sysfatal("error0 in dump block %lud", db);

		db = strtoul(f[2], 0, 0);
		cb = strtoul(f[3], 0, 0);
		nc = strtoul(f[4], 0, 0);

		Crseek(cd, cb*Blocksize);
		m = cb*Blocksize+nc;
		while(Croffset(cd) < m && (p = Crdline(cd, '\n')) != nil){
			p[Clinelen(cd)-1] = '\0';
			if(tokenize(p, f, 2) != 2 || (f[0][0] != 'D' && f[0][0] != 'F')
			|| strlen(f[0]) != 7 || !isalldigit(f[0]+1))
				break;
	
			addtx(atom(f[1]), atom(f[0]));
		}
	}
	if(map)
		cd->nconform = map->nt;
	else
		cd->nconform = 0;
}
