/*
 * © Copyright 1996-2012 ECMWF.
 *
 * This software is licensed under the terms of the Apache Licence Version 2.0
 * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
 * In applying this licence, ECMWF does not waive the privileges and immunities
 * granted to it by virtue of its status as an intergovernmental organisation nor
 * does it submit to any jurisdiction.
 */

#include <arpa/inet.h>
#include <ctype.h>
#include <errno.h>
#include <netdb.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <signal.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include "mars.h"
#if defined(sgi) || defined(fujitsu) || defined(sun)
#include <sys/filio.h> /* For FIONREAD */
#endif
#ifdef sgi
#include <bstring.h>
#endif

int _tcpdbg = 0;

static void pbuf(char* name, char* buf, int len) {
    if (_tcpdbg) {
        int col = 0;
        printf("%s len = %d\n", name, len);
        while (len--) {
            if (isprint(*buf)) {
                putchar(*buf);
                if ((++col % 60) == 0)
                    putchar('\n');
            }
            buf++;
        }
        putchar('\n');
    }
}

void socket_buffers(int s) {
    int flg              = 0;
    socklen_t flgsize    = sizeof(flg);
    static boolean quiet = false;
#if defined(__linux__)
    // For Linux we ignore if the effective size is 2x what is set
    // see Linux 'man 7 socket'
    // when set using setsockopt() the Linux kernel doubles the socket buffer size
    // to allow space for bookkeeping overhead and  this  doubled  value  is
    // returned  by  getsockopt(). The minimum (doubled) value for this option is 2048.
    int buffer_is_twice_the_size = 2;
#else
    int buffer_is_twice_the_size = 1;
#endif

    if (mars.so_sndbuf) {
        if (!quiet) {
            if (getsockopt(s, SOL_SOCKET, SO_SNDBUF, &flg, &flgsize) < 0)
                marslog(LOG_WARN | LOG_PERR, "getsockopt SO_SNDBUF");
            flg /= buffer_is_twice_the_size;

            marslog(LOG_INFO, "Setting SO_SNDBUF to %d (%s)", mars.so_sndbuf, bytename(mars.so_sndbuf));
            marslog(LOG_INFO, "Current value is %d (%s)", flg, bytename(flg));
        }
        if (setsockopt(s, SOL_SOCKET, SO_SNDBUF, &mars.so_sndbuf, sizeof(mars.so_sndbuf)) < 0)
            marslog(LOG_WARN | LOG_PERR, "setsockopt SO_SNDBUF");

        if (getsockopt(s, SOL_SOCKET, SO_SNDBUF, &flg, &flgsize) < 0)
            marslog(LOG_WARN | LOG_PERR, "getsockopt SO_SNDBUF");

        flg /= buffer_is_twice_the_size;

        if (flg != mars.so_sndbuf) {
            if (!quiet) {
                marslog(LOG_WARN, "SO_SNDBUF limited by kernel to %d (%s)",
                        flg, bytename(flg));
            }
        }
    }

    if (mars.so_rcvbuf) {
        if (!quiet) {
            if (getsockopt(s, SOL_SOCKET, SO_RCVBUF, &flg, &flgsize) < 0)
                marslog(LOG_WARN | LOG_PERR, "getsockopt SO_RCVBUF");
            flg /= buffer_is_twice_the_size;

            marslog(LOG_INFO, "Setting SO_RCVBUF to %d (%s)", mars.so_rcvbuf, bytename(mars.so_rcvbuf));
            marslog(LOG_INFO, "Current value is %d (%s)", flg, bytename(flg));
        }
        if (setsockopt(s, SOL_SOCKET, SO_RCVBUF, &mars.so_rcvbuf, sizeof(mars.so_rcvbuf)) < 0)
            marslog(LOG_WARN | LOG_PERR, "setsockopt SO_RCVBUF");

        if (getsockopt(s, SOL_SOCKET, SO_RCVBUF, &flg, &flgsize) < 0)
            marslog(LOG_WARN | LOG_PERR, "getsockopt SO_RCVBUF");

        flg /= buffer_is_twice_the_size;

        if (flg != mars.so_rcvbuf) {
            if (!quiet) {
                marslog(LOG_WARN, "SO_RCVBUF limited by kernel to %d (%s)", flg, bytename(flg));
            }
        }
    }

    /* See https://linux.die.net/man/7/tcp */


#ifdef TCP_MAXSEG
    if (mars.tcp_maxseg) {
        if (!quiet) {
            if (getsockopt(s, IPPROTO_TCP, TCP_MAXSEG, &flg, &flgsize) < 0)
                marslog(LOG_WARN | LOG_PERR, "getsockopt TCP_MAXSEG");

            marslog(LOG_INFO, "Setting TCP_MAXSEG to %d (%s)", mars.tcp_maxseg, bytename(mars.tcp_maxseg));
            marslog(LOG_INFO, "Current value is %d (%s)", flg, bytename(flg));
        }
        if (setsockopt(s, IPPROTO_TCP, TCP_MAXSEG, &mars.tcp_maxseg, sizeof(mars.tcp_maxseg)) < 0)
            marslog(LOG_WARN | LOG_PERR, "setsockopt TCP_MAXSEG");

        if (getsockopt(s, IPPROTO_TCP, TCP_MAXSEG, &flg, &flgsize) < 0)
            marslog(LOG_WARN | LOG_PERR, "getsockopt TCP_MAXSEG");

        if (flg != mars.tcp_maxseg) {
            if (!quiet) {
                marslog(LOG_WARN, "TCP_MAXSEG limited by kernel to %d (%s)", flg, bytename(flg));
            }
        }
    }
#endif

#ifdef TCP_WINDOW_CLAMP
    if (mars.tcp_window_clamp) {
        if (!quiet) {
            if (getsockopt(s, IPPROTO_TCP, TCP_WINDOW_CLAMP, &flg, &flgsize) < 0)
                marslog(LOG_WARN | LOG_PERR, "getsockopt TCP_WINDOW_CLAMP");
            marslog(LOG_INFO, "Setting TCP_WINDOW_CLAMP to %d (%s)", mars.tcp_window_clamp, bytename(mars.tcp_window_clamp));
            marslog(LOG_INFO, "Current value is %d (%s)", flg, bytename(flg));
        }

        if (setsockopt(s, IPPROTO_TCP, TCP_WINDOW_CLAMP, &mars.tcp_window_clamp, sizeof(mars.tcp_window_clamp)) < 0)
            marslog(LOG_WARN | LOG_PERR, "setsockopt TCP_WINDOW_CLAMP");

        if (getsockopt(s, IPPROTO_TCP, TCP_WINDOW_CLAMP, &flg, &flgsize) < 0)
            marslog(LOG_WARN | LOG_PERR, "getsockopt TCP_WINDOW_CLAMP");

        if (flg != mars.tcp_window_clamp) {
            if (!quiet) {
                marslog(LOG_WARN, "TCP_WINDOW_CLAMP limited by kernel to %d (%s)", flg, bytename(flg));
            }
        }
    }
#endif


#ifdef TCP_CONGESTION
    if (mars.tcp_congestion) {
        socklen_t len;
        char congestion[1024];

        if (!quiet) {
            len = sizeof(congestion) - 1;
            strcpy(congestion, "unknown");
            if (getsockopt(s, IPPROTO_TCP, TCP_CONGESTION, congestion, &len) < 0)
                marslog(LOG_WARN | LOG_PERR, "getsockopt TCP_CONGESTION");
            marslog(LOG_INFO, "Setting TCP_CONGESTION to '%s'", mars.tcp_congestion);
            marslog(LOG_INFO, "Current value is '%s'", congestion);
        }

        if (setsockopt(s, IPPROTO_TCP, TCP_CONGESTION, mars.tcp_congestion, strlen(mars.tcp_congestion)) < 0) {
            if (!quiet) {
                marslog(LOG_WARN | LOG_PERR, "setsockopt TCP_CONGESTION '%s'", mars.tcp_congestion);
                marslog(LOG_WARN, "To find out about valid values:");
                marslog(LOG_WARN, "cat /proc/sys/net/ipv4/tcp_available_congestion_control");
            }
        }

        len = sizeof(congestion) - 1;
        strcpy(congestion, "unknown");
        if (getsockopt(s, IPPROTO_TCP, TCP_CONGESTION, congestion, &len) < 0)
            marslog(LOG_WARN | LOG_PERR, "getsockopt TCP_CONGESTION");

        if (!EQ(congestion, mars.tcp_congestion)) {
            if (!quiet) {
                marslog(LOG_WARN, "TCP_CONGESTION limited by kernel to '%s'", congestion);
            }
        }
    }
#endif

    quiet = true;
}

int writetcp(void* p, void* buf, int len) {
    size_t i;
    size_t cnt;
    int soc = *(int*)p;
    char* b = (char*)buf;

    pbuf("writetcp", (char*)buf, len);

    for (cnt = len; cnt > 0; cnt -= i, b += i)
        switch (i = write(soc, b, cnt)) {
            case 0:
                /* marslog(LOG_EROR,"tcp write: premature eof"); */
                return -1;
                /* break; */

            case -1:
                /* marslog(LOG_EROR|LOG_PERR,"tcp write failed"); */
                return -1;
                /* break; */
        }


    return len;
}

boolean tcp_read_ready(int soc) {
    struct timeval timeout = {
        0,
        100,
    }; /* 1 10th sec */
    fd_set readfds;

    FD_ZERO(&readfds);
    FD_SET(soc, &readfds);

    for (;;)
        switch (select(FD_SETSIZE,
                       &readfds,
                       NULL, NULL, &timeout)) {
                /* time out */
            case 0:
                return false;
                /* break; */

            case -1:
                if (errno != EINTR) {
                    marslog(LOG_EROR | LOG_PERR, "select");
                    return true;
                }
                break;

            default:
                return true;
                /* break; */
        }
}

int readtcp(void* p, void* buf, int len) {
    struct timeval timeout = {20, 0}; /* 20 sec. */
    int soc                = *(int*)p;
    int l                  = len;
    fd_set readfds;

    if (l == 0)
        return 0;

    FD_ZERO(&readfds);
    FD_SET(soc, &readfds);


    if (select(FD_SETSIZE,
               &readfds, NULL, NULL, &timeout)
        == 0) {
        /* after 20 sec. , send 0 bytes */
        if (write(soc, buf, 0) != 0) {
            marslog(LOG_EROR, "tcp read: write(0) failed");
            return 0;
        }
    }


    switch (l = read(soc, buf, l)) {
        case 0:
            /* marslog(LOG_EROR,"tcp read: premature eof"); */
            return -1;
            /* break; */

        case -1:
            /* marslog(LOG_EROR|LOG_PERR,"tcp read failed"); */
            return -1;
            /* break; */
    }
    pbuf("readtcp", (char*)buf, l);

    return l;
}

boolean is_hostname(const char* host) {
#ifdef INADDR_NONE

    in_addr_t addr;
    in_addr_t none = INADDR_NONE;

#else

#ifdef __alpha
    unsigned int none            = (unsigned int)~0;
#elif defined(fujitsu)
    u_int none = (u_int)~0;
#elif defined(__64BIT__)
    uint32_t none = (uint32_t)-1;
#else
    unsigned long none = (unsigned long)-1;
#endif

#endif

    if (inet_addr(host) != none)
        return true;

    return (gethostbyname(host) != NULL);
}

void traceroute(struct sockaddr_in* from) {
    char* tracecmd = getenv("MARS_TRACEROUTE");

    if (!tracecmd)
        return;

    if (from->sin_family == AF_INET) {
        char* net = inet_ntoa(from->sin_addr);
        struct hostent* remote;
        char cmd[2048];

        remote = gethostbyaddr((char*)&from->sin_addr,
                               sizeof(from->sin_addr),
                               from->sin_family);

        if (remote) {
            sprintf(cmd, "%s %s", tracecmd, remote->h_name);
            if (system(cmd) != 0) {
                marslog(LOG_EROR | LOG_PERR, "Command \"%s\" failed", cmd);
            }
        }
    }
}


static int _call_server(const char* host, int port, int retries, const request* config) {
    struct sockaddr_in s_in;
    struct hostent* him;
    int s;
    int status;
    int tries = 0;
    int flg;

    char buf[1024];

#ifdef INADDR_NONE

    in_addr_t addr;
    in_addr_t none = INADDR_NONE;


#else

#ifdef __alpha
    unsigned int addr;
    unsigned int none = (unsigned int)~0;
#elif defined(fujitsu)
    u_int addr;
    u_int none = (u_int)~0;
#elif defined(__64BIT__)
    unsigned long addr;
    uint32_t none = (uint32_t)-1;
#else
    unsigned long addr;
    unsigned long none = (unsigned long)-1;
#endif

#endif

    boolean quiet = (retries <= 0);

    if (strchr(host, ':')) {
        char* p = strchr(host, ':');
        bzero(buf, sizeof(buf));
        strncpy(buf, host, p - host);
        port = atoi(p + 1);
        host = buf;
    }

    if (retries <= 0)
        retries = -retries;

    bzero(&s_in, sizeof(s_in));

    s_in.sin_port   = htons(port);
    s_in.sin_family = AF_INET;

    marslog(LOG_DBUG, "Calling \"%s\" port %d", host, port);

    addr                 = inet_addr(host);
    s_in.sin_addr.s_addr = addr;

    if (addr == none) {
        if ((him = gethostbyname(host)) == NULL) {
            marslog(LOG_EROR, "unknown host : %s", host);
            return -1;
        }

        s_in.sin_family = him->h_addrtype;

        bcopy(him->h_addr_list[0], &s_in.sin_addr, him->h_length);
    }

    do {

        s = socket(AF_INET, SOCK_STREAM, 0);
        if (s < 0) {
            marslog(LOG_EROR | LOG_PERR, "socket");
            return (-1);
        }

        socket_buffers(s);

        if (getenv("MARS_BIND")) {
            struct sockaddr_in sin;
            memset(&sin, 0, sizeof(struct sockaddr_in));
            sin.sin_port   = htons(0);
            sin.sin_family = AF_INET;

            marslog(LOG_INFO, "Binding socket to %s", getenv("MARS_BIND"));
            sin.sin_addr.s_addr = inet_addr(getenv("MARS_BIND"));

            if (bind(s, (struct sockaddr*)&sin, sizeof(sin)) == -1) {
                marslog(LOG_EROR | LOG_PERR, "bind");
                close(s);
                return -1;
            }
        }

        status = connect(s, (struct sockaddr*)&s_in, sizeof(s_in));

        if (status < 0) {
            if (!quiet)
                marslog(LOG_WARN | LOG_PERR, "connect : %s %d", host, port);
            socket_close(s);

            if (retries != 0 && ++tries >= retries) {
                if (!quiet)
                    marslog(LOG_EROR, "To many retries. Giving up.");
                return -1;
            }

            sleep(5);
        }
        else
            marslog(LOG_DBUG, "Connected to address %s",
                    inet_ntoa(s_in.sin_addr));

    } while (status < 0);

    flg = 1;
    if (setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &flg, sizeof(flg)) < 0)
        marslog(LOG_WARN | LOG_PERR, "setsockopt SO_KEEPALIVE");

    /* Dont't get killed by pipes */
    signal(SIGPIPE, SIG_IGN);

    return (s); /* OK */
}

int call_server(const char* host, int port, int retries, const char* proxy) {
    const request* config = NULL;
    if (proxy) {
        config = find_proxy(proxy);
        if (!config) {
            marslog(LOG_EXIT, "Cannot find proxy %s", proxy);
            return -1;
        }
    }
    tcp_connector connect = config ? proxy_for(config) : &_call_server;
    return (*connect)(host, port, retries, config);
}

static void bytes_left(int s) {
    int nbytes = 0;

    if (s == -1)
        return;

    if (ioctl(s, FIONREAD, &nbytes) == -1)
        marslog(LOG_PERR, "Cannot determine socket status");
    else if (nbytes)
        marslog(LOG_WARN, "Closing connection with %d byte%s outstanding",
                nbytes, nbytes > 1 ? "s" : "");
}

int socket_close(int s) {
    bytes_left(s);
    return close(s);
}

int socket_file_close(FILE* f) {
    bytes_left(fileno(f));
    return fclose(f);
}
