/* Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *  * Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *  * Neither the name of NVIDIA CORPORATION nor the names of its
 *    contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

//
// DESCRIPTION:   Common egl stream functions
//

#include "eglstrm_common.h"

EGLStreamKHR g_producerEglStream = EGL_NO_STREAM_KHR;
EGLStreamKHR g_consumerEglStream = EGL_NO_STREAM_KHR;
EGLDisplay g_producerEglDisplay = EGL_NO_DISPLAY;
EGLDisplay g_consumerEglDisplay = EGL_NO_DISPLAY;
int cudaDevIndexProd = -1;
int cudaDevIndexCons = -1;

#if defined(EXTENSION_LIST)
EXTENSION_LIST(EXTLST_DECL)
typedef void (*extlst_fnptr_t)(void);
static struct {
  extlst_fnptr_t *fnptr;
  char const *name;
  bool is_dgpu;  // This function is need only for dgpu case
} extensionList[] = {EXTENSION_LIST(EXTLST_ENTRY)};

int eglSetupExtensions(bool isCrossDevice) {
  unsigned int i;

  for (i = 0; i < (sizeof(extensionList) / sizeof(*extensionList)); i++) {
    // load the dgpu function only if we are running cross device test
    if ((!extensionList[i].is_dgpu) ||
        (extensionList[i].is_dgpu == isCrossDevice)) {
      *extensionList[i].fnptr = eglGetProcAddress(extensionList[i].name);
      if (*extensionList[i].fnptr == NULL) {
        printf("Couldn't get address of %s()\n", extensionList[i].name);
        return 0;
      }
    }
  }

  return 1;
}

int EGLStreamInit(bool isCrossDevice, int isConsumer,
                  EGLNativeFileDescriptorKHR fileDesc) {
  static const EGLint streamAttrFIFOMode[] = {
      EGL_STREAM_FIFO_LENGTH_KHR, 5, EGL_SUPPORT_REUSE_NV, EGL_FALSE, EGL_NONE};
  EGLDisplay eglDisplay[2] = {0};
  EGLStreamKHR eglStream[2] = {0};
  EGLBoolean eglStatus;

#define MAX_EGL_DEVICES 4

  EGLDeviceEXT devices[MAX_EGL_DEVICES];
  EGLint numDevices = 0;

  eglStatus = eglQueryDevicesEXT(MAX_EGL_DEVICES, devices, &numDevices);
  if (eglStatus != EGL_TRUE) {
    printf("Error querying EGL devices\n");
    goto Done;
  }

  if (numDevices == 0) {
    printf("No EGL devices found\n");
    eglStatus = EGL_FALSE;
    goto Done;
  }

  // If cross device, create discrete GPU stream first and then create the
  // integrated GPU stream to connect to it via fd. The other way round fails
  // in producer connect.
  //
  // TODO: Find out if this EGL behavior is by design.
  if (isConsumer) {
    int egl_device_id = 0;
    for (egl_device_id = 0; egl_device_id < numDevices; egl_device_id++) {
      EGLAttrib cuda_device;
      eglStatus = eglQueryDeviceAttribEXT(devices[egl_device_id],
                                          EGL_CUDA_DEVICE_NV, &cuda_device);
      if (eglStatus == EGL_TRUE) {
        cudaDevIndexCons = cuda_device;  // We select first EGL-CUDA Capable
                                         // device for consumer.
        printf(
            "Found EGL-CUDA Capable device with CUDA Device id = %d out of "
            "egl_device_id = %d\n",
            (int)cudaDevIndexCons, egl_device_id);
        break;
      }
    }

    if (egl_device_id >= numDevices) {
      printf("No CUDA Capable EGL Device found.. Waiving execution\n");
      goto Done;
    }

    g_consumerEglDisplay = eglGetPlatformDisplayEXT(
        EGL_PLATFORM_DEVICE_EXT, (void *)devices[egl_device_id], NULL);
    if (g_consumerEglDisplay == EGL_NO_DISPLAY) {
      printf("Could not get EGL display from device. \n");
      eglStatus = EGL_FALSE;
      goto Done;
    }

    eglStatus = eglInitialize(g_consumerEglDisplay, 0, 0);
    if (!eglStatus) {
      printf("EGL failed to initialize. \n");
      eglStatus = EGL_FALSE;
      goto Done;
    }

    g_consumerEglStream =
        eglCreateStreamKHR(g_consumerEglDisplay, streamAttrFIFOMode);
    if (g_consumerEglStream == EGL_NO_STREAM_KHR) {
      printf("Could not create EGL stream.\n");
      eglStatus = EGL_FALSE;
      goto Done;
    }

    eglStatus = eglStreamAttribKHR(g_consumerEglDisplay, g_consumerEglStream,
                                   EGL_CONSUMER_LATENCY_USEC_KHR, 16000);
    if (eglStatus != EGL_TRUE) {
      printf("eglStreamAttribKHR EGL_CONSUMER_LATENCY_USEC_KHR failed\n");
      goto Done;
    }

    eglStatus =
        eglStreamAttribKHR(g_consumerEglDisplay, g_consumerEglStream,
                           EGL_CONSUMER_ACQUIRE_TIMEOUT_USEC_KHR, 16000);
    if (eglStatus != EGL_TRUE) {
      printf(
          "eglStreamAttribKHR EGL_CONSUMER_ACQUIRE_TIMEOUT_USEC_KHR failed\n");
      goto Done;
    }
  }

  if (!isConsumer) {  // Producer

    if (fileDesc == EGL_NO_FILE_DESCRIPTOR_KHR) {
      printf("Cuda Producer received bad file descriptor\n");
      eglStatus = EGL_FALSE;
      goto Done;
    }

    int egl_device_id = 0;
    int egl_cuda_devices = 0;
    for (egl_device_id = 0; egl_device_id < numDevices; egl_device_id++) {
      EGLAttrib cuda_device = -1;
      eglStatus = eglQueryDeviceAttribEXT(devices[egl_device_id],
                                          EGL_CUDA_DEVICE_NV, &cuda_device);
      if (eglStatus == EGL_TRUE) {
        egl_cuda_devices++;
        if (isCrossDevice && (egl_cuda_devices > 1)) {
          // We select second EGL-CUDA Capable device for producer.
          cudaDevIndexProd = (int)cuda_device;
          printf(
              "Found EGL-CUDA Capable device with CUDA Device id = %d "
              "egl_device_id = %d \n",
              (int)cudaDevIndexProd, egl_device_id);
          break;
        }
        if (!isCrossDevice) {
          // We select first EGL-CUDA Capable device for producer same as
          // consumer.
          cudaDevIndexProd = (int)cuda_device;
          printf(
              "Found EGL-CUDA Capable device with CUDA Device id = %d "
              "egl_device_id = %d \n",
              (int)cudaDevIndexProd, egl_device_id);
          break;
        }
      }
    }

    if (egl_device_id >= numDevices) {
      printf("No CUDA Capable EGL Device found.. Waiving execution\n");
      goto Done;
    }

    g_producerEglDisplay = eglGetPlatformDisplayEXT(
        EGL_PLATFORM_DEVICE_EXT, (void *)devices[egl_device_id], NULL);
    if (g_producerEglDisplay == EGL_NO_DISPLAY) {
      printf("Could not get EGL display from device. \n");
      eglStatus = EGL_FALSE;
      goto Done;
    }

    eglStatus = eglInitialize(g_producerEglDisplay, 0, 0);
    if (!eglStatus) {
      printf("EGL failed to initialize. \n");
      eglStatus = EGL_FALSE;
      goto Done;
    }

    g_producerEglStream =
        eglCreateStreamFromFileDescriptorKHR(g_producerEglDisplay, fileDesc);
    close(fileDesc);

    if (g_producerEglStream == EGL_NO_STREAM_KHR) {
      printf("CUDA Producer Could not create EGL stream.\n");
      eglStatus = EGL_FALSE;
      goto Done;
    } else {
      printf("Producer created EGLStream for the GPU.\n");
    }
  }

Done:
  return eglStatus == EGL_TRUE ? 1 : 0;
}

void EGLStreamFini(void) {
  if (g_producerEglStream != EGL_NO_STREAM_KHR) {
    eglDestroyStreamKHR(g_producerEglDisplay, g_producerEglStream);
  }
  if (g_consumerEglStream != g_producerEglStream) {
    if (g_consumerEglStream != EGL_NO_STREAM_KHR) {
      eglDestroyStreamKHR(g_consumerEglDisplay, g_consumerEglStream);
    }
  }
}

int UnixSocketConnect(const char *socket_name) {
  int sock_fd = -1;
  struct sockaddr_un sock_addr;
  int wait_loop = 0;

  sock_fd = socket(PF_UNIX, SOCK_STREAM, 0);
  if (sock_fd < 0) {
    printf("%s: socket create failed.\n", __func__);
    return -1;
  }

  if (verbose) printf("%s: send_fd: sock_fd: %d\n", __func__, sock_fd);

  memset(&sock_addr, 0, sizeof(struct sockaddr_un));
  sock_addr.sun_family = AF_UNIX;
  strncpy(sock_addr.sun_path, socket_name, sizeof(sock_addr.sun_path) - 1);

  while (connect(sock_fd, (const struct sockaddr *)&sock_addr,
                 sizeof(struct sockaddr_un))) {
    if (wait_loop < 60) {
      if (!wait_loop)
        printf("Waiting for EGL stream producer ");
      else
        printf(".");
      fflush(stdout);
      sleep(1);
      wait_loop++;
    } else {
      printf("\n%s: Waiting timed out\n", __func__);
      return -1;
    }
  }
  if (wait_loop) printf("\n");

  if (verbose) printf("%s: Wait is done\n", __func__);

  return sock_fd;
}

/* Send <fd_to_send> (a file descriptor) to another process */
/* over a unix domain socket named <socket_name>.           */
/* <socket_name> can be any nonexistant filename.           */
int EGLStreamSendfd(int send_fd, int fd_to_send) {
  struct msghdr msg;
  struct iovec iov[1];
  char ctrl_buf[CMSG_SPACE(sizeof(int))];
  struct cmsghdr *cmsg = NULL;
  void *data;
  int res;
  memset(&msg, 0, sizeof(msg));

  iov[0].iov_len = 1;             // must send at least 1 byte
  iov[0].iov_base = (void *)"x";  // any byte value (value ignored)
  msg.msg_iov = iov;
  msg.msg_iovlen = 1;

  memset(ctrl_buf, 0, sizeof(ctrl_buf));
  msg.msg_control = ctrl_buf;
  msg.msg_controllen = sizeof(ctrl_buf);

  cmsg = CMSG_FIRSTHDR(&msg);
  cmsg->cmsg_level = SOL_SOCKET;
  cmsg->cmsg_type = SCM_RIGHTS;
  cmsg->cmsg_len = CMSG_LEN(sizeof(int));
  data = CMSG_DATA(cmsg);
  *(int *)data = fd_to_send;

  msg.msg_controllen = cmsg->cmsg_len;

  res = sendmsg(send_fd, &msg, 0);
  if (res <= 0) {
    printf("%s: sendmsg failed", __func__);
    return -1;
  }

  return 0;
}

/* Listen on a unix domain socket named <socket_name>.     */
/* Connect to it and return connect_fd                     */
int UnixSocketCreate(const char *socket_name) {
  int listen_fd;
  struct sockaddr_un sock_addr;
  int connect_fd;
  struct sockaddr_un connect_addr;
  socklen_t connect_addr_len = 0;

  listen_fd = socket(PF_UNIX, SOCK_STREAM, 0);
  if (listen_fd < 0) {
    printf("%s: socket create failed", __func__);
    return -1;
  }

  if (verbose) printf("%s: listen_fd: %d\n", __func__, listen_fd);

  unlink(socket_name);

  memset(&sock_addr, 0, sizeof(struct sockaddr_un));
  sock_addr.sun_family = AF_UNIX;
  strncpy(sock_addr.sun_path, socket_name, sizeof(sock_addr.sun_path) - 1);

  if (bind(listen_fd, (const struct sockaddr *)&sock_addr,
           sizeof(struct sockaddr_un))) {
    printf("i%s: bind error", __func__);
    return -1;
  }

  if (listen(listen_fd, 1)) {
    printf("%s: listen error", __func__);
    return -1;
  }

  connect_fd =
      accept(listen_fd, (struct sockaddr *)&connect_addr, &connect_addr_len);

  if (verbose) printf("%s: connect_fd: %d\n", __func__, connect_fd);

  close(listen_fd);
  unlink(socket_name);
  if (connect_fd < 0) {
    printf("%s: accept failed\n", __func__);
    return -1;
  }

  return connect_fd;
}

/* receive a file descriptor from another process.         */
/* Returns the file descriptor.  Note: the integer value   */
/* of the file descriptor may be different from the        */
/* integer value in the other process, but the file        */
/* descriptors in each process will refer to the same file */
/* object in the kernel.                                   */
int EGLStreamReceivefd(int connect_fd) {
  struct msghdr msg;
  struct iovec iov[1];
  char msg_buf[1];
  char ctrl_buf[CMSG_SPACE(sizeof(int))];
  struct cmsghdr *cmsg;
  void *data;
  int recvfd;

  memset(&msg, 0, sizeof(msg));

  iov[0].iov_base = msg_buf;
  iov[0].iov_len = sizeof(msg_buf);
  msg.msg_iov = iov;
  msg.msg_iovlen = 1;

  msg.msg_control = ctrl_buf;
  msg.msg_controllen = sizeof(ctrl_buf);

  if (recvmsg(connect_fd, &msg, 0) <= 0) {
    printf("%s: recvmsg failed", __func__);
    return -1;
  }

  cmsg = CMSG_FIRSTHDR(&msg);
  if (!cmsg) {
    printf("%s: NULL message header\n", __func__);
    return -1;
  }
  if (cmsg->cmsg_level != SOL_SOCKET) {
    printf("%s: Message level is not SOL_SOCKET\n", __func__);
    return -1;
  }
  if (cmsg->cmsg_type != SCM_RIGHTS) {
    printf("%s: Message type is not SCM_RIGHTS\n", __func__);
    return -1;
  }

  data = CMSG_DATA(cmsg);
  recvfd = *(int *)data;

  return recvfd;
}

#endif
