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

#define VK_USE_PLATFORM_XLIB_KHR
#include <vulkan/vulkan.h>

#include <gmodule.h>
#include <glib.h>
#include <glib/gprintf.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <graphene.h>

#include "gulkan-descriptor-set.h"
#include "gulkan-renderer.h"

#define FRAMES_IN_FLIGHT 2

typedef struct {
  float position[2];
  float uv[2];
} Vertex;

static const Vertex vertices[4] = {
  {{-1.f, -1.f}, {1.f, 0.f}},
  {{ 1.f, -1.f}, {0.f, 0.f}},
  {{ 1.f,  1.f}, {0.f, 1.f}},
  {{-1.f,  1.f}, {1.f, 1.f}}
};

static const uint16_t indices[6] = {0, 1, 2, 2, 3, 0};

typedef struct {
  float mvp[16];
} Transformation;

struct _GulkanRenderer
{
  GulkanClient parent_type;

  VkSwapchainKHR swap_chain;
  VkImageView *swapchain_image_views;
  VkFormat swapchain_image_format;
  uint32_t swapchain_image_count;
  VkExtent2D swapchain_extent;

  VkRenderPass render_pass;
  VkDescriptorSetLayout descriptor_set_layout;

  VkPipeline graphics_pipeline;
  VkPipelineLayout pipeline_layout;

  VkFramebuffer *framebuffers;

  VkBuffer uniform_buffer;
  VkDeviceMemory uniform_buffer_memory;

  VkDescriptorPool descriptor_pool;

  VkDescriptorSet *descriptor_sets;

  VkBuffer vertex_buffer;
  VkDeviceMemory vertex_buffer_memory;
  VkBuffer index_buffer;
  VkDeviceMemory index_buffer_memory;

  VkSemaphore *submit_semaphores;
  VkSemaphore *present_semaphores;
  VkFence *fences;

  VkCommandBuffer *draw_cmd_buffers;

  VkSampler sampler;

  size_t current_frame;

  VkSurfaceKHR surface;
};

G_DEFINE_TYPE (GulkanRenderer, gulkan_renderer,
               GULKAN_TYPE_CLIENT)

static void
gulkan_renderer_finalize (GObject *gobject);


static void
gulkan_renderer_class_init (GulkanRendererClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  object_class->finalize = gulkan_renderer_finalize;
}

static void
gulkan_renderer_init (GulkanRenderer *self)
{
  self->current_frame = 0;
  self->swap_chain = VK_NULL_HANDLE;
  self->surface = VK_NULL_HANDLE;
  self->sampler = VK_NULL_HANDLE;
}

GulkanRenderer *
gulkan_renderer_new (void)
{
  return (GulkanRenderer*) g_object_new (GULKAN_TYPE_RENDERER, 0);
}

static void
gulkan_renderer_finalize (GObject *gobject)
{
  GulkanRenderer *self = GULKAN_RENDERER (gobject);

  GulkanClient *client = GULKAN_CLIENT (gobject);

  VkDevice device = gulkan_client_get_device_handle (client);
  /* Idle the device to make sure no work is outstanding */
  gulkan_device_wait_idle (gulkan_client_get_device (client));

  /* Check if rendering was initialized */
  if (self->swap_chain != VK_NULL_HANDLE)
    {
      for (size_t i = 0; i < FRAMES_IN_FLIGHT; i++)
        {
          vkDestroySemaphore (device, self->present_semaphores[i], NULL);
          vkDestroySemaphore (device, self->submit_semaphores[i], NULL);
          vkDestroyFence (device, self->fences[i], NULL);
        }

      g_free (self->present_semaphores);
      g_free (self->submit_semaphores);
      g_free (self->fences);

      g_free (self->draw_cmd_buffers);
      g_free (self->descriptor_sets);

      vkDestroyDescriptorPool (device, self->descriptor_pool, NULL);

      vkDestroySampler (device, self->sampler, NULL);

      vkDestroyBuffer (device, self->vertex_buffer, NULL);
      vkFreeMemory (device, self->vertex_buffer_memory, NULL);

      vkDestroyBuffer (device, self->index_buffer, NULL);
      vkFreeMemory (device, self->index_buffer_memory, NULL);


      vkDestroyBuffer (device, self->uniform_buffer, NULL);
      vkFreeMemory (device, self->uniform_buffer_memory, NULL);

      for (uint32_t i = 0; i < self->swapchain_image_count; i++)
        vkDestroyFramebuffer (device, self->framebuffers[i], NULL);

      g_free (self->framebuffers);

      vkDestroyPipeline (device, self->graphics_pipeline, NULL);
      vkDestroyPipelineLayout (device, self->pipeline_layout, NULL);

      vkDestroyDescriptorSetLayout (device, self->descriptor_set_layout, NULL);

      vkDestroyRenderPass (device, self->render_pass, NULL);

      for (uint32_t i = 0; i < self->swapchain_image_count; i++)
        vkDestroyImageView (device, self->swapchain_image_views[i], NULL);

      g_free (self->swapchain_image_views);

      vkDestroySwapchainKHR (device, self->swap_chain, NULL);
    }

  VkInstance instance = gulkan_client_get_instance_handle (client);
  if (self->surface != VK_NULL_HANDLE)
    vkDestroySurfaceKHR (instance, self->surface, NULL);

  G_OBJECT_CLASS (gulkan_renderer_parent_class)->finalize (gobject);
}

static VkImageView
_create_image_view (VkDevice device, VkImage image, VkFormat format)
{
  VkImageViewCreateInfo info = {
    .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
    .image = image,
    .viewType = VK_IMAGE_VIEW_TYPE_2D,
    .format = format,
    .subresourceRange = {
      .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
      .baseMipLevel = 0,
      .levelCount = 1,
      .baseArrayLayer = 0,
      .layerCount = 1
    }
  };

  VkImageView image_view;
  VkResult res = vkCreateImageView (device, &info, NULL, &image_view);
  vk_check_error ("vkCreateImageView", res, VK_NULL_HANDLE)

  return image_view;
}

static bool
_create_render_pass (VkDevice device,
                     VkFormat format,
                     VkRenderPass *render_pass)
{
  VkRenderPassCreateInfo info = {
    .sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO,
    .attachmentCount = 1,
    .pAttachments = &(VkAttachmentDescription) {
      .format = format,
      .samples = VK_SAMPLE_COUNT_1_BIT,
      .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
      .storeOp = VK_ATTACHMENT_STORE_OP_STORE,
      .stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE,
      .stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE,
      .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
      .finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
    },
    .subpassCount = 1,
    .pSubpasses = &(VkSubpassDescription) {
      .pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
      .colorAttachmentCount = 1,
      .pColorAttachments = &(VkAttachmentReference) {
        .attachment = 0,
        .layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
      },
      .pResolveAttachments = NULL,
    },
    .dependencyCount = 1,
    .pDependencies = &(VkSubpassDependency) {
      .srcSubpass = VK_SUBPASS_EXTERNAL,
      .dstSubpass = 0,
      .srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
      .dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
      .srcAccessMask = 0,
      .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT |
                       VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT
    }
  };

  VkResult res = vkCreateRenderPass (device, &info, NULL, render_pass);
  vk_check_error ("vkCreateRenderPass", res, false)

  return true;
}

static bool
_create_descriptor_set_layout (VkDevice device,
                               VkDescriptorSetLayout *descriptor_set_layout)
{
  VkDescriptorSetLayoutCreateInfo info = {
    .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
    .bindingCount = 2,
    .pBindings = (VkDescriptorSetLayoutBinding[]) {
      {
        .binding = 0,
        .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
        .descriptorCount = 1,
        .stageFlags = VK_SHADER_STAGE_VERTEX_BIT,
      },
      {
        .binding = 1,
        .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
        .descriptorCount = 1,
        .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT,
      }
    }
  };

  VkResult res = vkCreateDescriptorSetLayout (device, &info,
                                              NULL, descriptor_set_layout);
  vk_check_error ("vkCreateDescriptorSetLayout", res, false)

  return true;
}

static bool
_create_graphics_pipeline (VkDevice device,
                           VkShaderModule vert_shader,
                           VkShaderModule frag_shader,
                           VkExtent2D extent,
                           VkDescriptorSetLayout descriptor_set_layout,
                           VkRenderPass render_pass,
                           VkPipeline *graphics_pipeline,
                           VkPipelineLayout *pipeline_layout)
{
  VkPipelineLayoutCreateInfo layout_info = {
    .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
    .setLayoutCount = 1,
    .pSetLayouts = &descriptor_set_layout
  };

  VkResult res;
  res = vkCreatePipelineLayout (device, &layout_info, NULL, pipeline_layout);
  vk_check_error ("vkCreatePipelineLayout", res, false)

  VkGraphicsPipelineCreateInfo pipeline_info = {
    .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
    .stageCount = 2,
    .pStages = (VkPipelineShaderStageCreateInfo[]) {
      {
        .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
        .stage = VK_SHADER_STAGE_VERTEX_BIT,
        .module = vert_shader,
        .pName = "main"
      },
      {
        .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
        .stage = VK_SHADER_STAGE_FRAGMENT_BIT,
        .module = frag_shader,
        .pName = "main"
      }
    },
    .pVertexInputState = &(VkPipelineVertexInputStateCreateInfo) {
      .sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
      .vertexBindingDescriptionCount = 1,
      .pVertexBindingDescriptions = &(VkVertexInputBindingDescription) {
        .binding = 0,
        .stride = sizeof(Vertex),
        .inputRate = VK_VERTEX_INPUT_RATE_VERTEX
      },
      .vertexAttributeDescriptionCount = 2,
      .pVertexAttributeDescriptions = (VkVertexInputAttributeDescription[]) {
        {
          .location = 0,
          .binding = 0,
          .format = VK_FORMAT_R32G32_SFLOAT,
          .offset = offsetof (Vertex, position)
        },
        {
          .location = 1,
          .binding = 0,
          .format = VK_FORMAT_R32G32_SFLOAT,
          .offset = offsetof (Vertex, uv)
        }
      },
    },
    .pInputAssemblyState = &(VkPipelineInputAssemblyStateCreateInfo) {
      .sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
      .topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST
    },
    .pViewportState = &(VkPipelineViewportStateCreateInfo) {
      .sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,
      .viewportCount = 1,
      .pViewports = &(VkViewport) {
        .x = 0.0f,
        .y = 0.0f,
        .width = (float) extent.width,
        .height = (float) extent.height,
        .minDepth = 0.0f,
        .maxDepth = 1.0f
      },
      .scissorCount = 1,
      .pScissors = &(VkRect2D) {
        .offset = {0, 0},
        .extent = extent
      }
    },
    .pRasterizationState = &(VkPipelineRasterizationStateCreateInfo) {
      .sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO,
      .polygonMode = VK_POLYGON_MODE_FILL,
      .cullMode = VK_CULL_MODE_NONE,
      .frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE,
      .lineWidth = 1.0f
    },
    .pMultisampleState = &(VkPipelineMultisampleStateCreateInfo) {
      .sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,
      .rasterizationSamples = VK_SAMPLE_COUNT_1_BIT,
    },
    .pColorBlendState = &(VkPipelineColorBlendStateCreateInfo) {
      .sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
      .attachmentCount = 1,
      .pAttachments = &(VkPipelineColorBlendAttachmentState) {
        .colorWriteMask =
          VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
          VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT
      },
     .blendConstants = {0.f, 0.f, 0.f, 0.f}
    },
    .layout = *pipeline_layout,
    .renderPass = render_pass
  };

  res = vkCreateGraphicsPipelines (device, VK_NULL_HANDLE, 1,
                                  &pipeline_info, NULL, graphics_pipeline);
  vk_check_error ("vkCreateGraphicsPipelines", res, false)

  return true;
}

static bool
_find_surface_format (VkPhysicalDevice device,
                      VkSurfaceKHR surface,
                      VkSurfaceFormatKHR *format)
{
  uint32_t num_formats;
  VkSurfaceFormatKHR *formats = NULL;
  vkGetPhysicalDeviceSurfaceFormatsKHR (device, surface, &num_formats, NULL);

  if (num_formats != 0)
    {
      formats = g_malloc (sizeof(VkSurfaceFormatKHR) * num_formats);
      vkGetPhysicalDeviceSurfaceFormatsKHR (device, surface,
                                           &num_formats, formats);
    }
  else
    {
      g_printerr ("Could enumerate surface formats.\n");
      return false;
    }

  for (uint32_t i = 0; i < num_formats; i++)
    if (formats[i].format == VK_FORMAT_B8G8R8A8_UNORM &&
        formats[i].colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR)
      {
        format->format = formats[i].format;
        format->colorSpace = formats[i].colorSpace;
        g_free (formats);
        return true;
      }

  g_free (formats);
  g_printerr ("Requested format not supported.\n");
  return false;
}

static bool
_find_surface_present_mode (VkPhysicalDevice device,
                            VkSurfaceKHR surface,
                            VkPresentModeKHR *present_mode)
{
  uint32_t num_present_modes;
  VkPresentModeKHR *present_modes;
  vkGetPhysicalDeviceSurfacePresentModesKHR (device, surface,
                                            &num_present_modes, NULL);

  if (num_present_modes != 0)
    {
      present_modes = g_malloc (sizeof (VkPresentModeKHR) * num_present_modes);
      vkGetPhysicalDeviceSurfacePresentModesKHR (device, surface,
                                                &num_present_modes,
                                                 present_modes);
    }
  else
    {
      g_printerr ("Could enumerate present modes.\n");
      return false;
    }

  for (uint32_t i = 0; i < num_present_modes; i++)
    if (present_modes[i] == VK_PRESENT_MODE_IMMEDIATE_KHR)
      {
        *present_mode = present_modes[i];
        g_free (present_modes);
        return true;
      }

  g_free (present_modes);
  g_printerr ("Requested present mode not supported.\n");
  return false;
}

static bool
_init_swapchain (VkDevice device,
                 VkPhysicalDevice physical_device,
                 VkSurfaceKHR surface,
                 VkSwapchainKHR *swapchain,
                 VkImageView **image_views,
                 VkFormat *image_format,
                 uint32_t *image_count,
                 VkExtent2D *extent)
{
  VkSurfaceFormatKHR surface_format = {};
  if (!_find_surface_format(physical_device, surface, &surface_format))
    return false;

  VkPresentModeKHR present_mode;
  if (!_find_surface_present_mode (physical_device, surface, &present_mode))
    return false;

  VkSurfaceCapabilitiesKHR surface_caps;
  vkGetPhysicalDeviceSurfaceCapabilitiesKHR (physical_device,
                                             surface, &surface_caps);

  *image_format = surface_format.format;
  *extent = surface_caps.currentExtent;

  VkSwapchainCreateInfoKHR info = {
    .sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR,
    .surface = surface,
    .minImageCount = surface_caps.minImageCount,
    .imageFormat = *image_format,
    .imageColorSpace = surface_format.colorSpace,
    .imageExtent = *extent,
    .imageArrayLayers = 1,
    .imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
    .preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR,
    .compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR,
    .presentMode = present_mode,
    .imageSharingMode = VK_SHARING_MODE_EXCLUSIVE,
    .clipped = VK_TRUE
  };

  VkResult res = vkCreateSwapchainKHR (device, &info, NULL, swapchain);
  vk_check_error ("vkCreateSwapchainKHR", res, false)

  res = vkGetSwapchainImagesKHR (device, *swapchain, image_count, NULL);

  g_print ("The swapchain has %d images\n", *image_count);

  VkImage *images = g_malloc (sizeof(VkImage) * *image_count);
  res = vkGetSwapchainImagesKHR (device, *swapchain, image_count, images);
  vk_check_error ("vkGetSwapchainImagesKHR", res, false)

  *image_views = g_malloc (sizeof(VkImage) * *image_count);
  for (uint32_t i = 0; i < *image_count; i++)
    (*image_views)[i] = _create_image_view (device, images[i], *image_format);

  g_free (images);

  return true;
}

static bool
_load_resource (const gchar* path, GBytes **res)
{
  GError *error = NULL;

  *res = g_resources_lookup_data (path, G_RESOURCE_LOOKUP_FLAGS_NONE, &error);

  if (error != NULL)
    {
      g_printerr ("Unable to read file: %s\n", error->message);
      g_error_free (error);
      return false;
    }

  return true;
}

bool
gulkan_renderer_create_shader_module (VkDevice device,
                                      const gchar* resource_name,
                                      VkShaderModule *module)
{
  GBytes *bytes;
  if (!_load_resource (resource_name, &bytes))
    return false;

  gsize size = 0;
  const uint32_t *buffer = g_bytes_get_data (bytes, &size);

  VkShaderModuleCreateInfo info = {
    .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
    .codeSize = size,
    .pCode = buffer,
  };

  VkResult res = vkCreateShaderModule (device, &info, NULL, module);
  vk_check_error ("vkCreateShaderModule", res, false)

  g_bytes_unref (bytes);

  return true;
}

static bool
_init_framebuffers (VkDevice device,
                    VkRenderPass render_pass,
                    VkExtent2D swapchain_extent,
                    uint32_t swapchain_image_count,
                    VkImageView *swapchain_image_views,
                    VkFramebuffer **framebuffers)
{
  *framebuffers = g_malloc (sizeof(VkFramebuffer) * swapchain_image_count);

  for (size_t i = 0; i < swapchain_image_count; i++)
    {
      VkFramebufferCreateInfo info = {
        .sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO,
        .renderPass = render_pass,
        .attachmentCount = 1,
        .pAttachments = &swapchain_image_views[i],
        .width = swapchain_extent.width,
        .height = swapchain_extent.height,
        .layers = 1
      };

      VkResult res =
        vkCreateFramebuffer (device, &info, NULL, &((*framebuffers)[i]));
      vk_check_error ("vkCreateFramebuffer", res, false)
    }
  return true;
}

static void
_copy_buffer (VkCommandBuffer cmd_buffer,
              VkBuffer src_buffer,
              VkBuffer dst_buffer,
              VkDeviceSize size)
{
  VkBufferCopy copy_region = {
    .size = size
  };

  vkCmdCopyBuffer (cmd_buffer, src_buffer, dst_buffer, 1, &copy_region);
}

static bool
_init_vertex_buffer (GulkanRenderer *self)
{
  VkDeviceSize buffer_size = sizeof (vertices[0]) * 4;

  GulkanClient *client = GULKAN_CLIENT (self);
  GulkanDevice *gkdevice = gulkan_client_get_device (client);
  VkDevice device = gulkan_client_get_device_handle (client);

  VkBuffer staging_buffer;
  VkDeviceMemory staging_buffer_memory;
  if (!gulkan_device_create_buffer (
      gkdevice, buffer_size,
      VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
      VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
      VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
     &staging_buffer, &staging_buffer_memory))
    return false;

  if (!gulkan_device_map_memory (gkdevice, vertices,
                                        buffer_size, staging_buffer_memory))
    return false;

  if (!gulkan_device_create_buffer (
      gkdevice, buffer_size,
      VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
      VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
     &self->vertex_buffer, &self->vertex_buffer_memory))
    return false;

  GulkanCommandBuffer cmd_buffer = {};
  if (!gulkan_client_begin_cmd_buffer (client, &cmd_buffer))
    return false;

  _copy_buffer (cmd_buffer.handle,
                staging_buffer, self->vertex_buffer, buffer_size);

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

  vkDestroyBuffer (device, staging_buffer, NULL);
  vkFreeMemory (device, staging_buffer_memory, NULL);

  return true;
}

static bool
_init_index_buffer (GulkanRenderer *self)
{
  VkDeviceSize buffer_size = sizeof (indices[0]) * 6;

  GulkanClient *client = GULKAN_CLIENT (self);
  GulkanDevice *gkdevice = gulkan_client_get_device (client);
  VkDevice device = gulkan_client_get_device_handle (client);

  VkBuffer staging_buffer;
  VkDeviceMemory staging_buffer_memory;
  if (!gulkan_device_create_buffer (
      gkdevice, buffer_size,
      VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
      VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
      VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
     &staging_buffer, &staging_buffer_memory))
    return false;

  if (!gulkan_device_map_memory (gkdevice, indices,
                                 buffer_size, staging_buffer_memory))
    return false;

  if (!gulkan_device_create_buffer (
      gkdevice, buffer_size,
      VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
      VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
     &self->index_buffer, &self->index_buffer_memory))
    return false;

  GulkanCommandBuffer cmd_buffer = {};
  if (!gulkan_client_begin_cmd_buffer (client, &cmd_buffer))
    return false;

  _copy_buffer (cmd_buffer.handle,
                staging_buffer, self->index_buffer, buffer_size);

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

  vkDestroyBuffer (device, staging_buffer, NULL);
  vkFreeMemory (device, staging_buffer_memory, NULL);

  return true;
}

static bool
_init_uniform_buffers (GulkanRenderer *self)
{
  GulkanClient *client = GULKAN_CLIENT (self);
  GulkanDevice *gkdevice = gulkan_client_get_device (client);

  if (!gulkan_device_create_buffer (
      gkdevice, sizeof (Transformation),
      VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
      VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
      VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
     &self->uniform_buffer,
     &self->uniform_buffer_memory))
      return false;

  return true;
}

static bool
_init_descriptor_sets (GulkanDevice *device,
                       VkDescriptorSetLayout *descriptor_set_layout,
                       VkDescriptorPool descriptor_pool,
                       uint32_t count,
                       VkDescriptorSet **descriptor_sets)
{
  *descriptor_sets = g_malloc (sizeof (VkDescriptorSet) * count);

  if (!gulkan_allocate_descritpor_set (device, descriptor_pool,
                                       descriptor_set_layout, count,
                                      &((*descriptor_sets)[0])))
    return false;

  return true;
}

static void
_update_descriptor_set (GulkanDevice *device,
                        VkBuffer uniform_buffer,
                        VkSampler texture_sampler,
                        VkImageView texture_image_view,
                        VkDescriptorSet descriptor_set)
{
  VkDevice vk_device = gulkan_device_get_handle (device);

  VkWriteDescriptorSet *descriptor_writes = (VkWriteDescriptorSet [])
  {
    {
      .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
      .dstSet = descriptor_set,
      .dstBinding = 0,
      .dstArrayElement = 0,
      .descriptorCount = 1,
      .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
      .pBufferInfo = &(VkDescriptorBufferInfo) {
        .buffer = uniform_buffer,
        .offset = 0,
        .range = sizeof (Transformation),
      },
      .pTexelBufferView = NULL
    },
    {
      .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
      .dstSet = descriptor_set,
      .dstBinding = 1,
      .dstArrayElement = 0,
      .descriptorCount = 1,
      .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
      .pImageInfo = &(VkDescriptorImageInfo) {
        .sampler = texture_sampler,
        .imageView = texture_image_view,
        .imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
      },
      .pTexelBufferView = NULL
    }
  };

  vkUpdateDescriptorSets (vk_device, 2, descriptor_writes, 0, NULL);
}

static bool
_init_texture_sampler (VkDevice device, VkSampler *sampler)
{
  VkSamplerCreateInfo info = {
    .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
    .magFilter = VK_FILTER_LINEAR,
    .minFilter = VK_FILTER_LINEAR,
    .mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR,
    .addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT,
    .addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT,
    .addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT,
    .anisotropyEnable = VK_TRUE,
    .maxAnisotropy = 16,
    .compareEnable = VK_FALSE,
    .compareOp = VK_COMPARE_OP_ALWAYS,
    .borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK,
    .unnormalizedCoordinates = VK_FALSE
  };

  VkResult res = vkCreateSampler (device, &info, NULL, sampler);
  vk_check_error ("vkCreateSampler", res, false)

  return true;
}

static bool
_init_draw_cmd_buffers (VkDevice device,
                        VkCommandPool command_pool,
                        VkRenderPass render_pass,
                        VkFramebuffer *framebuffers,
                        VkExtent2D swapchain_extent,
                        VkPipeline pipeline,
                        VkBuffer vertex_buffer,
                        VkBuffer index_buffer,
                        VkPipelineLayout pipeline_layout,
                        VkDescriptorSet *descriptor_sets,
                        uint32_t count,
                        VkCommandBuffer **cmd_buffers)
{
  *cmd_buffers = g_malloc (sizeof (VkCommandBuffer) * count);

  VkCommandBufferAllocateInfo alloc_info = {
    .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
    .commandPool = command_pool,
    .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
    .commandBufferCount = count
  };

  VkResult res = vkAllocateCommandBuffers (device, &alloc_info, *cmd_buffers);
  vk_check_error ("vkAllocateCommandBuffers", res, false)

  for (size_t i = 0; i < count; i++)
    {
      VkCommandBufferBeginInfo begin_info = {
        .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
        .flags = VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT
      };

      VkCommandBuffer cmd_buffer = (*cmd_buffers)[i];

      res = vkBeginCommandBuffer (cmd_buffer, &begin_info);
      vk_check_error ("vkBeginCommandBuffer", res, false)

      VkClearValue clear_color = {
        .color = {
          .float32 = {0.0f, 0.0f, 0.0f, 1.0f}
        }
      };

      VkRenderPassBeginInfo render_pass_info = {
        .sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
        .renderPass = render_pass,
        .framebuffer = framebuffers[i],
        .renderArea = {
          .offset = {0, 0},
          .extent = swapchain_extent
        },
        .clearValueCount = 1,
        .pClearValues = &clear_color
      };

      vkCmdBeginRenderPass (cmd_buffer, &render_pass_info,
                            VK_SUBPASS_CONTENTS_INLINE);

      vkCmdBindPipeline (cmd_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);

      vkCmdBindVertexBuffers (cmd_buffer, 0, 1,
                              (VkBuffer[]){ vertex_buffer },
                              (VkDeviceSize[]){ 0 });

      vkCmdBindIndexBuffer (cmd_buffer, index_buffer, 0, VK_INDEX_TYPE_UINT16);

      vkCmdBindDescriptorSets (cmd_buffer,
                               VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout,
                               0, 1, descriptor_sets, 0, NULL);

      vkCmdDrawIndexed (cmd_buffer, G_N_ELEMENTS (indices), 1, 0, 0, 0);

      vkCmdEndRenderPass (cmd_buffer);

      res = vkEndCommandBuffer (cmd_buffer);
      vk_check_error ("vkEndCommandBuffer", res, false)
    }

  return true;
}

static bool
_init_synchronization (VkDevice device,
                       VkSemaphore **submit_semaphores,
                       VkSemaphore **present_semaphores,
                       VkFence **fences)
{
  *submit_semaphores = g_malloc (sizeof(VkSemaphore) * FRAMES_IN_FLIGHT);
  *present_semaphores = g_malloc (sizeof(VkSemaphore) * FRAMES_IN_FLIGHT);
  *fences = g_malloc (sizeof(VkFence) * FRAMES_IN_FLIGHT);

  VkSemaphoreCreateInfo semaphore_info = {
    .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO
  };

  VkFenceCreateInfo fence_info = {
    .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
    .flags = VK_FENCE_CREATE_SIGNALED_BIT
  };

  VkResult res;
  for (size_t i = 0; i < FRAMES_IN_FLIGHT; i++)
    {
      res = vkCreateSemaphore (device, &semaphore_info,
                               NULL, &((*submit_semaphores)[i]));
      vk_check_error ("vkCreateSemaphore", res, false)

      res = vkCreateSemaphore (device, &semaphore_info,
                               NULL, &((*present_semaphores)[i]));
      vk_check_error ("vkCreateSemaphore", res, false)

      res = vkCreateFence (device, &fence_info, NULL, &((*fences)[i]));
      vk_check_error ("vkCreateFence", res, false)
    }

  return true;
}

bool
gulkan_renderer_update_texture (GulkanRenderer *self,
                                GulkanTexture  *texture)
{
  GulkanClient *client = GULKAN_CLIENT (self);
  GulkanDevice *gkdevice = gulkan_client_get_device (client);
  VkDevice device = gulkan_client_get_device_handle (client);

  if (self->sampler != VK_NULL_HANDLE)
    vkDestroySampler (device, self->sampler, NULL);

  if (!_init_texture_sampler (device, &self->sampler))
    return false;

  VkImageView vk_image_view = gulkan_texture_get_image_view (texture);
  _update_descriptor_set (gkdevice,
                          self->uniform_buffer,
                          self->sampler,
                          vk_image_view,
                         *self->descriptor_sets);

  VkCommandPool command_pool = gulkan_client_get_command_pool (client);
  if (!_init_draw_cmd_buffers (device,
                               command_pool,
                               self->render_pass,
                               self->framebuffers,
                               self->swapchain_extent,
                               self->graphics_pipeline,
                               self->vertex_buffer,
                               self->index_buffer,
                               self->pipeline_layout,
                               self->descriptor_sets,
                               self->swapchain_image_count,
                              &self->draw_cmd_buffers))
    return false;

  return true;
}


bool
gulkan_renderer_init_rendering (GulkanRenderer *self,
                                VkSurfaceKHR surface,
                                GulkanTexture *texture)
{
  GulkanClient *client = GULKAN_CLIENT (self);
  GulkanDevice *gkdevice = gulkan_client_get_device (client);
  VkDevice device = gulkan_client_get_device_handle (client);
  VkPhysicalDevice vk_physical_device =
    gulkan_client_get_physical_device_handle (client);

  self->surface = surface;

  g_print ("Device surface support: %d\n",
           gulkan_device_queue_supports_surface (gkdevice, surface));

  if (!_init_swapchain (device,
                        vk_physical_device,
                        surface,
                       &self->swap_chain,
                       &self->swapchain_image_views,
                       &self->swapchain_image_format,
                       &self->swapchain_image_count,
                       &self->swapchain_extent))
      return false;

  _create_render_pass (device,
                       self->swapchain_image_format,
                       &self->render_pass);

  _create_descriptor_set_layout (device, &self->descriptor_set_layout);

  VkShaderModule fragment_module;
  if (!gulkan_renderer_create_shader_module (device,
                                             "/shaders/texture.frag.spv",
                                            &fragment_module))
    return false;

  VkShaderModule vertex_module;
  if (!gulkan_renderer_create_shader_module (device,
                                             "/shaders/texture.vert.spv",
                                            &vertex_module))
    return false;

  if (!_create_graphics_pipeline (device,
                                  vertex_module,
                                  fragment_module,
                                  self->swapchain_extent,
                                  self->descriptor_set_layout,
                                  self->render_pass,
                                 &self->graphics_pipeline,
                                 &self->pipeline_layout))
    return false;

  vkDestroyShaderModule (device, fragment_module, NULL);
  vkDestroyShaderModule (device, vertex_module, NULL);

  if (!_init_framebuffers (device,
                           self->render_pass,
                           self->swapchain_extent,
                           self->swapchain_image_count,
                           self->swapchain_image_views,
                          &self->framebuffers))
    return false;

  if (!_init_vertex_buffer (self))
    return false;

  if (!_init_index_buffer (self))
    return false;

  if (!_init_uniform_buffers (self))
    return false;

  uint32_t set_count = 1;
  VkDescriptorPoolSize pool_sizes[] = {
    {
      .descriptorCount = set_count,
      .type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER
    },
    {
      .descriptorCount = set_count,
      .type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER
    }
  };
  if (!GULKAN_INIT_DECRIPTOR_POOL (gkdevice, pool_sizes,
                                   set_count, &self->descriptor_pool))
     return false;

  if (!_init_descriptor_sets (gkdevice,
                            &self->descriptor_set_layout,
                             self->descriptor_pool,
                             1,
                            &self->descriptor_sets))
    return false;

  if (!gulkan_renderer_update_texture (self,
                                       texture))
    return false;

  VkCommandPool command_pool = gulkan_client_get_command_pool (client);
  if (!_init_draw_cmd_buffers (device,
                               command_pool,
                               self->render_pass,
                               self->framebuffers,
                               self->swapchain_extent,
                               self->graphics_pipeline,
                               self->vertex_buffer,
                               self->index_buffer,
                               self->pipeline_layout,
                               self->descriptor_sets,
                               self->swapchain_image_count,
                              &self->draw_cmd_buffers))
    return false;

  if (!_init_synchronization (device,
                             &self->submit_semaphores,
                             &self->present_semaphores,
                             &self->fences))
    return false;

  return true;
}

static bool
_update_uniform_buffer (GulkanDevice *device,
                        VkDeviceMemory uniform_buffer_memory)
{
  graphene_vec3_t eye;
  graphene_vec3_init (&eye, 0.0f, 0.0f, -1.0f);

  graphene_vec3_t center;
  graphene_vec3_init (&center, 0.0f, 0.0f, 0.0f);

  graphene_matrix_t view;
  graphene_matrix_init_look_at (&view, &eye, &center, graphene_vec3_y_axis());

  graphene_matrix_t ortho;
  graphene_matrix_init_ortho (&ortho, -1.f, 1.f, -1.f, 1.f, 1.f, -1.f);

  graphene_matrix_t mvp;
  graphene_matrix_multiply (&view, &ortho, &mvp);

  Transformation ubo = {};
  graphene_matrix_to_float (&mvp, ubo.mvp);

  if (!gulkan_device_map_memory (device, &ubo,
                                 sizeof(ubo), uniform_buffer_memory))
    return false;

  return true;
}

bool
gulkan_renderer_draw (GulkanRenderer *self)
{
  GulkanClient *client = GULKAN_CLIENT (self);
  GulkanDevice *gkdevice = gulkan_client_get_device (client);
  VkDevice device = gulkan_client_get_device_handle (client);
  VkQueue vk_queue = gulkan_device_get_queue_handle (gkdevice);

  VkSemaphore submit_semaphore = self->submit_semaphores[self->current_frame];
  VkSemaphore present_semaphore = self->present_semaphores[self->current_frame];

  vkWaitForFences (device, 1, &self->fences[self->current_frame],
                   VK_TRUE, UINT64_MAX);

  uint32_t image_index;
  VkResult res;
  res = vkAcquireNextImageKHR (device, self->swap_chain, UINT64_MAX,
                               submit_semaphore,
                               VK_NULL_HANDLE, &image_index);
  vk_check_error ("vkAcquireNextImageKHR", res, false)

  _update_uniform_buffer (gkdevice, self->uniform_buffer_memory);

  VkSubmitInfo submit_info = {
    .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
    .waitSemaphoreCount = 1,
    .pWaitSemaphores = (VkSemaphore[]) { submit_semaphore },
    .pWaitDstStageMask = (VkPipelineStageFlags[]) {
      VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT
    },
    .commandBufferCount = 1,
    .pCommandBuffers = &self->draw_cmd_buffers[image_index],
    .signalSemaphoreCount = 1,
    .pSignalSemaphores = (VkSemaphore[]) { present_semaphore }
  };

  vkResetFences (device, 1, &self->fences[self->current_frame]);

  if (vkQueueSubmit (vk_queue, 1, &submit_info,
                     self->fences[self->current_frame]) != VK_SUCCESS)
    {
      g_printerr ("Failed to submit draw command buffer.\n");
      return false;
    }

  VkPresentInfoKHR present_info = {
    .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,
    .waitSemaphoreCount = 1,
    .pWaitSemaphores = (VkSemaphore[]) { present_semaphore },
    .swapchainCount = 1,
    .pSwapchains = (VkSwapchainKHR[]) { self->swap_chain },
    .pImageIndices = &image_index,
  };

  res = vkQueuePresentKHR (vk_queue, &present_info);
  vk_check_error ("vkQueuePresentKHR", res, false)
  self->current_frame = (self->current_frame + 1) % FRAMES_IN_FLIGHT;

  return true;
}

