/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
│vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8                                :vi│
╞══════════════════════════════════════════════════════════════════════════════╡
│ Copyright 2020 Justine Alexandra Roberts Tunney                              │
│                                                                              │
│ Permission to use, copy, modify, and/or distribute this software for         │
│ any purpose with or without fee is hereby granted, provided that the         │
│ above copyright notice and this permission notice appear in all copies.      │
│                                                                              │
│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL                │
│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED                │
│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE             │
│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL         │
│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR        │
│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER               │
│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR             │
│ PERFORMANCE OF THIS SOFTWARE.                                                │
╚─────────────────────────────────────────────────────────────────────────────*/
#include "libc/alg/arraylist.internal.h"
#include "libc/bits/safemacros.internal.h"
#include "libc/calls/calls.h"
#include "libc/calls/struct/iovec.h"
#include "libc/errno.h"
#include "libc/fmt/fmt.h"
#include "libc/log/check.h"
#include "libc/log/log.h"
#include "libc/runtime/gc.internal.h"
#include "libc/runtime/interruptiblecall.h"
#include "libc/runtime/runtime.h"
#include "libc/sock/sock.h"
#include "libc/stdio/stdio.h"
#include "libc/str/str.h"
#include "libc/sysv/consts/af.h"
#include "libc/sysv/consts/exit.h"
#include "libc/sysv/consts/ipproto.h"
#include "libc/sysv/consts/msg.h"
#include "libc/sysv/consts/poll.h"
#include "libc/sysv/consts/so.h"
#include "libc/sysv/consts/sock.h"
#include "libc/sysv/consts/sol.h"
#include "libc/x/x.h"
#include "third_party/getopt/getopt.h"

/**
 * @fileoverview Asynchronous TCP/UDP Echo Server.
 *
 *   make -j8 o/default/tool/net/echoserver.com
 *   o/default/tool/net/echoserver.com udp:0.0.0.0:7 tcp:0.0.0.0:7
 */

enum SocketKind {
  kSocketServer,
  kSocketClient,
};

struct Message {
  struct iovec data;
  struct sockaddr_in dest;
  uint32_t destsize;
};

struct Messages {
  size_t i, n;
  struct Message *p;
};

struct Socket {
  int64_t fd;
  enum SocketKind kind;
  int type;
  int protocol;
  struct sockaddr_in addr;
  struct Messages egress; /* LIFO */
};

struct Sockets {
  size_t i, n;
  struct Socket *p;
};

struct Polls {
  size_t i, n;
  struct pollfd *p;
};

struct Sockets g_sockets;
struct Polls g_polls;

nodiscard char *DescribeAddress(struct sockaddr_in *addr) {
  char ip4buf[16];
  return xasprintf("%s:%hu",
                   inet_ntop(addr->sin_family, &addr->sin_addr.s_addr, ip4buf,
                             sizeof(ip4buf)),
                   ntohs(addr->sin_port));
}

nodiscard char *DescribeSocket(struct Socket *s) {
  return xasprintf("%s:%s", s->protocol == IPPROTO_UDP ? "udp" : "tcp",
                   gc(DescribeAddress(&s->addr)));
}

wontreturn void ShowUsageAndExit(bool iserror) {
  FILE *f = iserror ? stderr : stdout;
  int rc = iserror ? EXIT_FAILURE : EXIT_SUCCESS;
  fprintf(f, "%s: %s %s\n", "Usage", __argv[0], "PROTOCOL:ADDR:PORT...");
  exit(rc);
}

void GetFlags(int argc, char *argv[]) {
  int opt;
  while ((opt = getopt(argc, argv, "h")) != -1) {
    switch (opt) {
      case 'h':
        ShowUsageAndExit(false);
      default:
        ShowUsageAndExit(true);
    }
  }
  if (optind == argc) ShowUsageAndExit(true);
}

void AddSocket(const struct Socket *s) {
  struct pollfd pfd;
  pfd.fd = s->fd;
  pfd.events = POLLIN;
  pfd.revents = 0;
  CHECK_NE(-1L, append(&g_sockets, s));
  CHECK_NE(-1L, append(&g_polls, &pfd));
}

void RemoveSocket(size_t i) {
  DCHECK_LT(i, g_sockets.i);
  LOGF("removing: %s", gc(DescribeSocket(&g_sockets.p[i])));
  CHECK_NE(-1, close(g_sockets.p[i].fd));
  while (g_sockets.p[i].egress.i) {
    free(g_sockets.p[i].egress.p[g_sockets.p[i].egress.i - 1].data.iov_base);
  }
  memcpy(&g_sockets.p[i], &g_sockets.p[i + 1],
         (intptr_t)&g_sockets.p[g_sockets.i] - (intptr_t)&g_sockets.p[i + 1]);
  memcpy(&g_polls.p[i], &g_polls.p[i + 1],
         (intptr_t)&g_polls.p[g_polls.i] - (intptr_t)&g_polls.p[i + 1]);
  g_sockets.i--;
  g_polls.i--;
}

void GetListeningAddressesFromCommandLine(int argc, char *argv[]) {
  int i;
  for (i = optind; i < argc; ++i) {
    struct Socket server;
    memset(&server, 0, sizeof(server));
    char scheme[4];
    unsigned char *ip4 = (unsigned char *)&server.addr.sin_addr.s_addr;
    uint16_t port;
    if (sscanf(argv[i], "%3s:%hhu.%hhu.%hhu.%hhu:%hu", scheme, &ip4[0], &ip4[1],
               &ip4[2], &ip4[3], &port) != 6) {
      fprintf(stderr, "error: bad ip4 uri\n");
      ShowUsageAndExit(true);
    }
    server.fd = -1;
    server.kind = kSocketServer;
    server.addr.sin_family = AF_INET;
    server.addr.sin_port = htons(port);
    if (strcasecmp(scheme, "tcp") == 0) {
      server.type = SOCK_STREAM;
      server.protocol = IPPROTO_TCP;
    } else if (strcasecmp(scheme, "udp") == 0) {
      server.type = SOCK_DGRAM;
      server.protocol = IPPROTO_UDP;
    } else {
      fprintf(stderr, "%s: %s\n", "error", "bad scheme (should be tcp or udp)");
      ShowUsageAndExit(true);
    }
    AddSocket(&server);
  }
}

void BeginListeningForIncomingTraffic(void) {
  size_t i;
  for (i = 0; i < g_sockets.i; ++i) {
    int yes = 1;
    struct Socket *s = &g_sockets.p[i];
    CHECK_NE(-1L, (g_polls.p[i].fd = s->fd =
                       socket(s->addr.sin_family, s->type, s->protocol)));
    CHECK_NE(-1L,
             setsockopt(s->fd, SOL_SOCKET, SO_REUSEPORT, &yes, sizeof(yes)));
    CHECK_NE(-1, bind(s->fd, &s->addr, sizeof(s->addr)));
    if (s->protocol == IPPROTO_TCP) {
      CHECK_NE(-1, listen(s->fd, 1));
    }
    uint32_t addrsize = sizeof(s->addr);
    CHECK_NE(-1, getsockname(s->fd, &s->addr, &addrsize));
    LOGF("listening on %s", gc(DescribeSocket(s)));
  }
}

void AcceptConnection(size_t i) {
  struct Socket *server = &g_sockets.p[i];
  struct Socket client;
  memset(&client, 0, sizeof(client));
  client.kind = kSocketClient;
  client.type = server->type;
  client.protocol = server->protocol;
  uint32_t addrsize = sizeof(client.addr);
  CHECK_NE(-1L, (client.fd = accept(server->fd, &client.addr, &addrsize)));
  LOGF("%s accepted %s", gc(DescribeSocket(server)),
       gc(DescribeSocket(&client)));
  AddSocket(&client);
}

bool ReceiveData(size_t i) {
  ssize_t got;
  struct Message msg;
  bool isudp = g_sockets.p[i].protocol == IPPROTO_UDP;
  memset(&msg, 0, sizeof(msg));
  msg.destsize = sizeof(msg.dest);
  msg.data.iov_len = PAGESIZE;
  msg.data.iov_base = xmalloc(msg.data.iov_len);
  CHECK_NE(-1L, (got = recvfrom(g_sockets.p[i].fd, msg.data.iov_base,
                                msg.data.iov_len, 0, isudp ? &msg.dest : NULL,
                                isudp ? &msg.destsize : NULL)));
  if (0 < got && got <= msg.data.iov_len) {
    LOGF("%s received %lu bytes from %s", gc(DescribeSocket(&g_sockets.p[i])),
         got, gc(DescribeAddress(&msg.dest)));
    msg.data.iov_base = xrealloc(msg.data.iov_base, (msg.data.iov_len = got));
    append(&g_sockets.p[i].egress, &msg);
    g_polls.p[i].events |= POLLOUT;
    return true;
  } else {
    RemoveSocket(i);
    free_s(&msg.data.iov_base);
    return false;
  }
}

void SendData(size_t i) {
  ssize_t sent;
  struct Socket *s = &g_sockets.p[i];
  struct Message *msg = &s->egress.p[s->egress.i - 1];
  bool isudp = s->protocol == IPPROTO_UDP;
  DCHECK(s->egress.i);
  CHECK_NE(-1L, (sent = sendto(s->fd, msg->data.iov_base, msg->data.iov_len, 0,
                               isudp ? &msg->dest : NULL,
                               isudp ? msg->destsize : 0)));
  LOGF("%s sent %lu bytes to %s", gc(DescribeSocket(s)), msg->data.iov_len,
       gc(DescribeAddress(&msg->dest)));
  if (!(msg->data.iov_len -= min((size_t)sent, (size_t)msg->data.iov_len))) {
    free_s(&msg->data.iov_base);
    if (!--s->egress.i) {
      g_polls.p[i].events &= ~POLLOUT;
    }
  }
}

void HandleSomeNetworkTraffic(void) {
  size_t i;
  int eventcount;
  CHECK_GE((eventcount = poll(g_polls.p, g_polls.i, -1)), 0);
  for (i = 0; eventcount && i < g_sockets.i; ++i) {
    if (!g_polls.p[i].revents) continue;
    --eventcount;
    if (g_polls.p[i].revents & (POLLERR | POLLHUP | POLLNVAL)) {
      CHECK_EQ(kSocketClient, g_sockets.p[i].kind);
      RemoveSocket(i);
    } else {
      if (g_polls.p[i].revents & POLLIN) {
        if (g_sockets.p[i].kind == kSocketServer &&
            g_sockets.p[i].protocol == IPPROTO_TCP) {
          AcceptConnection(i);
        } else {
          if (!ReceiveData(i)) continue;
        }
      }
      if (g_polls.p[i].revents & POLLOUT) {
        SendData(i);
      }
    }
  }
}

void EchoServer(void) {
  for (;;) HandleSomeNetworkTraffic();
}

int main(int argc, char *argv[]) {
  STATIC_YOINK("isatty");
  GetFlags(argc, argv);
  GetListeningAddressesFromCommandLine(argc, argv);
  BeginListeningForIncomingTraffic();
  struct InterruptibleCall icall;
  memset(&icall, 0, sizeof(icall));
  interruptiblecall(&icall, (void *)EchoServer, 0, 0, 0, 0);
  fputc('\r', stderr);
  LOGF("%s", "shutting down...");
  size_t i;
  for (i = g_sockets.i; i; --i) RemoveSocket(i - 1);
  return 0;
}
