#include <stdint.h>

#include "socket.h"


#ifdef _WIN32

/*** BEGIN WIN32 CODE ***/

socket_t socket_open_nonblocking(int domain, int type, int protocol) {

    // Open socket

    SOCKET sock = socket(domain, type, protocol);
    if (sock == INVALID_SOCKET) {
        return -1;
    }

    // Set to non-blocking
    unsigned long one = 1;
    ioctlsocket(sock, FIONBIO, &one);

    return sock;

}

int socket_connect_nonblocking(socket_t sock, const struct sockaddr *addr, socklen_t addrlen) {

    int status = connect(sock, addr, addrlen);
    int error = WSAGetLastError();

    if (status && (error == WSAEWOULDBLOCK)) {
        return 0;
    }

    return error;

}

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-variable"

int socket_set_tcp_options(socket_t sock, DWORD nodelay, DWORD keepalive, DWORD keepidle, DWORD keepintvl, DWORD keepcnt) {

    /*** WARNING ***

        Setting the options below appear to have no effect on Windows...
        (at least not when compiling with MinGW-w64)

        So, this function doesn't really do anything on Windows.
        I'm leaving it here for reference, but its functionality shouldn't be relied upon.

    */

    if (setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (const char *)&nodelay, sizeof(DWORD)) < 0) {
        return WSAGetLastError();
    }

    // Keep-Alive:
    // Starting with Windows 10 version 1703, setsockopt() can be used like on Linux.
    // With older versions, we're slightly out of luck...

    #if defined SO_KEEPALIVE && defined TCP_KEEPIDLE && defined TCP_KEEPINTVL && defined TCP_KEEPCNT

        if (setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, (const char *)&keepalive, sizeof(DWORD)) < 0) {
            return WSAGetLastError();
        }

        if (keepalive) {
            if (setsockopt(sock, IPPROTO_TCP, TCP_KEEPIDLE, (const char *)&keepidle, sizeof(DWORD)) < 0) {
                return WSAGetLastError();
            }
            if (setsockopt(sock, IPPROTO_TCP, TCP_KEEPINTVL, (const char *)&keepintvl, sizeof(DWORD)) < 0) {
                return WSAGetLastError();
            }
            if (setsockopt(sock, IPPROTO_TCP, TCP_KEEPCNT, (const char *)&keepcnt, sizeof(DWORD)) < 0) {
                return WSAGetLastError();
            }
        }

    #else

        /*
            Setting the keep-alive packet count (TCP_KEEPCNT) is not supported !!

            On Windows Vista and later, the number of keep-alive probes (data retransmissions) is
            set to 10 and cannot be changed.

            On Windows Server 2003, Windows XP, and Windows 2000, the default setting for number of
            keep-alive probes is 5. The number of keep-alive probes is controllable through the
            TcpMaxDataRetransmissions and PPTPTcpMaxDataRetransmissions registry settings.

            Source: https://docs.microsoft.com/en-us/windows/win32/winsock/sio-keepalive-vals
        */

        struct tcp_keepalive opts = {
            .onoff = keepalive,
            .keepalivetime = keepidle * 1000,
            .keepaliveinterval = keepintvl * 1000
        };

        DWORD result;

        if (WSAIoctl(sock, SIO_KEEPALIVE_VALS, &opts, sizeof(opts), NULL, 0, &result, NULL, NULL)) {
            return WSAGetLastError();
        }

    #endif

    return 0;

}

#pragma GCC diagnostic pop

/*** END WIN32 CODE ***/

#else

/*** BEGIN LINUX/UNIX CODE ***/

#include <errno.h>

socket_t socket_open_nonblocking(int domain, int type, int protocol) {

    socket_t sock;

    #ifdef SOCK_NONBLOCK
        sock = socket(domain, type | SOCK_NONBLOCK, protocol);
    #else
        sock = socket(domain, type, protocol);
        if (sock != -1) {
            int status = fcntl(sock, F_SETFL, fcntl(sock, F_GETFL, 0) | O_NONBLOCK);
            if (status == -1) {
                close(sock);
                return -1;
            }
        }
    #endif

    return sock;

}

int socket_connect_nonblocking(socket_t sock, const struct sockaddr *addr, socklen_t addrlen) {

    errno = 0;

    int status = connect(sock, addr, addrlen);

    if (! (status && (errno == EINPROGRESS))) {
        return errno;
    }

    return 0;

}

int socket_set_tcp_options(socket_t sock, int nodelay, int keepalive, int keepidle, int keepintvl, int keepcnt) {

    if (setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(int)) < 0) {
        return errno;
    }

    if (setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(int)) < 0) {
        return errno;
    }

    if (keepalive) {

        #ifdef __APPLE__
            if (setsockopt(sock, IPPROTO_TCP, TCP_KEEPALIVE, &keepidle, sizeof(int)) < 0) {
                return errno;
            }
        #else
            if (setsockopt(sock, IPPROTO_TCP, TCP_KEEPIDLE, &keepidle, sizeof(int)) < 0) {
                return errno;
            }
        #endif

        if (setsockopt(sock, IPPROTO_TCP, TCP_KEEPINTVL, &keepintvl, sizeof(int)) < 0) {
            return errno;
        }

        if (setsockopt(sock, IPPROTO_TCP, TCP_KEEPCNT, &keepcnt, sizeof(int)) < 0) {
            return errno;
        }

        int timeout_s = keepidle + (keepintvl * keepcnt);

        #if defined TCP_USER_TIMEOUT
            int timeout_ms = timeout_s * 1000;
            if (setsockopt(sock, IPPROTO_TCP, TCP_USER_TIMEOUT, &timeout_ms, sizeof(int)) < 0) {
                return errno;
            }
        #elif defined TCP_RXT_CONNDROPTIME
            if (setsockopt(sock, IPPROTO_TCP, TCP_RXT_CONNDROPTIME, &timeout_s, sizeof(int)) < 0) {
                return errno;
            }
        #endif

    }

    return 0;

}

/*** END LINUX/UNIX CODE ***/

#endif
