#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include "evas_cs.h"

#ifdef EVAS_CSERVE

EAPI Server *
evas_cserve_server_add(void)
{
   Server *s;
   char buf[PATH_MAX];
   struct sockaddr_un socket_unix;
   struct linger lin;
   mode_t pmode;
   int socket_unix_len;
   
   s = calloc(1, sizeof(Server));
   if (!s) return NULL;
   s->ch[0].fd = -1;
   snprintf(buf, sizeof(buf), "/tmp/.evas-cserve-%x", getuid());
   s->socket_path = strdup(buf);
   if (!s->socket_path)
     {
        free(s);
        return NULL;
     }
   pmode = umask(~(S_IRUSR | S_IWUSR));
   start:
   s->ch[0].fd = socket(AF_UNIX, SOCK_STREAM, 0);
   if (s->ch[0].fd < 0) goto error;
   if (fcntl(s->ch[0].fd, F_SETFL, O_NONBLOCK) < 0) goto error;
   if (fcntl(s->ch[0].fd, F_SETFD, FD_CLOEXEC) < 0) goto error;
   lin.l_onoff = 1;
   lin.l_linger = 0;
   if (setsockopt(s->ch[0].fd, SOL_SOCKET, SO_LINGER, &lin, sizeof(struct linger)) < 0)
     goto error;
   socket_unix.sun_family = AF_UNIX;
   strncpy(socket_unix.sun_path, buf, sizeof(socket_unix.sun_path));
   socket_unix_len = LENGTH_OF_SOCKADDR_UN(&socket_unix);
   if (bind(s->ch[0].fd, (struct sockaddr *)&socket_unix, socket_unix_len) < 0)
     {
        if ((connect(s->ch[0].fd, (struct sockaddr *)&socket_unix, socket_unix_len) < 0) &&
            (unlink(s->socket_path) >= 0))
          {
             close(s->ch[0].fd);
             goto start;
          }
        else
          goto error;
     }
   if (listen(s->ch[0].fd, 4096) < 0) goto error;
   umask(pmode);
   return s;
   error:
   umask(pmode);
   if (s->ch[0].fd >= 0) close(s->ch[0].fd);
   free(s->socket_path);
   free(s);
   return NULL;
}

EAPI void
evas_cserve_server_del(Server *s)
{
   Client *c;
   
   EINA_LIST_FREE(s->clients, c)
     {
        LKL(c->lock);
        close(c->fd);
        if (c->buf) free(c->buf);
        if (c->inbuf) free(c->inbuf);
        LKD(c->lock);
        free(c);
     }
   close(s->ch[0].fd);
   unlink(s->socket_path);
   free(s->socket_path);
   free(s);
}

static void
server_accept(Server *s)
{
   Client *c;
   int new_fd;
   struct sockaddr_in incoming;
   size_t size_in;
   
   size_in = sizeof(struct sockaddr_in);
   new_fd = accept(s->ch[0].fd, (struct sockaddr *)&incoming, (socklen_t *)&size_in);
   if (new_fd < 0) return;
   fcntl(new_fd, F_SETFL, O_NONBLOCK);
   fcntl(new_fd, F_SETFD, FD_CLOEXEC);
   c = calloc(1, sizeof(Client));
   if (!c)
     {
        close(new_fd);
        return;
     }
   c->server = s;
   c->fd = new_fd;
   LKI(c->lock);
   s->clients = eina_list_append(s->clients, c);
}

static void
client_flush(Client *c)
{
   int num;
   
   num = write(c->fd, c->buf, c->bufsize);
   if (num < 0)
     {
        c->dead = 1;
        return;
     }
   if (num < c->bufsize)
     {
        unsigned char *buf;
        
        buf = malloc(c->bufsize - num);
        if (buf)
          {
             memcpy(buf, c->buf + num, c->bufsize - num);
             free(c->buf);
             c->bufsize = c->bufsize - num;
             c->bufalloc = c->bufsize;
             c->buf = buf;
          }
     }
   else
     {
        free(c->buf);
        c->buf = NULL;
        c->bufsize = 0;
        c->bufalloc = 0;
     }
}

static void
client_buf_add(Client *c, unsigned char *data, int size)
{
   int newsize;
   unsigned char *buf;

   newsize = c->bufsize + size;
   if (newsize > c->bufalloc)
     {
        c->bufalloc = newsize + 16384;
        buf = realloc(c->buf, c->bufalloc);
        if (buf) c->buf = buf;
        else return;
     }
   memcpy(c->buf + c->bufsize, data, size);
   c->bufsize += size;
}

static void
client_write(Client *c, unsigned char *data, int size)
{
   int num;
   
   if (!c->buf)
     {
        num = write(c->fd, data, size);
        if (num != size)
          client_buf_add(c, data + num, size - num);
     }
   else
     {
        client_buf_add(c, data, size);
     }
}

EAPI void
evas_cserve_client_send(Client *c, int opcode, int size, unsigned char *data)
{
   unsigned char *data2;
   int *ints;
   
   data2 = malloc(size + (sizeof(int) * 3));
   if (!data2) return;
   ints = (int *)data2;
   ints[0] = size;
   ints[1] = opcode;
//   LKL(c->lock);
   c->req_to++;
   ints[2] = c->req_to;
   memcpy(data2 + (sizeof(int) * 3), data, size);
   client_write(c, data2, size + (sizeof(int) * 3));
//   LKU(c->lock);
   free(data2);
}

static void
server_message_handle(Server *s, Client *c, int opcode, int size, unsigned char *data)
{
   if (s->func) s->func(s->data, s, c, opcode, size, data);
}

EAPI void
evas_cserve_server_message_handler_set(Server *s, int (*func) (void *fdata, Server *s, Client *c, int opcode, int size, unsigned char *data), void *data)
{
   s->func = func;
   s->data = data;
}

static int
server_parse(Server *s, Client *c)
{
   int *ints;
   unsigned char *data, *newbuf;

   if (c->inbufsize < (int)sizeof(int)) return 0;
   ints = (int *)((c->inbuf));
   if ((ints[0] < 0) || (ints[0] > (1024 * 1024)))
     return 0;
   if (c->inbufsize < (ints[0] + ((int)sizeof(int) * 3)))
     {
        return 0;
     }
   data = c->inbuf + (sizeof(int) * 3);
   if (ints[2] != (c->req_from + 1))
     {
        ERR("EEK! sequence number mismatch from client with pid: %i."
               "---- num %i is not 1 more than %i"
               ,
               c->pid, ints[2], c->req_from);
        return 0;
     }
   c->req_from++;
   server_message_handle(s, c, ints[1], ints[0], data);
   c->inbufalloc -= ints[0] + (sizeof(int) * 3);
   if (c->inbufalloc == 0)
     {
        free(c->inbuf);
        c->inbuf = NULL;
        c->inbufsize = 0;
        return 0;
     }
   newbuf = malloc(c->inbufalloc);
   if (!newbuf)
     {
        c->inbufalloc += ints[0] + (sizeof(int) * 3);
        /* fixme - bad situation */
        return 0;
     }
   memcpy(newbuf, c->inbuf + ints[0] + (sizeof(int) * 3), c->inbufalloc);
   c->inbufsize -= ints[0] + (sizeof(int) * 3);
   free(c->inbuf);
   c->inbuf = newbuf;
   return 1;
}

static void
server_data(Server *s, Client *c, unsigned char *data, int size)
{
   if (!c->inbuf)
     {
        c->inbuf = malloc(size);
        if (c->inbuf)
          {
             memcpy(c->inbuf, data, size);
             c->inbufalloc = size;
             c->inbufsize = size;
          }
        else
          {
             /* fixme - bad situation */
             return;
          }
     }
   else
     {
        int size2;
        
        size2 = c->inbufsize + size;
        if (size2 > c->inbufalloc)
          {
             unsigned char *newbuf;
             
             c->inbufalloc = size2;
             newbuf = realloc(c->inbuf, c->inbufalloc);
             if (newbuf) c->inbuf = newbuf;
             else size2 = 0;
          }
        if (size2 > 0)
          {
             memcpy(c->inbuf + c->inbufsize, data, size);
             c->inbufsize = size2;
          }
        else
          {
             /* fixme - bad situation */
             return;
          }
     }
   while (server_parse(s, c));
}

EAPI void
evas_cserve_server_wait(Server *s, int timeout)
{
   fd_set rset, wset, xset;
   int maxfd;
   int ret;
   struct timeval to;
   Eina_List *l, *dead = NULL;
   Client *c;
   
   maxfd = 0;
   FD_ZERO(&rset);
   FD_ZERO(&wset);
   FD_ZERO(&xset);
   FD_SET(s->ch[0].fd, &rset);
   if (s->ch[0].fd > maxfd) maxfd = s->ch[0].fd;
   EINA_LIST_FOREACH(s->clients, l, c)
     {
        FD_SET(c->fd, &rset);
        if (c->buf)
          FD_SET(c->fd, &wset);
        if (c->fd > maxfd) maxfd = c->fd;
     }
   if (timeout > 0)
     {
        to.tv_sec = timeout / 1000000;
        to.tv_usec = timeout % 1000000;
        ret = select(maxfd + 1, &rset, &wset, &xset, &to);
     }
   else
     ret = select(maxfd + 1, &rset, &wset, &xset, NULL);
   if (ret < 1) return;
   
   EINA_LIST_FOREACH(s->clients, l, c)
     {
        if (c->dead) continue;
        if (FD_ISSET(c->fd, &rset))
          {
	     unsigned char buf[16384];
             int num;
             
             errno = 0;
             num = read(c->fd, buf, sizeof(buf));
             if (num <= 0)
               {
                  c->dead = 1;
                  dead = eina_list_append(dead, c);
               }
             else if (num > 0)
               {
                  server_data(s, c, buf, num);
               }
          }
        else if (FD_ISSET(c->fd, &wset))
          {
             client_flush(c);
             if (c->dead) dead = eina_list_append(dead, c);
          }
     }
   if (FD_ISSET(s->ch[0].fd, &rset))
     {
        server_accept(s);
     }
   EINA_LIST_FREE(dead, c)
     {
        LKL(c->lock);
        if (c->func) c->func(c->data, c);
        s->clients = eina_list_remove(s->clients, c);
        close(c->fd);
        if (c->buf) free(c->buf);
        if (c->inbuf) free(c->inbuf);
        LKD(c->lock);
        free(c);
     }
}

#endif
