/*
 * gulkan
 * Copyright 2018 Collabora Ltd.
 * Author: Lubosz Sarnecki <lubosz.sarnecki@collabora.com>
 * SPDX-License-Identifier: MIT
 */

#include <glib.h>
#include <gdk-pixbuf/gdk-pixbuf.h>

#include <amdgpu_drm.h>
#include <amdgpu.h>
#include <fcntl.h>

#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>

#include "gulkan.h"

#include "dmabuf_content.h"

typedef struct Example
{
  GulkanTexture *texture;
  GLFWwindow *window;
  GMainLoop *loop;
  VkSurfaceKHR surface;
  GulkanRenderer *renderer;
  bool should_quit;
  amdgpu_bo_handle amd_bo;
  amdgpu_device_handle amd_dev;
} Example;

static void *
allocate_dmabuf_amd (Example *self, gsize size, int *fd)
{
  /* use render node to avoid needing to authenticate: */
  int dev_fd = open ("/dev/dri/renderD128", 02, 0);

  uint32_t major_version;
  uint32_t minor_version;
  int ret = amdgpu_device_initialize (dev_fd,
                                     &major_version,
                                     &minor_version,
                                     &self->amd_dev);
  if (ret < 0)
    {
      g_printerr ("Could not create amdgpu device: %s\n", strerror (-ret));
      return NULL;
    }

  g_print ("Initialized amdgpu drm device with fd %d. Version %d.%d\n",
           dev_fd, major_version, minor_version);

  struct amdgpu_bo_alloc_request alloc_buffer =
  {
    .alloc_size = (uint64_t) size,
    .preferred_heap = AMDGPU_GEM_DOMAIN_GTT,
  };

  ret = amdgpu_bo_alloc (self->amd_dev, &alloc_buffer, &self->amd_bo);
  if (ret < 0)
    {
      g_printerr ("amdgpu_bo_alloc failed: %s\n", strerror(-ret));
      return NULL;
    }

  uint32_t shared_handle;
  ret = amdgpu_bo_export (self->amd_bo,
                          amdgpu_bo_handle_type_dma_buf_fd,
                         &shared_handle);

  if (ret < 0)
    {
      g_printerr ("amdgpu_bo_export failed: %s\n", strerror (-ret));
      return NULL;
    }

  *fd = (int) shared_handle;
  void *cpu_buffer;
  ret = amdgpu_bo_cpu_map (self->amd_bo, &cpu_buffer);

  if (ret < 0)
    {
      g_printerr ("amdgpu_bo_cpu_map failed: %s\n", strerror (-ret));
      return NULL;
    }

  return cpu_buffer;
}

static void
key_callback (GLFWwindow* window, int key,
              int scancode, int action, int mods)
{
  (void) scancode;
  (void) mods;

  if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
    {
      Example *self = (Example*) glfwGetWindowUserPointer (window);
      self->should_quit = true;
    }
}

static void
init_glfw (Example *self, guint width, guint height)
{
  glfwInit();

  glfwWindowHint (GLFW_CLIENT_API, GLFW_NO_API);
  glfwWindowHint (GLFW_RESIZABLE, false);

  self->window = glfwCreateWindow ((int)width, (int)height, "Vulkan Dmabuf",
                                   NULL, NULL);

  glfwSetKeyCallback (self->window, key_callback);

  glfwSetWindowUserPointer (self->window, self);
}

static gboolean
draw_cb (gpointer data)
{
  Example *self = (Example*) data;

  glfwPollEvents ();
  if (glfwWindowShouldClose (self->window) || self->should_quit)
    {
      g_main_loop_quit (self->loop);
      return FALSE;
    }

  gulkan_renderer_draw (self->renderer);

  GulkanClient *client = GULKAN_CLIENT (self->renderer);
  gulkan_device_wait_idle (gulkan_client_get_device (client));

  return TRUE;
}

#define ALIGN(_v, _d) (((_v) + ((_d) - 1)) & ~((_d) - 1))

int
main () {
  Example example = {
    .should_quit = false
  };

  /* create dmabuf */
  guint width = 1280;
  guint height = 720;

  int fd = -1;
  guint stride = (guint) ALIGN ((int)width, 32) * 4;
  gsize size = stride * height;
  char* map = (char*) allocate_dmabuf_amd (&example, size, &fd);

  dma_buf_fill (map, width, height, stride);

  init_glfw (&example, width, height);

  example.loop = g_main_loop_new (NULL, FALSE);

  example.renderer = gulkan_renderer_new ();

  uint32_t num_glfw_extensions = 0;
  const char** glfw_extensions;
  glfw_extensions = glfwGetRequiredInstanceExtensions (&num_glfw_extensions);

  GSList *instance_ext_list = NULL;
  for (uint32_t i = 0; i < num_glfw_extensions; i++)
    {
      char *instance_ext = g_strdup (glfw_extensions[i]);
      instance_ext_list = g_slist_append (instance_ext_list, instance_ext);
    }

  const gchar *instance_extensions[] =
  {
    VK_KHR_EXTERNAL_MEMORY_CAPABILITIES_EXTENSION_NAME,
    VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME
  };
  for (uint32_t i = 0; i < G_N_ELEMENTS (instance_extensions); i++)
    {
      char *instance_ext = g_strdup (instance_extensions[i]);
      instance_ext_list = g_slist_append (instance_ext_list, instance_ext);
    }

  const gchar *device_extensions[] =
  {
    VK_KHR_SWAPCHAIN_EXTENSION_NAME,
    VK_EXT_EXTERNAL_MEMORY_DMA_BUF_EXTENSION_NAME,
    VK_KHR_EXTERNAL_MEMORY_EXTENSION_NAME,
    VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME
  };

  GSList *device_ext_list = NULL;
  for (uint32_t i = 0; i < G_N_ELEMENTS (device_extensions); i++)
    {
      char *device_ext = g_strdup (device_extensions[i]);
      device_ext_list = g_slist_append (device_ext_list, device_ext);
    }

  GulkanClient *client = GULKAN_CLIENT (example.renderer);
  if (!gulkan_client_init_vulkan (client,
                                  instance_ext_list,
                                  device_ext_list))
  {
    g_printerr ("Unable to initialize Vulkan!\n");
    return -1;
  }

  GulkanDevice *device = gulkan_client_get_device (client);
  VkInstance instance = gulkan_client_get_instance_handle (client);
  if (glfwCreateWindowSurface (instance,
                               example.window, NULL,
                              &example.surface) != VK_SUCCESS)
    {
      g_printerr ("Unable to create surface.\n");
      return -1;
    }

  example.texture = gulkan_texture_new_from_dmabuf (device,
                                                    fd, width, height,
                                                    VK_FORMAT_B8G8R8A8_UNORM);

  if (example.texture == NULL)
    {
      g_printerr ("Unable to initialize vulkan dmabuf texture.\n");
      return -1;
    }

  gulkan_client_transfer_layout (client, example.texture,
                                 VK_IMAGE_LAYOUT_UNDEFINED,
                                 VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);

  if (!gulkan_renderer_init_rendering (example.renderer,
                                       example.surface,
                                       example.texture))
    return -1;

  g_timeout_add (20, draw_cb, &example);
  g_main_loop_run (example.loop);
  g_main_loop_unref (example.loop);

  g_slist_free (instance_ext_list);
  g_slist_free (device_ext_list);

  g_object_unref (example.texture);
  g_object_unref (example.renderer);

  glfwDestroyWindow (example.window);
  glfwTerminate ();

  int ret = amdgpu_bo_free(example.amd_bo);
  if (ret < 0)
    {
      g_printerr ("Could not free amdgpu buffer: %s\n", strerror (-ret));
      return -1;
    }

  ret = amdgpu_device_deinitialize (example.amd_dev);
  if (ret < 0)
    {
      g_printerr ("Could not free amdgpu device: %s\n", strerror (-ret));
      return -1;
    }

  return 0;
}
