/* SPDX-License-Identifier: BSD-3-Clause */
/*
 * Copyright (c) 2021-2022, 2024 Samuel Thibault
 */

/*
 * This simple test configures slirp and tries to ping it
 *
 * Note: to make this example actually be able to use the outside world, you
 * need to either
 * - run as root
 * - set /proc/sys/net/ipv4/ping_group_range to allow sending ICMP echo requests
 * - run a UDP echo server on the target
 */

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <assert.h>

#include "libslirp.h"

//#define _WIN32
#ifdef _WIN32
#define inet_aton slirp_inet_aton
#else
#include <sys/socket.h>
#include <arpa/inet.h>
#include <poll.h>
#endif

/* Dumb simulation tick: 100ms */
#define TICK 100

static Slirp *slirp;
static bool done;
static int64_t mytime;

/* Print a frame for debugging */
static void print_frame(const uint8_t *data, size_t len) {
    int i;

    printf("\ngot packet size %zu:\n", len);
    for (i = 0; i < len; i++) {
        if (i && i % 16 == 0)
            printf("\n");
        printf("%s%02x", i % 16 ? " " : "", data[i]);
    }
    if (len % 16 != 0)
        printf("\n");
    printf("\n");
}

/* Classical 16bit checksum */
static void checksum(uint8_t *data, size_t size, uint8_t *cksum) {
    uint32_t sum = 0;
    int i;

    cksum[0] = 0;
    cksum[1] = 0;

    for (i = 0; i+1 < size; i += 2)
        sum += (((uint16_t) data[i]) << 8) + data[i+1];
    if (i < size) /* Odd number of bytes */
        sum += ((uint16_t) data[i]) << 8;

    sum = (sum & 0xffff) + (sum >> 16);
    sum = (sum & 0xffff) + (sum >> 16);
    sum = ~sum;

    cksum[0] = sum >> 8;
    cksum[1] = sum;
}

/* This is called when receiving a packet from the virtual network, for the
 * guest */
static slirp_ssize_t send_packet(const void *buf, size_t len, void *opaque) {
    const uint8_t *data = buf;

    assert(len >= 14);

    if (data[12] == 0x86 &&
        data[13] == 0xdd) {
        /* Ignore IPv6 */
        return len;
    }

    print_frame(data, len);

    if (data[12] == 0x08 &&
        data[13] == 0x06) {
        /* ARP */
        /* We expect receiving an ARP request for our address */

        /* Ethernet address type */
        assert(data[14] == 0x00);
        assert(data[15] == 0x01);

        /* IPv4 address type */
        assert(data[16] == 0x08);
        assert(data[17] == 0x00);

        /* Ethernet addresses are 6 bytes long */
        assert(data[18] == 0x06);

        /* IPv4 addresses are 4 bytes long */
        assert(data[19] == 0x04);

        /* Opcode: ARP request */
        assert(data[20] == 0x00);
        assert(data[21] == 0x01);

        /* Ok, reply! */
        uint8_t myframe[] = {
            /*** Ethernet ***/
            /* dst */
            0x52, 0x55, 0x0a, 0x00, 0x02, 0x02,
            /* src */
            0x52, 0x55, 0x0a, 0x00, 0x02, 0x0e,
            /* Type: ARP */
            0x08, 0x06,

            /* ether,   IPv4,       */
            0x00, 0x01, 0x08, 0x00,
            /* elen, IPlen */
            0x06, 0x04,
            /* ARP reply */
            0x00, 0x02,

            /* Our ethernet address */
            0x52, 0x55, 0x0a, 0x00, 0x02, 0x0e,
            /* Our IP address */
            0x0a, 0x00, 0x02, 0x0e,

            /* Host ethernet address */
            0x52, 0x55, 0x0a, 0x00, 0x02, 0x02,
            /* Host IP address */
            0x0a, 0x00, 0x02, 0x02,
        };

        slirp_input(slirp, myframe, sizeof(myframe));
    }

    if (data[12] == 0x08 &&
        data[13] == 0x00) {
        /* IPv4 */
        assert(len >= 14 + 20);

        /* We expect receiving the ICMP echo reply for our echo request */

        /* IPv + hlen */
        assert(data[14] == 0x45);

        /* proto: ICMP */
        assert(data[23] == 0x01);

        /* ICMP */
        assert(len >= 14 + 20 + 8 + 4);

        /* ICMP type: reply */
        assert(data[34] == 0x00);

        /* Check the data */
        assert(data[42] == 0xde);
        assert(data[43] == 0xad);
        assert(data[44] == 0xbe);
        assert(data[45] == 0xef);

        /* Got the answer! */
        printf("got it!\n");
        done = 1;
    }

    return len;
}

static void guest_error(const char *msg, void *opaque) {
    printf("guest error %s\n",  msg);
}


/*
 * Dumb timer implementation
 */
static int64_t clock_get_ns(void *opaque) {
    return mytime;
}

struct timer {
    SlirpTimerId id;
    void *cb_opaque;
    int64_t expire;
    struct timer *next;
};

static struct timer *timer_queue;

static void *timer_new_opaque(SlirpTimerId id, void *cb_opaque, void *opaque) {
    struct timer *new_timer = malloc(sizeof(*new_timer));
    new_timer->id = id;
    new_timer->cb_opaque = cb_opaque;
    new_timer->next = NULL;
    return new_timer;
}

static void timer_free(void *_timer, void *opaque) {
    struct timer *timer = _timer;
    struct timer **t;

    for (t = &timer_queue; *t != NULL; *t = (*t)->next) {
        if (*t == timer) {
            /* Not expired yet, drop it */
            *t = timer->next;
            break;
        }
    }

    free(timer);
}

static void timer_mod(void *_timer, int64_t expire_time, void *opaque) {
    struct timer *timer = _timer;
    struct timer **t;

    timer->expire = expire_time * 1000 * 1000;

    for (t = &timer_queue; *t != NULL; *t = (*t)->next) {
        if (expire_time < (*t)->expire)
            break;
    }

    timer->next = *t;
    *t = timer;
}

static void timer_check(Slirp *slirp) {
    while (timer_queue && timer_queue->expire <= mytime)
    {
        struct timer *t = timer_queue;
        printf("handling %p at time %lu\n",
               t, (unsigned long) timer_queue->expire);
        timer_queue = t->next;
        slirp_handle_timer(slirp, t->id, t->cb_opaque);
    }
}

static uint32_t timer_timeout(void) {
    if (timer_queue)
    {
        uint32_t timeout = (timer_queue->expire - mytime) / (1000 * 1000);
        if (timeout < TICK)
            return timeout;
    }

    return TICK;
}


/*
 * Dumb polling implementation
 */
static int npoll;
static void register_poll_socket(slirp_os_socket fd, void *opaque) {
    /* We might want to prepare for polling on fd */
    npoll++;
}

static void unregister_poll_socket(slirp_os_socket fd, void *opaque) {
    /* We might want to clear polling on fd */
    npoll--;
}

static void notify(void *opaque) {
    /* No need for this in single-thread case */
}

#ifdef _WIN32
/* select() variant */
static fd_set readfds, writefds, exceptfds;
static unsigned int maxfd;
static int add_poll_cb(slirp_os_socket fd, int events, void *opaque)
{
    if (events & SLIRP_POLL_IN)
        FD_SET(fd, &readfds);
    if (events & SLIRP_POLL_OUT)
        FD_SET(fd, &writefds);
    if (events & SLIRP_POLL_PRI)
        FD_SET(fd, &exceptfds);
    if (maxfd < fd)
        maxfd = fd;
    return fd;
}

static int get_revents_cb(int idx, void *opaque)
{
    int event = 0;
    if (FD_ISSET(idx, &readfds))
        event |= SLIRP_POLL_IN;
    if (FD_ISSET(idx, &writefds))
        event |= SLIRP_POLL_OUT;
    if (FD_ISSET(idx, &exceptfds))
        event |= SLIRP_POLL_PRI;
    return event;
}

static void dopoll(uint32_t timeout) {
    int err;
    FD_ZERO(&readfds);
    FD_ZERO(&writefds);
    FD_ZERO(&exceptfds);
    maxfd = 0;

    slirp_pollfds_fill_socket(slirp, &timeout, add_poll_cb, NULL);
    printf("we will use timeout %u\n", (unsigned) timeout);

    struct timeval tv = {
        .tv_sec = timeout / 1000,
        .tv_usec = (timeout % 1000) * 1000,
    };
    err = select(maxfd+1, &readfds, &writefds, &exceptfds, &tv);

    slirp_pollfds_poll(slirp, err < 0, get_revents_cb, NULL);
}
#else
/* poll() variant */
static struct pollfd *fds;
static int cur_poll;
static int add_poll_cb(slirp_os_socket fd, int events, void *opaque)
{
    short poll_events = 0;

    assert(cur_poll < npoll);
    fds[cur_poll].fd = fd;

    if (events & SLIRP_POLL_IN)
        poll_events |= POLLIN;
    if (events & SLIRP_POLL_OUT)
        poll_events |= POLLOUT;
    if (events & SLIRP_POLL_PRI)
        poll_events |= POLLPRI;
    fds[cur_poll].events = poll_events;

    return cur_poll++;
}

static int get_revents_cb(int idx, void *opaque)
{
    return fds[idx].revents;
}

static void dopoll(uint32_t timeout) {
    int err;
    fds = malloc(sizeof(*fds) * npoll);
    cur_poll = 0;

    slirp_pollfds_fill_socket(slirp, &timeout, add_poll_cb, NULL);
    printf("we will use timeout %u\n", (unsigned) timeout);

    err = poll(fds, cur_poll, timeout);

    slirp_pollfds_poll(slirp, err < 0, get_revents_cb, NULL);

    free(fds);
}
#endif


static struct SlirpCb callbacks = {
    .send_packet = send_packet,
    .guest_error = guest_error,
    .clock_get_ns = clock_get_ns,
    .timer_new_opaque = timer_new_opaque,
    .timer_free = timer_free,
    .timer_mod = timer_mod,
    .register_poll_socket = register_poll_socket,
    .unregister_poll_socket = unregister_poll_socket,
    .notify = notify,
};


int main(int argc, char *argv[]) {
    SlirpConfig config = {
        .version = 4,
        .restricted = false,
        .in_enabled = true,
        .vnetwork.s_addr = htonl(0x0a000200),
        .vnetmask.s_addr = htonl(0xffffff00),
        .vhost.s_addr = htonl(0x0a000202),
        .vdhcp_start.s_addr = htonl(0x0a00020f),
        .vnameserver.s_addr = htonl(0x0a000203),
        .disable_host_loopback = false,
        .enable_emu = false,
        .disable_dns = false,
    };
    uint32_t timeout = 0;

    printf("Slirp version %s\n", slirp_version_string());

#if !defined(_WIN32)
    inet_pton(AF_INET6, "fec0::", &config.vprefix_addr6);
    config.vprefix_len = 64;
    config.vhost6 = config.vprefix_addr6;
    config.vhost6.s6_addr[15] = 2;
    config.vnameserver6 = config.vprefix_addr6;
    config.vnameserver6.s6_addr[15] = 2;
    config.in6_enabled = true,
#endif

    slirp = slirp_new(&config, &callbacks, NULL);

    /* Send echo request */
    uint8_t myframe[] = {
        /*** Ethernet ***/
        /* dst */
        0x52, 0x55, 0x0a, 0x00, 0x02, 0x02,
        /* src */
        0x52, 0x55, 0x0a, 0x00, 0x02, 0x0e,
        /* Type: IPv4 */
        0x08, 0x00,

        /*** IPv4 ***/
        /* vhl,tos, len         */
        0x45, 0x00, 0x00, 0x20,
        /* id,      off (DF)    */
        0x68, 0xd7, 0x40, 0x00,
        /* ttl,pro, cksum      */
        0x40, 0x01, 0x00, 0x00,
        /* src                  */
        0x0a, 0x00, 0x02, 0x0e,
        /* dst                  */
        0x00, 0x00, 0x00, 0x00,

        /*** ICMPv4 ***/
        /* type, code, cksum    */
        0x08, 0x00, 0x00, 0x00,
        /* id,     seq          */
        0x01, 0xec, 0x00, 0x01,
        /* data                 */
        0xde, 0xad, 0xbe, 0xef,
    };

    struct in_addr in_addr = { .s_addr = htonl(0x0a000202) };
    if (argc > 1) {
        if (inet_aton(argv[1], &in_addr) == 0) {
            printf("usage: %s [destination IPv4 address]\n", argv[0]);
            exit(EXIT_FAILURE);
        }
    }
    uint32_t addr = ntohl(in_addr.s_addr);
    myframe[30] = addr >> 24;
    myframe[31] = addr >> 16;
    myframe[32] = addr >> 8;
    myframe[33] = addr >> 0;

    /* IPv4 header checksum */
    checksum(&myframe[14], 20, &myframe[24]);
    /* ICMP header checksum */
    checksum(&myframe[34], 12, &myframe[36]);

    slirp_input(slirp, myframe, sizeof(myframe));

    /* Wait for echo reply */
    while (!done) {
        printf("time %lu\n", (unsigned long) mytime);

        timer_check(slirp);
        /* Here we make the virtual time wait like the real time, but we could
         * make it wait differently */
        timeout = timer_timeout();
        printf("we wish timeout %u\n", (unsigned) timeout);

        dopoll(timeout);

        /* Fake that the tick elapsed */
        mytime += TICK * 1000 * 1000;
    }

    slirp_cleanup(slirp);
}
