#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 byte array to hex
*/
static char
hex(int x)
{
       if(x < 10)
               return x + '0';
       return x - 10 + 'a';
}
extern char*
tohex(char *hdr, uchar *p, int len)
{
       char *s, *sp;
       int hlen;

       hlen = strlen(hdr);
       s = malloc(hlen + 2*len + 1);
       sp = s;
       strcpy(sp, hdr);
       sp += hlen;
       for(; len > 0; len--){
               *sp++ = hex(*p>>4);
               *sp++ = hex(*p & 0xf);
               p++;
       }
       *sp = 0;
       return s;
}

/*
*  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)
{
       int i;
       char *s;

       for(i = 0; i < n; i++)
               if(!isprint(p[i]))
                       return tohex("id", p, n);
       s = malloc(n + 1);
       memmove(s, p, n);
       s[n] = 0;
       return s;
}

/*
*  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;
               }
       }
       if(tries >= 5)
               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.
*/
static 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 x[IPaddrlen];

       maskip(iip->ipmask, ip, x);
       return ipcmp(x, 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\n",
               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;
}