//This is the entrypoint of the api SVK. SVK stands for Simpler Vulkan. 

// 20260327: Changed Model to SVKModel to use Model for its original purpose (LoadModel and DrawModel). /Ingemar

#ifndef SVK_HPP
#define SVK_HPP

#include "svk_types.hpp"
#include "svk_descriptors.hpp"
#include "svk_loader.hpp"
#include "svk_initializers.hpp"
#include "svk_pipelines.hpp"
#include "svk_functions.hpp"
#include "delta_handler.hpp"
#include <bits/stdc++.h>

constexpr unsigned int FRAME_OVERLAP = 2;

class SVK {
public:
        void initWindowSize(int w, int h);

	// Allocates size for a vertex buffer where allocationSize is No of elements * sizeof(element).
	// To upload to this buffer, call uploadToGPUBuffer().
	void allocateVertexBuffer(AllocatedBuffer& buffer, size_t const& allocationSize);
	// Allocates size for an index buffer where allocationSize is No of elements * sizeof(element).
	// To upload to this buffer, call uploadtoGPUBuffer().
	void allocateIndexBuffer(AllocatedBuffer& buffer, size_t const& allocationSize);
	// Allocates size for an index buffer where allocationSize is sizeof(struct).
	// Struct refers to a struct, or any other container that contains the data to be uploaded as a push constant.
	// To upload to this buffer, call uploadtoGPUBuffer().
	void allocateUniformBuffer(AllocatedBuffer& buffer, size_t const& allocationSize);

	// Uploads data from bufferData to buffer where bufferSize is No of elements * sizeof(element).
	void uploadToGPUBuffer(AllocatedBuffer& buffer, void const* bufferData, size_t const& bufferSize);
	// Uploads data from bufferData to buffer where bufferSize is No of elements * sizeof(element).
	void uploadToUniformBuffer(AllocatedBuffer& buffer, void const* bufferData, size_t const& bufferSize);

	
	// Returns the rendering info to be used when calling VkCmdBeginRendering().
	VkRenderingInfo getRenderingInfo();
	// Returns the rendering info to be used when calling VkCmdBeginRendering().
	// This one has no depth culling and is an alternative to initializing a pipeline
	// with no depth culling.
	VkRenderingInfo getRenderingInfoNoDepthCulling();

	// Initializes all the default data and setup needed for Vulkan to work
	void init();

	// Initializes a descriptor set layout
	// shaderStages declares in which stage of the shader that the bindings are used
	// usually it's either VK_SHADER_STAGE_VERTEX_BIT, VK_SHADER_STAGE_FRAGMENT_BIT, or VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_VERTEX_BIT.
	void initDescriptor(VkDescriptorSetLayout& descriptorSetLayout,
						VkShaderStageFlags const& shaderStages,
						std::vector<std::pair<uint32_t, VkDescriptorType>> const& bindings);
	
	// Used when initializing a descriptor set layout for a shader that doesn't need any uniforms.
	void initEmptyDescriptor(VkDescriptorSetLayout& descriptorSetLayout);


	// Initializes the pipeline layout where pushConstantSizeVertex and pushConstantSizeFragment are sizeof(struct).
	// Struct refers to a struct, or any other container that contains the data to be uploaded as a push constant.
	void initPipelineLayout(VkPipelineLayout& pipelineLayout,
							VkDescriptorSetLayout const& descriptorSetLayout,
							uint64_t const& pushConstantSizeVertex = 0,
							uint64_t const& pushConstantSizeFragment = 0);
	
	// Initializes the pipeline layout where pushConstantSize is sizeof(struct).
	// Struct refers to a struct, or any other container that contains the data to be uploaded as a push constant.
	void initPipelineLayoutCombinedPushConstants(VkPipelineLayout& pipelineLayout,
						VkDescriptorSetLayout const& descriptorSetLayout,
						uint64_t const& pushConstantSize);

	// Initializes the pipeline.
	void initPipeline(VkPipeline& pipeline,
					  VkPipelineLayout const& pipelineLayout,
					  VertexInputDescriptionHandler const& handler,
					  std::string const& vertPath,
					  std::string const& fragPath);

	// Initializes the pipeline for the Vertex struct.
	// VertexType must be the same as the one used for the SVKModel or Object that will use this pipeline.
	template <VertexType type>
	void initVertexPipeline(VkPipeline& pipeline,
					  		VkPipelineLayout const& pipelineLayout,
					  		std::string const& vertPath,
					  		std::string const& fragPath);

	// Binds the pipeline, vertexbuffer, descriptor set layout, the index buffer (if the object has one),
	// and any uniform buffers and textures of the object
	template <VertexType type>
	void bindObject(VkCommandBuffer cmd, Object<type>& object);

	// Either calls vkCmDDraw or vkCmdDrawIndexed (if the object has any indices).
	template <VertexType type>
	void drawObject(VkCommandBuffer cmd, Object<type>& object);

	// Calls bindObject() and then drawObject().
	template <VertexType type>
	void bindAndDrawObject(VkCommandBuffer cmd, Object<type>& object);

	// Loads a model. If the file doesn't have an attribute specified by VertexType the program will most likely 
	// run into a segmentation fault.
	template <VertexType type>
	void loadModel(SVKModel<type>& model, char const* path);

	// Loads a model based on the data provided. If any of the parameters are null they will be skipped.
	template <VertexType type>
	void loadCustomModel(SVKModel<type>& model,
						 std::vector<vec3>* vertices,
						 std::vector<vec3>* normals,
						 std::vector<vec2>* texCoords,
						 std::vector<uint32_t>* indices);

	// Loads a TGA texture.
	void loadTGATexture(AllocatedImage& texture, char const* path);

	// Creates a texture of a single color.
	void createSingleColorTexture(AllocatedImage& texture, vec4 const& v);

	// Creates a texture of a single color.
	void createSingleColorTexture(AllocatedImage& texture,
								  uint8_t const& red,
								  uint8_t const& green,
								  uint8_t const& blue,
								  uint8_t const& alpha);

	// Sets the background color of the program.
	void setBackgroundColor(vec3 const& color);

	// Sets the background color of the program.
	void setBackgroundColor(float const& red,
							float const& green,
							float const& blue);

	// Sets the cull mode that will be used by any initPipeline() or initVertexPipeline() calls after this point.
	void setCullMode(VkCullModeFlagBits const& value);
	// Sets whether depth testing is enabled for any initPipeline() or initVertexPipeline() calls after this point.
	void setDepthTestEnabled(bool const& value);
	// Sets the polygon mode that will be used by any initPipeline() or initVertexPipeline() calls after this point.
	void setPolygonMode(VkPolygonMode const& value);

	// Binds a drawing function that will be called by the main loop each frame.
	void bindDrawingFunction(std::function<void(VkCommandBuffer cmd)>&& function);
	// Binds a mouse function that will be called whenever the program detects mouse movement.
	void bindMouseMotionFunction(std::function<void(uint32_t xRel, uint32_t yRel)>&& function);
	// Added by Ingemar
	void bindKeydownFunction(std::function<void(const char* name)>&& function);
//	void bindKeydownFunction(std::function<void(std::string name)>&& function);


	// Prints some of the device limits.
	void printDeviceLimits(std::ostream& os) const;
	
	// Returns whether or not a key is pressed. Some keys do not work
	// but every key between A-Z as well as space are guaranteed to work.
	bool keyIsDown(std::string const& key) const;

        // More useful calls added by Ingemar 2026-04-03
        bool mouseIsDown(int button) const;
        void getMouse(int *x, int *y) const;

	// Returns the current aspect ratio of the screen.
	float getAspectRatio() const;

	// Enables OpenGL's depth clip control, setting the depth range to be between [-1, 1] instead of [0, 1].
	// This function must be called before init().
	void enableDepthClipControl();

	// Makes the frame rate uncapped rather than synced to the refresh rate of the monitor.
	// This function must be called before init().
	void uncapFrameRate();

	// Sets the name of the window.
	// This function must be called before init().
	void setWindowName(std::string const& name);

	// Sets the severety at which the debug utils messenger will report issues.
	// This function must be called before init().
	void setDebugMessengerSeverity(VkDebugUtilsMessageSeverityFlagBitsEXT const& severity);

	// Returns the most recent delta.
	double getDelta() const;
	// Returns the time since the program was started.
	double getTimeSinceStart() const;

	// Starts the main loop
	void run();

	// Frees data to avoid memory leaks and other issues
	void cleanup();

	// Returns the current instance of SVK.
	static SVK& get();

// Made public by Ingemar
	VkDevice _device;
//	FrameData& _getCurrentFrame() { return _frames[_frameNumber % FRAME_OVERLAP]; };
	VkPhysicalDevice _chosenGPU;
	
// Added by Ingemar

void hideMouse();
void showMouse();
void quit();

      // Call this before binding textures
      void clearTextures();

        int bindTexture(VkCommandBuffer cmd, AllocatedImage *texture, VkDescriptorSetLayout descriptorSetLayout, VkPipelineLayout& pipelineLayout);
//        void bindUniform(VkCommandBuffer cmd, int binding, char *buffer, int buffersize, VkDescriptorSetLayout descriptorSetLayout, VkPipelineLayout& pipelineLayout);
        void bindUniform(VkCommandBuffer cmd, int binding, AllocatedBuffer buffer, int buffersize, VkDescriptorSetLayout descriptorSetLayout, VkPipelineLayout& pipelineLayout);

private:
	struct DeletionQueue
	{
		std::deque<std::function<void()>> deletors;

		void push_function(std::function<void()>&& function)
		{
			deletors.push_back(function);
		}

		void flush()
		{
			for (auto it = deletors.rbegin(); it != deletors.rend(); ++it)
				(*it)();

			deletors.clear();
		}
	};

	struct FrameData
	{
		VkSemaphore _swapchainSemaphore, _renderSemaphore;
		VkFence _renderFence;

		VkCommandPool _commandPool;
		VkCommandBuffer _mainCommandBuffer;

		DeletionQueue _deletionQueue;
		DescriptorAllocatorGrowable _frameDescriptors;
	};


	void _initVulkan();
	void _initSwapchain();
	void _initCommands();
	void _initSyncStructures();
	void _initDescriptors();
	void _initDefaultData();

	void _draw();

	void _allocateStagingBuffer(AllocatedBuffer& buffer, size_t const& allocationSize);
	void _allocateGPUBuffer(AllocatedBuffer& buffer, size_t const& allocSize, VkBufferUsageFlags const& usage);
	void _allocateCPUBuffer(AllocatedBuffer& buffer, size_t const& allocSize, VkBufferUsageFlags const& usage);

	void _destroyBuffer(AllocatedBuffer& buffer);

	template <VertexType type>
	void _generateGPUBuffers(SVKModel<type>& model);
	template <VertexType type>
	void _generateGPUBuffersNoIndex(SVKModel<type>& model);

	void _createSwapchain(uint32_t width, uint32_t height);
	void _createAllocatedImages();
	void _createImage(AllocatedImage&, VkExtent3D size, VkFormat format, VkImageUsageFlags usage, bool mipmapped = false);
	void _createImage(AllocatedImage&, void* data, VkExtent3D size, VkFormat format, VkImageUsageFlags usage, bool mipmapped = false);
	void _resizeSwapchain();
	void _destroySwapchain();
	void _destroyAllocatedImages();
	void _destroyImage(AllocatedImage& img);
	void _destroyDescriptors();
	void _drawBackground(VkCommandBuffer cmd);
	void _setViewportAndScissor(VkCommandBuffer cmd);

	void _immediateSubmit(std::function<void(VkCommandBuffer cmd)>&& function);
	void _switchMouseMode();

	FrameData& _getCurrentFrame() { return _frames[_frameNumber % FRAME_OVERLAP]; };
      
	
	bool _depthClipControlEnabled{false};
	bool _uncappedFrameRate{false};
	bool _depthTestEnabled{true};
	VkPolygonMode _polygonMode{VK_POLYGON_MODE_FILL};
	VkCullModeFlagBits _cullMode{VK_CULL_MODE_NONE};
	float _renderScale{1.0f};
	vec4 _backgroundColor{0.2, 0.4, 0.8, 1.0};
	std::string _windowName{"Simpler Vulkan Application"};
	VkDebugUtilsMessageSeverityFlagBitsEXT _debugMessengerSeverity{VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT};
	DeltaHandler _deltaHandler;

	std::function<void(VkCommandBuffer cmd)> _userDrawFunction;
	std::function<void(uint32_t xRel, uint32_t yRel)> _userMouseMotionFunction;
	std::function<void(const char*)> _userKeydownFunction;


	VkInstance _instance;
	VkDebugUtilsMessengerEXT _debug_messenger;
//	VkPhysicalDevice _chosenGPU;
//	VkDevice _device;
	VkSurfaceKHR _surface;
/*	VkExtent2D _windowExtent{1700, 900}; Old default dimensions - why? */
	VkExtent2D _windowExtent{800, 800};
	bool _isInitialized{false};
	unsigned int _frameNumber{0};

	VkSwapchainKHR _swapchain;
	VkFormat _swapchainImageFormat;
	std::vector<VkImage> _swapchainImages;
	std::vector<VkImageView> _swapchainImageViews;
	VkExtent2D _swapchainExtent;

	FrameData _frames[FRAME_OVERLAP];
	VkQueue _graphicsQueue;
	uint32_t _graphicsQueueFamily;

	DeletionQueue _mainDeletionQueue;
	VmaAllocator _allocator;
	DescriptorAllocator _globalDescriptorAllocator;

	AllocatedImage _drawImage;
	AllocatedImage _depthImage;
	VkExtent2D _drawExtent;

	VkRenderingAttachmentInfo _colorAttachment;
	VkRenderingAttachmentInfo _depthAttachment;
	
	VkDescriptorSet _drawImageDescriptors;
	VkDescriptorSetLayout _drawImageDescriptorLayout;

	VkFence _immFence;
	VkCommandBuffer _immCommandBuffer;
	VkCommandPool _immCommandPool;


	bool _resizeRequested = false;
	bool _isMouseHidden = false;
	bool gQuit = false;

	VkDescriptorSetLayout _singleImageDescriptorLayout;

	AllocatedImage _whiteImage;
	AllocatedImage _blackImage;
	AllocatedImage _greyImage;
	AllocatedImage _errorCheckerboardImage;

	VkSampler _defaultSamplerLinear;
	VkSampler _defaultSamplerNearest;

	struct SDL_Window* _window{ nullptr };
};

template <VertexType type>
void SVK::initVertexPipeline(VkPipeline& pipeline,
							 VkPipelineLayout const& pipelineLayout,
							 std::string const& vertPath,
							 std::string const& fragPath)
{
	VertexInputDescriptionHandler handler;

	handler.bindingDescriptions.push_back(Vertex<type>::getBindingDescription());

	std::array<VkVertexInputAttributeDescription, getAttributeDescriptionCount(type)> attributeDescriptions{Vertex<type>::getAttributeDescription()};

	handler.attributeDescriptions.assign(std::begin(attributeDescriptions), std::end(attributeDescriptions));

	initPipeline(pipeline, pipelineLayout, handler, vertPath, fragPath);
}

template <VertexType type>
void SVK::bindObject(VkCommandBuffer cmd, Object<type>& object)
{
	vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, *object.pipeline);

	VkDescriptorSet objectSet = _getCurrentFrame()._frameDescriptors.allocate(_device, *object.descriptorSetLayout);
	DescriptorWriter descriptorWriter;
	for (typename Object<type>::UniformData& uniformData : object.uniformBuffers)
		descriptorWriter.write_buffer(uniformData.binding, uniformData.buffer->buffer, uniformData.size, 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER);
	for (std::pair<uint32_t, AllocatedImage*>& texture : object.textures)
		descriptorWriter.write_image(texture.first, texture.second->imageView, _defaultSamplerNearest, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
	descriptorWriter.update_set(_device, objectSet);

	vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, *object.pipelineLayout, 0, 1, &objectSet, 0, nullptr);

	VkDeviceSize offsets[] = {0};
	vkCmdBindVertexBuffers(cmd, 0, 1, &object.model->vertexBuffer.buffer, offsets);
	if (object.model->indices.size() != 0)
		vkCmdBindIndexBuffer(cmd, object.model->indexBuffer.buffer, 0, VK_INDEX_TYPE_UINT32);
}

template <VertexType type>
void SVK::drawObject(VkCommandBuffer cmd, Object<type>& object)
{
	if (object.model->indices.size() != 0)
		vkCmdDrawIndexed(cmd, object.model->indices.size(), 1, 0, 0, 0);
	else
		vkCmdDraw(cmd, object.model->vertices.size(), 1, 0, 0);
}

template <VertexType type>
void SVK::bindAndDrawObject(VkCommandBuffer cmd, Object<type>& object)
{
	bindObject(cmd, object);
	drawObject(cmd, object);
}

template <VertexType type>
void SVK::loadModel(SVKModel<type>& model, char const* path)
{
	_loadModel<type>(model, path);

	_generateGPUBuffers(model);
}

template <VertexType type>
void SVK::loadCustomModel(SVKModel<type>& model,
						  std::vector<vec3>* vertices,
						  std::vector<vec3>* normals,
						  std::vector<vec2>* texCoords,
						  std::vector<uint32_t>* indices)
{
	std::unordered_map<Vertex<type>, uint32_t> uniqueVertices{};
	if (!(indices == nullptr)){
		for (unsigned int i = 0; i < (*indices).size(); ++i){
			Vertex<type> vertex;
			vertex.position = (*vertices)[(*indices)[i]];
			if (type & VERTEX_PN){
				vertex.normal = (*normals)[(*indices)[i]];
			}
			if (type & VERTEX_PT){
				vertex.texCoord = (*texCoords)[(*indices)[i]];
				vertex.texCoord.y = 1.0f - vertex.texCoord.y;
			}
			if (uniqueVertices.count(vertex) == 0){
				uniqueVertices[vertex] = static_cast<uint32_t>(model.vertices.size());
				model.vertices.push_back(vertex);
			}
			model.indices.push_back(uniqueVertices[vertex]);
		}
		_generateGPUBuffers(model);
	}
	else{
		for (unsigned int i = 0; i < (*vertices).size(); ++i){
			Vertex<type> vertex;
			vertex.position = (*vertices)[i];
			if (type & VERTEX_PN)
				vertex.normal = (*normals)[i];
			if (type & VERTEX_PT){
				vertex.texCoord = (*texCoords)[i];
				vertex.texCoord.y = 1.0f - vertex.texCoord.y;
			}
			model.vertices.push_back(vertex);
			
		}
		_generateGPUBuffersNoIndex(model);
	}
}

template <VertexType type>
void SVK::_generateGPUBuffers(SVKModel<type>& model)
{
	const size_t vertexBufferSize = model.vertices.size() * sizeof(Vertex<type>);
	const size_t indexBufferSize = model.indices.size() * sizeof(uint32_t);
	const void* verticesData = model.vertices.data();
	const void* indicesData = model.indices.data();

	allocateVertexBuffer(model.vertexBuffer, vertexBufferSize);
	uploadToGPUBuffer(model.vertexBuffer, verticesData, vertexBufferSize);

	allocateIndexBuffer(model.indexBuffer, indexBufferSize);
	uploadToGPUBuffer(model.indexBuffer, indicesData, indexBufferSize);
}

template <VertexType type>
void SVK::_generateGPUBuffersNoIndex(SVKModel<type>& model)
{
	const size_t vertexBufferSize = model.vertices.size() * sizeof(Vertex<type>);
	const void* verticesData = model.vertices.data();

	allocateVertexBuffer(model.vertexBuffer, vertexBufferSize);
	uploadToGPUBuffer(model.vertexBuffer, verticesData, vertexBufferSize);
}

#endif // SVK_HPP
