/*
 * Copyright 2022-2025 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_sdl(void *obj)
{
	if(SDL_Init(SDL_INIT_EVERYTHING) < 0)
	{
		std::string error{"SDL could not initialize! SDL Error → "};
		error += SDL_GetError();
		throw error;
	}

	if(SDL_Vulkan_LoadLibrary(nullptr) != 0)
	{
		SDL_Quit();
		std::string error{"SDL could not initialize Vulkan! SDL_Error → "};
		error += SDL_GetError();
		throw CommandError{error};
	}
}

void
unload_sdl(void *obj)
{
	SDL_Vulkan_UnloadLibrary();
	SDL_Quit();
}

void
load_sdl_mixer(void *obj)
{
	int flags = MIX_INIT_OGG|MIX_INIT_MOD;
	int initted = Mix_Init(flags);
	if(initted&flags != flags)
	{
		std::string error{"Could not initialize SDL mixer → "};
		error += Mix_GetError();
		throw CommandError{error};
	}
}

void
unload_sdl_mixer(void *obj)
{
	while(Mix_Init(0)) Mix_Quit();
}

void
load_sdl_open_audio(void *obj)
{
	if(Mix_OpenAudio(22050, MIX_DEFAULT_FORMAT, 2, 1024) == -1)
	{
		std::string error{"Could not open SDL mixer audio → "};
		error += Mix_GetError();
		throw CommandError{error};
	}
}

void
unload_sdl_open_audio(void *obj)
{
	Mix_CloseAudio();
}

void
load_window(void *obj)
{
	BluCat::INT::core.window = nullptr;
	BluCat::INT::core.window = SDL_CreateWindow(
		BluCat::INT::core.game_name.c_str(),
		SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
		BluCat::INT::core.display_width, BluCat::INT::core.display_height,
		SDL_WINDOW_VULKAN);
	if(BluCat::INT::core.window == nullptr)
	{
		std::string error{"Window could not be created! SDL_Error → "};
		error += SDL_GetError();
		throw CommandError{error};
	}
}

void
unload_window(void *obj)
{
	SDL_DestroyWindow(BluCat::INT::core.window);
}

void
load_vk_instance(void *obj)
{
	std::vector<const char*> vk_extensions;
	std::vector<const char*> vk_required_layers_names;

	// Get extensions.
	{
		uint32_t vk_extensions_count;
		std::vector<const char*> vk_required_extensions;

		// Load debuging layers.
#ifdef DEBUG
		vk_required_extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
		vk_required_layers_names.push_back("VK_LAYER_KHRONOS_validation");
#endif

		// Get extensions for SDL.
		{
			uint32_t vk_sdl_extension_count;
			std::vector<const char*> vk_sdl_extensions;

			if(!SDL_Vulkan_GetInstanceExtensions(
					 BluCat::INT::core.window, &vk_sdl_extension_count, nullptr))
			{
				std::string error{
					"Vulkan extensions could not be loaded by SDL! SDL_Error: "};
				error += SDL_GetError();
				throw CommandError{error};
			}

			vk_sdl_extensions.resize(vk_sdl_extension_count);
			SDL_Vulkan_GetInstanceExtensions(
				BluCat::INT::core.window, &vk_sdl_extension_count,
				vk_sdl_extensions.data());

			// Combine all extensions.
			vk_extensions_count = vk_sdl_extension_count +
				vk_required_extensions.size();
			vk_extensions.resize(vk_extensions_count);
			for(auto i{0}; i < vk_sdl_extension_count; i++)
				vk_extensions[i] = vk_sdl_extensions[i];
			for(auto i{0}; i < vk_required_extensions.size(); i++)
				vk_extensions[i + vk_sdl_extension_count] = vk_required_extensions[i];
		}

#ifdef DEBUG
		BluCat::INT::core.log.message(Log::Level::Trace, "Enabled VK extensions.");
		for(auto vk_extension: vk_extensions)
		{
			std::string message{"Extension name: "};
			message += vk_extension;
			BluCat::INT::core.log.message(Log::Level::Trace, message);
		}
#endif
	}

	// Get available instance layers.
	{
		uint32_t vk_available_layers_count;
		std::vector<VkLayerProperties> vk_available_layers;
		std::vector<const char*> vk_available_layers_names;

		vkEnumerateInstanceLayerProperties(&vk_available_layers_count, nullptr);
		vk_available_layers.resize(vk_available_layers_count);
		vkEnumerateInstanceLayerProperties(&vk_available_layers_count,
																			 vk_available_layers.data());
		vk_available_layers_names.resize(vk_available_layers_count);
#ifdef DEBUG
		BluCat::INT::core.log.message(
			Log::Level::Trace, "Available VK instance layers.");
#endif
		for(uint32_t i = 0; i < vk_available_layers_count; i++)
		{
#ifdef DEBUG
			std::stringstream message{};
			message << "\nname: " << vk_available_layers[i].layerName << std::endl;
			message << "Description: " << vk_available_layers[i].description <<
				std::endl;
			message << "Spec version: " << vk_available_layers[i].specVersion <<
				std::endl;
			message << "Implementation version: " <<
				vk_available_layers[i].implementationVersion << std::endl;
			BluCat::INT::core.log.message(Log::Level::Trace, message.str());
#endif

			vk_available_layers_names[i] = vk_available_layers[i].layerName;
		}

		// If required layers are not all available.
		if(!std::includes(
				 vk_available_layers_names.begin(), vk_available_layers_names.end(),
				 vk_required_layers_names.begin(), vk_required_layers_names.begin()))
			throw CommandError{"Some required Vulkan layers are not available."};
	}

	{
		VkApplicationInfo app_info;
		app_info.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
		app_info.pNext = nullptr;
		app_info.pApplicationName = BluCat::INT::core.game_name.c_str();
		app_info.applicationVersion = VK_MAKE_VERSION(
			BluCat::INT::core.game_version_major,
			BluCat::INT::core.game_version_minor,
			BluCat::INT::core.game_version_patch);
		app_info.pEngineName = "BluCat::GRA";
		app_info.engineVersion = VK_MAKE_VERSION(
			BLU_CAT_VERSION_MAJOR,
			BLU_CAT_VERSION_MINOR,
			BLU_CAT_VERSION_PATCH);
		app_info.apiVersion = VK_API_VERSION_1_0;

		VkInstanceCreateInfo create_info;
		create_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
		create_info.pNext = nullptr;
		create_info.flags = 0;
		create_info.pApplicationInfo = &app_info;
		create_info.enabledExtensionCount = vk_extensions.size();
		create_info.ppEnabledExtensionNames = vk_extensions.data();
		create_info.enabledLayerCount = vk_required_layers_names.size();
		create_info.ppEnabledLayerNames = vk_required_layers_names.data();

		VkResult result =
			vkCreateInstance(&create_info, nullptr, &BluCat::INT::core.vk_instance);
		if(result != VK_SUCCESS)
		{
			std::string error{""};
			switch(result)
			{
			case VK_ERROR_LAYER_NOT_PRESENT:
				error = " Layer not present.";
				break;
			case VK_ERROR_EXTENSION_NOT_PRESENT:
				error = " Extension not present.";
			}
			error = "Failed to create Vulkan instance." + error;
			throw CommandError{error};
		}
	}
}

void
unload_vk_instance(void *obj)
{
	vkDestroyInstance(BluCat::INT::core.vk_instance, nullptr);
}

void
load_window_surface(void *obj)
{
	if(!SDL_Vulkan_CreateSurface(
			 BluCat::INT::core.window, BluCat::INT::core.vk_instance,
			 &BluCat::INT::core.window_surface))
	{
		std::string error{"Failed to create a window surface → "};
		error += SDL_GetError();
		throw CommandError{error};
	}
}

void
unload_window_surface(void *obj)
{
	vkDestroySurfaceKHR(
		BluCat::INT::core.vk_instance, BluCat::INT::core.window_surface, nullptr);
}

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_sdl, &unload_sdl},
	{&load_sdl_mixer, &unload_sdl_mixer},
	{&load_sdl_open_audio, &unload_sdl_open_audio},
	{&load_window, &unload_window},
	{&load_vk_instance, &unload_vk_instance},
	{&load_window_surface, &unload_window_surface},
  {&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;

}