#define _DEFAULT_SOURCE
#define _FILE_OFFSET_BITS 64
#include <errno.h>
#include <netdb.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <termios.h>
#include <unistd.h>
#include "c9.h"
#include "parg.h"

enum
{
	Msize = 8192,

	Rootfid = 0,
	Indexfid,
	Chatfid,

	Error = 1<<0,
	Joined = 1<<1,
	Disconnected = 1<<2,
};

typedef struct C9aux C9aux;
typedef struct REntry REntry;

struct C9aux
{
	C9ctx c;
	int f;
	int flags;
	uint8_t rdbuf[Msize];
	uint8_t wrbuf[Msize];
	uint32_t wroff;
};

static const char *nick;
static int printjoin, needopen;
static uint64_t chatoff, skipuntil;
//static char *regsrv = "tcp!registry.9p.zone!6675";
static char *chatsrv = "tcp!chat.9p.zone!9990";
static const char *channel = "chat";

static uint8_t *
ctxread(C9ctx *ctx, uint32_t size, int *err)
{
	uint32_t n;
	int r;
	C9aux *a;

	a = ctx->aux;
	*err = 0;
	for(n = 0; n < size; n += r){
		if((r = read(a->f, a->rdbuf+n, size-n)) <= 0){
			if(errno == EINTR)
				continue;
			a->flags |= Disconnected;
			close(a->f);
			return NULL;
		}
	}

	return a->rdbuf;
}

static int
wrsend(C9aux *a)
{
	uint32_t n;
	int w;

	for(n = 0; n < a->wroff; n += w){
		if((w = write(a->f, a->wrbuf+n, a->wroff-n)) <= 0){
			if(errno == EINTR)
				continue;
			if(errno != EPIPE) /* remote end closed */
				perror("write");
			return -1;
		}
	}
	a->wroff = 0;

	return 0;
}

static uint8_t *
ctxbegin(C9ctx *ctx, uint32_t size)
{
	uint8_t *b;
	C9aux *a;

	a = ctx->aux;
	if(a->wroff + size > sizeof(a->wrbuf)){
		if(wrsend(a) != 0 || a->wroff + size > sizeof(a->wrbuf))
			return NULL;
	}
	b = a->wrbuf + a->wroff;
	a->wroff += size;

	return b;
}

static int
ctxend(C9ctx *ctx)
{
	(void)ctx;
	return 0;
}

static int
dial(char *s)
{
	struct addrinfo hint = {
		.ai_flags = AI_ADDRCONFIG,
		.ai_family = AF_UNSPEC,
		0
	}, *r, *a;
	char host[64], *port;
	int e, f, yes;

	if(strncmp(s, "udp!", 4) == 0){
		hint.ai_socktype = SOCK_DGRAM;
		hint.ai_protocol = IPPROTO_UDP;
	}else if(strncmp(s, "tcp!", 4) == 0){
		hint.ai_socktype = SOCK_STREAM;
		hint.ai_protocol = IPPROTO_TCP;
	}else{
		fprintf(stderr, "invalid dial string: %s\n", s);
		return -1;
	}
	if((port = strchr(s+4, '!')) == NULL){
		fprintf(stderr, "invalid dial string: %s\n", s);
		return -1;
	}
	if(snprintf(host, sizeof(host), "%.*s", (int)(port-s-4), s+4) >= (int)sizeof(host)){
		fprintf(stderr, "host name too large: %s\n", s);
		return -1;
	}
	port++;
	if((e = getaddrinfo(host, port, &hint, &r)) != 0){
		fprintf(stderr, "%s: %s\n", gai_strerror(e), s);
		return -1;
	}
	f = -1;
	yes = 1;
	for(a = r; a != NULL; a = a->ai_next){
		if((f = socket(a->ai_family, a->ai_socktype, a->ai_protocol)) < 0)
			continue;
		if(connect(f, a->ai_addr, a->ai_addrlen) == 0){
			setsockopt(f, SOL_SOCKET, TCP_NODELAY, &yes, sizeof(yes));
			break;
		}
		close(f);
		f = -1;
	}
	freeaddrinfo(r);

	return f;
}

static void
output(uint8_t *d, int sz)
{
	int i, j;

	for(i = j = 0; i < sz; i++){
		d[j] = d[i];
		if(d[j] > 31 || d[j] == '\t' || d[j] == '\n')
			j++;
	}
	write(1, d, j);
}

static void
ctxchatR(C9ctx *ctx, C9r *r)
{
	C9aux *a;
	C9tag tag;
	const char *path[2];
	char buf[64];
	int n;

	a = ctx->aux;
	switch(r->type){
	case Rversion:
		c9attach(ctx, &tag, Rootfid, C9nofid, "none", NULL);
		break;

	case Rattach:
		path[0] = channel;
		path[1] = NULL;
		c9walk(ctx, &tag, Rootfid, Chatfid, path);
		needopen = 1;
		break;

	case Rwalk:
		needopen = needopen && c9open(ctx, &tag, Chatfid, C9rdwr);
		break;

	case Rread:
		if(chatoff >= skipuntil)
			output(r->read.data, r->read.size);
		chatoff += r->read.size;
		/* fallthrough */
	case Ropen:
		if((a->flags & Joined) == 0 && printjoin){
			n = snprintf(buf, sizeof(buf), "JOIN %s to chat\n", nick);
			c9write(ctx, &tag, Chatfid, 0, buf, n);
			a->flags |= Joined;
		}
		c9read(ctx, &tag, Chatfid, chatoff, chatoff < skipuntil ? skipuntil-chatoff : Msize);
		break;

	case Rerror:
		fprintf(stderr, "chat error: %s\n", r->error);
		a->flags |= Error;
		break;

	default:
		break;
	}
}

static void
ctxregistryR(C9ctx *ctx, C9r *r)
{
	char *s, *b, *e;
	C9aux *a;
	C9tag tag;
	const char *path[2];

	a = ctx->aux;
	switch(r->type){
	case Rversion:
		c9attach(ctx, &tag, Rootfid, C9nofid, "none", NULL);
		break;

	case Rattach:
		path[0] = "index";
		path[1] = NULL;
		c9walk(ctx, &tag, Rootfid, Indexfid, path);
		break;

	case Rwalk:
		c9open(ctx, &tag, Indexfid, C9read);
		break;

	case Ropen:
		c9read(ctx, &tag, Indexfid, 0, Msize);
		break;

	case Rread:
		r->read.data[r->read.size] = 0;
		for(s = (char*)r->read.data;;){
			if((s = strstr(s, "chat")) == NULL)
				break;
			for(b = s; b != (char*)r->read.data && *b != '\n'; b--);
			if(*b == '\n')
				b++;
			if((s = strchr(s, '\n')) == NULL)
				s = (char*)&r->read.data[r->read.size];
			else
				*s++ = 0;
			if(strstr(b, "tlssrv") == NULL && (e = strchr(b, ' ')) != NULL){
				*e = 0;
fallback:
				close(a->f);
				if((a->f = dial(b)) < 0)
					exit(1);
				a->flags = 0;
				ctx->r = ctxchatR;
				a->wroff = 0;
				c9version(ctx, &tag, Msize);
				if(wrsend(a) != 0)
					exit(1);
				return;
			}
		}
		b = chatsrv;
		goto fallback;

	case Rerror:
		fprintf(stderr, "registry error: %s\n", r->error);
		a->flags |= Error;
		break;

	default:
		break;
	}
}

__attribute__((format(printf, 1, 2)))
static void
ctxerror(const char *fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	vfprintf(stderr, fmt, ap);
	fprintf(stderr, "\n");
	va_end(ap);
}

static C9aux *
srv(char *s)
{
	C9aux *c;
	int f;

	if((f = dial(s)) < 0)
		return NULL;
	if((c = calloc(1, sizeof(*c))) == NULL){
		close(f);
		return NULL;
	}
	c->f = f;
	c->c.read = ctxread;
	c->c.begin = ctxbegin;
	c->c.end = ctxend;
	c->c.error = ctxerror;
	c->c.aux = c;

	return c;
}

static C9aux *
registry(void)
{
	C9aux *a;
	C9tag tag;
	int i;

	for(i = 0; i < 10; i++){
//		if((a = srv(regsrv)) == NULL){
			if((a = srv(chatsrv)) != NULL){
				a->c.r = ctxchatR;
				c9version(&a->c, &tag, Msize);
				wrsend(a);
				return a;
			}
//		}else{
//			break;
//		}
		sleep(10);
	}

	if(a == NULL)
		return NULL;
	a->c.r = ctxregistryR;
	c9version(&a->c, &tag, Msize);
	wrsend(a);
	while(c9proc(&a->c) == 0 && a->c.r == ctxregistryR)
		wrsend(a);

	return a;
}

static int
chatrw(C9aux *a)
{
	struct timeval t;
	int n, sz, sz0;
	fd_set r, e;
	C9tag tag;
	C9ctx *ctx;
	char *s;

	FD_ZERO(&r);
	FD_SET(a->f, &r);
	FD_SET(0, &r);
	FD_ZERO(&e);
	FD_SET(a->f, &e);
	FD_SET(0, &e);
	memset(&t, 0, sizeof(t));
	t.tv_sec = 10;
	for(;;){
		errno = 0;
		if(select(a->f + 1, &r, NULL, &e, &t) < 0 || FD_ISSET(a->f, &e) || FD_ISSET(0, &e)){
			if(errno == EINTR)
				continue;
			return -1;
		}
		break;
	}

	ctx = &a->c;
	if(FD_ISSET(a->f, &r)){
		c9proc(ctx);
	}else if(FD_ISSET(0, &r)){
		s = (char*)a->rdbuf;
		sz = sz0 = sprintf(s, "%s → ", nick);
		for(;;){
			if((n = read(0, s+sz, sizeof(a->rdbuf)-sz)) > 0)
				sz += n;
			else
				exit(0);
			if(s[sz-1] != '\n'){
				s[sz-1] = '\n';
			}else{
				if(s[sz0] != '\n')
					c9write(ctx, &tag, Chatfid, 0, s, sz);
				break;
			}
		}
	}else{
		const char *path[] = {NULL};
		c9walk(ctx, &tag, Rootfid, Rootfid, path);
	}

	return 0;
}

int
main(int argc, char **argv)
{
	struct parg_state ps;
	struct termios t;
	C9aux *a;
	int c, noecho;

	parg_init(&ps);
	noecho = 0;
	while((c = parg_getopt(&ps, argc, argv, "hjedc:")) >= 0){
		switch(c){
		case 1:
			if(nick != NULL){
				fprintf(stderr, "only one nickname can be specified\n");
				return 1;
			}
			nick = ps.optarg;
			break;
		case 'c':
			channel = ps.optarg;
			break;
		case 'j':
			printjoin = 1;
			break;
		case 'e':
			noecho = 1;
			break;
		case 'h':
			fprintf(stderr, "usage: 9gc [-j] [-e] [-c CHANNEL] NICKNAME\n");
			return 0;
			break;
		case '?':
			fprintf(stderr, "unknown option -%c\n", ps.optopt);
			return 1;
			break;
		default:
			fprintf(stderr, "unhandled option -%c\n", c);
			return 1;
			break;
		}
	}

	if(nick == NULL){
		fprintf(stderr, "no nickname specified\n");
		return 1;
	}

	if(noecho && tcgetattr(0, &t) == 0){
		t.c_lflag &= ~ECHO;
		tcsetattr(STDIN_FILENO, TCSANOW, &t);
	}

	for(;;){
		if((a = registry()) == NULL)
			return 1;
		while (chatrw(a) == 0 && wrsend(a) == 0);
		if(a->flags & (Disconnected|Error)){
			a->flags &= ~(Disconnected|Error);
			skipuntil = chatoff;
			chatoff = 0;
			free(a);
		}else{
			printf("exiting\n");
			exit(1);
		}
	}

	return 0;
}
