// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "flutter/vulkan/vulkan_proc_table.h"

#include <dlfcn.h>

#include "flutter/fml/logging.h"

#define ACQUIRE_PROC(name, context)                          \
  if (!(name = AcquireProc("vk" #name, context))) {          \
    FML_DLOG(INFO) << "Could not acquire proc: vk" << #name; \
    return false;                                            \
  }

namespace vulkan {

VulkanProcTable::VulkanProcTable()
    : handle_(nullptr), acquired_mandatory_proc_addresses_(false) {
  acquired_mandatory_proc_addresses_ =
      OpenLibraryHandle() && SetupLoaderProcAddresses();
}

VulkanProcTable::~VulkanProcTable() {
  CloseLibraryHandle();
}

bool VulkanProcTable::HasAcquiredMandatoryProcAddresses() const {
  return acquired_mandatory_proc_addresses_;
}

bool VulkanProcTable::IsValid() const {
  return instance_ && device_;
}

bool VulkanProcTable::AreInstanceProcsSetup() const {
  return instance_;
}

bool VulkanProcTable::AreDeviceProcsSetup() const {
  return device_;
}

bool VulkanProcTable::SetupLoaderProcAddresses() {
  if (handle_ == nullptr) {
    return true;
  }

  GetInstanceProcAddr =
#if VULKAN_LINK_STATICALLY
      GetInstanceProcAddr = &vkGetInstanceProcAddr;
#else   // VULKAN_LINK_STATICALLY
      reinterpret_cast<PFN_vkGetInstanceProcAddr>(
          dlsym(handle_, "vkGetInstanceProcAddr"));
#endif  // VULKAN_LINK_STATICALLY

  if (!GetInstanceProcAddr) {
    FML_DLOG(WARNING) << "Could not acquire vkGetInstanceProcAddr.";
    return false;
  }

  VulkanHandle<VkInstance> null_instance(VK_NULL_HANDLE, nullptr);

  ACQUIRE_PROC(CreateInstance, null_instance);
  ACQUIRE_PROC(EnumerateInstanceExtensionProperties, null_instance);
  ACQUIRE_PROC(EnumerateInstanceLayerProperties, null_instance);

  return true;
}

bool VulkanProcTable::SetupInstanceProcAddresses(
    const VulkanHandle<VkInstance>& handle) {
  ACQUIRE_PROC(CreateDevice, handle);
  ACQUIRE_PROC(DestroyDevice, handle);
  ACQUIRE_PROC(DestroyInstance, handle);
  ACQUIRE_PROC(EnumerateDeviceLayerProperties, handle);
  ACQUIRE_PROC(EnumeratePhysicalDevices, handle);
  ACQUIRE_PROC(GetDeviceProcAddr, handle);
  ACQUIRE_PROC(GetPhysicalDeviceFeatures, handle);
  ACQUIRE_PROC(GetPhysicalDeviceQueueFamilyProperties, handle);
#if OS_ANDROID
  ACQUIRE_PROC(GetPhysicalDeviceSurfaceCapabilitiesKHR, handle);
  ACQUIRE_PROC(GetPhysicalDeviceSurfaceFormatsKHR, handle);
  ACQUIRE_PROC(GetPhysicalDeviceSurfacePresentModesKHR, handle);
  ACQUIRE_PROC(GetPhysicalDeviceSurfaceSupportKHR, handle);
  ACQUIRE_PROC(DestroySurfaceKHR, handle);
  ACQUIRE_PROC(CreateAndroidSurfaceKHR, handle);
#endif  // OS_ANDROID

  // The debug report functions are optional. We don't want proc acquisition to
  // fail here because the optional methods were not present (since ACQUIRE_PROC
  // returns false on failure). Wrap the optional proc acquisitions in an
  // anonymous lambda and invoke it. We don't really care about the result since
  // users of Debug reporting functions check for their presence explicitly.
  [this, &handle]() -> bool {
    ACQUIRE_PROC(CreateDebugReportCallbackEXT, handle);
    ACQUIRE_PROC(DestroyDebugReportCallbackEXT, handle);
    return true;
  }();

  instance_ = {handle, nullptr};
  return true;
}

bool VulkanProcTable::SetupDeviceProcAddresses(
    const VulkanHandle<VkDevice>& handle) {
  ACQUIRE_PROC(AllocateCommandBuffers, handle);
  ACQUIRE_PROC(AllocateMemory, handle);
  ACQUIRE_PROC(BeginCommandBuffer, handle);
  ACQUIRE_PROC(BindImageMemory, handle);
  ACQUIRE_PROC(CmdPipelineBarrier, handle);
  ACQUIRE_PROC(CreateCommandPool, handle);
  ACQUIRE_PROC(CreateFence, handle);
  ACQUIRE_PROC(CreateImage, handle);
  ACQUIRE_PROC(CreateSemaphore, handle);
  ACQUIRE_PROC(DestroyCommandPool, handle);
  ACQUIRE_PROC(DestroyFence, handle);
  ACQUIRE_PROC(DestroyImage, handle);
  ACQUIRE_PROC(DestroySemaphore, handle);
  ACQUIRE_PROC(DeviceWaitIdle, handle);
  ACQUIRE_PROC(EndCommandBuffer, handle);
  ACQUIRE_PROC(FreeCommandBuffers, handle);
  ACQUIRE_PROC(FreeMemory, handle);
  ACQUIRE_PROC(GetDeviceQueue, handle);
  ACQUIRE_PROC(GetImageMemoryRequirements, handle);
  ACQUIRE_PROC(QueueSubmit, handle);
  ACQUIRE_PROC(QueueWaitIdle, handle);
  ACQUIRE_PROC(ResetCommandBuffer, handle);
  ACQUIRE_PROC(ResetFences, handle);
  ACQUIRE_PROC(WaitForFences, handle);
#if OS_ANDROID
  ACQUIRE_PROC(AcquireNextImageKHR, handle);
  ACQUIRE_PROC(CreateSwapchainKHR, handle);
  ACQUIRE_PROC(DestroySwapchainKHR, handle);
  ACQUIRE_PROC(GetSwapchainImagesKHR, handle);
  ACQUIRE_PROC(QueuePresentKHR, handle);
#endif  // OS_ANDROID
#if OS_FUCHSIA
  ACQUIRE_PROC(GetMemoryZirconHandleFUCHSIA, handle);
  ACQUIRE_PROC(ImportSemaphoreZirconHandleFUCHSIA, handle);
#endif  // OS_FUCHSIA
  device_ = {handle, nullptr};
  return true;
}

bool VulkanProcTable::OpenLibraryHandle() {
#if VULKAN_LINK_STATICALLY
  static char kDummyLibraryHandle = '\0';
  handle_ = reinterpret_cast<decltype(handle_)>(&kDummyLibraryHandle);
  return true;
#else   // VULKAN_LINK_STATICALLY
  dlerror();  // clear existing errors on thread.
  handle_ = dlopen("libvulkan.so", RTLD_NOW | RTLD_LOCAL);
  if (handle_ == nullptr) {
    FML_DLOG(WARNING) << "Could not open the vulkan library: " << dlerror();
    return false;
  }
  return true;
#endif  // VULKAN_LINK_STATICALLY
}

bool VulkanProcTable::CloseLibraryHandle() {
#if VULKAN_LINK_STATICALLY
  handle_ = nullptr;
  return true;
#else
  if (handle_ != nullptr) {
    dlerror();  // clear existing errors on thread.
    if (dlclose(handle_) != 0) {
      FML_DLOG(ERROR) << "Could not close the vulkan library handle. This "
                         "indicates a leak.";
      FML_DLOG(ERROR) << dlerror();
    }
    handle_ = nullptr;
  }
  return handle_ == nullptr;
#endif
}

PFN_vkVoidFunction VulkanProcTable::AcquireProc(
    const char* proc_name,
    const VulkanHandle<VkInstance>& instance) const {
  if (proc_name == nullptr || !GetInstanceProcAddr) {
    return nullptr;
  }

  // A VK_NULL_HANDLE as the instance is an acceptable parameter.
  return GetInstanceProcAddr(instance, proc_name);
}

PFN_vkVoidFunction VulkanProcTable::AcquireProc(
    const char* proc_name,
    const VulkanHandle<VkDevice>& device) const {
  if (proc_name == nullptr || !device || !GetDeviceProcAddr) {
    return nullptr;
  }

  return GetDeviceProcAddr(device, proc_name);
}

GrVkGetProc VulkanProcTable::CreateSkiaGetProc() const {
  if (!IsValid()) {
    return nullptr;
  }

  return [this](const char* proc_name, VkInstance instance, VkDevice device) {
    if (device != VK_NULL_HANDLE) {
      auto result = AcquireProc(proc_name, {device, nullptr});
      if (result != nullptr) {
        return result;
      }
    }

    return AcquireProc(proc_name, {instance, nullptr});
  };
}

}  // namespace vulkan