/**************************************************************************
 *
 * Copyright 2015, 2018 Collabora
 * All Rights Reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sub license, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice (including the
 * next paragraph) shall be included in all copies or substantial portions
 * of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 **************************************************************************/

#ifdef HAVE_LIBDRM
#include <xf86drm.h>
#endif
#include "util/compiler.h"
#include "util/macros.h"

#include "eglcurrent.h"
#include "egldevice.h"
#include "egllog.h"
#include "eglglobals.h"
#include "egltypedefs.h"


struct _egl_device {
   _EGLDevice *Next;

   const char *extensions;

   EGLBoolean MESA_device_software;
   EGLBoolean EXT_device_drm;
   EGLBoolean EXT_device_drm_render_node;

#ifdef HAVE_LIBDRM
   drmDevicePtr device;
#endif
};

void
_eglFiniDevice(void)
{
   _EGLDevice *dev_list, *dev;

   /* atexit function is called with global mutex locked */

   dev_list = _eglGlobal.DeviceList;

   /* The first device is static allocated SW device */
   assert(dev_list);
   assert(_eglDeviceSupports(dev_list, _EGL_DEVICE_SOFTWARE));
   dev_list = dev_list->Next;

   while (dev_list) {
      /* pop list head */
      dev = dev_list;
      dev_list = dev_list->Next;

#ifdef HAVE_LIBDRM
      assert(_eglDeviceSupports(dev, _EGL_DEVICE_DRM));
      drmFreeDevice(&dev->device);
#endif
      free(dev);
   }

   _eglGlobal.DeviceList = NULL;
}

EGLBoolean
_eglCheckDeviceHandle(EGLDeviceEXT device)
{
   _EGLDevice *cur;

   mtx_lock(_eglGlobal.Mutex);
   cur = _eglGlobal.DeviceList;
   while (cur) {
      if (cur == (_EGLDevice *) device)
         break;
      cur = cur->Next;
   }
   mtx_unlock(_eglGlobal.Mutex);
   return (cur != NULL);
}

_EGLDevice _eglSoftwareDevice = {
   /* TODO: EGL_EXT_device_drm support for KMS + llvmpipe */
   .extensions = "EGL_MESA_device_software EGL_EXT_device_drm_render_node",
   .MESA_device_software = EGL_TRUE,
   .EXT_device_drm_render_node = EGL_TRUE,
};

#ifdef HAVE_LIBDRM
/*
 * Negative value on error, zero if newly added, one if already in list.
 */
static int
_eglAddDRMDevice(drmDevicePtr device, _EGLDevice **out_dev)
{
   _EGLDevice *dev;

   if ((device->available_nodes & (1 << DRM_NODE_PRIMARY |
                                   1 << DRM_NODE_RENDER)) == 0)
      return -1;

   dev = _eglGlobal.DeviceList;

   /* The first device is always software */
   assert(dev);
   assert(_eglDeviceSupports(dev, _EGL_DEVICE_SOFTWARE));

   while (dev->Next) {
      dev = dev->Next;

      assert(_eglDeviceSupports(dev, _EGL_DEVICE_DRM));
      if (drmDevicesEqual(device, dev->device) != 0) {
         if (out_dev)
            *out_dev = dev;
         return 1;
      }
   }

   dev->Next = calloc(1, sizeof(_EGLDevice));
   if (!dev->Next) {
      if (out_dev)
         *out_dev = NULL;
      return -1;
   }

   dev = dev->Next;
   dev->extensions = "EGL_EXT_device_drm";
   dev->EXT_device_drm = EGL_TRUE;
   dev->device = device;

   /* TODO: EGL_EXT_device_drm_render_node support for kmsro + renderonly */
   if (device->available_nodes & (1 << DRM_NODE_RENDER)) {
      dev->extensions = "EGL_EXT_device_drm EGL_EXT_device_drm_render_node";
      dev->EXT_device_drm_render_node = EGL_TRUE;
   }

   if (out_dev)
      *out_dev = dev;

   return 0;
}
#endif

/* Adds a device in DeviceList, if needed for the given fd.
 *
 * If a software device, the fd is ignored.
 */
_EGLDevice *
_eglAddDevice(int fd, bool software)
{
   _EGLDevice *dev;

   mtx_lock(_eglGlobal.Mutex);
   dev = _eglGlobal.DeviceList;

   /* The first device is always software */
   assert(dev);
   assert(_eglDeviceSupports(dev, _EGL_DEVICE_SOFTWARE));
   if (software)
      goto out;

#ifdef HAVE_LIBDRM
   drmDevicePtr device;

   if (drmGetDevice2(fd, 0, &device) != 0) {
      dev = NULL;
      goto out;
   }

   /* Device is not added - error or already present */
   if (_eglAddDRMDevice(device, &dev) != 0)
      drmFreeDevice(&device);
#else
   _eglLog(_EGL_FATAL, "Driver bug: Built without libdrm, yet looking for HW device");
   dev = NULL;
#endif

out:
   mtx_unlock(_eglGlobal.Mutex);
   return dev;
}

EGLBoolean
_eglDeviceSupports(_EGLDevice *dev, _EGLDeviceExtension ext)
{
   switch (ext) {
   case _EGL_DEVICE_SOFTWARE:
      return dev->MESA_device_software;
   case _EGL_DEVICE_DRM:
      return dev->EXT_device_drm;
   case _EGL_DEVICE_DRM_RENDER_NODE:
      return dev->EXT_device_drm_render_node;
   default:
      assert(0);
      return EGL_FALSE;
   };
}

/* Ideally we'll have an extension which passes the render node,
 * instead of the card one + magic.
 *
 * Then we can move this in _eglQueryDeviceStringEXT below. Until then
 * keep it separate.
 */
const char *
_eglGetDRMDeviceRenderNode(_EGLDevice *dev)
{
#ifdef HAVE_LIBDRM
   return dev->device->nodes[DRM_NODE_RENDER];
#else
   return NULL;
#endif
}

EGLBoolean
_eglQueryDeviceAttribEXT(_EGLDevice *dev, EGLint attribute,
                         EGLAttrib *value)
{
   switch (attribute) {
   default:
      _eglError(EGL_BAD_ATTRIBUTE, "eglQueryDeviceAttribEXT");
      return EGL_FALSE;
   }
}

const char *
_eglQueryDeviceStringEXT(_EGLDevice *dev, EGLint name)
{
   switch (name) {
   case EGL_EXTENSIONS:
      return dev->extensions;
   case EGL_DRM_DEVICE_FILE_EXT:
      if (!_eglDeviceSupports(dev, _EGL_DEVICE_DRM))
         break;
#ifdef HAVE_LIBDRM
      return dev->device->nodes[DRM_NODE_PRIMARY];
#else
      /* This should never happen: we don't yet support EGL_DEVICE_DRM for the
       * software device, and physical devices are only exposed when libdrm is
       * available. */
      assert(0);
      break;
#endif
   case EGL_DRM_RENDER_NODE_FILE_EXT:
      if (!_eglDeviceSupports(dev, _EGL_DEVICE_DRM_RENDER_NODE))
         break;
#ifdef HAVE_LIBDRM
      return dev->device ? dev->device->nodes[DRM_NODE_RENDER] : NULL;
#else
      /* Physical devices are only exposed when libdrm is available. */
      assert(_eglDeviceSupports(dev, _EGL_DEVICE_SOFTWARE));
      return NULL;
#endif
   }
   _eglError(EGL_BAD_PARAMETER, "eglQueryDeviceStringEXT");
   return NULL;
}

/* Do a fresh lookup for devices.
 *
 * Walks through the DeviceList, discarding no longer available ones
 * and adding new ones as applicable.
 *
 * Must be called with the global lock held.
 */
static int
_eglRefreshDeviceList(void)
{
   ASSERTED _EGLDevice *dev;
   int count = 0;

   dev = _eglGlobal.DeviceList;

   /* The first device is always software */
   assert(dev);
   assert(_eglDeviceSupports(dev, _EGL_DEVICE_SOFTWARE));
   count++;

#ifdef HAVE_LIBDRM
   drmDevicePtr devices[64];
   int num_devs, ret;

   num_devs = drmGetDevices2(0, devices, ARRAY_SIZE(devices));
   for (int i = 0; i < num_devs; i++) {
      if (!(devices[i]->available_nodes & (1 << DRM_NODE_RENDER)))
         continue;

      ret = _eglAddDRMDevice(devices[i], NULL);

      /* Device is not added - error or already present */
      if (ret != 0)
         drmFreeDevice(&devices[i]);

      if (ret >= 0)
         count++;
   }
#endif

   return count;
}

EGLBoolean
_eglQueryDevicesEXT(EGLint max_devices,
                    _EGLDevice **devices,
                    EGLint *num_devices)
{
   _EGLDevice *dev, *devs;
   int i = 0, num_devs;

   if ((devices && max_devices <= 0) || !num_devices)
      return _eglError(EGL_BAD_PARAMETER, "eglQueryDevicesEXT");

   mtx_lock(_eglGlobal.Mutex);

   num_devs = _eglRefreshDeviceList();
   devs = _eglGlobal.DeviceList;

   /* bail early if we only care about the count */
   if (!devices) {
      *num_devices = num_devs;
      goto out;
   }

   /* Push the first device (the software one) to the end of the list.
    * Sending it to the user only if they've requested the full list.
    *
    * By default, the user is likely to pick the first device so having the
    * software (aka least performant) one is not a good idea.
    */
   *num_devices = MIN2(num_devs, max_devices);

   for (i = 0, dev = devs->Next; dev && i < max_devices; i++) {
      devices[i] = dev;
      dev = dev->Next;
   }

   /* User requested the full device list, add the sofware device. */
   if (max_devices >= num_devs) {
      assert(_eglDeviceSupports(devs, _EGL_DEVICE_SOFTWARE));
      devices[num_devs - 1] = devs;
   }

out:
   mtx_unlock(_eglGlobal.Mutex);

   return EGL_TRUE;
}
