#define _DEFAULT_SOURCE
#define _POSIX_C_SOURCE 200809L
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <time.h>

static volatile sig_atomic_t stopped;

static void
stop_ping(int sig)
{
    (void)sig;
    stopped = 1;
}

static uint16_t
checksum(const unsigned char *data, size_t len)
{
    uint32_t sum = 0;
    while (len >= 2) {
        sum += (uint32_t)data[0] * 256 + data[1];
        data += 2;
        len -= 2;
    }
    if (len) sum += (uint32_t)data[0] * 256;
    while (sum >> 16) sum = (sum & 0xffff) + (sum >> 16);
    return (uint16_t)~sum;
}

static double
now_ms(void)
{
    struct timespec ts;
    if (clock_gettime(CLOCK_MONOTONIC, &ts) < 0) return -1;
    return (double)ts.tv_sec * 1000 + (double)ts.tv_nsec / 1000000;
}

static int
match_reply(const unsigned char *packet, size_t size, struct in_addr target,
            uint16_t id, uint16_t seq, int *ttl)
{
    struct iphdr ip;
    struct icmphdr icmp;
    size_t header, total;
    if (size < sizeof(ip)) return 0;
    memcpy(&ip, packet, sizeof(ip));
    if (ip.version != 4 || ip.ihl < 5 || ip.protocol != IPPROTO_ICMP ||
        ip.saddr != target.s_addr || (ntohs(ip.frag_off) & (IP_MF | IP_OFFMASK)))
        return 0;
    header = (size_t)ip.ihl * 4;
    total = ntohs(ip.tot_len);
    if (total < header || total > size || total - header != 64) return 0;
    memcpy(&icmp, packet + header, sizeof(icmp));
    if (icmp.type != ICMP_ECHOREPLY || icmp.code != 0 ||
        ntohs(icmp.un.echo.id) != id || ntohs(icmp.un.echo.sequence) != seq ||
        checksum(packet + header, total - header) != 0)
        return 0;
    *ttl = ip.ttl;
    return (int)(total - header);
}

int
main(int argc, char **argv)
{
    struct addrinfo hints = {0}, *addresses;
    struct sockaddr_in target;
    struct sigaction action = {0};
    unsigned char request[64] = {0}, reply[65536];
    char address[INET_ADDRSTRLEN];
    uint16_t id = (uint16_t)getpid(), seq = 0;
    unsigned long transmitted = 0, received = 0;
    double minimum = 0, maximum = 0, sum = 0;
    int fd, result, status = 0;

    if (argc != 2) {
        fprintf(stderr, "usage: %s host\n", argv[0]);
        return 1;
    }
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_RAW;
    hints.ai_protocol = IPPROTO_ICMP;
    result = getaddrinfo(argv[1], NULL, &hints, &addresses);
    if (result != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(result));
        return 1;
    }
    target = *(const struct sockaddr_in *)addresses->ai_addr;
    freeaddrinfo(addresses);
    if (inet_ntop(AF_INET, &target.sin_addr, address, sizeof(address)) == NULL) {
        perror("inet_ntop");
        return 1;
    }
    fd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
    if (fd < 0) { perror("socket"); return 1; }
    if (fd >= FD_SETSIZE) {
        fprintf(stderr, "socket descriptor exceeds select limit\n");
        close(fd);
        return 1;
    }
    action.sa_handler = stop_ping;
    sigemptyset(&action.sa_mask);
    if (sigaction(SIGINT, &action, NULL) < 0) {
        perror("sigaction");
        close(fd);
        return 1;
    }
    if (printf("PING %s (%s)\n", argv[1], address) < 0 || fflush(stdout) == EOF) {
        perror("stdout");
        close(fd);
        return 1;
    }
    while (!stopped) {
        struct icmphdr echo = {0};
        double sent, deadline;
        ssize_t n;
        int answered = 0;
        ++seq;
        echo.type = ICMP_ECHO;
        echo.un.echo.id = htons(id);
        echo.un.echo.sequence = htons(seq);
        memcpy(request, &echo, sizeof(echo));
        echo.checksum = htons(checksum(request, sizeof(request)));
        memcpy(request, &echo, sizeof(echo));
        sent = now_ms();
        if (sent < 0) { perror("clock_gettime"); status = 1; break; }
        do {
            n = sendto(fd, request, sizeof(request), 0,
                       (struct sockaddr *)&target, sizeof(target));
        } while (n < 0 && errno == EINTR && !stopped);
        if (n == (ssize_t)sizeof(request)) ++transmitted;
        if (stopped) break;
        if (n != (ssize_t)sizeof(request)) {
            if (n >= 0) errno = EIO;
            perror("sendto");
            status = 1;
            break;
        }
        deadline = sent + 1000;
        while (!stopped) {
            fd_set readable;
            struct timeval timeout;
            double now = now_ms(), elapsed;
            long remaining;
            int ttl, bytes;
            if (now < 0) { perror("clock_gettime"); status = 1; goto done; }
            if (now >= deadline) break;
            remaining = (long)((deadline - now) * 1000) + 1;
            timeout.tv_sec = remaining / 1000000;
            timeout.tv_usec = remaining % 1000000;
            FD_ZERO(&readable);
            FD_SET(fd, &readable);
            result = select(fd + 1, &readable, NULL, NULL, &timeout);
            if (result < 0 && errno == EINTR) continue;
            if (result < 0) { perror("select"); status = 1; goto done; }
            if (result == 0) break;
            n = recvfrom(fd, reply, sizeof(reply), 0, NULL, NULL);
            if (n < 0 && errno == EINTR) continue;
            if (n < 0) { perror("recvfrom"); status = 1; goto done; }
            bytes = match_reply(reply, (size_t)n, target.sin_addr, id, seq, &ttl);
            if (!bytes || answered) continue;
            now = now_ms();
            if (now < 0) { perror("clock_gettime"); status = 1; goto done; }
            elapsed = now - sent;
            if (!received || elapsed < minimum) minimum = elapsed;
            if (!received || elapsed > maximum) maximum = elapsed;
            sum += elapsed;
            ++received;
            answered = 1;
            printf("%d bytes from %s: icmp_seq=%u ttl=%d time=%.1f ms\n",
                   bytes, address, (unsigned int)seq, ttl, elapsed);
            if (fflush(stdout) == EOF) { perror("stdout"); status = 1; goto done; }
        }
    }
done:
    close(fd);
    printf("\n--- %s ping statistics ---\n", argv[1]);
    printf("%lu packets transmitted, %lu received, %.0f%% packet loss\n",
           transmitted, received, transmitted ?
           100.0 * (double)(transmitted - received) / (double)transmitted : 0.0);
    if (received)
        printf("rtt min/avg/max = %.1f/%.1f/%.1f ms\n", minimum,
               sum / (double)received, maximum);
    if (fflush(stdout) == EOF) { perror("stdout"); status = 1; }
    return status;
}
