#include <u.h>
#include <libc.h>
#include <bio.h>
#include <ctype.h>

#include "common.h"

#define TIMEOUTMS 10000
#define URICAPACITY 256

typedef enum {
	METHODGET,
	METHODHEAD,
	METHODCOUNT,
} Method;

typedef enum {
	HEADERREFERER,
	HEADERCOUNT,
} Header;

typedef struct {
	Method method;
	char uri[URICAPACITY + 2];
	long urilen;
	int version;
	char* referer;
} Request;

static Request rq = {0};

void
runcmdcode(int code)
{
	char codestr[4];
	snprint(codestr, sizeof(codestr), "%d", code);
	putenv("statuscode", codestr);
	runcmd();
}

void
fail(int code, char *phrase, ...)
{
	va_list headers;
	char *key, *value;
	alarm(0);
	va_start(headers, phrase);
	if (rq.version >= 10) {
		print("HTTP/1.1 %d %s\r\n", code, phrase);
		while ((key = va_arg(headers, char *))) {
			value = va_arg(headers, char *);
			print("%s: %s\r\n", key, value);
		}
		print("\r\n");
	}
	va_end(headers);
	runcmdcode(code);
	exits(0);
}

#define fail302(loc)     fail(302, "Found", "Location", loc, nil)
#define fail400()        fail(400, "Bad Request", nil)
#define fail404()        fail(404, "Not Found", nil)
#define fail405(methods) fail(405, "Method Not Allowed", "Allow", methods, nil)
#define fail414()        fail(414, "URI Too Long", nil)

void
notehandler(void *, char *note)
{
	if (strcmp(note, "alarm") != 0) {
		noted(NDFLT);
	}
	/*
	 * Do not throw the 408 Request Timeout.
	 * For some reason, some clients send a bunch of empty requests along with
	 * the main one, resulting in erratic timeouts and horrible user experience.
	 * Simply closing the connection mitigates this.
	 */
	exits(0);
}

Biobuf bp;

char
next1(void)
{
	char c;
	if ((c = Bgetc(&bp)) < 0) sysfatal("read");
	alarm(TIMEOUTMS);
	return c;
}

void
nextn(void *buf, long nbytes)
{
	char *bufc;
	char c;
	bufc = buf;
	while (nbytes) {
		c = next1();
		*bufc++ = c;
		--nbytes;
	}
}

#define ALPHA      "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
#define DIGITS     "0123456789"
#define istchar(c) ((c) ? strchr("!#$%&'*+-.^_`|~" DIGITS ALPHA, (c)) != nil : 0)
#define ispchar(c) ((c) ? strchr(ALPHA DIGITS "-._~!$&'()*+,;=:@", (c)) != nil : 0)
#define isvchar(c) ((c) >= 0x21 && (c) <= 0x7e)
#define todigit(c) ((c) - '0')

int
toxdigit(char c)
{
	if (c >= 'a' && c <= 'f') return c - 'a' + 10;
	if (c >= 'A' && c <= 'F') return c - 'A' + 10;
	return todigit(c);
}

#define isempty(s) (*(s) == '\0')

static char* methodnames[METHODCOUNT] = {
	[METHODGET] = "GET",
	[METHODHEAD] = "HEAD",
};

int
readmethod(void)
{
	int empty = 1, invalid = 0;
	char c;
	int i, m;
	int charstocheck[METHODCOUNT];
	for (m = 0; m < METHODCOUNT; ++m) charstocheck[m] = strlen(methodnames[m]);
	i = 0;
	while ((c = next1()) != ' ') {
		if (invalid) continue;
		empty = 0;
		for (m = 0; m < METHODCOUNT; ++m) {
			if (charstocheck[m] == -1) continue;
			if (methodnames[m][i] != c) charstocheck[m] = 0;
			--charstocheck[m];
		}
		++i;
		if (!istchar(c)) invalid = 1;
	}
	if (empty) invalid = 1;
	if (invalid) return -1;
	for (m = 0; m < METHODCOUNT; ++m) if (charstocheck[m] == 0) {
		rq.method = m;
		return 0;
	}
	rq.method = METHODCOUNT; /* Unknown method */
	return 0;
}

int
readuri(int *cont)
{
	char *buf;
	char c;
	char en[2];
	int hi, lo;
	int empty = 1, invalid = 0, overflow = 0, query = 0;
	buf = rq.uri;
	rq.urilen = 0;
	while ((c = next1()) != ' ' && c != '\n' && (c != '\r' || ((c = next1()) != '\n'))) {
		if (invalid) continue;
		if (query) continue;
		if (c == '?') { query = 1; continue; }
		if (rq.urilen == URICAPACITY) { overflow = 1; continue; }
		if (empty && c != '/') { invalid = 1; continue; }
		empty = 0;
		if (c == '%') {
			nextn(en, 2);
			if (!isxdigit(en[0]) || !isxdigit(en[1])) { invalid = 1; continue; }
			hi = toxdigit(en[0]);
			lo = toxdigit(en[1]);
			c = hi * 16 + lo;
		} else if (c != '/') {
			if (!ispchar(c)) { invalid = 1; continue; }
		}
		*buf++ = c;
		++rq.urilen;
	}
	*cont = c == ' ';
	if (empty) invalid = 1;
	if (invalid) return -1;
	if (overflow) return -2;
	return 0;
}

int
readversion(void)
{
	char s[10];
	long major;
	long minor;
	nextn(s, sizeof(s));
	if (strncmp(s, "HTTP/", 5) != 0
	    || !isdigit(s[5])
	    || s[6] != '.'
	    || !isdigit(s[7])
	    || s[8] != '\r'
	    || s[9] != '\n') return -1;
	major = todigit(s[5]);
	minor = todigit(s[7]);
	rq.version = major * 10 + minor;
	return 0;
}

void
readrequestline(void)
{
	int uri, method, cont, version;
	method = readmethod();
	uri = readuri(&cont);
	if (cont) {
		version = readversion();
	} else {
		rq.version = 9;
		version = 0;
	}
	if (version < 0) exits(0);
	if (rq.version == 9) {
		if (rq.method != METHODGET) fail400();
	} else if (rq.version >= 10) {
		if (method < 0) fail400();
	} else {
		/* We don't know how to respond for this version, so just exit. */
		exits(0);
	}
	if (uri == -1) fail400();
	if (uri == -2) fail414();
}

static char* headernames[HEADERCOUNT] = {
	[HEADERREFERER] = "Referer",
};

int
readheadername(void)
{
	int empty = 1;
	char c;
	int i, h;
	int charstocheck[HEADERCOUNT];
	for (h = 0; h < HEADERCOUNT; ++h) charstocheck[h] = strlen(headernames[h]);
	i = 0;
	while ((c = next1()) != ':' && (c != '\r' || ((c = next1()) != '\n'))) {
		empty = 0;
		for (h = 0; h < HEADERCOUNT; ++h) {
			if (charstocheck[h] == -1) continue;
			if (tolower(headernames[h][i]) != tolower(c)) charstocheck[h] = 0;
			--charstocheck[h];
		}
		++i;
		if (!istchar(c)) fail400();
	}
	if (empty) return -1;
	for (h = 0; h < HEADERCOUNT; ++h) if (charstocheck[h] == 0) return h;
	return HEADERCOUNT; /* Unknown header */
}

void
readheaderreferer(void)
{
	static char buf[URICAPACITY + 1];
	long n = 0;
	char c;
	while ((c = next1()) == ' ' || c == '\t');
	while (c != '\r' || ((c = next1()) != '\n')) {
		if (!isvchar(c)) fail400();
		if (n == URICAPACITY) continue;
		buf[n++] = c;
		c = next1();
	}
	buf[n] = '\0';
	rq.referer = buf;
}

int
readheader(void)
{
	char c;
	int name;
	if ((name = readheadername()) < 0)
		return 0;
	switch (name) {
	case HEADERREFERER:
		readheaderreferer();
		break;
	default:
		while ((c = next1()) != '\r' || ((c = next1()) != '\n'));
		break;
	}
	return 1;
}

void
readheaders(void)
{
	/* TODO: support folding? */
	while (readheader());
}

int
urifmt(Fmt *f)
{
	char *s, c;
	int i, res;
	s = va_arg(f->args, char *);
	assert(f->flags & FmtPrec);
	for (i = f->prec; i > 0; --i) {
		c = *s++;
		res = isgraph(c) ? fmtprint(f, "%c", c) : fmtprint(f, "%%%02hhx", c);
		if (res < 0) return res;
	}
	return 0;
}

static char *wdayname[7] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
static char *monname[12] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };

int
datefmt(Fmt *f)
{
	Tm *tm;
	ulong t;
	t = va_arg(f->args, ulong);
	tm = gmtime(t);
	return fmtprint(f, "%s, %.2d %s %.4d %.2d:%.2d:%.2d GMT",
		wdayname[tm->wday], tm->mday, monname[tm->mon], tm->year+1900,
		tm->hour, tm->min, tm->sec);
}

void
servefd(int fd)
{
	Dir *dir;
	char *contenttype;
	if (rq.version >= 10) {
		if (!(dir = dirfstat(fd))) sysfatal("stat: %r");
		print("HTTP/1.0 200 OK\r\n");
		print("Content-Length: %lld\r\n", dir->length);
		if ((contenttype = mimetype(dir->name))) {
			print("Content-Type: %s\r\n", contenttype);
		}
		print("Last-Modified: %D\r\n", dir->mtime);
		print("Date: %D\r\n", time(0));
		print("Cache-Control: must-revalidate\r\n");
		print("\r\n");
		free(dir);
	}
	switch (rq.method) {
	case METHODGET:
		printfile(fd);
	case METHODHEAD:
		break;
	default:
		fail405("GET, HEAD");
	}
}

void
servecwd(void)
{
	if (rq.version >= 10) {
		print("HTTP/1.0 200 OK\r\n");
		print("Content-Type: text/html\r\n");
		print("\r\n");
	}
	switch (rq.method) {
	case METHODGET:
		runcmdcode(200);
	case METHODHEAD:
		break;
	default:
		fail405("GET, HEAD");
	}
}

void
usage(void)
{
	fprint(2, "usage: %s [-r root] [-v] [-R remote] cmd [args...]\n", argv0);
	exits("usage");
}

void
main(int argc, char *argv[])
{
	int fd, canonical, verbose;
	Dir *dir;
	char *remote, *root;
	verbose = 0;
	remote = nil;
	root = nil;
	ARGBEGIN {
	case 'r':
		root = EARGF(usage());
		break;
	case 'R':
		remote = EARGF(usage());
		break;
	case 'v':
		verbose = 1;
		break;
	default:
		usage();
	} ARGEND;
	if (argc < 1) usage();
	if (root && chdir(root) < 0) sysfatal("chdir %s: %r", root);
	setcmd(*argv, argv);
	notify(notehandler);
	fmtinstall('u', urifmt);
	fmtinstall('D', datefmt);
	Binit(&bp, 0, OREAD);
	alarm(TIMEOUTMS);
	readrequestline();
	if (rq.version >= 10) readheaders();
	alarm(0);
	if (verbose) {
		if (remote) {
			if (rq.referer) syslog(0, "dratva", "HTTP  	%s	%.*u	Referer: %s", remote, rq.urilen, rq.uri, rq.referer);
			else            syslog(0, "dratva", "HTTP  	%s	%.*u", remote, rq.urilen, rq.uri);
		} else {
			if (rq.referer) syslog(0, "dratva", "HTTP  	%.*u	Referer: %s", rq.urilen, rq.uri, rq.referer);
			else            syslog(0, "dratva", "HTTP  	%.*u", rq.urilen, rq.uri);
		}
	}
	if ((fd = traverse(rq.uri, &rq.urilen, &canonical)) < 0) fail404();
	if (!canonical) {
		rq.uri[rq.urilen] = '\0';
		fail302(rq.uri);
	}
	if (!(dir = dirfstat(fd))) sysfatal("stat: %r");
	if (dir->mode & DMDIR) servecwd(); else servefd(fd);
	//close(fd);
	exits(0);
}
