/*
 * Copyright 2022-2024 Frederico de Oliveira Linhares
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "core.hpp"

namespace
{

#ifdef DEBUG
VKAPI_ATTR VkBool32 VKAPI_CALL
vk_debug_callback(
	VkDebugUtilsMessageSeverityFlagBitsEXT message_severity,
	VkDebugUtilsMessageTypeFlagsEXT message_type,
	const VkDebugUtilsMessengerCallbackDataEXT* callback_data,
	void* _obj)
{
	// Set level.
	Log::Level log_level;
	switch(message_severity)
	{
	case VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT:
		log_level = Log::Level::Trace;
		break;
	case VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT:
		log_level = Log::Level::Information;
		break;
	case VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT:
		log_level = Log::Level::Warning;
		break;
	case VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT:
	default:
		log_level = Log::Level::Error;
		break;
	}

	// Log message.
	BluCat::INT::core.log.message(log_level, callback_data->pMessage);

	return VK_FALSE;
}
#endif

void
load_threads(void *obj)
{
	auto num_threads{std::thread::hardware_concurrency() - 1};
	for(auto i{0}; i < num_threads; i++)
		BluCat::INT::core.threads.emplace_back(
			BluCat::INT::core.workers.emplace_back(&BluCat::INT::core.job_queue));
}

void
unload_threads(void *obj)
{
	BluCat::INT::core.job_queue.stop();
	for(auto &t: BluCat::INT::core.threads) t.join();
}

void
load_fps(void *obj)
{
  using namespace std::chrono;

	BluCat::INT::core.max_frame_duration =
		duration<long long, std::milli>(1000 / BluCat::INT::core.fps);
	// FIXME: actually calculates the real delta time.
	BluCat::INT::core.delta_time = 1.0f / BluCat::INT::core.fps;
}

void
load_font_library(void *obj)
{
	FT_Error error{FT_Init_FreeType(&BluCat::INT::core.font_library)};
	if(error) throw CommandError{"Failed to open the FreeType library."};
}

void
unload_font_library(void *obj)
{
	FT_Done_FreeType(BluCat::INT::core.font_library);
}

#ifdef DEBUG
void
load_vk_debug_callback(void *obj)
{
  PFN_vkCreateDebugUtilsMessengerEXT debug_messenger;

  // A Vulkan instance extension named VK_EXT_debug_utils and a Vulkan instance
  // layer named VK_LAYER_LUNARG_standard_validation are required to enable
  // this callback. These instance extension and instance layer are loaded at
  // Instance::load_blucat_instance.
  VkDebugUtilsMessengerCreateInfoEXT create_info;
  create_info.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
  create_info.pNext = nullptr;
  create_info.messageSeverity =
      VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT |
      VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT |
      VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT |
      VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
  create_info.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT |
			    VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT |
			    VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
  create_info.pfnUserCallback = vk_debug_callback;
  create_info.pUserData = nullptr;
  create_info.flags = 0;

  debug_messenger = (PFN_vkCreateDebugUtilsMessengerEXT)
    vkGetInstanceProcAddr(BluCat::INT::core.vk_instance,
			  "vkCreateDebugUtilsMessengerEXT");

  if(debug_messenger(BluCat::INT::core.vk_instance, &create_info, nullptr,
		     &BluCat::INT::core.vk_callback) != VK_SUCCESS)
    CommandError{"Failed to setup debug callback for Vulkan."};
}

void
unload_vk_debug_callback(void *obj)
{
  PFN_vkDestroyDebugUtilsMessengerEXT debug_messenger;

  debug_messenger = (PFN_vkDestroyDebugUtilsMessengerEXT)
    vkGetInstanceProcAddr(BluCat::INT::core.vk_instance,
			  "vkDestroyDebugUtilsMessengerEXT");

  debug_messenger(BluCat::INT::core.vk_instance, BluCat::INT::core.vk_callback, nullptr);
}
#endif

void
load_devices(void *obj)
{
  uint32_t devices_count;
  std::vector<VkPhysicalDevice> vk_physical_devices;

  // Enumerate physical devices
  {
    vkEnumeratePhysicalDevices(
			BluCat::INT::core.vk_instance, &devices_count, nullptr);
    if(devices_count < 1)
      CommandError{"Failed to find GPUs with Vulkan support."};
    vk_physical_devices.resize(devices_count);
    vkEnumeratePhysicalDevices(
      BluCat::INT::core.vk_instance, &devices_count, vk_physical_devices.data());
  }

#ifdef DEBUG
  BluCat::INT::core.log.message(Log::Level::Trace, "Physical devices properties");
#endif

  BluCat::INT::core.vk_devices.reserve(devices_count);
  for(auto i = 0; i < devices_count; i++)
  {
    // Use swapchain on first device.
    if(i == 0)
    {
      BluCat::INT::core.vk_devices.emplace_back(vk_physical_devices[i], true);
      BluCat::INT::core.vk_device_with_swapchain = &BluCat::INT::core.vk_devices[i];
    }
    else
      BluCat::INT::core.vk_devices.emplace_back(vk_physical_devices[i], false);
  }
}

void
unload_devices(void *obj)
{
  BluCat::INT::core.vk_devices.clear();
}

static void
load_swapchain(void *obj)
{
  try { BluCat::INT::core.vk_swapchain = new BluCat::GRA::Swapchain(); }
  catch(const CommandError &error)
  {
    std::string error_message{"Failed to create swapchain → "};
    error_message += error.what();
    throw CommandError{error_message};
  }
}

void
unload_swapchain(void *obj)
{
  delete BluCat::INT::core.vk_swapchain;
}

void
load_framebuffer(void *obj)
{
  try
  {
    BluCat::INT::core.vk_framebuffer = new BluCat::GRA::Framebuffer();
  }
  catch(const CommandError &e)
  {
    throw CommandError{"Failed to create framebuffer."};
  }
}

void
unload_framebuffer(void *obj)
{
  delete BluCat::INT::core.vk_framebuffer;
}

void
load_render_pass(void *obj)
{
  try
  {
    BluCat::INT::core.vk_render_pass = new BluCat::GRA::RenderPass();
  }
  catch(const CommandError &e)
  {
    throw CommandError{"Failed to create render pass."};
  }
}

void
unload_render_pass(void *obj)
{
  delete BluCat::INT::core.vk_render_pass;
}

void
load_descriptor_set_layout(void *obj)
{
  try
  {
    BluCat::INT::core.vk_descriptor_set_layout = new BluCat::GRA::DescriptorSetLayout();
  }
  catch(const CommandError &e)
  {
    throw CommandError{"Failed to create descriptor set layouts."};
  }
}

void
unload_descriptor_set_layout(void *obj)
{
  delete BluCat::INT::core.vk_descriptor_set_layout;
}

void
load_graphics_pipeline_3d_layout(void *obj)
{
  try
  {
    BluCat::INT::core.vk_graphics_pipeline_3d_layout =
      new BluCat::GRA::GraphicsPipeline3DLayout();
  }
  catch(const CommandError &e)
  {
    throw CommandError{"Failed to create 3d graphics pipeline."};
  }
}

void
unload_graphics_pipeline_3d_layout(void *obj)
{
  delete BluCat::INT::core.vk_graphics_pipeline_3d_layout;
}

void
load_graphics_pipeline_2d_solid_layout(void *obj)
{
  try
  {
    BluCat::INT::core.vk_graphics_pipeline_2d_solid_layout =
      new BluCat::GRA::GraphicsPipeline2DSolidLayout();
  }
  catch(const CommandError &e)
  {
    throw CommandError{"Failed to create 2d graphics pipeline."};
  }
}

void
unload_graphics_pipeline_2d_solid_layout(void *obj)
{
  delete BluCat::INT::core.vk_graphics_pipeline_2d_solid_layout;
}

void
load_graphics_pipeline_2d_wired_layout(void *obj)
{
  try
  {
    BluCat::INT::core.vk_graphics_pipeline_2d_wired_layout =
      new BluCat::GRA::GraphicsPipeline2DWiredLayout();
  }
  catch(const CommandError &e)
  {
    throw CommandError{"Failed to create 2d graphics pipeline."};
  }
}

void
unload_graphics_pipeline_2d_wired_layout(void *obj)
{
  delete BluCat::INT::core.vk_graphics_pipeline_2d_wired_layout;
}

void
load_light(void *obj)
{
  try
  {
    BluCat::INT::core.vk_light = new BluCat::GRA::Light();
  }
  catch(const CommandError &e)
  {
    throw CommandError{"Failed to descriptor sets for light."};
  }
}

void
unload_light(void *obj)
{
  delete BluCat::INT::core.vk_light;
}

void
load_graphics_pipeline_3d(void *obj)
{
  try
  {
    BluCat::INT::core.vk_graphics_pipeline_3d =
      std::make_unique<BluCat::GRA::GraphicsPipeline3D>();
  }
  catch(const CommandError &e)
  {
    throw CommandError{"Failed to create 3d graphics pipeline."};
  }
}

void
unload_graphics_pipeline_3d(void *obj)
{
  BluCat::INT::core.vk_graphics_pipeline_3d = nullptr;
}

void
load_graphics_pipeline_3d_skeletal(void *obj)
{
  try
  {
    BluCat::INT::core.vk_graphics_pipeline_3d_skeletal =
      std::make_unique<BluCat::GRA::GraphicsPipeline3DSkeletal>();
  }
  catch(const CommandError &e)
  {
    throw CommandError{"Failed to create 3d skeletal graphics pipeline."};
  }
}

void
unload_graphics_pipeline_3d_skeletal(void *obj)
{
  BluCat::INT::core.vk_graphics_pipeline_3d_skeletal = nullptr;
}

void
load_graphics_pipeline_sprite_3d(void *obj)
{
  try
  {
    BluCat::INT::core.vk_graphics_pipeline_sprite_3d =
      std::make_unique<BluCat::GRA::GraphicsPipelineSprite3D>();
  }
  catch(const CommandError &e)
  {
    throw CommandError{"Failed to create sprite 3d graphics pipeline."};
  }
}

void
unload_graphics_pipeline_sprite_3d(void *obj)
{
  BluCat::INT::core.vk_graphics_pipeline_sprite_3d = nullptr;
}

void
load_graphics_pipeline_2d_solid(void *obj)
{
  try
  {
    BluCat::INT::core.vk_graphics_pipeline_2d_solid =
      std::make_unique<BluCat::GRA::GraphicsPipeline2DSolid>();
  }
  catch(const CommandError &e)
  {
    throw CommandError{"Failed to create 2d graphics pipeline."};
  }
}

void
unload_graphics_pipeline_2d_solid(void *obj)
{
  BluCat::INT::core.vk_graphics_pipeline_2d_solid = nullptr;
}

void
load_graphics_pipeline_2d_wired(void *obj)
{
  try
  {
    BluCat::INT::core.vk_graphics_pipeline_2d_wired =
      std::make_unique<BluCat::GRA::GraphicsPipeline2DWired>();
  }
  catch(const CommandError &e)
  {
    throw CommandError{"Failed to create 2d graphics pipeline."};
  }
}

void
unload_graphics_pipeline_2d_wired(void *obj)
{
  BluCat::INT::core.vk_graphics_pipeline_2d_wired = nullptr;
}

void
load_renderer(void *obj)
{
  try
  {
    glm::vec4 region(
      0.f, 0.f,
      static_cast<float>(BluCat::INT::core.display_width),
      static_cast<float>(BluCat::INT::core.display_height));
		BluCat::INT::core.vk_renderer = new BluCat::GRA::Renderer(
      {},
      {std::make_shared<BluCat::GRA::View3D>(region, region.z, region.w)});
  }
  catch(const CommandError &e)
  {
    throw CommandError{"Failed to create renderer."};
  }
}

void
unload_renderer(void *obj)
{
  delete BluCat::INT::core.vk_renderer;
}

}

namespace BluCat::INT
{

std::random_device random_seed;
std::mt19937 random_number_generator;

const CommandChain Core::loader{
  {&load_threads, &unload_threads},
  {&load_fps, nullptr},
  {&load_font_library, &unload_font_library},
#ifdef DEBUG
  {&load_vk_debug_callback, &unload_vk_debug_callback},
#endif
  {&load_devices, &unload_devices},
  {&load_swapchain, &unload_swapchain},

  {&load_render_pass, &unload_render_pass},
  {&load_framebuffer, &unload_framebuffer},
  {&load_descriptor_set_layout, &unload_descriptor_set_layout},
  {&load_graphics_pipeline_3d_layout,
   &unload_graphics_pipeline_3d_layout},
  {&load_graphics_pipeline_2d_solid_layout,
   &unload_graphics_pipeline_2d_solid_layout},
  {&load_graphics_pipeline_2d_wired_layout,
   &unload_graphics_pipeline_2d_wired_layout},
  {&load_light, &unload_light},
	{&load_graphics_pipeline_3d_skeletal,
	 &unload_graphics_pipeline_3d_skeletal},
  {&load_graphics_pipeline_3d, &unload_graphics_pipeline_3d},
  {&load_graphics_pipeline_sprite_3d,
   &unload_graphics_pipeline_sprite_3d},
  {&load_graphics_pipeline_2d_solid, &unload_graphics_pipeline_2d_solid},
  {&load_graphics_pipeline_2d_wired, &unload_graphics_pipeline_2d_wired},
  {&load_renderer, &unload_renderer},
};

Core core;

}