#include <stdio.h>
#include <errno.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

#include "lib.h"

typedef struct Account Account;
typedef struct Cmd Cmd;
typedef struct Code Code;
typedef struct Trans Trans;
typedef struct Date Date;

struct Code
{
	Code *next;
	char *code;
	char *desc;
};

struct Trans
{
	Trans *next;

	Account *src;
	Account *dst;

	int num;
	int year;
	int month;
	int day;
	double amount;
	Code *code;
	char *desc;
};

struct Date
{
	int year;
	int month;
	int day;
};

struct Account
{
	char *name;
	char *desc;
	Account	*children;
	Account *link;

	double balance;
};

Code *codes;		// code list
int ntrans;		// number of transactions
Trans **strans;		// sorted transaction array
Trans *trans;		// raw transaction list
Trans *tlast;		// last transaction in raw list

struct Cmd
{
	char *match;
	void (*func)(Account*, char*, char*);
};

static void cmd_acct(Account*, char*, char*);
static void cmd_addcode(Account*, char*, char*);
static void cmd_addtrans(Account*, char*, char*);
static void cmd_assert(Account*, char*, char*);
static void cmd_setyear(Account*, char*, char*);
static void balance(Account*);
static void dump(Account*);
static void fatal(char *, ...);
static Account *findaccount(Account*, char*);
static Code *findcode(Account*, char*);
static int getfields(char*, char**, int);
static char *gettok(char**);
static Account *parse(FILE*);
static void parsedate(char*, Date*);
static char *skipws(char*);
static void sort(Account*);
static void warn(char *, ...);

static Cmd cmds[] =
{
	{"acct", cmd_acct},
	{"assert", cmd_assert},
	{"code", cmd_addcode},
	{"year", cmd_setyear},
	{NULL, NULL},
};

Date enddate;		// (for balance, dump, etc)
int fatalwarning = 1;
char *file = "<stdin>";
int lineno;
char *prog;
int year;

int
main(int argc, char *argv[])
{
	char *ab, *end;
	Account *acct;
	int dodump;
	FILE *in;

	ab = NULL;
	dodump = 0;
	end = NULL;
	in = stdin;
	prog = argv[0];

	ARGBEGIN {
	case 'b':
		ab = ARGF();
		if(ab == NULL)
			goto usage;
		break;
	case 'd':
		dodump=1;
		break;
	case 'e':
		end = ARGF();
		if(end == NULL)
			goto usage;
		break;
	case 'w':
		fatalwarning = !fatalwarning;
		break;
	default:
	usage:
		fprintf(stderr, "usage: %s [-b acct] [-d] [-e yyyy/mm/dd] [file]\n", prog);
		exit(1);
	} ARGEND;

	if(argc > 0) {
		file = argv[0];
		if(file == NULL)
			goto usage;
		in = fopen(file, "r");
		if(in == NULL)
			fatal("open: %s\n", strerror(errno));
	}

	acct = parse(in);
	sort(acct);
	parsedate(end, &enddate);
	if(dodump)
		dump(acct);
	if(ab != NULL) {
		acct = findaccount(acct, ab);
		if(acct == NULL)
			fatal("%s: unknown account", ab);
		balance(acct);
		printf("%s [%s] balance = %.2f\n", acct->desc, acct->name, acct->balance);
	}
	return 0;
}

static int
tcompare(const void *a, const void *b)
{
	const Trans *ta, *tb;

	ta = *(Trans**)a;
	tb = *(Trans**)b;

	if(ta->year < tb->year)
		return -1;
	if(ta->year > tb->year)
		return 1;

	if(ta->month < tb->month)
		return -1;
	if(ta->month > tb->month)
		return 1;

	if(ta->day < tb->day)
		return -1;
	if(ta->day > tb->day)
		return 1;

	if(ta->amount > 0) {
		if(tb->amount < 0)
			return -1;
		return ta->amount - tb->amount;
	}
	if(tb->amount > 0)
		return 1;

	return 0;
}

static void
sort(Account *acct)
{
	int i;
	Trans *tp;

	strans = malloc(sizeof(strans[0])*ntrans);
	if(strans == NULL)
		fatal("malloc transaction sort buf");

	i = 0;
	for(tp = trans; tp != NULL; tp = tp->next)
		strans[i++] = tp;

	if(i != ntrans)
		fatal("%d != %d\n", i, trans);

	qsort(strans, ntrans, sizeof(strans[0]), tcompare);
}

static void
parsedate(char *end, Date *d)
{
	char *p;
	if(end == NULL) {
		d->year = 0;
		d->month = 0;
		d->day = 0;
		return;
	}
	p = strchr(end, '/');
	if(p == NULL)
		fatal("bad end date 1");
	*p++ = '\0';
	d->year = strtoul(end, NULL, 0);

	end = p;
	p = strchr(end, '/');
	if(p == NULL)
		fatal("bad end date 2");
	*p++ = '\0';
	d->month = strtoul(end, NULL, 0);

	d->day = strtoul(p, NULL, 0);
}

static int
ispast(Trans *t, Date *d)
{
	if(d->year == 0)
		return 0;

	if(t->year > d->year)
		return 1;
	if(t->year == d->year && t->month > d->month)
		return 1;
	if(t->year == d->year && t->month == d->month && t->day > d->day)
		return 1;
	return 0;
}

static void
dump(Account *acct)
{
	Code *c;
	int i;
	Trans *t;

	for(c = codes; c != NULL; c = c->next)
		printf("code %s %s\n", c->code, c->desc);

	for(i = 0; i < ntrans; i++) {
		t = strans[i];
		if(ispast(t, &enddate))
			break;
		if(t->num == 0)
			printf("-	");
		else
			printf("%d	", t->num);
		printf("%d/%d/%d	%c%.2f	%s	%s\n",
				t->year, t->month, t->day,
				t->amount < 0 ? '-' : (t->amount > 0 ? '+' : ' '),
				t->amount < 0 ? -t->amount : t->amount,
				t->code->code,
				t->desc == NULL ? "" : t->desc);
	}
}

static void
overaccounts(Account *parent, void (*func)(Account*))
{
	Account *c;

	for(c = parent->children; c != NULL; c = c->link)
		overaccounts(c, func);
	func(parent);
}

static double
childbalance(Account *parent)
{
	Account *c;

	for(c = parent->children; c != NULL; c = c->link)
		parent->balance += childbalance(c);

	return parent->balance;
}


static void
balance(Account *acct)
{
	int i;
	Trans *t;
	Code *vcode;

	vcode = findcode(acct, "VOID");
	if(vcode == NULL)
		warn("no void code");

	for(i = 0; i < ntrans; i++) {
		t = strans[i];
		if(ispast(t, &enddate))
			break;
		if(t->code != vcode) {
			t->src->balance -= t->amount;
			t->dst->balance += t->amount;
		}
	}
	childbalance(acct);
}

/*
 * XXX quick and dirty
 */

static void
clearbal(Account *a)
{
	a->balance = 0;
}

static void
cmd_assert(Account *root, char *cmd, char *buf)
{
	int b, bal;
	double dbal;
	char *fields[3];
	Account *a;

	if(getfields(buf, fields, nelem(fields)) != nelem(fields))
		fatal("missing field(s)");

	if(strcmp(fields[0], "balance") != 0)
		fatal("syntax error");

	sort(root);
	balance(root);
	a = findaccount(root, fields[1]);
	if(a == NULL) {
		warn("balance assertion failed: '%s': unknown account", fields[1]);
		goto cleanup;
	}

	/*
	 * XXX this integer conversion is necessary because the
	 * balance assertion was failing even though the balance was
	 * the same.
	 */
	dbal = strtod(fields[2], NULL);
	bal = rint(dbal * 100.0);
	b = rint(a->balance * 100.0);
	if(b != bal)
		warn("%s balance assertion failed (%.2f != %.2f) difference %.2f", a->name, a->balance, dbal, a->balance - dbal);

cleanup:
	free(strans);
	strans = NULL;
	overaccounts(root, clearbal);
}

static void
newaccount(Account *parent, char *name, char *desc)
{
	Account *na;

	na = malloc(sizeof(*na));
	if(na == NULL)
		fatal("failed to allocate memory for new account");
	memset(na, 0, sizeof(*na));

	na->name = strdup(name);
	na->desc = strdup(desc);
	if(na->name == NULL || na->desc == NULL)
		fatal("create new account");

	na->link = parent->children;
	parent->children = na;
}

static Account*
findaccount(Account *acct, char *name)
{
	Account *c;

	if(strcmp(name, ".") == 0 || strcmp(acct->name, name) == 0)
		return acct;

	for(c = acct->children; c != NULL; c = c->link) {
		acct = findaccount(c, name);
		if(acct != NULL)
			return acct;
	}
	return NULL;
}
		
static void
cmd_acct(Account *root, char *cmd, char *buf)
{
	char *fields[3];
	Account *parent;

	USED(root);

	if(getfields(buf, fields, nelem(fields)) != nelem(fields))
		fatal("missing field(s)");

	parent = findaccount(root, fields[0]);
	if(parent != NULL)
		fatal("account %s already exists", parent->name);

	parent = findaccount(root, fields[1]);
	if(parent == NULL)
		fatal("parent account '%s' doesn't exist", fields[1]);

	newaccount(parent, fields[0], fields[2]);
}

static void
cmd_addcode(Account *acct, char *cmd, char *buf)
{
	Code *c;
	char *fields[2];

	USED(cmd);

	c = malloc(sizeof(*c));
	if(c == NULL)
		fatal("malloc code");

	if(getfields(buf, fields, nelem(fields)) != nelem(fields))
		fatal("missing field(s)");

	c->code = strdup(fields[0]);
	if(c->code == NULL)
		fatal("strdup code");

	c->desc = strdup(fields[1]);
	if(c->desc == NULL)
		fatal("strdup code desc");

	c->next = codes;
	codes = c;
}

static Code*
findcode(Account *acct, char *code)
{
	Code *c;

	for(c = codes; c != NULL; c = c->next) {
		if(strcmp(c->code, code) == 0)
			return c;
	}
	return NULL;
}

static void
cmd_setyear(Account *acct, char *cmd, char *buf)
{
	int y;

	USED(cmd);

	y = strtol(buf, NULL, 0);
	if(y <= 0)
		fatal("bad year");

	year = y;
}

static void
cmd_addtrans(Account *acct, char *num, char *buf)
{
	Trans *t;
	char *fields[5];
	char *p, *src;

	memset(fields, 0, sizeof(fields));

	t = malloc(sizeof(*t));
	if(t == NULL)
		fatal("malloc transaction");
	memset(t, 0, sizeof(*t));

	if(getfields(buf, fields, nelem(fields)) < nelem(fields)-1)
		fatal("missing field(s)");

	if(strcmp(num, "-") != 0)
		t->num = strtol(num, NULL, 0);

	if(year == 0)
		fatal("year must be set before any transactions");
	t->year = year;

	p = strchr(fields[0], '/');
	if(p == NULL)
		fatal("syntax error in date field");
	*p++ = '\0';
	t->month = strtoul(fields[0], NULL, 0);
	t->day = strtoul(p, NULL, 0);

	p = fields[1];
	if(*p == '-' || *p == '+')
		fatal("use account src->dst for debit/credit");
	t->amount = strtod(p, NULL);

	t->code = findcode(acct, fields[2]);
	if(t->code == NULL)
		fatal("unknown code");

	src = fields[3];
	if(strcmp(src, "none") != 0) {
		p = strchr(src, '>');
		if(p == NULL || p == src || p[-1] != '-')
			fatal("syntax error in account field");
		p[-1] = '\0';
		p++;
		if(*src == '\0')
			src = "unknown";
		t->src = findaccount(acct, src);
		if(t->src == NULL)
			fatal("unknown src account '%s'", src);
		t->dst = findaccount(acct, p);
		if(t->dst == NULL)
			fatal("unknown dst account '%s'", p);
	}

	if(fields[4] != NULL)
		t->desc = strdup(fields[4]);

	if(tlast == NULL)
		trans = t;
	else
		tlast->next = t;
	tlast = t;
	ntrans++;
}

static Account*
parse(FILE *in)
{
	Account *acct;
	char *buf, line[1024], *tok;
	Cmd *c;

	acct = malloc(sizeof(*acct));
	if(acct == NULL)
		fatal("malloc account");
	memset(acct, 0, sizeof(*acct));
	acct->name = ".";
	acct->desc = "root";

	newaccount(acct, "unknown", "<unknown>");

next:
	while(fgets(line, sizeof(line)-1, in) != NULL) {
		lineno++;
		buf = strchr(line, '\n');
		if(buf != NULL)
			*buf = '\0';
		buf = line;
		tok = gettok(&buf);
		if(*tok == '\0')
			continue;
		if(tok == NULL)
			fatal("syntax error");
		tok = skipws(tok);
		if(tok[0] == '#')
			continue;
		c = cmds;
		while(c->match != NULL) {
			if(strcmp(c->match, tok) == 0) {
				if(c->func == NULL)
					fatal("internal error: missing func");
				c->func(acct, tok, buf);
				goto next;
			}
			c++;
		}
		cmd_addtrans(acct, tok, buf);
	}
	return acct;
}

static char*
skipws(char *buf)
{
	while(*buf == ' ' || *buf == '\t')
		buf++;

	return buf;
}

static char*
gettok(char **buf)
{
	char *b, *start;

	if(*buf == NULL)
		return NULL;

	b = skipws(*buf);
	start = b;
	while(*b != ' ' && *b != '\t' && *b != '\0')
		b++;
	if(*b == '\0') {
		*buf = NULL;
		return start;
	}

	*b++ = '\0';
	*buf = b;

	return start;
}

static int
getfields(char *buf, char **fields, int nfields)
{
	int n;

	nfields--;
	for(n=0; n < nfields; n++) {
		*fields = gettok(&buf);
		if(*fields == NULL)
			return n;
		fields++;
	}
	if(buf == NULL)
		return n;
	*fields = skipws(buf);
	return n+1;
}

static void
fatal(char *fmt, ...)
{
	char buf[1024];
	va_list ap;

	va_start(ap, fmt);
	vsnprintf(buf, sizeof(buf)-1, fmt, ap);

	if(lineno > 0)
		fprintf(stderr, "%s: %s:%d %s\n", prog, file, lineno, buf);
	else
		fprintf(stderr, "%s: %s: %s\n", prog, file, buf);
	exit(1);
}

static void
warn(char *fmt, ...)
{
	char buf[1024];
	va_list ap;

	va_start(ap, fmt);
	vsnprintf(buf, sizeof(buf)-1, fmt, ap);

	fprintf(stderr, "%s: %s:%d warning: %s\n", prog, file, lineno, buf);

	if(!fatalwarning)
		return;

	exit(1);
}

