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

#include "gulkan-client.h"

typedef struct _GulkanClientPrivate
{
  GObject object;

  VkCommandPool command_pool;

  GulkanInstance *instance;
  GulkanDevice *device;
} GulkanClientPrivate;

G_DEFINE_TYPE_WITH_PRIVATE (GulkanClient, gulkan_client, G_TYPE_OBJECT)

static void
gulkan_client_finalize (GObject *gobject);

static void
gulkan_client_class_init (GulkanClientClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = gulkan_client_finalize;
}

static void
gulkan_client_init (GulkanClient *self)
{
  GulkanClientPrivate *priv = gulkan_client_get_instance_private (self);
  priv->instance = gulkan_instance_new ();
  priv->device = gulkan_device_new ();
  priv->command_pool = VK_NULL_HANDLE;
}

GulkanClient *
gulkan_client_new (void)
{
  return (GulkanClient*) g_object_new (GULKAN_TYPE_CLIENT, 0);
}

static void
gulkan_client_finalize (GObject *gobject)
{
  GulkanClient *self = GULKAN_CLIENT (gobject);

  GulkanClientPrivate *priv = gulkan_client_get_instance_private (self);
  VkDevice vk_device = gulkan_device_get_handle (priv->device);

  if (priv->command_pool != VK_NULL_HANDLE)
    vkDestroyCommandPool (vk_device, priv->command_pool, NULL);

  g_object_unref (priv->device);
  g_object_unref (priv->instance);
}

bool
gulkan_client_init_command_pool (GulkanClient *self)
{
  GulkanClientPrivate *priv = gulkan_client_get_instance_private (self);
  VkDevice vk_device = gulkan_device_get_handle (priv->device);

  VkCommandPoolCreateInfo command_pool_info =
    {
      .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
      .queueFamilyIndex = gulkan_device_get_queue_family_index (priv->device),
      .flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT
    };

  VkResult res = vkCreateCommandPool (vk_device,
                                     &command_pool_info,
                                      NULL, &priv->command_pool);
  vk_check_error ("vkCreateCommandPool", res, false)
  return true;
}

bool
gulkan_cmd_buffer_begin (GulkanCommandBuffer *buffer,
                         GulkanDevice        *device,
                         VkCommandPool        command_pool)
{
  VkDevice vk_device = gulkan_device_get_handle (device);

  VkCommandBufferAllocateInfo command_buffer_info =
  {
    .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
    .commandBufferCount = 1,
    .commandPool = command_pool,
  };

  VkResult res;
  res = vkAllocateCommandBuffers (vk_device, &command_buffer_info,
                                 &buffer->handle);
  vk_check_error ("vkAllocateCommandBuffers", res, false)

  VkFenceCreateInfo fence_info = {
    .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO
  };
  res = vkCreateFence (vk_device, &fence_info,
                       NULL, &buffer->fence);
  vk_check_error ("vkCreateFence", res, false)

  VkCommandBufferBeginInfo command_buffer_begin_info =
  {
    .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
    .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT
  };
  res = vkBeginCommandBuffer (buffer->handle,
                             &command_buffer_begin_info);
  vk_check_error ("vkBeginCommandBuffer", res, false)

  return true;
}

bool
gulkan_cmd_buffer_submit (GulkanCommandBuffer *buffer,
                          GulkanDevice        *device,
                          VkCommandPool        command_pool)
{
  VkDevice vk_device = gulkan_device_get_handle (device);
  VkQueue vk_queue = gulkan_device_get_queue_handle (device);

  if (buffer == NULL || buffer->handle == VK_NULL_HANDLE)
    {
      g_printerr ("Trying to submit empty GulkanCommandBuffer\n.");
      return false;
    }

  VkResult res = vkEndCommandBuffer (buffer->handle);
  vk_check_error ("vkEndCommandBuffer", res, false)

  VkSubmitInfo submit_info = {
    .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
    .commandBufferCount = 1,
    .pCommandBuffers = &buffer->handle
  };

  res = vkQueueSubmit (vk_queue, 1,
                      &submit_info, buffer->fence);
  vk_check_error ("vkQueueSubmit", res, false)

  vkQueueWaitIdle (vk_queue);

  vkFreeCommandBuffers (vk_device, command_pool,
                        1, &buffer->handle);
  vkDestroyFence (vk_device, buffer->fence, NULL);

  return true;
}

bool
gulkan_client_begin_cmd_buffer (GulkanClient        *self,
                                GulkanCommandBuffer *buffer)
{
  GulkanClientPrivate *priv = gulkan_client_get_instance_private (self);
  return gulkan_cmd_buffer_begin (buffer, priv->device, priv->command_pool);
}

bool
gulkan_client_submit_cmd_buffer (GulkanClient        *self,
                                 GulkanCommandBuffer *buffer)
{
  GulkanClientPrivate *priv = gulkan_client_get_instance_private (self);
  return gulkan_cmd_buffer_submit (buffer, priv->device, priv->command_pool);
}

bool
gulkan_client_upload_pixels (GulkanClient   *self,
                             GulkanTexture  *texture,
                             guchar         *pixels,
                             gsize           size,
                             VkImageLayout   layout)
{
  GulkanCommandBuffer buffer = {};
  if (!gulkan_client_begin_cmd_buffer (self, &buffer))
    return false;

  if (!gulkan_texture_upload_pixels (texture, buffer.handle,
                                     pixels, size, layout))
    return false;

  if (!gulkan_client_submit_cmd_buffer (self, &buffer))
    return false;

  return true;
}

GulkanTexture *
gulkan_client_texture_new_from_cairo_surface (GulkanClient    *self,
                                              cairo_surface_t *surface,
                                              VkFormat         format,
                                              VkImageLayout    layout)
{
  GulkanCommandBuffer buffer = {};
  if (!gulkan_client_begin_cmd_buffer (self, &buffer))
    return NULL;

  GulkanClientPrivate *priv = gulkan_client_get_instance_private (self);

  GulkanTexture *texture =
    gulkan_texture_new_from_cairo_surface (priv->device, buffer.handle,
                                           surface, format, layout);

  if (texture == NULL)
    return texture;

  if (!gulkan_client_submit_cmd_buffer (self, &buffer))
    return NULL;

  return texture;
}

GulkanTexture *
gulkan_client_texture_new_from_pixbuf (GulkanClient *self,
                                       GdkPixbuf    *pixbuf,
                                       VkFormat      format,
                                       VkImageLayout layout,
                                       bool          create_mipmaps)
{
  GulkanCommandBuffer buffer = {};
  if (!gulkan_client_begin_cmd_buffer (self, &buffer))
    return NULL;

  GulkanClientPrivate *priv = gulkan_client_get_instance_private (self);

  GulkanTexture *texture =
    gulkan_texture_new_from_pixbuf (priv->device, buffer.handle,
                                    pixbuf, format, layout, create_mipmaps);

  if (texture == NULL)
    return texture;

  if (!gulkan_client_submit_cmd_buffer (self, &buffer))
    return NULL;

  return texture;
}

bool
gulkan_client_upload_pixbuf (GulkanClient  *self,
                             GulkanTexture *texture,
                             GdkPixbuf     *pixbuf,
                             VkImageLayout  layout)
{
  guchar *pixels = gdk_pixbuf_get_pixels (pixbuf);
  gsize size = gdk_pixbuf_get_byte_length (pixbuf);

  return gulkan_client_upload_pixels (self, texture, pixels, size, layout);
}

bool
gulkan_client_transfer_layout_full (GulkanClient        *self,
                                    GulkanTexture       *texture,
                                    VkAccessFlags        src_access_mask,
                                    VkAccessFlags        dst_access_mask,
                                    VkImageLayout        src_layout,
                                    VkImageLayout        dst_layout,
                                    VkPipelineStageFlags src_stage_mask,
                                    VkPipelineStageFlags dst_stage_mask)
{
  GulkanClientPrivate *priv = gulkan_client_get_instance_private (self);
  GulkanCommandBuffer cmd_buffer = {};
  if (!gulkan_client_begin_cmd_buffer (self, &cmd_buffer))
    return false;

  gulkan_texture_transfer_layout_full (texture, priv->device,
                                       cmd_buffer.handle,
                                       src_access_mask, dst_access_mask,
                                       src_layout, dst_layout,
                                       src_stage_mask, dst_stage_mask);

  if (!gulkan_client_submit_cmd_buffer (self, &cmd_buffer))
    return false;

  return true;
}

bool
gulkan_client_transfer_layout (GulkanClient  *self,
                               GulkanTexture *texture,
                               VkImageLayout  src_layout,
                               VkImageLayout  dst_layout)
{
  GulkanClientPrivate *priv = gulkan_client_get_instance_private (self);
  GulkanCommandBuffer cmd_buffer = {};
  if (!gulkan_client_begin_cmd_buffer (self, &cmd_buffer))
    return false;

  gulkan_texture_transfer_layout (texture, priv->device,
                                  cmd_buffer.handle,
                                  src_layout, dst_layout);

  if (!gulkan_client_submit_cmd_buffer (self, &cmd_buffer))
    return false;

  return true;
}

bool
gulkan_client_upload_cairo_surface (GulkanClient    *self,
                                    GulkanTexture   *texture,
                                    cairo_surface_t *surface,
                                    VkImageLayout    layout)
{
  guchar *pixels = cairo_image_surface_get_data (surface);
  gsize size = (gsize)cairo_image_surface_get_stride (surface) *
               (gsize)cairo_image_surface_get_height (surface);

  return gulkan_client_upload_pixels (self, texture, pixels, size, layout);
}

bool
gulkan_client_init_vulkan_full (GulkanClient    *self,
                                GSList          *instance_extensions,
                                GSList          *device_extensions,
                                VkPhysicalDevice physical_device)
{
  GulkanClientPrivate *priv = gulkan_client_get_instance_private (self);
  if (!gulkan_instance_create (priv->instance,
                               instance_extensions))
    {
      g_printerr ("Failed to create instance.\n");
      return false;
    }

  if (!gulkan_device_create (priv->device, priv->instance,
                             physical_device, device_extensions))
    {
      g_printerr ("Failed to create device.\n");
      return false;
    }

  if (!gulkan_client_init_command_pool (self))
    {
      g_printerr ("Failed to create command pool.\n");
      return false;
    }

  return true;
}

bool
gulkan_client_init_vulkan (GulkanClient *self,
                           GSList       *instance_extensions,
                           GSList       *device_extensions)
{
  return gulkan_client_init_vulkan_full (self, instance_extensions,
                                         device_extensions,
                                         VK_NULL_HANDLE);
}

VkPhysicalDevice
gulkan_client_get_physical_device_handle (GulkanClient *self)
{
  GulkanClientPrivate *priv = gulkan_client_get_instance_private (self);
  return gulkan_device_get_physical_handle (priv->device);
}

VkDevice
gulkan_client_get_device_handle (GulkanClient *self)
{
  GulkanClientPrivate *priv = gulkan_client_get_instance_private (self);
  return gulkan_device_get_handle (priv->device);
}

GulkanDevice *
gulkan_client_get_device (GulkanClient *self)
{
  GulkanClientPrivate *priv = gulkan_client_get_instance_private (self);
  return priv->device;
}

VkInstance
gulkan_client_get_instance_handle (GulkanClient *self)
{
  GulkanClientPrivate *priv = gulkan_client_get_instance_private (self);
  return gulkan_instance_get_handle (priv->instance);
}

GulkanInstance *
gulkan_client_get_instance (GulkanClient *self)
{
  GulkanClientPrivate *priv = gulkan_client_get_instance_private (self);
  return priv->instance;
}

VkCommandPool
gulkan_client_get_command_pool (GulkanClient *self)
{
  GulkanClientPrivate *priv = gulkan_client_get_instance_private (self);
  return priv->command_pool;
}
