/*
 * prep - prepare plan9 disk partition
 */
#include <u.h>
#include <libc.h>
#include <bio.h>
#include <disk.h>
#include "edit.h"

enum {
	Maxpath = 128,
};

static int	blank;
static int	file;
static int	doautox;
static int	printflag;
static Part	**opart;
static int	nopart;
static char	*osecbuf;
static char	*secbuf;
static int	rdonly;
static int	dowrite;
static int	docache;
static int	donvram;

static void	autoxpart(Edit*);
static vlong	memsize(void);
static Part	*mkpart(char*, vlong, vlong, int);
static void	rdpart(Edit*);
static void	wrpart(Edit*);
static void	checkfat(Disk*);

static void 	cmdsum(Edit*, Part*, vlong, vlong);
static char 	*cmdadd(Edit*, char*, vlong, vlong);
static char 	*cmddel(Edit*, Part*);
static char 	*cmdokname(Edit*, char*);
static char 	*cmdwrite(Edit*);
static char	*cmdctlprint(Edit*, int, char**);

Edit edit = {
	.add=	cmdadd,
	.del=	cmddel,
	.okname=cmdokname,
	.sum=	cmdsum,
	.write=	cmdwrite,

	.unit=	"sector",
};

typedef struct Auto Auto;
struct Auto
{
	char *name;
	uvlong	min;
	uvlong	max;
	uint weight;
	uchar	alloc;
	uvlong	size;
};

#define GB (1024*1024*1024)
#define MB (1024*1024)
#define KB (1024)

/*
 * Order matters -- this is the layout order on disk.
 */
Auto autox[] = 
{
	{	"9fat",		10*MB,	100*MB,	10,	},
	{	"nvram",	512,	512,	1,	},
	{	"fscfg",	512,	512,	1,	},
	{	"fs",		200*MB,	0,	10,	},
	{	"fossil",	200*MB,	0,	4,	},
	{	"arenas",	500*MB,	0,	20,	},
	{	"isect",	25*MB,	0,	1,	},
	{	"other",	200*MB,	0,	4,	},
	{	"swap",		100*MB,	512*MB,	1,	},
	{	"cache",	50*MB,	1*GB,	2,	},
};

void
usage(void)
{
	fprint(2, "usage: disk/prep [bcfprw] [-a partname]... [-s sectorsize] /dev/sdC0/plan9\n");
	exits("usage");
}

void
main(int argc, char **argv)
{
	int i;
	char *p;
	Disk *disk;
	vlong secsize;

	secsize = 0;
	ARGBEGIN{
	case 'a':
		p = EARGF(usage());
		for(i=0; i<nelem(autox); i++){
			if(strcmp(p, autox[i].name) == 0){
				if(autox[i].alloc){
					fprint(2, "you said -a %s more than once.\n", p);
					usage();
				}
				autox[i].alloc = 1;
				break;
			}
		}
		if(i == nelem(autox)){
			fprint(2, "don't know how to create autoxmatic partition %s\n", p);
			usage();
		}
		doautox = 1;
		break;
	case 'b':
		blank++;
		break;
	case 'c':
		docache++;
		break;
	case 'f':
		file++;
		break;
	case 'n':
		donvram++;
		break;
	case 'p':
		printflag++;
		rdonly++;
		break;
	case 'r':
		rdonly++;
		break;
	case 's':
		secsize = atoi(ARGF());
		break;
	case 'w':
		dowrite++;
		break;
	default:
		usage();
	}ARGEND;

	if(argc != 1)
		usage();

	disk = opendisk(argv[0], rdonly, file);
	if(disk == nil) {
		fprint(2, "cannot open disk: %r\n");
		exits("opendisk");
	}

	if(secsize != 0) {
		disk->secsize = secsize;
		disk->secs = disk->size / secsize;
	}
	edit.end = disk->secs;

	checkfat(disk);

	secbuf = emalloc(disk->secsize+1);
	osecbuf = emalloc(disk->secsize+1);
	edit.disk = disk;

	if(blank == 0)
		rdpart(&edit);

	opart = emalloc(edit.npart*sizeof(opart[0]));

	/* save old partition table */
	for(i=0; i<edit.npart; i++)
		opart[i] = edit.part[i];
	nopart = edit.npart;

	if(printflag) {
		runcmd(&edit, "P");
		exits(0);
	}

	if(doautox)
		autoxpart(&edit);

	if(dowrite) {
		runcmd(&edit, "w");
		exits(0);
	}

	runcmd(&edit, "p");
	for(;;) {
		fprint(2, ">>> ");
		runcmd(&edit, getline(&edit));
	}
}

static void
cmdsum(Edit *edit, Part *p, vlong a, vlong b)
{
	vlong sz, div;
	char *suf, *name;
	char c;

	c = p && p->changed ? '\'' : ' ';
	name = p ? p->name : "empty";

	sz = (b-a)*edit->disk->secsize;
	if(sz >= 1*GB){
		suf = "GB";
		div = GB;
	}else if(sz >= 1*MB){
		suf = "MB";
		div = MB;
	}else if(sz >= 1*KB){
		suf = "KB";
		div = KB;
	}else{
		suf = "B ";
		div = 1;
	}

	if(div == 1)
		print("%c %-12s %*lld %-*lld (%lld sectors, %lld %s)\n", c, name,
			edit->disk->width, a, edit->disk->width, b, b-a, sz, suf);
	else
		print("%c %-12s %*lld %-*lld (%lld sectors, %lld.%.2d %s)\n", c, name,
			edit->disk->width, a, edit->disk->width, b, b-a,
			sz/div, (int)(((sz%div)*100)/div), suf);
}

static char*
cmdadd(Edit *edit, char *name, vlong start, vlong end)
{
	if(start < 2 && strcmp(name, "9fat") != 0)
		return "overlaps with the pbs and/or the partition table";

	return addpart(edit, mkpart(name, start, end, 1));
}

static char*
cmddel(Edit *edit, Part *p)
{
	return delpart(edit, p);
}

static char*
cmdwrite(Edit *edit)
{
	wrpart(edit);
	return nil;
}

static char isfrog[256]={
	/*NUL*/	1, 1, 1, 1, 1, 1, 1, 1,
	/*BKS*/	1, 1, 1, 1, 1, 1, 1, 1,
	/*DLE*/	1, 1, 1, 1, 1, 1, 1, 1,
	/*CAN*/	1, 1, 1, 1, 1, 1, 1, 1,
	[' ']	1,
	['/']	1,
	[0x7f]	1,
};

static char*
cmdokname(Edit*, char *elem)
{
	for(; *elem; elem++)
		if(isfrog[*(uchar*)elem])
			return "bad character in name";
	return nil;
}

/*
 *  return memory size in bytes
 */
static vlong
memsize(void)
{
	int fd, n, by2pg;
	char *p;
	char buf[128];
	vlong mem;

	p = getenv("cputype");
	if(p && (strcmp(p, "68020") == 0 || strcmp(p, "alpha") == 0))
		by2pg = 8*1024;
	else
		by2pg = 4*1024;

	mem = 64*1024*1024;
	fd = open("/dev/swap", OREAD);
	if(fd < 0)
		return mem;
	n = read(fd, buf, sizeof(buf)-1);
	close(fd);
	if(n <= 0)
		return mem;
	buf[n] = 0;
	p = strchr(buf, '/');
	if(p)
		mem = strtoul(p+1, 0, 0) * (vlong)by2pg;
	return mem;
}

static Part*
mkpart(char *name, vlong start, vlong end, int changed)
{
	Part *p;

	p = emalloc(sizeof(*p));
	p->name = estrdup(name);
	p->ctlname = estrdup(name);
	p->start = start;
	p->end = end;
	p->changed = changed;
	return p;
}

/* plan9 partition is first sector of the disk */
static void
rdpart(Edit *edit)
{
	int i, nline, nf, waserr;
	char *line[128];
	vlong a, b;
	char *f[5];
	char *err;
	Disk *disk;

	disk = edit->disk;
	seek(disk->fd, disk->secsize, 0);
	if(readn(disk->fd, osecbuf, disk->secsize) != disk->secsize)
		return;
	osecbuf[disk->secsize] = '\0';
	memmove(secbuf, osecbuf, disk->secsize+1);

	if(strncmp(secbuf, "part", 4) != 0){
		fprint(2, "no plan9 partition table found\n");
		return;
	}

	waserr = 0;
	nline = getfields(secbuf, line, nelem(line), 1, "\n");
	for(i=0; i<nline; i++){
		if(strncmp(line[i], "part", 4) != 0) {
		Error:
			if(waserr == 0)
				fprint(2, "syntax error reading partition\n");
			waserr = 1;
			continue;
		}

		nf = getfields(line[i], f, nelem(f), 1, " \t\r");
		if(nf != 4 || strcmp(f[0], "part") != 0)
			goto Error;

		a = strtoll(f[2], 0, 0);
		b = strtoll(f[3], 0, 0);
		if(a >= b)
			goto Error;

		if(err = addpart(edit, mkpart(f[1], a, b, 0))) {
			fprint(2, "?%s: not continuing\n", err);
			exits("partition");
		}
	}
}

static vlong
min(vlong a, vlong b)
{
	if(a < b)
		return a;
	return b;
}

static void
autoxpart(Edit *edit)
{
	int i, totw, futz;
	vlong secs, secsize, s;
	char *err;

	if(edit->npart > 0) {
		if(doautox)
			fprint(2, "partitions already exist; not repartitioning\n");
		return;
	}

	secs = edit->disk->secs;
	secsize = edit->disk->secsize;
	for(;;){
		/* compute total weights */
		totw = 0;
		for(i=0; i<nelem(autox); i++){
			if(autox[i].alloc==0 || autox[i].size)
				continue;
			totw += autox[i].weight;
		}
		if(totw == 0)
			break;

		if(secs <= 0){
			fprint(2, "ran out of disk space during autoxpartition.\n");
			return;
		}

		/* assign any minimums for small disks */
		futz = 0;
		for(i=0; i<nelem(autox); i++){
			if(autox[i].alloc==0 || autox[i].size)
				continue;
			s = (secs*autox[i].weight)/totw;
			if(s < autox[i].min/secsize){
				autox[i].size = autox[i].min/secsize;
				secs -= autox[i].size;
				futz = 1;
				break;
			}
		}
		if(futz)
			continue;

		/* assign any maximums for big disks */
		futz = 0;
		for(i=0; i<nelem(autox); i++){
			if(autox[i].alloc==0 || autox[i].size)
				continue;
			s = (secs*autox[i].weight)/totw;
			if(autox[i].max && s > autox[i].max/secsize){
				autox[i].size = autox[i].max/secsize;
				secs -= autox[i].size;
				futz = 1;
				break;
			}
		}
		if(futz)
			continue;

		/* finally, assign partition sizes according to weights */
		for(i=0; i<nelem(autox); i++){
			if(autox[i].alloc==0 || autox[i].size)
				continue;
			s = (secs*autox[i].weight)/totw;
			autox[i].size = s;

			/* use entire disk even in face of rounding errors */
			secs -= autox[i].size;
			totw -= autox[i].weight;
		}
	}

	for(i=0; i<nelem(autox); i++)
		if(autox[i].alloc)
			print("%s %llud\n", autox[i].name, autox[i].size);

	s = 0;
	for(i=0; i<nelem(autox); i++){
		if(autox[i].alloc == 0)
			continue;
		if(err = addpart(edit, mkpart(autox[i].name, s, s+autox[i].size, 1)))
			fprint(2, "addpart %s: %s\n", autox[i].name, err);
		s += autox[i].size;
	}
}

static void
restore(Edit *edit, int ctlfd)
{
	int i;
	vlong offset;

	offset = edit->disk->offset;
	fprint(2, "attempting to restore partitions to previous state\n");
	if(seek(edit->disk->wfd, edit->disk->secsize, 0) != 0){
		fprint(2, "cannot restore: error seeking on disk\n");
		exits("inconsistent");
	}

	if(write(edit->disk->wfd, osecbuf, edit->disk->secsize) != edit->disk->secsize){
		fprint(2, "cannot restore: couldn't write old partition table to disk\n");
		exits("inconsistent");
	}

	if(ctlfd >= 0){
		for(i=0; i<edit->npart; i++)
			fprint(ctlfd, "delpart %s", edit->part[i]->name);
		for(i=0; i<nopart; i++){
			if(fprint(ctlfd, "part %s %lld %lld", opart[i]->name, opart[i]->start+offset, opart[i]->end+offset) < 0){
				fprint(2, "restored disk partition table but not kernel; reboot\n");
				exits("inconsistent");
			}
		}
	}
	exits("restored");
}

static void
wrpart(Edit *edit)
{
	int i, n;
	Disk *disk;

	disk = edit->disk;

	memset(secbuf, 0, disk->secsize);
	n = 0;
	for(i=0; i<edit->npart; i++)
		n += snprint(secbuf+n, disk->secsize-n, "part %s %lld %lld\n", 
			edit->part[i]->name, edit->part[i]->start, edit->part[i]->end);

	if(seek(disk->wfd, disk->secsize, 0) != disk->secsize){
		fprint(2, "error seeking %d %lld on disk: %r\n", disk->wfd, disk->secsize);
		exits("seek");
	}

	if(write(disk->wfd, secbuf, disk->secsize) != disk->secsize){
		fprint(2, "error writing partition table to disk\n");
		restore(edit, -1);
	}

	if(ctldiff(edit, disk->ctlfd) < 0)
		fprint(2, "?warning: partitions could not be updated in devsd\n");
}

/*
 * Look for a boot sector in sector 1, as would be
 * the case if editing /dev/sdC0/data when that
 * was really a bootable disk.
 */
static void
checkfat(Disk *disk)
{
	uchar buf[32];

	if(seek(disk->fd, disk->secsize, 0) < 0
	|| read(disk->fd, buf, sizeof(buf)) < sizeof(buf))
		return;

	if(buf[0] != 0xEB || buf[1] != 0x3C || buf[2] != 0x90)
		return;

	fprint(2, 
		"there's a fat partition where the\n"
		"plan9 partition table would go.\n"
		"if you really want to overwrite it, zero\n"
		"the second sector of the disk and try again\n");

	exits("fat partition");
}
