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

#include "common.h"

#define TIMEOUTMS 10000
#define URICAPACITY 1024

void
fail(int code, char *reason)
{
	print("%d %s\r\n", code, reason);
	exits(0);
}

#define fail31(uri) fail(31, uri)
#define fail51()    fail(51, "NOT FOUND")
#define fail53()    fail(53, "PROXY REQUEST REFUSED")
#define fail59()    fail(59, "BAD REQUEST")

void
notehandler(void *, char *note)
{
	if (strcmp(note, "alarm") != 0) {
		noted(NDFLT);
	}
	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"
/* TODO: Fix handling nil in these macros */
#define isschar(c) strchr(ALPHA DIGITS "+-.", c)
#define isrchar(c) strchr(ALPHA DIGITS "-._~!$&'()*+,;=", c)
#define ispchar(c) strchr(ALPHA DIGITS "-._~!$&'()*+,;=:@", c)
#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 assert59(c) do { if (!(c)) fail59(); } while (0)

void
readscheme(void)
{
	int i;
	char c;
	for (i = 0; (c = next1()) != ':'; ++i) {
		assert59((i == 0 && isalpha(c)) || (i > 0 && isschar(c)));
		if (i >= 6 || tolower(c) != "gemini"[i]) fail53();
	}
}

/*
	dec8 terminator
*/
void
readdec8(char *terminators)
{
	char first, second, third, c;
	if ((first = next1()) == '0') {
		assert59((c = next1()) && strchr(terminators, c));
		return;
	}
	assert59(isdigit(first));
	if ((second = next1()) && strchr(terminators, second)) return;
	assert59(isdigit(second));
	if ((third = next1()) && strchr(terminators, third)) return;
	assert59(isdigit(third));
	assert59(100*todigit(first) + 10*todigit(second) + todigit(third) < 256);
	assert59((c = next1()) && strchr(terminators, c));
}

/*
	*(4-parsed)HEXDIGIT terminator
*/
void
readsegment(char c, char *terminators, int parsed, char *t)
{
	int i;
	for (i = 0; i < 4 - parsed; ++i) {
		if ((c = next1()) && strchr(terminators, c)) {
			if (t) *t = c;
			return;
		}
		assert59(isxdigit(c));
	}
	assert59((c = next1()) && strchr(terminators, c));
	if (t) *t = c;
}

/*
	(h16 terminator) / (IPv4address "]")
*/
void
readsegmentoripv4bracket(char first, char *terminators, char *t)
{
	char second, third, fourth;
	if (!isdigit(first)) {
		readsegment(first, terminators, 1, t);
		return;
	}
	if ((second = next1()) && strchr(terminators, second)) {
		if (t) *t = second;
		return;
	}
	if (second == '.') { readdec8("."); readdec8("."); readdec8("]"); if (t) *t = ']'; return; }
	assert59(isxdigit(second));
	if (!isdigit(second)) {
		readsegment(second, terminators, 2, t);
		return;
	}
	if ((third = next1()) && strchr(terminators, third)) {
		if (t) *t = third;
		return;
	}
	if (third == '.') { readdec8("."); readdec8("."); readdec8("]"); if (t) *t = ']'; return; }
	assert59(isxdigit(third));
	if (!isdigit(third)) {
		readsegment(third, terminators, 3, t);
		return;
	}
	if ((fourth = next1()) && strchr(terminators, fourth)) {
		if (t) *t = fourth;
		return;
	}
	if (fourth == '.') {
		assert59(100*todigit(first) + 10*todigit(second) + todigit(third) < 256);
		readdec8("."); readdec8("."); readdec8("]"); if (t) *t = ']'; return;
	}
	assert59(isxdigit(fourth));
	readsegment(fourth, terminators, 4, t);
}

/*
	  "]"
	/ h16 "]"
	/ *(n-2)(h16 ":") ((h16 ":" h16) / IPv4address) "]"
*/
void
readellision(int n)
{
	char c;
	int i;
	if ((c = next1()) == ']') return;
	assert59(n > 0);
	assert59(isxdigit(c));
	if (n == 1) {
		readsegment(c, "]", 1, nil);
		return;
	}
	if (readsegmentoripv4bracket(c, ":]", &c), c == ']') return;
	for (i = 0; i < n - 2; ++i) {
		assert59(isxdigit(c = next1()));
		if (readsegmentoripv4bracket(c, ":]", &c), c == ']') return;
	}
	assert59(isxdigit(c = next1()));
	readsegment(c, "]", 1, nil);
}

void
readipv6addr(char c)
{
	int rem = 8;
	if (c == ':') {
		assert59(next1() == ':');
		readellision(rem - 1);
		return;
	}
	while (rem > 2) {
		readsegment(c, ":", 1, nil);
		--rem;
		if ((c = next1()) == ':') {
			readellision(rem - 1);
			return;
		}
	}
	/* 2 segments left at this point */
	assert59(isxdigit(c = next1()));
	if (readsegmentoripv4bracket(c, ":", &c), c == ']') return;
	if ((c = next1()) == ':') {
		assert59(next1() == ']');
		return;
	}
	assert59(isxdigit(c));
	readsegment(c, "]", 1, nil);
}

void
readipfutureaddr(void)
{
	char c;
	assert59(isxdigit(next1()));
	assert59(next1() == '.');
	c = next1();
	do {
		assert59(isrchar(c));
	} while ((c = next1()) != ']');
}

void
readipv4addrorregname(char c, char *t)
{
	do {
		if (c == '%') {
			assert59(isxdigit(next1()));
			assert59(isxdigit(next1()));
			continue;
		}
		assert59(isrchar(c));
	} while ((c = next1()) != ':' && c != '/' && (c != '\r' || (c = next1()) != '\n'));
	*t = c;
}

void
readhost(char *t)
{
	char c;
	assert59(next1() == '/');
	assert59(next1() == '/');
	if ((c = next1()) == '[') {
		if ((c = next1()) == 'v' || c == 'V') readipfutureaddr(); else readipv6addr(c);
		switch (c = next1()) {
		case ':':
		case '/':
			*t = c;
			return;
		case '\r':
			if (next1() == '\n') {
				*t = '\n';
				return;
			}
			break;
		}
		fail59();
	} else {
		readipv4addrorregname(c, t);
	}
}

void
readport(char *t)
{
	char c;
	while ((c = next1()) != '/' && (c != '\r' && (c = next1()) != '\n')) {
		assert59(isdigit(c));
	}
	*t = c;
}

long
readpath(char c, char *uri, long maxlen)
{
	long len = 0;
	char en[2];
	int hi, lo;
	int query = 0;
	do {
		if (query) continue;
		if (c == '?') { query = 1; continue; }
		if (c == '%') {
			nextn(en, 2);
			assert59(isxdigit(en[0]));
			assert59(isxdigit(en[1]));
			hi = toxdigit(en[0]);
			lo = toxdigit(en[1]);
			c = 16*hi + lo;
		} else if (c != '/') {
			assert59(ispchar(c));
		}
		assert59(len < maxlen);
		*uri++ = c;
		len++;
	} while ((c = next1()) != '\r' || (c = next1()) != '\n');
	return len;
}

long
readurl(char *uri, long maxlen)
{
	char c;
	readscheme();
	switch (readhost(&c), c) {
	case ':':
		if (readport(&c), c == '\n') return 0;
		/* Fallthrough */
	case '/':
		return readpath('/', uri, URICAPACITY);
	case '\n':
		return 0;
	}
	return 0;
}

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;
}

void
servefd(int fd)
{
	Dir *dir;
	char *contenttype;
	if (!(dir = dirfstat(fd))) sysfatal("stat %s: %r");
	contenttype = mimetype(dir->name);
	free(dir);
	if (!contenttype) contenttype = "application/octet-stream";
	print("20 %s\r\n", contenttype);
	printfile(fd);
}

void
servecwd(void)
{
	print("20 text/gemini; charset=utf-8\r\n");
	runcmd();
}

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

void
main(int argc, char *argv[])
{
	char uri[URICAPACITY + 2];
	long urilen;
	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);
	Binit(&bp, 0, OREAD);
	alarm(TIMEOUTMS);
	urilen = readurl(uri, URICAPACITY);
	alarm(0);
	if (verbose) {
		if (remote) syslog(0, "dratva", "Gemini	%s	%.*u", remote, urilen, uri);
		else        syslog(0, "dratva", "Gemini	%.*u", urilen, uri);
	}
	if ((fd = traverse(uri, &urilen, &canonical)) < 0) fail51();
	if (!canonical) {
		uri[urilen] = '\0';
		fail31(uri);
	}
	if (!(dir = dirfstat(fd))) sysfatal("stat: %r");
	if (dir->mode & DMDIR) servecwd(); else servefd(fd);
	exits(0);
}
