/* vim: set sw=4 ts=4 sts=4 et: */
#define _POSIX_C_SOURCE 200809L
#define _XOPEN_SOURCE 700
#include "l4posix.h"

#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

char* lbs_posix_strcat (const char* str, ...) {
    va_list ap;
    char* strp;
    char* strnow;
    char* newstr;
    int len = strlen (str);

    va_start (ap, str);
    while ((strp = va_arg (ap, char*)) != NULL) {
        len += strlen (strp);
    }
    va_end (ap);

    newstr = malloc (len + 1);

    va_start (ap, str);
    strnow = stpcpy (newstr, str);
    while ((strp = va_arg (ap, char*)) != NULL) {
        strnow = stpcpy (strnow, strp);
    }
    newstr[len] = '\0';
    va_end (ap);

    return newstr;
}

int lbs_posix_add_fd (int fd, int fdflags) {
    int orgfd = fcntl (fd, F_GETFD);
    if (orgfd < 0) {
        return -1;
    }
    return fcntl (fd, F_SETFD, orgfd | fdflags);
}

int lbs_posix_del_fd (int fd, int fdflags) {
    int orgfd = fcntl (fd, F_GETFD);
    if (orgfd < 0) {
        return -1;
    }
    return fcntl (fd, F_SETFD, orgfd & ~(fdflags));
}

int lbs_posix_add_fl (int fd, int flflags) {
    int orgfl = fcntl (fd, F_GETFL);
    if (orgfl < 0) {
        return -1;
    }
    return fcntl (fd, F_SETFL, orgfl | flflags);
}

int lbs_posix_del_fl (int fd, int flflags) {
    int orgfl = fcntl (fd, F_GETFL);
    if (orgfl < 0) {
        return -1;
    }
    return fcntl (fd, F_SETFL, orgfl & ~(flflags));
}

char* lbs_posix_readlink (const char* filename) {
    struct stat st;
    if (lstat (filename, &st) < 0) {
        return NULL;
    }
    size_t bufsize = st.st_size ? st.st_size : 8192;
    char* buf = malloc (bufsize + 1);
    if (buf == NULL) {
        return NULL;
    }
    ssize_t written = readlink (filename, buf, bufsize);
    if (written < 0) {
        free (buf);
        return NULL;
    }
    buf[written] = '\0';
    return buf;
}

char* lbs_posix_getcwd (void) {
    char *cwd, *result;
    size_t size = pathconf (".", _PC_PATH_MAX);

    size = size > 0 ? size : 256;
    size = size > 8192 ? 8192 : size;
    cwd = malloc (sizeof (char) * size);

    while ((result = getcwd (cwd, size)) == NULL && errno == ERANGE) {
        size *= 2;
        cwd = realloc (cwd, size);
    }

    return cwd;
}

size_t lbs_posix_write_all (int fd, const char* str, size_t size) {
    ssize_t wtn = 0;
    if (size <= 0) {
        size = strlen (str);
    }
    size_t rem = size;
    while (rem > 0) {
        wtn = write (fd, str, rem);
        if (wtn < 0) {
            if (errno != EINTR && errno != EAGAIN) {
                break;
            }
            continue;
        }
        str += wtn;
        rem -= wtn;
    }

    rem = rem > 0 ? rem : 0;
    return size - rem;
}

void lbs_posix_buffer_init (LbsPosixBuffer* buf) {
    buf->buf_start = 0;
    buf->buf_len = 0;
    buf->buf_line = NULL;
    buf->buf_line_len = 0;
    buf->buf_error = false;
    buf->buf_eof = false;
}

void lbs_posix_buffer_clear (LbsPosixBuffer* buf, bool initial) {
    buf->buf_start = 0;
    buf->buf_len = 0;

    if (!initial) {
        free (buf->buf_line);
    }
    buf->buf_line = NULL;
    buf->buf_line_len = 0;
    buf->buf_error = false;
    buf->buf_eof = false;
}

char* lbs_posix_buffer_getline (int fd, LbsPosixBuffer* buf, int delim) {
    if (buf->buf_error || buf->buf_eof) {
        return NULL;
    }

    if (buf->buf_len == 0) {
        int rval = read (fd, buf->buf, LBS_POSIX_BUFFER_SIZE);
        if (rval < 0) {
            if (errno != EAGAIN && errno != EINTR) {
                buf->buf_error = true;
            }
            return NULL;
        }
        if (rval == 0) {
            buf->buf_eof = true;
            return NULL;
        }
        buf->buf_start = 0;
        buf->buf_len = rval;
    }

    int i;
    for (i = 0; i < buf->buf_len; i++) {
        if (buf->buf[buf->buf_start + i] == delim) {
            break;
        }
    }

    int buf_line_start = buf->buf_line_len;
    buf->buf_line_len += i;
    buf->buf_line = realloc (buf->buf_line, buf->buf_line_len + 1);
    memcpy (buf->buf_line + buf_line_start, buf->buf + buf->buf_start, i);
    buf->buf_line[buf->buf_line_len] = '\0';

    /* remove CR if delim is LF and delim is found */
    if (i < buf->buf_len && delim == '\n' && buf->buf_line_len - 1 >= 0 &&
        buf->buf_line[buf->buf_line_len - 1] == '\r') {
        buf->buf_line[buf->buf_line_len - 1] = '\0';
        buf->buf_line_len--;
    }

    int buf_len_saved = buf->buf_len;
    buf->buf_start += i + 1;
    buf->buf_len -= i + 1;
    if (buf->buf_len <= 0) {
        buf->buf_start = 0;
        buf->buf_len = 0;
    }

    if (i < buf_len_saved) {
        /* delim is found */
        char* newstr = buf->buf_line;
        buf->buf_line = NULL;
        buf->buf_line_len = 0;
        memmove (buf->buf, buf->buf + buf->buf_start, buf->buf_len);
        buf->buf_start = 0;
        return newstr;
    }

    return NULL;
}

#define SOCKADDR(x)     ((struct sockaddr*)(x))
#define SOCKADDR_UN(x)  ((struct sockaddr_un*)(x))
#define SOCKADDR_IN(x)  ((struct sockaddr_in*)(x))
#define SOCKADDR_IN6(x) ((struct sockaddr_in6*)(x))

static char* lbs_posix_socket_name (
    int sockfd, int (*getter) (int, struct sockaddr*, socklen_t*)) {
    
    struct sockaddr_storage sock;
    socklen_t socklen = sizeof (sock);

    memset (&sock, 0, socklen);
    if ((*getter)(sockfd, SOCKADDR (&sock), &socklen) < 0) {
        return strdup ("invalid socket");
    }

    int domain = sock.ss_family;
    if (domain == AF_UNIX) {
        return strdup ("local process");
    }

    socklen_t ipstrlen;
    void* ipnet;
    char* ipstr;
    if (domain == AF_INET) {
        ipstrlen = INET_ADDRSTRLEN;
        ipnet = &(SOCKADDR_IN (&sock)->sin_addr);
    } else {
        ipstrlen = INET6_ADDRSTRLEN;
        ipnet = &(SOCKADDR_IN6 (&sock)->sin6_addr);
    }

    ipstr = malloc (ipstrlen);
    if (inet_ntop (domain, ipnet, ipstr, ipstrlen) == NULL) {
        free (ipstr);
        return strdup ("unknown address");
    }

    return ipstr;
}

char* lbs_posix_socket_sockname (int sockfd) {
    return lbs_posix_socket_name (sockfd, getsockname);
}

char* lbs_posix_socket_peername (int sockfd) {
    return lbs_posix_socket_name (sockfd, getpeername);
}

void lbs_posix_exchange_data (int fd[2], LbsPosixProgress prog_cb) {
    int nfds, active;
    fd_set rset, wset;
    fd_set rres, wres;
    LbsPosixBuffer buf[2];

    FD_ZERO (&rset);
    FD_SET (fd[0], &rset);
    FD_SET (fd[1], &rset);
    FD_ZERO (&wset);

    active = 2;
    nfds = (fd[0] > fd[1] ? fd[0] : fd[1]) + 1;
    lbs_posix_buffer_clear (&buf[0], true);
    lbs_posix_buffer_clear (&buf[1], true);

    while (active && (*prog_cb) (fd, buf)) {
        rres = rset;
        wres = wset;
        if (select (nfds, &rres, &wres, NULL, NULL) >= 0) {
            for (int i = 0; i < 2; i++) {
                if (buf[i].buf_len) {
                    /* read buffer full */
                    if (FD_ISSET (fd[!i], &wres)) {
                        int wb = write (fd[!i],
                            buf[i].buf + buf[i].buf_start, buf[i].buf_len);
                        if (wb > 0) {
                            buf[i].buf_start += wb;
                            buf[i].buf_len -= wb;
                            if (!buf[i].buf_len) {
                                FD_CLR (fd[!i], &wset);
                                FD_SET (fd[i], &rset);
                            }
                        } else {
                            if (wb < 0 && (errno == EAGAIN || errno == EINTR)) {
                                continue;
                            } else {
                                FD_CLR (fd[!i], &wset);
                                active = 0;
                            }
                        }
                    }
                } else {
                    /* read buffer empty */
                    if (FD_ISSET (fd[i], &rres)) {
                        int rb = read (fd[i], buf[i].buf, LBS_POSIX_BUFFER_SIZE);
                        if (rb > 0) {
                            buf[i].buf_start = 0;
                            buf[i].buf_len = rb;
                            FD_CLR (fd[i], &rset);
                            FD_SET (fd[!i], &wset);
                        } else {
                            if (rb < 0 && (errno == EAGAIN || errno == EINTR)) {
                                continue;
                            } else{
                                FD_CLR (fd[i], &rset);
                                active = 0;
                            }
                        }
                    }
                }
            }
        }
    }
}