#include "Limelight-internal.h"

#define TEST_PORT_TIMEOUT_SEC 3

#define RCV_BUFFER_SIZE_MIN  32767
#define RCV_BUFFER_SIZE_STEP 16384

#if defined(__vita__)
#define TCPv4_MSS 512
#else
#define TCPv4_MSS 536
#endif
#define TCPv6_MSS 1220

#if defined(LC_WINDOWS)

#ifndef SIO_UDP_CONNRESET
#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12)
#endif

static HMODULE WlanApiLibraryHandle;
static HANDLE WlanHandle;

#if defined(LC_WINDOWS_DESKTOP)
DWORD (WINAPI *pfnWlanOpenHandle)(DWORD dwClientVersion, PVOID pReserved, PDWORD pdwNegotiatedVersion, PHANDLE phClientHandle);
DWORD (WINAPI *pfnWlanCloseHandle)(HANDLE hClientHandle, PVOID pReserved);
DWORD (WINAPI *pfnWlanEnumInterfaces)(HANDLE hClientHandle, PVOID pReserved, PWLAN_INTERFACE_INFO_LIST *ppInterfaceList);
VOID (WINAPI *pfnWlanFreeMemory)(PVOID pMemory);
DWORD (WINAPI *pfnWlanSetInterface)(HANDLE hClientHandle, CONST GUID *pInterfaceGuid, WLAN_INTF_OPCODE OpCode, DWORD dwDataSize, CONST PVOID pData, PVOID pReserved);
#endif

#ifndef WLAN_API_MAKE_VERSION
#define WLAN_API_MAKE_VERSION(_major, _minor)   (((DWORD)(_minor)) << 16 | (_major))
#endif

#endif

#ifdef __3DS__
in_port_t n3ds_udp_port = 47998;
static const int n3ds_max_buf_size = 0x20000;
#endif

void addrToUrlSafeString(struct sockaddr_storage* addr, char* string, size_t stringLength)
{
    char addrstr[URLSAFESTRING_LEN];

#ifdef AF_INET6
    if (addr->ss_family == AF_INET6) {
        struct sockaddr_in6* sin6 = (struct sockaddr_in6*)addr;
        inet_ntop(addr->ss_family, &sin6->sin6_addr, addrstr, sizeof(addrstr));

        // IPv6 addresses need to be enclosed in brackets for URLs
        snprintf(string, stringLength, "[%s]", addrstr);
    }
    else
#endif
    {
        struct sockaddr_in* sin = (struct sockaddr_in*)addr;
        inet_ntop(addr->ss_family, &sin->sin_addr, addrstr, sizeof(addrstr));

        // IPv4 addresses are returned without changes
        snprintf(string, stringLength, "%s", addrstr);
    }
}

void shutdownTcpSocket(SOCKET s) {
    // Calling shutdown() prior to close wakes up callers
    // blocked in connect(), recv(), and friends.
    shutdown(s, SHUT_RDWR);
}

int setNonFatalRecvTimeoutMs(SOCKET s, int timeoutMs) {
#if defined(LC_WINDOWS)
    // Windows says that SO_RCVTIMEO puts the socket into an indeterminate state
    // when a timeout occurs. MSDN doesn't go into it any more than that, but it
    // seems likely that they are referring to the inability to know whether a
    // cancelled request consumed some data or not (very relevant for stream-based
    // protocols like TCP). Since our sockets are UDP which is already unreliable,
    // losing some data in a very rare case is fine, especially because we get to
    // halve the number of syscalls per packet by avoiding select().
    return setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeoutMs, sizeof(timeoutMs));
#elif defined(__WIIU__) || defined(__3DS__)
    // timeouts aren't supported on Wii U or 3DS
    return -1;
#else
    struct timeval val;

    val.tv_sec = 0;
    val.tv_usec = timeoutMs * 1000;

    return setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (char*)&val, sizeof(val));
#endif
}

int pollSockets(struct pollfd* pollFds, int pollFdsCount, int timeoutMs) {
#if defined(LC_WINDOWS)
    // We could have used WSAPoll() but it has some nasty bugs
    // https://daniel.haxx.se/blog/2012/10/10/wsapoll-is-broken/
    //
    // We'll emulate WSAPoll() with select(). Fortunately, Microsoft's definition
    // of fd_set does not have the same stack corruption hazards that UNIX does.
    fd_set readFds, writeFds, exceptFds;
    int i, err, nfds;
    struct timeval tv;

    FD_ZERO(&readFds);
    FD_ZERO(&writeFds);
    FD_ZERO(&exceptFds);

    nfds = 0;
    for (i = 0; i < pollFdsCount; i++) {
        // Clear revents on input like poll() does
        pollFds[i].revents = 0;

        if (pollFds[i].events & POLLIN) {
            FD_SET(pollFds[i].fd, &readFds);
        }
        if (pollFds[i].events & POLLOUT) {
            FD_SET(pollFds[i].fd, &writeFds);

            // Windows signals failed connections as an exception,
            // while Linux signals them as writeable.
            FD_SET(pollFds[i].fd, &exceptFds);
        }
    }

    tv.tv_sec = timeoutMs / 1000;
    tv.tv_usec = (timeoutMs % 1000) * 1000;

    err = select(nfds, &readFds, &writeFds, &exceptFds, timeoutMs >= 0 ? &tv : NULL);
    if (err <= 0) {
        // Error or timeout
        return err;
    }

    for (i = 0; i < pollFdsCount; i++) {
        if (FD_ISSET(pollFds[i].fd, &readFds)) {
            pollFds[i].revents |= POLLRDNORM;
        }

        if (FD_ISSET(pollFds[i].fd, &writeFds)) {
            pollFds[i].revents |= POLLWRNORM;
        }

        if (FD_ISSET(pollFds[i].fd, &exceptFds)) {
            pollFds[i].revents |= POLLERR;
        }
    }

    return err;
#elif defined(__3DS__)
    int err;
    u64 poll_start = osGetTime();
    for (u64 i = poll_start; (i - poll_start) < timeoutMs; i = osGetTime()) {
        err = poll(pollFds, pollFdsCount, 0); // This is running for 14ms
        if (err) {
            break;
        }
        svcSleepThread(1000);
    }
    return err;
#else
    return poll(pollFds, pollFdsCount, timeoutMs);
#endif
}

bool isSocketReadable(SOCKET s) {
    struct pollfd pfd;
    int err;

    pfd.fd = s;
    pfd.events = POLLIN;
    err = pollSockets(&pfd, 1, 0);
    if (err <= 0) {
        return false;
    }

    return true;
}

int recvUdpSocket(SOCKET s, char* buffer, int size, bool useSelect) {
    int err;

    do {
        if (useSelect) {
            struct pollfd pfd;

            // Wait up to 100 ms for the socket to be readable
            pfd.fd = s;
            pfd.events = POLLIN;
            err = pollSockets(&pfd, 1, UDP_RECV_POLL_TIMEOUT_MS);
            if (err <= 0) {
                // Return if an error or timeout occurs
                return err;
            }

            // This won't block since the socket is readable
            err = (int)recvfrom(s, buffer, size, 0, NULL, NULL);
        }
        else {
            // The caller has already configured a timeout on this
            // socket via SO_RCVTIMEO, so we can avoid a syscall
            // for each packet.
            err = (int)recvfrom(s, buffer, size, 0, NULL, NULL);
            if (err < 0 &&
                    (LastSocketError() == EWOULDBLOCK ||
                     LastSocketError() == EINTR ||
                     LastSocketError() == EAGAIN ||
         #if defined(LC_WINDOWS)
                     // This error is specific to overlapped I/O which isn't even
                     // possible to perform with recvfrom(). It seems to randomly
                     // be returned instead of WSAETIMEDOUT on certain systems.
                     LastSocketError() == WSA_IO_PENDING ||
         #endif
                     LastSocketError() == ETIMEDOUT)) {
                // Return 0 for timeout
                return 0;
            }
        }

    // We may receive an error due to a previous ICMP Port Unreachable error received
    // by this socket. We want to ignore those and continue reading. If the remote party
    // is really dead, ENet or TCP connection failures will trigger connection teardown.
#if defined(LC_WINDOWS)
    } while (err < 0 && LastSocketError() == WSAECONNRESET);
#else
    } while (err < 0 && LastSocketError() == ECONNREFUSED);
#endif

    return err;
}

void closeSocket(SOCKET s) {
#if defined(LC_WINDOWS)
    closesocket(s);
#else
    close(s);
#endif
}

// These set "safe" host or link-local QoS options that we can unconditionally
// set without having to worry about routers blockholing the traffic.
static void setSocketQos(SOCKET s, int socketQosType) {
#ifdef SO_NET_SERVICE_TYPE
    int value;
    switch (socketQosType) {
    case SOCK_QOS_TYPE_BEST_EFFORT:
        value = NET_SERVICE_TYPE_BE;
        break;
    case SOCK_QOS_TYPE_AUDIO:
        value = NET_SERVICE_TYPE_VO;
        break;
    case SOCK_QOS_TYPE_VIDEO:
        value = NET_SERVICE_TYPE_VI;
        break;
    default:
        Limelog("Unknown QoS type: %d\n", socketQosType);
        return;
    }

    // iOS/macOS
    if (setsockopt(s, SOL_SOCKET, SO_NET_SERVICE_TYPE, (char*)&value, sizeof(value)) < 0) {
        Limelog("setsockopt(SO_NET_SERVICE_TYPE, %d) failed: %d\n", value, (int)LastSocketError());
    }
#endif
#ifdef SO_PRIORITY
    int value;
    switch (socketQosType) {
    case SOCK_QOS_TYPE_BEST_EFFORT:
        value = 0;
        break;
    case SOCK_QOS_TYPE_AUDIO:
        value = 6;
        break;
    case SOCK_QOS_TYPE_VIDEO:
        value = 5;
        break;
    default:
        Limelog("Unknown QoS type: %d\n", socketQosType);
        return;
    }

    // Linux
    if (setsockopt(s, SOL_SOCKET, SO_PRIORITY, (char*)&value, sizeof(value)) < 0) {
        Limelog("setsockopt(SO_PRIORITY, %d) failed: %d\n", value, (int)LastSocketError());
    }
#endif
}

SOCKET bindUdpSocket(int addressFamily, struct sockaddr_storage* localAddr, SOCKADDR_LEN addrLen, int bufferSize, int socketQosType) {
    SOCKET s;
    LC_SOCKADDR bindAddr;
    int err;

    s = createSocket(addressFamily, SOCK_DGRAM, IPPROTO_UDP, false);
    if (s == INVALID_SOCKET) {
        return INVALID_SOCKET;
    }

    // Use localAddr to bind if it was provided
    if (localAddr && localAddr->ss_family != 0) {
        memcpy(&bindAddr, localAddr, addrLen);
        SET_PORT(&bindAddr, 0);
    }
    else {
        // Otherwise wildcard bind to the specified address family
        memset(&bindAddr, 0, sizeof(bindAddr));
        SET_FAMILY(&bindAddr, addressFamily);

#ifdef AF_INET6
        LC_ASSERT(addressFamily == AF_INET || addressFamily == AF_INET6);
        addrLen = (addressFamily == AF_INET ? sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6));
#else
        LC_ASSERT(addressFamily == AF_INET);
        addrLen = sizeof(struct sockaddr_in);
#endif
    }

#ifdef __3DS__
    // binding to wildcard port is broken on the 3DS, so we need to define a port manually
    struct sockaddr_in *n3ds_addr = &bindAddr;
    n3ds_addr->sin_port = htons(n3ds_udp_port++);
#endif
    if (bind(s, (struct sockaddr*) &bindAddr, addrLen) == SOCKET_ERROR) {
        err = LastSocketError();
        Limelog("bind() failed: %d\n", err);
        closeSocket(s);
        SetLastSocketError(err);
        return INVALID_SOCKET;
    }

#if defined(LC_DARWIN)
    {
        // Disable SIGPIPE on iOS
        int val = 1;
        setsockopt(s, SOL_SOCKET, SO_NOSIGPIPE, (char*)&val, sizeof(val));
    }
#elif defined(LC_WINDOWS)
    {
        // Disable WSAECONNRESET for UDP sockets on Windows
        BOOL val = FALSE;
        DWORD bytesReturned = 0;
        if (WSAIoctl(s, SIO_UDP_CONNRESET, &val, sizeof(val), NULL, 0, &bytesReturned, NULL, NULL) != 0) {
            Limelog("WSAIoctl(SIO_UDP_CONNRESET) failed: %d\n", LastSocketError());
        }
    }
#elif defined(__WIIU__)
    {
        // Enable usage of userbuffers on Wii U
        int val = 1;
        setsockopt(s, SOL_SOCKET, SO_RUSRBUF, &val, sizeof(val));
    }
#endif

    // Enable QOS for the socket (best effort)
    if (socketQosType != SOCK_QOS_TYPE_BEST_EFFORT) {
        setSocketQos(s, socketQosType);
    }

#ifdef __3DS__
    if (bufferSize == 0 || bufferSize > n3ds_max_buf_size)
        bufferSize = n3ds_max_buf_size;
#endif
    if (bufferSize != 0) {
        // We start at the requested recv buffer value and step down until we find
        // a value that the OS will accept.
        for (;;) {
            err = setsockopt(s, SOL_SOCKET, SO_RCVBUF, (char*)&bufferSize, sizeof(bufferSize));
            if (err == 0) {
                // Successfully set a buffer size
                break;
            }
            else if (bufferSize <= RCV_BUFFER_SIZE_MIN) {
                // Failed to set a buffer size within the allowable range
                Limelog("Set rcv buffer size failed: %d\n", LastSocketError());
                break;
            }
            else if (bufferSize - RCV_BUFFER_SIZE_STEP <= RCV_BUFFER_SIZE_MIN) {
                // Last shot - we're trying the minimum
                bufferSize = RCV_BUFFER_SIZE_MIN;
            }
            else {
                // Lower the requested size by another step
                bufferSize -= RCV_BUFFER_SIZE_STEP;
            }
        }

#if defined(LC_DEBUG)
        if (err == 0) {
            Limelog("Selected receive buffer size: %d\n", bufferSize);
        }
        else {
            Limelog("Unable to set receive buffer size: %d\n", LastSocketError());
        }

        {
            SOCKADDR_LEN len = sizeof(bufferSize);
            if (getsockopt(s, SOL_SOCKET, SO_RCVBUF, (char*)&bufferSize, &len) == 0) {
                Limelog("Actual receive buffer size: %d\n", bufferSize);
            }
        }
#endif
    }

    return s;
}

int setSocketNonBlocking(SOCKET s, bool enabled) {
#if defined(__vita__) || defined(__HAIKU__)
    int val = enabled ? 1 : 0;
    return setsockopt(s, SOL_SOCKET, SO_NONBLOCK, (char*)&val, sizeof(val));
#elif defined(O_NONBLOCK)
    return fcntl(s, F_SETFL, (enabled ? O_NONBLOCK : 0) | (fcntl(s, F_GETFL) & ~O_NONBLOCK));
#elif defined(FIONBIO)
#ifdef LC_WINDOWS
    u_long val = enabled ? 1 : 0;
#else
    int val = enabled ? 1 : 0;
#endif
    return ioctlsocket(s, FIONBIO, &val);
#else
#error Please define your platform non-blocking sockets API!
#endif
}

SOCKET createSocket(int addressFamily, int socketType, int protocol, bool nonBlocking) {
    SOCKET s;

    s = socket(addressFamily, socketType, protocol);
    if (s == INVALID_SOCKET) {
        Limelog("socket() failed: %d\n", (int)LastSocketError());
        return INVALID_SOCKET;
    }

#ifdef LC_DARWIN
    {
        // Disable SIGPIPE on iOS
        int val = 1;
        setsockopt(s, SOL_SOCKET, SO_NOSIGPIPE, (char*)&val, sizeof(val));
    }
#endif

    if (nonBlocking) {
        setSocketNonBlocking(s, true);
    }

    return s;
}

SOCKET connectTcpSocket(struct sockaddr_storage* dstaddr, SOCKADDR_LEN addrlen, unsigned short port, int timeoutSec) {
    SOCKET s;
    LC_SOCKADDR addr;
    struct pollfd pfd;
    int err;
    int val;

    // Create a non-blocking TCP socket
    s = createSocket(dstaddr->ss_family, SOCK_STREAM, IPPROTO_TCP, true);
    if (s == INVALID_SOCKET) {
        return INVALID_SOCKET;
    }

    // Some broken routers/firewalls (or routes with multiple broken routers) may result in TCP packets
    // being dropped without without us receiving an ICMP Fragmentation Needed packet. For example,
    // a router can elect to drop rather than fragment even without DF set. A misconfigured firewall
    // or router on the path back to us may block the ICMP Fragmentation Needed packet required for
    // PMTUD to work and thus we end up with a black hole route. Some OSes recover from this better
    // than others, but we can avoid the issue altogether by capping our MSS to the value mandated
    // by RFC 879 and RFC 2460.
    //
    // Note: This only changes the max packet size we can *receive* from the host PC.
    // We still must split our own sends into smaller chunks with TCP_NODELAY enabled to
    // avoid MTU issues on the way out to to the target.
#if defined(LC_WINDOWS)
    // Windows doesn't support setting TCP_MAXSEG but IP_PMTUDISC_DONT forces the MSS to the protocol
    // minimum which is what we want here. Linux doesn't do this (disabling PMTUD just avoids setting DF).
    if (dstaddr->ss_family == AF_INET) {
        val = IP_PMTUDISC_DONT;
        if (setsockopt(s, IPPROTO_IP, IP_MTU_DISCOVER, (char*)&val, sizeof(val)) < 0) {
            Limelog("setsockopt(IP_MTU_DISCOVER, IP_PMTUDISC_DONT) failed: %d\n", val, (int)LastSocketError());
        }
    }
    else {
        val = IP_PMTUDISC_DONT;
        if (setsockopt(s, IPPROTO_IPV6, IPV6_MTU_DISCOVER, (char*)&val, sizeof(val)) < 0) {
            Limelog("setsockopt(IPV6_MTU_DISCOVER, IP_PMTUDISC_DONT) failed: %d\n", val, (int)LastSocketError());
        }
    }
#elif defined(TCP_NOOPT)
    // On BSD-based OSes (including macOS/iOS), TCP_NOOPT seems to be the only way to
    // restrict MSS to the minimum. It strips all options out of the SYN packet which
    // forces the remote party to fall back to the minimum MSS. TCP_MAXSEG doesn't seem
    // to work correctly for outbound connections on macOS/iOS.
    val = 1;
    if (setsockopt(s, IPPROTO_TCP, TCP_NOOPT, (char*)&val, sizeof(val)) < 0) {
        Limelog("setsockopt(TCP_NOOPT, %d) failed: %d\n", val, (int)LastSocketError());
    }
#elif defined(TCP_MAXSEG)
    val = dstaddr->ss_family == AF_INET ? TCPv4_MSS : TCPv6_MSS;
    if (setsockopt(s, IPPROTO_TCP, TCP_MAXSEG, (char*)&val, sizeof(val)) < 0) {
        Limelog("setsockopt(TCP_MAXSEG, %d) failed: %d\n", val, (int)LastSocketError());
    }
#endif

    // Start connection
    memcpy(&addr, dstaddr, addrlen);
    SET_PORT(&addr, port);
    err = connect(s, (struct sockaddr*) &addr, addrlen);
    if (err < 0) {
        err = (int)LastSocketError();
        if (err != EWOULDBLOCK && err != EAGAIN && err != EINPROGRESS) {
            goto Exit;
        }
    }

    // Wait for the connection to complete or the timeout to elapse
    pfd.fd = s;
    pfd.events = POLLOUT;
    err = pollSockets(&pfd, 1, timeoutSec * 1000);
    if (err < 0) {
        // pollSockets() failed
        err = LastSocketError();
        Limelog("pollSockets() failed: %d\n", err);
        closeSocket(s);
        SetLastSocketError(err);
        return INVALID_SOCKET;
    }
    else if (err == 0) {
        // pollSockets() timed out
        Limelog("Connection timed out after %d seconds (TCP port %u)\n", timeoutSec, port);
        closeSocket(s);
        SetLastSocketError(ETIMEDOUT);
        return INVALID_SOCKET;
    }
#ifdef __3DS__ //SO_ERROR is unreliable on 3DS
    else {
        char test_buffer[1];
        err = (int)recv(s, test_buffer, 1, MSG_PEEK);
        if (err < 0 &&
            (LastSocketError() == EWOULDBLOCK ||
            LastSocketError() == EAGAIN)) {
            err = 0;
        }
    }
#else
    else {
        // The socket was signalled
        SOCKADDR_LEN len = sizeof(err);
        getsockopt(s, SOL_SOCKET, SO_ERROR, (char*)&err, &len);
        if (err != 0 || (pfd.revents & POLLERR)) {
            // Get the error code
            err = (err != 0) ? err : LastSocketFail();
        }
    }
#endif

    // Disable non-blocking I/O now that the connection is established
    setSocketNonBlocking(s, false);

Exit:
    if (err != 0) {
        Limelog("connect() failed: %d\n", err);
        closeSocket(s);
        SetLastSocketError(err);
        return INVALID_SOCKET;
    }

    return s;
}

// See TCP_MAXSEG note in connectTcpSocket() above for more information.
// TCP_NODELAY must be enabled on the socket for this function to work!
int sendMtuSafe(SOCKET s, char* buffer, int size) {
    int bytesSent = 0;

    while (bytesSent < size) {
        int bytesToSend = size - bytesSent > TCPv4_MSS ?
                          TCPv4_MSS : size - bytesSent;

        if (send(s, &buffer[bytesSent], bytesToSend, 0) < 0) {
            return -1;
        }

        bytesSent += bytesToSend;
    }

    return bytesSent;
}

int enableNoDelay(SOCKET s) {
    int err;
    int val;

    val = 1;
    err = setsockopt(s, IPPROTO_TCP, TCP_NODELAY, (char*)&val, sizeof(val));
    if (err == SOCKET_ERROR) {
        return LastSocketError();
    }

    return 0;
}

static bool isPrivateNetworkAddressV4(struct sockaddr_in* address, bool matchCGN)
{
    unsigned int addr;

    memcpy(&addr, &address->sin_addr, sizeof(addr));
    addr = htonl(addr);

    // 10.0.0.0/8
    if ((addr & 0xFF000000) == 0x0A000000) {
        return true;
    }
    // 172.16.0.0/12
    else if ((addr & 0xFFF00000) == 0xAC100000) {
        return true;
    }
    // 192.168.0.0/16
    else if ((addr & 0xFFFF0000) == 0xC0A80000) {
        return true;
    }
    // 169.254.0.0/16
    else if ((addr & 0xFFFF0000) == 0xA9FE0000) {
        return true;
    }
    // 100.64.0.0/10
    else if (matchCGN && (addr & 0xFFC00000) == 0x64400000) {
        return true;
    }
    else {
        return false;
    }
}

int resolveHostName(const char* host, int family, int tcpTestPort, struct sockaddr_storage* addr, SOCKADDR_LEN* addrLen)
{
    struct addrinfo hints, *res, *currentAddr;
    int err;
    bool needsFallbackV4 = false;

    memset(&hints, 0, sizeof(hints));
    hints.ai_family = family;
    hints.ai_flags = AI_ADDRCONFIG;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    err = getaddrinfo(host, NULL, &hints, &res);
    if (err != 0) {
        Limelog("getaddrinfo(%s) failed: %d\n", host, err);
        return err;
    }
    else if (res == NULL) {
        Limelog("getaddrinfo(%s) returned success without addresses\n", host);
        return -1;
    }

#ifdef AF_INET6
    {
        struct sockaddr_in sin4;

        memset(&sin4, 0, sizeof(sin4));
        sin4.sin_family = AF_INET;

        // As a workaround for broken 464XLAT on iOS where the CLAT synthesizes IPv6
        // addresses for on-link IPv4 destinations on other interfaces, we will try
        // again with an IPv4-only resolution if we detect that getaddrinfo() resolved
        // a private IPv4 address to a single IPv6 address and that address didn't work.
        needsFallbackV4 =
            family == AF_UNSPEC &&
            res->ai_family == AF_INET6 &&
            res->ai_next == NULL &&
            inet_pton(AF_INET, host, &sin4.sin_addr) == 1 &&
            isPrivateNetworkAddressV4(&sin4, true);
    }
#endif

    for (currentAddr = res; currentAddr != NULL; currentAddr = currentAddr->ai_next) {
        // Use the test port to ensure this address is working if:
        // a) We have multiple addresses
        // b) The caller asked us to test even with a single address
        // c) We got an IPv6 address synthesized from an IPv4 address
        if (tcpTestPort != 0 && (res->ai_next != NULL || (tcpTestPort & TCP_PORT_FLAG_ALWAYS_TEST) || needsFallbackV4)) {
            SOCKET testSocket = connectTcpSocket((struct sockaddr_storage*)currentAddr->ai_addr,
                                                 (SOCKADDR_LEN)currentAddr->ai_addrlen,
                                                 tcpTestPort & TCP_PORT_MASK,
                                                 TEST_PORT_TIMEOUT_SEC);
            if (testSocket == INVALID_SOCKET) {
                // Try the next address
                continue;
            }
            else {
                closeSocket(testSocket);
            }
        }

        memcpy(addr, currentAddr->ai_addr, currentAddr->ai_addrlen);
        *addrLen = (SOCKADDR_LEN)currentAddr->ai_addrlen;

        freeaddrinfo(res);
        return 0;
    }

    freeaddrinfo(res);

    if (needsFallbackV4) {
        // Fallback to IPv4-only if we didn't find a working address (see comment above)
        return resolveHostName(host, AF_INET, tcpTestPort, addr, addrLen);
    }
    else {
        Limelog("No working addresses found for host: %s\n", host);
        return -1;
    }
}

#ifdef AF_INET6
bool isInSubnetV6(struct sockaddr_in6* sin6, unsigned char* subnet, int prefixLength) {
    int i;

    for (i = 0; i < prefixLength; i++) {
        unsigned char mask = 1 << (i % 8);
        if ((sin6->sin6_addr.s6_addr[i / 8] & mask) != (subnet[i / 8] & mask)) {
            return false;
        }
    }

    return true;
}
#endif

bool isPrivateNetworkAddress(struct sockaddr_storage* address) {
    if (address->ss_family == AF_INET) {
        return isPrivateNetworkAddressV4((struct sockaddr_in*)address, false);
    }
#ifdef AF_INET6
    else if (address->ss_family == AF_INET6) {
        struct sockaddr_in6* sin6 = (struct sockaddr_in6*)address;
        static unsigned char linkLocalPrefix[] = {0xfe, 0x80};
        static unsigned char siteLocalPrefix[] = {0xfe, 0xc0};
        static unsigned char uniqueLocalPrefix[] = {0xfc, 0x00};

        // fe80::/10
        if (isInSubnetV6(sin6, linkLocalPrefix, 10)) {
            return true;
        }
        // fec0::/10
        else if (isInSubnetV6(sin6, siteLocalPrefix, 10)) {
            return true;
        }
        // fc00::/7
        else if (isInSubnetV6(sin6, uniqueLocalPrefix, 7)) {
            return true;
        }
    }
#endif

    return false;
}

bool isNat64SynthesizedAddress(struct sockaddr_storage* address) {
#ifdef AF_INET6
    if (address->ss_family == AF_INET6) {
        struct sockaddr_in6* sin6 = (struct sockaddr_in6*)address;
        struct addrinfo hints, *res, *currentAddr;
        int err;

        memset(&hints, 0, sizeof(hints));
        hints.ai_family = AF_INET6;
        hints.ai_flags = AI_ADDRCONFIG;
        hints.ai_socktype = SOCK_STREAM;
        hints.ai_protocol = IPPROTO_TCP;
        err = getaddrinfo("ipv4only.arpa.", NULL, &hints, &res);
        if (err != 0) {
            Limelog("Client is not running in NAT64 environment (%d)\n", err);
            return false;
        }
        else if (res == NULL) {
            Limelog("getaddrinfo(ipv4only.arpa.) returned success without addresses\n");
            return false;
        }

        for (currentAddr = res; currentAddr != NULL; currentAddr = currentAddr->ai_next) {
            struct sockaddr_in6* candidate6 = (struct sockaddr_in6*)currentAddr->ai_addr;
            static const unsigned char wellKnownAddresses[2][4] = {
                { 0xC0, 0x00, 0x00, 0xAA }, // 192.0.0.170
                { 0xC0, 0x00, 0x00, 0xAB }, // 192.0.0.171
            };

            if (candidate6->sin6_family != AF_INET6) {
                // This shouldn't be possible but check anyway
                continue;
            }

            for (int i = 0; i < 2; i++) {
                int foundCount = 0;
                int prefixLen = 0;
                int suffixStart = 0;

                // Search for each well-known IPv4 address at all locations specified by
                // https://datatracker.ietf.org/doc/html/rfc6052#section-2.2
                if (memcmp(&candidate6->sin6_addr.s6_addr[4], wellKnownAddresses[i], 4) == 0) {
                    foundCount++;

                    prefixLen = 4;
                    suffixStart = 9;
                }
                if (memcmp(&candidate6->sin6_addr.s6_addr[5], &wellKnownAddresses[i][0], 3) == 0 &&
                    memcmp(&candidate6->sin6_addr.s6_addr[9], &wellKnownAddresses[i][3], 1) == 0) {
                    foundCount++;

                    prefixLen = 5;
                    suffixStart = 10;
                }
                if (memcmp(&candidate6->sin6_addr.s6_addr[6], &wellKnownAddresses[i][0], 2) == 0 &&
                    memcmp(&candidate6->sin6_addr.s6_addr[9], &wellKnownAddresses[i][2], 2) == 0) {
                    foundCount++;

                    prefixLen = 6;
                    suffixStart = 11;
                }
                if (memcmp(&candidate6->sin6_addr.s6_addr[7], &wellKnownAddresses[i][0], 1) == 0 &&
                    memcmp(&candidate6->sin6_addr.s6_addr[9], &wellKnownAddresses[i][1], 3) == 0) {
                    foundCount++;

                    prefixLen = 7;
                    suffixStart = 12;
                }
                if (memcmp(&candidate6->sin6_addr.s6_addr[9], &wellKnownAddresses[i], 4) == 0) {
                    foundCount++;

                    prefixLen = 8;
                    suffixStart = 13;
                }
                if (memcmp(&candidate6->sin6_addr.s6_addr[12], &wellKnownAddresses[i], 4) == 0) {
                    foundCount++;

                    prefixLen = 12;
                    suffixStart = 16;
                }

                // We must find the well-known address exactly once. If we find it zero or multiple
                // times, we must try the second well-known address or other AAAA records.
                if (foundCount != 1) {
                    continue;
                }

                // We have a valid NAT64 address identified, so we know we're running in an NAT64 environment.
                //
                // Now we must check to see if the address we resolved for the remote host actually falls
                // within the NAT64 range to see if we must restrict ourselves to the IPv4 MTU.
                if (memcmp(&sin6->sin6_addr.s6_addr[0], &candidate6->sin6_addr.s6_addr[0], prefixLen) == 0 &&
                    (suffixStart == 16 || memcmp(&sin6->sin6_addr.s6_addr[suffixStart],
                                                 &candidate6->sin6_addr.s6_addr[suffixStart],
                                                 16 - suffixStart) == 0)) {
                    freeaddrinfo(res);
                    return true;
                }
                else {
                    // This one didn't match, so let's break out of the loop and try the next AAAA record.
                    break;
                }
            }
        }

        freeaddrinfo(res);
        return false;
    }
#endif

    return false;
}

// Enable platform-specific low latency options (best-effort)
void enterLowLatencyMode(void) {
#if defined(LC_WINDOWS_DESKTOP)
    DWORD negotiatedVersion;
    PWLAN_INTERFACE_INFO_LIST wlanInterfaceList;
    DWORD i;

    // Reduce timer period to increase wait precision
    timeBeginPeriod(1);

    // Load wlanapi.dll dynamically because it will not always be present on Windows Server SKUs.
    WlanApiLibraryHandle = LoadLibraryA("wlanapi.dll");
    if (WlanApiLibraryHandle == NULL) {
        Limelog("WLANAPI is not supported on this OS\n");
        return;
    }

    pfnWlanOpenHandle = (void*)GetProcAddress(WlanApiLibraryHandle, "WlanOpenHandle");
    pfnWlanCloseHandle = (void*)GetProcAddress(WlanApiLibraryHandle, "WlanCloseHandle");
    pfnWlanFreeMemory = (void*)GetProcAddress(WlanApiLibraryHandle, "WlanFreeMemory");
    pfnWlanEnumInterfaces = (void*)GetProcAddress(WlanApiLibraryHandle, "WlanEnumInterfaces");
    pfnWlanSetInterface = (void*)GetProcAddress(WlanApiLibraryHandle, "WlanSetInterface");

    if (pfnWlanOpenHandle == NULL || pfnWlanCloseHandle == NULL ||
            pfnWlanFreeMemory == NULL || pfnWlanEnumInterfaces == NULL || pfnWlanSetInterface == NULL) {
        LC_ASSERT(pfnWlanOpenHandle != NULL);
        LC_ASSERT(pfnWlanCloseHandle != NULL);
        LC_ASSERT(pfnWlanFreeMemory != NULL);
        LC_ASSERT(pfnWlanEnumInterfaces != NULL);
        LC_ASSERT(pfnWlanSetInterface != NULL);

        // This should never happen since that would mean Microsoft removed a public API, but
        // we'll check and fail gracefully just in case.
        FreeLibrary(WlanApiLibraryHandle);
        WlanApiLibraryHandle = NULL;
        return;
    }

    // Use the Vista+ WLAN API version
    LC_ASSERT(WlanHandle == NULL);
    if (pfnWlanOpenHandle(WLAN_API_MAKE_VERSION(2, 0), NULL, &negotiatedVersion, &WlanHandle) != ERROR_SUCCESS) {
        WlanHandle = NULL;
        return;
    }

    if (pfnWlanEnumInterfaces(WlanHandle, NULL, &wlanInterfaceList) != ERROR_SUCCESS) {
        pfnWlanCloseHandle(WlanHandle, NULL);
        WlanHandle = NULL;
        return;
    }

    for (i = 0; i < wlanInterfaceList->dwNumberOfItems; i++) {
        if (wlanInterfaceList->InterfaceInfo[i].isState == wlan_interface_state_connected) {
            DWORD error;
            BOOL value;

            // Enable media streaming mode for 802.11 wireless interfaces to reduce latency and
            // unneccessary background scanning operations that cause packet loss and jitter.
            //
            // https://docs.microsoft.com/en-us/windows-hardware/drivers/network/oid-wdi-set-connection-quality
            // https://docs.microsoft.com/en-us/previous-versions/windows/hardware/wireless/native-802-11-media-streaming
            value = TRUE;
            error = pfnWlanSetInterface(WlanHandle, &wlanInterfaceList->InterfaceInfo[i].InterfaceGuid,
                                        wlan_intf_opcode_media_streaming_mode, sizeof(value), &value, NULL);
            if (error == ERROR_SUCCESS) {
                Limelog("WLAN interface %d is now in low latency mode\n", i);
            }
        }
    }

    pfnWlanFreeMemory(wlanInterfaceList);
#else
#endif
}

void exitLowLatencyMode(void) {
#if defined(LC_WINDOWS_DESKTOP)
    // Closing our WLAN client handle will undo our optimizations
    if (WlanHandle != NULL) {
        pfnWlanCloseHandle(WlanHandle, NULL);
        WlanHandle = NULL;
    }

    // Release the library reference to wlanapi.dll
    if (WlanApiLibraryHandle != NULL) {
        pfnWlanOpenHandle = NULL;
        pfnWlanCloseHandle = NULL;
        pfnWlanFreeMemory = NULL;
        pfnWlanEnumInterfaces = NULL;
        pfnWlanSetInterface = NULL;

        FreeLibrary(WlanApiLibraryHandle);
        WlanApiLibraryHandle = NULL;
    }

    // Restore original timer period
    timeEndPeriod(1);
#else
#endif
}

int initializePlatformSockets(void) {
#if defined(LC_WINDOWS)
    WSADATA data;
    return WSAStartup(MAKEWORD(2, 0), &data);
#elif defined(__vita__) || defined(__WIIU__) || defined(__3DS__)
    return 0; // already initialized
#elif defined(LC_POSIX) && !defined(LC_CHROME)
    // Disable SIGPIPE signals to avoid us getting
    // killed when a socket gets an EPIPE error
    struct sigaction sa;
    sigemptyset(&sa.sa_mask);
    sa.sa_handler = SIG_IGN;
    sa.sa_flags = 0;
    if (sigaction(SIGPIPE, &sa, 0) == -1) {
        perror("sigaction");
        return -1;
    }
    return 0;
#else
    return 0;
#endif
}

void cleanupPlatformSockets(void) {
#if defined(LC_WINDOWS)
    WSACleanup();
#else
#endif
}
