#include <arpa/inet.h>
#include <linux/if.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <unistd.h>

#include <atomic>
#include <mutex>
#include <thread>

#include "utils/RWLock.h"

// Defined only in ifc_utils.c, in the kernel, and in the NDK.
#ifndef SIOCKILLADDR
#define SIOCKILLADDR 0x8939
#endif

#ifndef TCP_LINGER2
#define TCP_LINGER2 8
#endif

#define KILL_INTERVAL_MS 10
#define CONNECT_THREADS 1

#define PERROR_EXIT(msg) { do { perror((msg)); exit(1); } while (0); };


// Ensures that sockets don't stay in TIME_WAIT state.
void setSoLinger(int s) {
    const struct linger l = {
        0,  // off
        0,  // 0 seconds
    };
    if (setsockopt(s, SOL_SOCKET, SO_LINGER, &l, sizeof(l)) == -1) {
        PERROR_EXIT("SO_LINGER");
    }
    const int nolinger = -1;
    if (setsockopt(s, SOL_TCP, TCP_LINGER2, &nolinger, sizeof(nolinger)) == -1) {
        PERROR_EXIT("TCP_LINGER2");
    }
}


// Binds to a random port on a random loopback address. We don't just use 127.0.0.1 because we don't
// want this test to kill unrelated connections on loopback.
int bindRandomAddr() {
    sockaddr_in sin;
    sin.sin_family = AF_INET;
    sin.sin_port = 0;
    sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);

    while (sin.sin_addr.s_addr == htonl(INADDR_LOOPBACK)) {
        arc4random_buf(
             ((uint8_t *) &sin.sin_addr.s_addr) + 1,
             sizeof(sin.sin_addr.s_addr) - 1);
    }

    int listensock;
    if ((listensock = socket(AF_INET, SOCK_STREAM, 0)) == -1) PERROR_EXIT("listensock");
    if (bind(listensock, (sockaddr *) &sin, sizeof(sin)) == -1) PERROR_EXIT("bind");
    if (listen(listensock, 10) == -1) PERROR_EXIT("listen");

    return listensock;
}


// Thread that calls SIOCKILLADDR in a loop.
void killSockets(sockaddr_in listenaddr, int intervalMs, android::RWLock *lock) {
    ifreq ifr;
    memset(&ifr, 0, sizeof(ifr));
    listenaddr.sin_port = 0;
    strncpy(ifr.ifr_name, "lo", strlen("lo"));
    memcpy(&ifr.ifr_addr, &listenaddr, sizeof(listenaddr));

    int ioctlsock = socket(AF_INET, SOCK_DGRAM, 0);
    if (ioctlsock == -1) PERROR_EXIT("ioctlsock");
    while(true) {
        lock->writeLock();
        if (ioctl(ioctlsock, SIOCKILLADDR, &ifr) != 0) {
            PERROR_EXIT("SIOCKILLADDR failed, did you run 32-bit userspace on a 64-bit kernel?");
        }
        lock->unlock();
        std::this_thread::sleep_for(std::chrono::milliseconds(intervalMs));
    }
}


// Thread that calls connect() in a loop.
void connectLoop(sockaddr_in listenaddr, int listensock,
        android::RWLock *lock, std::atomic<unsigned int> *attempts) {
    while(true) {
        int s = socket(AF_INET, SOCK_STREAM, 0);
        setSoLinger(s);

        // Don't call SIOCKILLADDR while connect() is running, or we'll end up with lots of
        // connections in state FIN_WAITx or TIME_WAIT, which will then slow down future
        // due to SYN retransmits.
        lock->readLock();
        if (connect(s, (sockaddr *) &listenaddr, sizeof(listenaddr)) == -1) PERROR_EXIT("connect");
        lock->unlock();

        send(s, "foo", 3, 0);
        int acceptedsock = accept(listensock, NULL, 0);
        if (close(acceptedsock) == -1) PERROR_EXIT("close");
        if (close(s) == -1) PERROR_EXIT("close");

        attempts->fetch_add(1);
    }
}


// Thread that prints progress every second.
void progressThread(std::atomic<unsigned int> *attempts) {
    uint32_t elapsed = 0;
    uint32_t total, previous = 0;
    while (true) {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        elapsed++;
        total = attempts->load();
        printf("%ds: %u cps, total %u\n", elapsed, total-previous, total);
        fflush(stdout);
        previous = total;
    }
}


int main() {
    int listensock = bindRandomAddr();
    struct sockaddr_in sin;
    socklen_t len = sizeof(sin);
    if (getsockname(listensock, (sockaddr *) &sin, &len) == -1) PERROR_EXIT("getsockname");

    printf("Using address %s:%d\n", inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));

    android::RWLock lock;
    std::atomic<unsigned int> attempts;
    attempts.store(0);

    std::thread t0(killSockets, sin, KILL_INTERVAL_MS, &lock);
    std::thread *connectThreads[CONNECT_THREADS];
    for (size_t i = 0; i < CONNECT_THREADS; i++) {
        connectThreads[i] = new std::thread(connectLoop, sin, listensock, &lock, &attempts);
    }
    std::thread t1(progressThread, &attempts);
    t1.join();

    return 0;
}
