#include <u.h>
#include <libc.h>
#include <ip.h>
#include <bio.h>
#include <ndb.h>
#include <ctype.h>
#include "dat.h"

/*
 *  format of a binding entry:
 *	char ipaddr[32];
 *	char id[32];
 *	char hwa[32];
 *	char otime[10];
 */
Binding *bcache;
uchar bfirst[IPaddrlen];
char *binddir = "/lib/ndb/dhcp";

/*
 *  convert a client id to a string.  If it's already
 *  ascii, leave it be.  Otherwise, convert it to hex.
 */
extern char*
toid(uchar *p, int n)
{
	static char id[Maxstr];
	int i;

	for(i = 0; i < n && isprint(p[i]); i++)
		;
	if(i == n)
		snprint(id, sizeof(id), "%.*s", n, (char*)p);
	else
		snprint(id, sizeof(id), "id%.*lH", n, p);
	return id;
}

/*
 *  increment an ip address
 */
static void
incip(uchar *ip)
{
	int i, x;

	for(i = IPaddrlen-1; i >= 0; i--){
		x = ip[i];
		x++;
		ip[i] = x;
		if((x & 0x100) == 0)
			break;
	}
}

/*
 *  find a binding for an id or hardware address
 */
static int
lockopen(char *file)
{
	char err[ERRMAX];
	int fd, tries;

	for(tries = 0; tries < 5; tries++){
		fd = open(file, ORDWR);
		if(fd >= 0)
			return fd;
		errstr(err, sizeof err);
		if(strstr(err, "lock")){
			/* wait for other process to let go of lock */
			sleep(250);

			/* try again */
			continue;
		}
		if(strstr(err, "exist")){
			/* no file, create an exclusive access file */
			fd = create(file, ORDWR, DMEXCL|0664);
			if(fd >= 0)
				return fd;
		}
	}
	return -1;
}

void
setbinding(Binding *b, char *id, long t)
{
	if(b->boundto)
		free(b->boundto);

	b->boundto = strdup(id);
	b->lease = t;
}

static void
parsebinding(Binding *b, char *buf)
{
	long t;
	char *id, *p;

	/* parse */
	t = atoi(buf);
	id = strchr(buf, '\n');
	if(id){
		*id++ = 0;
		p = strchr(id, '\n');
		if(p)
			*p = 0;
	} else
		id = "";

	/* replace any past info */
	setbinding(b, id, t);
}

static int
writebinding(int fd, Binding *b)
{
	Dir *d;

	seek(fd, 0, 0);
	if(fprint(fd, "%ld\n%s\n", b->lease, b->boundto) < 0)
		return -1;
	d = dirfstat(fd);
	if(d == nil)
		return -1;
	b->q.type = d->qid.type;
	b->q.path = d->qid.path;
	b->q.vers = d->qid.vers;
	free(d);
	return 0;
}

/*
 *  synchronize cached binding with file.  the file always wins.
 */
int
syncbinding(Binding *b, int returnfd)
{
	char buf[512];
	int i, fd;
	Dir *d;

	snprint(buf, sizeof(buf), "%s/%I", binddir, b->ip);
	fd = lockopen(buf);
	if(fd < 0){
		/* assume someone else is using it */
		b->lease = time(0) + OfferTimeout;
		return -1;
	}

	/* reread if changed */
	d = dirfstat(fd);
	if(d != nil)	/* BUG? */
	if(d->qid.type != b->q.type || d->qid.path != b->q.path || d->qid.vers != b->q.vers){
		i = read(fd, buf, sizeof(buf)-1);
		if(i < 0)
			i = 0;
		buf[i] = 0;
		parsebinding(b, buf);
		b->lasttouched = d->mtime;
		b->q.path = d->qid.path;
		b->q.vers = d->qid.vers;
	}

	free(d);

	if(returnfd)
		return fd;

	close(fd);
	return 0;
}

extern int
samenet(uchar *ip, Info *iip)
{
	uchar net[IPaddrlen];

	/* directly connected, check local networks */
	if(iip->ifc != nil)
		return ipremoteonifc(iip->ifc, ip) != nil;

	/* relay agent, check upstream network */
	maskip(ip, iip->ipmask, net);
	return ipcmp(net, iip->ipnet) == 0;
}

/*
 *  create a record for each binding
 */
extern void
initbinding(uchar *first, int n)
{
	while(n-- > 0){
		iptobinding(first, 1);
		incip(first);
	}
}

/*
 *  find a binding for a specific ip address
 */
extern Binding*
iptobinding(uchar *ip, int mk)
{
	Binding *b;

	for(b = bcache; b; b = b->next){
		if(ipcmp(b->ip, ip) == 0){
			syncbinding(b, 0);
			return b;
		}
	}

	if(mk == 0)
		return 0;
	b = malloc(sizeof(*b));
	memset(b, 0, sizeof(*b));
	ipmove(b->ip, ip);
	b->next = bcache;
	bcache = b;
	syncbinding(b, 0);
	return b;
}

static void
lognolease(Binding *b)
{
	/* renew the old binding, and hope it eventually goes away */
	b->offer = 5*60;
	commitbinding(b);

	/* complain if we haven't in the last 5 minutes */
	if(now - b->lastcomplained < 5*60)
		return;
	syslog(0, blog, "dhcp: lease for %I to %s ended at %ld but still in use",
		b->ip, b->boundto != nil ? b->boundto : "?", b->lease);
	b->lastcomplained = now;
}

/*
 *  find a free binding for a hw addr or id on the same network as iip
 */
extern Binding*
idtobinding(char *id, Info *iip, int ping)
{
	Binding *b, *oldest;
	int oldesttime;

	/*
	 *  first look for an old binding that matches.  that way
	 *  clients will tend to keep the same ip addresses.
	 */
	for(b = bcache; b; b = b->next){
		if(b->boundto && strcmp(b->boundto, id) == 0){
			if(!samenet(b->ip, iip))
				continue;

			/* check with the other servers */
			syncbinding(b, 0);
			if(strcmp(b->boundto, id) == 0)
				return b;
		}
	}

	/*
	 *  look for oldest binding that we think is unused
	 */
	for(;;){
		oldest = nil;
		oldesttime = 0;
		for(b = bcache; b; b = b->next){
			if(b->tried != now)
			if(b->lease < now && b->expoffer < now && samenet(b->ip, iip))
			if(oldest == nil || b->lasttouched < oldesttime){
				/* sync and check again */
				syncbinding(b, 0);
				if(b->lease < now && b->expoffer < now && samenet(b->ip, iip))
				if(oldest == nil || b->lasttouched < oldesttime){
					oldest = b;
					oldesttime = b->lasttouched;
				}
			}
		}
		if(oldest == nil)
			break;

		/* make sure noone is still using it */
		oldest->tried = now;
		if(ping == 0 || icmpecho(oldest->ip) == 0)
			return oldest;

		lognolease(oldest);	/* sets lastcomplained */
	}

	/* try all bindings */
	for(b = bcache; b; b = b->next){
		syncbinding(b, 0);
		if(b->tried != now)
		if(b->lease < now && b->expoffer < now && samenet(b->ip, iip)){
			b->tried = now;
			if(ping == 0 || icmpecho(b->ip) == 0)
				return b;

			lognolease(b);
		}
	}

	/* nothing worked, give up */
	return 0;
}

/*
 *  create an offer
 */
extern void
mkoffer(Binding *b, char *id, long leasetime)
{
	if(leasetime <= 0){
		if(b->lease > now + minlease)
			leasetime = b->lease - now;
		else
			leasetime = minlease;
	}
	if(b->offeredto)
		free(b->offeredto);
	b->offeredto = strdup(id);
	b->offer = leasetime;
	b->expoffer = now + OfferTimeout;
}

/*
 *  find an offer for this id
 */
extern Binding*
idtooffer(char *id, Info *iip)
{
	Binding *b;

	/* look for an offer to this id */
	for(b = bcache; b; b = b->next){
		if(b->offeredto && strcmp(b->offeredto, id) == 0 && samenet(b->ip, iip)){
			/* make sure some other system hasn't stolen it */
			syncbinding(b, 0);
			if(b->lease < now
			|| (b->boundto && strcmp(b->boundto, b->offeredto) == 0))
				return b;
		}
	}
	return 0;
}

/*
 *  commit a lease, this could fail
 */
extern int
commitbinding(Binding *b)
{
	int fd;
	long now;

	now = time(0);

	if(b->offeredto == 0)
		return -1;
	fd = syncbinding(b, 1);
	if(fd < 0)
		return -1;
	if(b->lease > now && b->boundto && strcmp(b->boundto, b->offeredto) != 0){
		close(fd);
		return -1;
	}
	setbinding(b, b->offeredto, now + b->offer);
	b->lasttouched = now;
	
	if(writebinding(fd, b) < 0){
		close(fd);
		return -1;
	}
	close(fd);
	return 0;
}

/*
 *  commit a lease, this could fail
 */
extern int
releasebinding(Binding *b, char *id)
{
	int fd;
	long now;

	now = time(0);

	fd = syncbinding(b, 1);
	if(fd < 0)
		return -1;
	if(b->lease > now && b->boundto && strcmp(b->boundto, id) != 0){
		close(fd);
		return -1;
	}
	b->lease = 0;
	b->expoffer = 0;
	
	if(writebinding(fd, b) < 0){
		close(fd);
		return -1;
	}
	close(fd);
	return 0;
}

int
validip(uchar *ip)
{
	if(ipcmp(ip, v4prefix) == 0)
		return 0;
	return isv4(ip);
}

int
validipmask(uchar *mask)
{
	unsigned x;

	if(memcmp(mask, IPallbits, IPv4off) != 0)
		return 0;
	x = ~(mask[IPv4off+0] << 24 | mask[IPv4off+1] << 16 | mask[IPv4off+2] << 8 | mask[IPv4off+3]);
	return ((x + 1U) & x) == 0;
}
