/* Copyright (c) 2014-2021, Michael Santos <michael.santos@gmail.com>
 *
 * 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 "alcove.h"
#include "alcove_call.h"

#include <sys/select.h>

static int alcove_list_to_fd_set(const char *arg, size_t len, int *index,
                                 fd_set *fdset, int *nfds);
static int alcove_fd_isset(char *buf, size_t len, int *index, fd_set *set);

/*
 * select(2)
 *
 */
ssize_t alcove_sys_select(alcove_state_t *ap, const char *arg, size_t len,
                          char *reply, size_t rlen) {
  int index = 0;
  int rindex = 0;
  int type = 0;
  int arity = 0;

  int nfds = 0;
  fd_set readfds;
  fd_set writefds;
  fd_set exceptfds;

  char buf[MAXATOMLEN] = {0};
  struct timeval tv;
  struct timeval *timeout = NULL;

  int rv = 0;

  UNUSED(ap);

  tv.tv_sec = 0;
  tv.tv_usec = 0;

  /* readfds */
  if (alcove_list_to_fd_set(arg, len, &index, &readfds, &nfds) < 0)
    return alcove_mk_errno(reply, rlen, EBADF);

  /* writefds */
  if (alcove_list_to_fd_set(arg, len, &index, &writefds, &nfds) < 0)
    return alcove_mk_errno(reply, rlen, EBADF);

  /* exceptfds */
  if (alcove_list_to_fd_set(arg, len, &index, &exceptfds, &nfds) < 0)
    return alcove_mk_errno(reply, rlen, EBADF);

  /* timeout */
  if (alcove_get_type(arg, len, &index, &type, &arity) < 0)
    return -1;

  switch (type) {
  case ERL_NIL_EXT:
    timeout = NULL;
    break;

  case ERL_ATOM_EXT:
    if (alcove_decode_null(arg, len, &index) < 0)
      return -1;

    timeout = NULL;
    break;

  case ERL_SMALL_TUPLE_EXT:
  case ERL_LARGE_TUPLE_EXT: {
    long long tv_sec = 0;
    long long tv_usec = 0;

    if (alcove_decode_tuple_header(arg, len, &index, &arity) < 0 || arity != 3)
      return -1;

    timeout = &tv;

    /* 'alcove_timeval' */
    if (alcove_decode_atom(arg, len, &index, buf) < 0)
      return -1;

    if (strcmp(buf, "alcove_timeval") != 0)
      return -1;

    /* sec */
    if (alcove_decode_longlong(arg, len, &index, &tv_sec) < 0)
      return -1;

    /* usec */
    if (alcove_decode_longlong(arg, len, &index, &tv_usec) < 0)
      return -1;

    tv.tv_sec = tv_sec;
    tv.tv_usec = tv_usec;
  }

  break;

  default:
    return -1;
  }

  rv = select(nfds + 1, &readfds, &writefds, &exceptfds, timeout);

  if (rv < 0)
    return alcove_mk_errno(reply, rindex, errno);

  ALCOVE_TUPLE4(reply, rlen, &rindex, "ok",
                alcove_fd_isset(reply, rlen, &rindex, &readfds),
                alcove_fd_isset(reply, rlen, &rindex, &writefds),
                alcove_fd_isset(reply, rlen, &rindex, &exceptfds));

  return rindex;
}

static int alcove_list_to_fd_set(const char *arg, size_t len, int *index,
                                 fd_set *fdset, int *nfds) {
  int type = 0;
  int arity = 0;

  int i = 0;
  int fd = -1;

  if (alcove_get_type(arg, len, index, &type, &arity) < 0)
    return -1;

  if (arity >= FD_SETSIZE)
    return -1;

  FD_ZERO(fdset);

  switch (type) {
  case ERL_STRING_EXT: {
    char tmp[FD_SETSIZE] = {0};

    if (alcove_decode_string(arg, len, index, tmp, sizeof(tmp)) < 0)
      return -1;

    for (i = 0; i < arity; i++) {
      fd = tmp[i];

      if (fd >= FD_SETSIZE || fcntl(fd, F_GETFD, 0) < 0)
        return -1;

      FD_SET(fd, fdset);
      *nfds = MAX(fd, *nfds);
    }
  }

  break;

  case ERL_LIST_EXT:
    if (alcove_decode_list_header(arg, len, index, &arity) < 0)
      return -1;

    for (i = 0; i < arity; i++) {
      if (alcove_decode_int(arg, len, index, &fd) < 0)
        return -1;

      if (fd >= FD_SETSIZE || fcntl(fd, F_GETFD, 0) < 0)
        return -1;

      FD_SET(fd, fdset);
      *nfds = MAX(fd, *nfds);
    }

    if (alcove_decode_list_header(arg, len, index, &arity) < 0 || arity != 0)
      return -1;

    break;

  case ERL_NIL_EXT:
    if (alcove_decode_list_header(arg, len, index, &arity) < 0 || arity != 0)
      return -1;

    break;

  default:
    return -1;
  }

  return 0;
}

static int alcove_fd_isset(char *buf, size_t len, int *index, fd_set *set) {
  int fd = 0;

  if (set == NULL)
    return 0;

  for (fd = 0; fd < FD_SETSIZE; fd++) {
    if (FD_ISSET(fd, set)) {
      if (alcove_encode_list_header(buf, len, index, 1) < 0)
        return -1;

      if (alcove_encode_long(buf, len, index, fd) < 0)
        return -1;
    }
  }

  if (alcove_encode_empty_list(buf, len, index) < 0)
    return -1;

  return 0;
}
