#include <iostream>
#include <vkcv/Core.hpp>
#include <GLFW/glfw3.h>
#include <vkcv/camera/CameraManager.hpp>
#include <chrono>
#include <vkcv/asset/asset_loader.hpp>
#include <vkcv/shader/GLSLCompiler.hpp>
#include <vkcv/Logger.hpp>
#include "Voxelization.hpp"
#include <glm/glm.hpp>
#include "vkcv/gui/GUI.hpp"
#include "ShadowMapping.hpp"
#include "BloomAndFlares.hpp"

int main(int argc, const char** argv) {
	const char* applicationName = "Voxelization";

	uint32_t windowWidth = 1280;
	uint32_t windowHeight = 720;
	const vkcv::Multisampling   msaa        = vkcv::Multisampling::MSAA4X;
	const bool                  usingMsaa   = msaa != vkcv::Multisampling::None;
	
	vkcv::Window window = vkcv::Window::create(
		applicationName,
		windowWidth,
		windowHeight,
		true
	);

	bool    isFullscreen            = false;
	int     windowedWidthBackup     = windowWidth;
	int     windowedHeightBackup    = windowHeight;
	int     windowedPosXBackup;
	int     windowedPosYBackup;
    glfwGetWindowPos(window.getWindow(), &windowedPosXBackup, &windowedPosYBackup);

	window.e_key.add([&](int key, int scancode, int action, int mods) {
		if (key == GLFW_KEY_F11 && action == GLFW_PRESS) {
			if (isFullscreen) {
				glfwSetWindowMonitor(
					window.getWindow(),
					nullptr,
					windowedPosXBackup,
					windowedPosYBackup,
					windowedWidthBackup,
					windowedHeightBackup,
					GLFW_DONT_CARE);
			}
			else {
				windowedWidthBackup     = windowWidth;
				windowedHeightBackup    = windowHeight;

				glfwGetWindowPos(window.getWindow(), &windowedPosXBackup, &windowedPosYBackup);

				GLFWmonitor*        monitor     = glfwGetPrimaryMonitor();
				const GLFWvidmode*  videoMode   = glfwGetVideoMode(monitor);

				glfwSetWindowMonitor(
					window.getWindow(),
					glfwGetPrimaryMonitor(),
					0,
					0,
					videoMode->width,
					videoMode->height,
					videoMode->refreshRate);
			}
			isFullscreen = !isFullscreen;
		}
	});

	vkcv::camera::CameraManager cameraManager(window);
	uint32_t camIndex  = cameraManager.addCamera(vkcv::camera::ControllerType::PILOT);
	uint32_t camIndex2 = cameraManager.addCamera(vkcv::camera::ControllerType::TRACKBALL);

	cameraManager.getCamera(camIndex).setPosition(glm::vec3(0.f, 0.f, 3.f));
	cameraManager.getCamera(camIndex).setNearFar(0.1f, 30.0f);
	cameraManager.getCamera(camIndex).setYaw(180.0f);
	cameraManager.getCamera(camIndex).setFov(glm::radians(37.8));	// fov of a 35mm lens
	
	cameraManager.getCamera(camIndex2).setNearFar(0.1f, 30.0f);

	vkcv::Core core = vkcv::Core::create(
		window,
		applicationName,
		VK_MAKE_VERSION(0, 0, 1),
		{ vk::QueueFlagBits::eTransfer,vk::QueueFlagBits::eGraphics, vk::QueueFlagBits::eCompute },
		{},
		{ "VK_KHR_swapchain" }
	);

	vkcv::asset::Scene mesh;

	const char* path = argc > 1 ? argv[1] : "resources/Sponza/Sponza.gltf";
	vkcv::asset::Scene scene;
	int result = vkcv::asset::loadScene(path, scene);

	if (result == 1) {
		std::cout << "Scene loading successful!" << std::endl;
	}
	else {
		std::cout << "Scene loading failed: " << result << std::endl;
		return 1;
	}

	// build index and vertex buffers
	assert(!scene.vertexGroups.empty());
	std::vector<std::vector<uint8_t>> vBuffers;
	std::vector<std::vector<uint8_t>> iBuffers;

	std::vector<vkcv::VertexBufferBinding> vBufferBindings;
	std::vector<std::vector<vkcv::VertexBufferBinding>> vertexBufferBindings;
	std::vector<vkcv::asset::VertexAttribute> vAttributes;

	for (int i = 0; i < scene.vertexGroups.size(); i++) {

		vBuffers.push_back(scene.vertexGroups[i].vertexBuffer.data);
		iBuffers.push_back(scene.vertexGroups[i].indexBuffer.data);

		auto& attributes = scene.vertexGroups[i].vertexBuffer.attributes;

		std::sort(attributes.begin(), attributes.end(), [](const vkcv::asset::VertexAttribute& x, const vkcv::asset::VertexAttribute& y) {
			return static_cast<uint32_t>(x.type) < static_cast<uint32_t>(y.type);
		});
	}

	std::vector<vkcv::Buffer<uint8_t>> vertexBuffers;
	for (const vkcv::asset::VertexGroup& group : scene.vertexGroups) {
		vertexBuffers.push_back(core.createBuffer<uint8_t>(
			vkcv::BufferType::VERTEX,
			group.vertexBuffer.data.size()));
		vertexBuffers.back().fill(group.vertexBuffer.data);
	}

	std::vector<vkcv::Buffer<uint8_t>> indexBuffers;
	for (const auto& dataBuffer : iBuffers) {
		indexBuffers.push_back(core.createBuffer<uint8_t>(
			vkcv::BufferType::INDEX,
			dataBuffer.size()));
		indexBuffers.back().fill(dataBuffer);
	}

	int vertexBufferIndex = 0;
	for (const auto& vertexGroup : scene.vertexGroups) {
		for (const auto& attribute : vertexGroup.vertexBuffer.attributes) {
			vAttributes.push_back(attribute);
			vBufferBindings.push_back(vkcv::VertexBufferBinding(attribute.offset, vertexBuffers[vertexBufferIndex].getVulkanHandle()));
		}
		vertexBufferBindings.push_back(vBufferBindings);
		vBufferBindings.clear();
		vertexBufferIndex++;
	}

	const vk::Format colorBufferFormat = vk::Format::eB10G11R11UfloatPack32;
	const vkcv::AttachmentDescription color_attachment(
		vkcv::AttachmentOperation::STORE,
		vkcv::AttachmentOperation::CLEAR,
		colorBufferFormat
	);
	
	const vk::Format depthBufferFormat = vk::Format::eD32Sfloat;
	const vkcv::AttachmentDescription depth_attachment(
		vkcv::AttachmentOperation::STORE,
		vkcv::AttachmentOperation::LOAD,
		depthBufferFormat
	);
	
	// forward shading config
	vkcv::PassConfig forwardPassDefinition({ color_attachment, depth_attachment }, msaa);
	vkcv::PassHandle forwardPass = core.createPass(forwardPassDefinition);

	vkcv::shader::GLSLCompiler compiler;

	vkcv::ShaderProgram forwardProgram;
	compiler.compile(vkcv::ShaderStage::VERTEX, std::filesystem::path("resources/shaders/shader.vert"), 
		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
		forwardProgram.addShader(shaderStage, path);
	});
	compiler.compile(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("resources/shaders/shader.frag"),
		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
		forwardProgram.addShader(shaderStage, path);
	});

	const std::vector<vkcv::VertexAttachment> vertexAttachments = forwardProgram.getVertexAttachments();

	std::vector<vkcv::VertexBinding> vertexBindings;
	for (size_t i = 0; i < vertexAttachments.size(); i++) {
		vertexBindings.push_back(vkcv::VertexBinding(i, { vertexAttachments[i] }));
	}
	const vkcv::VertexLayout vertexLayout (vertexBindings);

	vkcv::DescriptorSetHandle forwardShadingDescriptorSet = 
		core.createDescriptorSet({ forwardProgram.getReflectedDescriptors()[0] });

	// depth prepass config
	vkcv::ShaderProgram depthPrepassShader;
	compiler.compile(vkcv::ShaderStage::VERTEX, std::filesystem::path("resources/shaders/depthPrepass.vert"),
		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
		depthPrepassShader.addShader(shaderStage, path);
	});
	compiler.compile(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("resources/shaders/depthPrepass.frag"),
		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
		depthPrepassShader.addShader(shaderStage, path);
	});

	const std::vector<vkcv::VertexAttachment> prepassVertexAttachments = depthPrepassShader.getVertexAttachments();

	std::vector<vkcv::VertexBinding> prepassVertexBindings;
	for (size_t i = 0; i < prepassVertexAttachments.size(); i++) {
		prepassVertexBindings.push_back(vkcv::VertexBinding(i, { prepassVertexAttachments[i] }));
	}
	const vkcv::VertexLayout prepassVertexLayout(prepassVertexBindings);

	const vkcv::AttachmentDescription prepassAttachment(
		vkcv::AttachmentOperation::STORE,
		vkcv::AttachmentOperation::CLEAR,
		depthBufferFormat);

	vkcv::PassConfig prepassPassDefinition({ prepassAttachment }, msaa);
	vkcv::PassHandle prepassPass = core.createPass(prepassPassDefinition);

	// create descriptor sets
	vkcv::SamplerHandle colorSampler = core.createSampler(
		vkcv::SamplerFilterType::LINEAR,
		vkcv::SamplerFilterType::LINEAR,
		vkcv::SamplerMipmapMode::LINEAR,
		vkcv::SamplerAddressMode::REPEAT
	);

	std::vector<vkcv::DescriptorSetHandle> materialDescriptorSets;
	std::vector<vkcv::Image> sceneImages;

	for (const auto& material : scene.materials) {
		int albedoIndex     = material.baseColor;
		int normalIndex     = material.normal;
		int specularIndex   = material.metalRough;

		if (albedoIndex < 0) {
			vkcv_log(vkcv::LogLevel::WARNING, "Material lacks albedo");
			albedoIndex = 0;
		}
		if (normalIndex < 0) {
			vkcv_log(vkcv::LogLevel::WARNING, "Material lacks normal");
			normalIndex = 0;
		}
		if (specularIndex < 0) {
			vkcv_log(vkcv::LogLevel::WARNING, "Material lacks specular");
			specularIndex = 0;
		}

		materialDescriptorSets.push_back(core.createDescriptorSet(forwardProgram.getReflectedDescriptors()[1]));

		vkcv::asset::Texture& albedoTexture     = scene.textures[albedoIndex];
		vkcv::asset::Texture& normalTexture     = scene.textures[normalIndex];
		vkcv::asset::Texture& specularTexture   = scene.textures[specularIndex];

		// albedo texture
		sceneImages.push_back(core.createImage(vk::Format::eR8G8B8A8Srgb, albedoTexture.w, albedoTexture.h, 1, true));
		sceneImages.back().fill(albedoTexture.data.data());
		sceneImages.back().generateMipChainImmediate();
		sceneImages.back().switchLayout(vk::ImageLayout::eShaderReadOnlyOptimal);
		const vkcv::ImageHandle albedoHandle = sceneImages.back().getHandle();

		// normal texture
		sceneImages.push_back(core.createImage(vk::Format::eR8G8B8A8Unorm, normalTexture.w, normalTexture.h, 1, true));
		sceneImages.back().fill(normalTexture.data.data());
		sceneImages.back().generateMipChainImmediate();
		sceneImages.back().switchLayout(vk::ImageLayout::eShaderReadOnlyOptimal);
		const vkcv::ImageHandle normalHandle = sceneImages.back().getHandle();

		// specular texture
		sceneImages.push_back(core.createImage(vk::Format::eR8G8B8A8Unorm, specularTexture.w, specularTexture.h, 1, true));
		sceneImages.back().fill(specularTexture.data.data());
		sceneImages.back().generateMipChainImmediate();
		sceneImages.back().switchLayout(vk::ImageLayout::eShaderReadOnlyOptimal);
		const vkcv::ImageHandle specularHandle = sceneImages.back().getHandle();

		vkcv::DescriptorWrites setWrites;
		setWrites.sampledImageWrites = {
			vkcv::SampledImageDescriptorWrite(0, albedoHandle),
			vkcv::SampledImageDescriptorWrite(2, normalHandle),
			vkcv::SampledImageDescriptorWrite(3, specularHandle)
		};
		setWrites.samplerWrites = {
			vkcv::SamplerDescriptorWrite(1, colorSampler),
		};
		core.writeDescriptorSet(materialDescriptorSets.back(), setWrites);
	}

	std::vector<vkcv::DescriptorSetHandle> perMeshDescriptorSets;
	for (const auto& vertexGroup : scene.vertexGroups) {
		perMeshDescriptorSets.push_back(materialDescriptorSets[vertexGroup.materialIndex]);
	}

	// prepass pipeline
	vkcv::DescriptorSetHandle prepassDescriptorSet = core.createDescriptorSet(std::vector<vkcv::DescriptorBinding>());

	vkcv::PipelineConfig prepassPipelineConfig{
		depthPrepassShader,
		windowWidth,
		windowHeight,
		prepassPass,
		vertexLayout,
		{ 
			core.getDescriptorSet(prepassDescriptorSet).layout,
			core.getDescriptorSet(perMeshDescriptorSets[0]).layout },
		true };
	prepassPipelineConfig.m_culling         = vkcv::CullMode::Back;
	prepassPipelineConfig.m_multisampling   = msaa;
	prepassPipelineConfig.m_depthTest       = vkcv::DepthTest::LessEqual;
	prepassPipelineConfig.m_alphaToCoverage = true;

	vkcv::PipelineHandle prepassPipeline = core.createGraphicsPipeline(prepassPipelineConfig);

	// forward pipeline
	vkcv::PipelineConfig forwardPipelineConfig {
		forwardProgram,
		windowWidth,
		windowHeight,
		forwardPass,
		vertexLayout,
		{	
			core.getDescriptorSet(forwardShadingDescriptorSet).layout, 
			core.getDescriptorSet(perMeshDescriptorSets[0]).layout },
		true
	};
    forwardPipelineConfig.m_culling         = vkcv::CullMode::Back;
	forwardPipelineConfig.m_multisampling   = msaa;
	forwardPipelineConfig.m_depthTest       = vkcv::DepthTest::Equal;
	forwardPipelineConfig.m_depthWrite      = false;
	
	vkcv::PipelineHandle forwardPipeline = core.createGraphicsPipeline(forwardPipelineConfig);
	
	if (!forwardPipeline) {
		std::cout << "Error. Could not create graphics pipeline. Exiting." << std::endl;
		return EXIT_FAILURE;
	}

	// sky
	struct SkySettings {
		glm::vec3   color;
		float       strength;
	};
	SkySettings skySettings;
	skySettings.color       = glm::vec3(0.15, 0.65, 1);
	skySettings.strength    = 5;

	const vkcv::AttachmentDescription skyColorAttachment(
		vkcv::AttachmentOperation::STORE,
		vkcv::AttachmentOperation::LOAD,
		colorBufferFormat);

	const vkcv::AttachmentDescription skyDepthAttachments(
		vkcv::AttachmentOperation::STORE,
		vkcv::AttachmentOperation::LOAD,
		depthBufferFormat);

	vkcv::PassConfig skyPassConfig({ skyColorAttachment, skyDepthAttachments }, msaa);
	vkcv::PassHandle skyPass = core.createPass(skyPassConfig);

	vkcv::ShaderProgram skyShader;
	compiler.compile(vkcv::ShaderStage::VERTEX, std::filesystem::path("resources/shaders/sky.vert"),
		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
		skyShader.addShader(shaderStage, path);
	});
	compiler.compile(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("resources/shaders/sky.frag"),
		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
		skyShader.addShader(shaderStage, path);
	});

	vkcv::PipelineConfig skyPipeConfig;
	skyPipeConfig.m_ShaderProgram       = skyShader;
	skyPipeConfig.m_Width               = windowWidth;
	skyPipeConfig.m_Height              = windowHeight;
	skyPipeConfig.m_PassHandle          = skyPass;
	skyPipeConfig.m_VertexLayout        = vkcv::VertexLayout();
	skyPipeConfig.m_DescriptorLayouts   = {};
	skyPipeConfig.m_UseDynamicViewport  = true;
	skyPipeConfig.m_multisampling       = msaa;
	skyPipeConfig.m_depthWrite          = false;

	vkcv::PipelineHandle skyPipe = core.createGraphicsPipeline(skyPipeConfig);

	// render targets
	vkcv::ImageHandle depthBuffer           = core.createImage(depthBufferFormat, windowWidth, windowHeight, 1, false, false, false, msaa).getHandle();

    const bool colorBufferRequiresStorage   = !usingMsaa;
	vkcv::ImageHandle colorBuffer           = core.createImage(colorBufferFormat, windowWidth, windowHeight, 1, false, colorBufferRequiresStorage, true, msaa).getHandle();

	vkcv::ImageHandle resolvedColorBuffer;
	if (usingMsaa) {
		resolvedColorBuffer = core.createImage(colorBufferFormat, windowWidth, windowHeight, 1, false, true, true).getHandle();
	}
	else {
		resolvedColorBuffer = colorBuffer;
	}

	const vkcv::ImageHandle swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle();

	bool renderVoxelVis = false;
	window.e_key.add([&renderVoxelVis](int key ,int scancode, int action, int mods) {
		if (key == GLFW_KEY_V && action == GLFW_PRESS) {
			renderVoxelVis = !renderVoxelVis;
		}
	});

	bool renderUI = true;
	window.e_key.add([&renderUI](int key, int scancode, int action, int mods) {
		if (key == GLFW_KEY_I && action == GLFW_PRESS) {
			renderUI = !renderUI;
		}
	});

	// tonemapping compute shader
	vkcv::ShaderProgram tonemappingProgram;
	compiler.compile(vkcv::ShaderStage::COMPUTE, "resources/shaders/tonemapping.comp", 
		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
		tonemappingProgram.addShader(shaderStage, path);
	});
	vkcv::DescriptorSetHandle tonemappingDescriptorSet = core.createDescriptorSet(
		tonemappingProgram.getReflectedDescriptors()[0]);
	vkcv::PipelineHandle tonemappingPipeline = core.createComputePipeline(
		tonemappingProgram,
		{ core.getDescriptorSet(tonemappingDescriptorSet).layout });

	// resolve compute shader
	vkcv::ShaderProgram resolveProgram;
	compiler.compile(vkcv::ShaderStage::COMPUTE, "resources/shaders/msaa4XResolve.comp",
		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
		resolveProgram.addShader(shaderStage, path);
	});
	vkcv::DescriptorSetHandle resolveDescriptorSet = core.createDescriptorSet(
		resolveProgram.getReflectedDescriptors()[0]);
	vkcv::PipelineHandle resolvePipeline = core.createComputePipeline(
		resolveProgram,
		{ core.getDescriptorSet(resolveDescriptorSet).layout });

	vkcv::SamplerHandle resolveSampler = core.createSampler(
		vkcv::SamplerFilterType::NEAREST,
		vkcv::SamplerFilterType::NEAREST,
		vkcv::SamplerMipmapMode::NEAREST,
		vkcv::SamplerAddressMode::CLAMP_TO_EDGE);

	// model matrices per mesh
	std::vector<glm::mat4> modelMatrices;
	modelMatrices.resize(scene.vertexGroups.size(), glm::mat4(1.f));
	for (const auto& mesh : scene.meshes) {
		const glm::mat4 m = *reinterpret_cast<const glm::mat4*>(&mesh.modelMatrix[0]);
		for (const auto& vertexGroupIndex : mesh.vertexGroups) {
			modelMatrices[vertexGroupIndex] = m;
		}
	}

	// prepare meshes
	std::vector<vkcv::Mesh> meshes;
	for (int i = 0; i < scene.vertexGroups.size(); i++) {
		vkcv::Mesh mesh(vertexBufferBindings[i], indexBuffers[i].getVulkanHandle(), scene.vertexGroups[i].numIndices);
		meshes.push_back(mesh);
	}

	std::vector<vkcv::DrawcallInfo> drawcalls;
	std::vector<vkcv::DrawcallInfo> prepassDrawcalls;
	for (int i = 0; i < meshes.size(); i++) {

		drawcalls.push_back(vkcv::DrawcallInfo(meshes[i], { 
			vkcv::DescriptorSetUsage(0, core.getDescriptorSet(forwardShadingDescriptorSet).vulkanHandle),
			vkcv::DescriptorSetUsage(1, core.getDescriptorSet(perMeshDescriptorSets[i]).vulkanHandle) }));
		prepassDrawcalls.push_back(vkcv::DrawcallInfo(meshes[i], {
			vkcv::DescriptorSetUsage(0, core.getDescriptorSet(prepassDescriptorSet).vulkanHandle),
			vkcv::DescriptorSetUsage(1, core.getDescriptorSet(perMeshDescriptorSets[i]).vulkanHandle) }));
	}

	vkcv::SamplerHandle voxelSampler = core.createSampler(
		vkcv::SamplerFilterType::LINEAR,
		vkcv::SamplerFilterType::LINEAR,
		vkcv::SamplerMipmapMode::LINEAR,
		vkcv::SamplerAddressMode::CLAMP_TO_EDGE);

	ShadowMapping shadowMapping(&core, vertexLayout);

	Voxelization::Dependencies voxelDependencies;
	voxelDependencies.colorBufferFormat = colorBufferFormat;
	voxelDependencies.depthBufferFormat = depthBufferFormat;
	voxelDependencies.vertexLayout = vertexLayout;
	Voxelization voxelization(
		&core,
		voxelDependencies,
		shadowMapping.getLightInfoBuffer(),
		shadowMapping.getShadowMap(),
		shadowMapping.getShadowSampler(),
		voxelSampler,
		msaa);

	BloomAndFlares bloomFlares(&core, colorBufferFormat, windowWidth, windowHeight);

	window.e_key.add([&](int key, int scancode, int action, int mods) {
		if (key == GLFW_KEY_R && action == GLFW_PRESS) {
			bloomFlares = BloomAndFlares(&core, colorBufferFormat, windowWidth, windowHeight);
		}
	});

	vkcv::Buffer<glm::vec3> cameraPosBuffer = core.createBuffer<glm::vec3>(vkcv::BufferType::UNIFORM, 1);

	struct VolumetricSettings {
		glm::vec3   scatteringCoefficient;
		float       ambientLight;
		glm::vec3   absorptionCoefficient;
	};
	vkcv::Buffer<VolumetricSettings> volumetricSettingsBuffer
		= core.createBuffer<VolumetricSettings>(vkcv::BufferType::UNIFORM ,1);

	// write forward pass descriptor set
	vkcv::DescriptorWrites forwardDescriptorWrites;
	forwardDescriptorWrites.uniformBufferWrites = {
		vkcv::UniformBufferDescriptorWrite(0, shadowMapping.getLightInfoBuffer()),
		vkcv::UniformBufferDescriptorWrite(3, cameraPosBuffer.getHandle()),
		vkcv::UniformBufferDescriptorWrite(6, voxelization.getVoxelInfoBufferHandle()),
		vkcv::UniformBufferDescriptorWrite(7, volumetricSettingsBuffer.getHandle())};
	forwardDescriptorWrites.sampledImageWrites = {
		vkcv::SampledImageDescriptorWrite(1, shadowMapping.getShadowMap()),
		vkcv::SampledImageDescriptorWrite(4, voxelization.getVoxelImageHandle()) };
	forwardDescriptorWrites.samplerWrites = { 
		vkcv::SamplerDescriptorWrite(2, shadowMapping.getShadowSampler()),
		vkcv::SamplerDescriptorWrite(5, voxelSampler) };
	core.writeDescriptorSet(forwardShadingDescriptorSet, forwardDescriptorWrites);

	vkcv::gui::GUI gui(core, window);

	glm::vec2   lightAnglesDegree               = glm::vec2(90.f, 0.f);
	glm::vec3   lightColor                      = glm::vec3(1);
	float       lightStrength                   = 25.f;
	float       maxShadowDistance               = 30.f;

	int     voxelVisualisationMip   = 0;
	float   voxelizationExtent      = 35.f;

	bool msaaCustomResolve = true;

	glm::vec3   scatteringColor     = glm::vec3(1);
	float       scatteringDensity   = 0.005;
	glm::vec3   absorptionColor     = glm::vec3(1);
	float       absorptionDensity   = 0.005;
	float       volumetricAmbient   = 0.2;

	auto start = std::chrono::system_clock::now();
	const auto appStartTime = start;
	while (window.isWindowOpen()) {
		vkcv::Window::pollEvents();

		uint32_t swapchainWidth, swapchainHeight;
		if (!core.beginFrame(swapchainWidth, swapchainHeight)) {
			continue;
		}
		
		if ((swapchainWidth < 2) || (swapchainHeight < 2)) {
			std::cerr << "A" << std::endl;
		}
		
		if ((windowWidth < 2) || (windowHeight < 2)) {
			std::cerr << "B" << std::endl;
		}

		if ((swapchainWidth != windowWidth) || ((swapchainHeight != windowHeight))) {
			depthBuffer         = core.createImage(depthBufferFormat, swapchainWidth, swapchainHeight, 1, false, false, false, msaa).getHandle();
			colorBuffer         = core.createImage(colorBufferFormat, swapchainWidth, swapchainHeight, 1, false, colorBufferRequiresStorage, true, msaa).getHandle();

			if (usingMsaa) {
				resolvedColorBuffer = core.createImage(colorBufferFormat, swapchainWidth, swapchainHeight, 1, false, true, true).getHandle();
			}
			else {
				resolvedColorBuffer = colorBuffer;
			}

			windowWidth = swapchainWidth;
			windowHeight = swapchainHeight;

			bloomFlares.updateImageDimensions(windowWidth, windowHeight);
		}
		
		if ((swapchainWidth < 2) || (swapchainHeight < 2)) {
			std::cerr << "C" << std::endl;
		}
		
		if ((windowWidth < 2) || (windowHeight < 2)) {
			std::cerr << "D" << std::endl;
		}

		auto end = std::chrono::system_clock::now();
		auto deltatime = std::chrono::duration_cast<std::chrono::microseconds>(end - start);

		// update descriptor sets which use swapchain image
		vkcv::DescriptorWrites tonemappingDescriptorWrites;
		tonemappingDescriptorWrites.sampledImageWrites  = { vkcv::SampledImageDescriptorWrite(0, resolvedColorBuffer) };
		tonemappingDescriptorWrites.samplerWrites       = { vkcv::SamplerDescriptorWrite(1, colorSampler) };
		tonemappingDescriptorWrites.storageImageWrites  = { vkcv::StorageImageDescriptorWrite(2, swapchainInput) };

		core.writeDescriptorSet(tonemappingDescriptorSet, tonemappingDescriptorWrites);

		// update resolve descriptor, color images could be changed
		vkcv::DescriptorWrites resolveDescriptorWrites;
		resolveDescriptorWrites.sampledImageWrites  = { vkcv::SampledImageDescriptorWrite(0, colorBuffer) };
		resolveDescriptorWrites.samplerWrites       = { vkcv::SamplerDescriptorWrite(1, resolveSampler) };
		resolveDescriptorWrites.storageImageWrites  = { vkcv::StorageImageDescriptorWrite(2, resolvedColorBuffer) };
		core.writeDescriptorSet(resolveDescriptorSet, resolveDescriptorWrites);

		start = end;
		cameraManager.update(0.000001 * static_cast<double>(deltatime.count()));
		cameraPosBuffer.fill({ cameraManager.getActiveCamera().getPosition() });

		auto cmdStream = core.createCommandStream(vkcv::QueueType::Graphics);

		voxelization.updateVoxelOffset(cameraManager.getActiveCamera());

		// shadow map
		glm::vec2 lightAngleRadian = glm::radians(lightAnglesDegree);
		shadowMapping.recordShadowMapRendering(
			cmdStream,
			lightAngleRadian,
			lightColor,
			lightStrength,
			maxShadowDistance,
			meshes,
			modelMatrices,
			cameraManager.getActiveCamera(),
			voxelization.getVoxelOffset(),
			voxelization.getVoxelExtent());

		// voxelization
		voxelization.setVoxelExtent(voxelizationExtent);
		voxelization.voxelizeMeshes(
			cmdStream,
			meshes, 
			modelMatrices,
			perMeshDescriptorSets);

		// depth prepass
		const glm::mat4 viewProjectionCamera = cameraManager.getActiveCamera().getMVP();

		std::vector<glm::mat4> prepassMatrices;
		for (const auto& m : modelMatrices) {
			prepassMatrices.push_back(viewProjectionCamera * m);
		}

		const vkcv::PushConstantData            prepassPushConstantData((void*)prepassMatrices.data(), sizeof(glm::mat4));
		const std::vector<vkcv::ImageHandle>    prepassRenderTargets = { depthBuffer };

		core.recordDrawcallsToCmdStream(
			cmdStream,
			prepassPass,
			prepassPipeline,
			prepassPushConstantData,
			prepassDrawcalls,
			prepassRenderTargets);

		core.recordImageMemoryBarrier(cmdStream, depthBuffer);

		// main pass
		std::vector<std::array<glm::mat4, 2>> mainPassMatrices;
		for (const auto& m : modelMatrices) {
			mainPassMatrices.push_back({ viewProjectionCamera * m, m });
		}

		VolumetricSettings volumeSettings;
		volumeSettings.scatteringCoefficient    = scatteringColor * scatteringDensity;
		volumeSettings.absorptionCoefficient    = absorptionColor * absorptionDensity;
		volumeSettings.ambientLight             = volumetricAmbient;
		volumetricSettingsBuffer.fill({ volumeSettings });

		const vkcv::PushConstantData            pushConstantData((void*)mainPassMatrices.data(), 2 * sizeof(glm::mat4));
		const std::vector<vkcv::ImageHandle>    renderTargets = { colorBuffer, depthBuffer };

		core.recordDrawcallsToCmdStream(
			cmdStream,
			forwardPass,
			forwardPipeline,
			pushConstantData,
			drawcalls,
			renderTargets);

		if (renderVoxelVis) {
			voxelization.renderVoxelVisualisation(cmdStream, viewProjectionCamera, renderTargets, voxelVisualisationMip);
		}

		// sky
		core.recordDrawcallsToCmdStream(
			cmdStream,
			skyPass,
			skyPipe,
			vkcv::PushConstantData((void*)&skySettings, sizeof(skySettings)),
			{ vkcv::DrawcallInfo(vkcv::Mesh({}, nullptr, 3), {}) },
			renderTargets);

		const uint32_t fullscreenLocalGroupSize = 8;
		const uint32_t fulsscreenDispatchCount[3] = {
			static_cast<uint32_t>(glm::ceil(windowWidth  / static_cast<float>(fullscreenLocalGroupSize))),
			static_cast<uint32_t>(glm::ceil(windowHeight / static_cast<float>(fullscreenLocalGroupSize))),
			1
		};

		if (usingMsaa) {
			if (msaaCustomResolve) {

				core.prepareImageForSampling(cmdStream, colorBuffer);
				core.prepareImageForStorage(cmdStream, resolvedColorBuffer);

				assert(msaa == vkcv::Multisampling::MSAA4X);	// shaders is written for msaa 4x
				core.recordComputeDispatchToCmdStream(
					cmdStream,
					resolvePipeline,
					fulsscreenDispatchCount,
					{ vkcv::DescriptorSetUsage(0, core.getDescriptorSet(resolveDescriptorSet).vulkanHandle) },
					vkcv::PushConstantData(nullptr, 0));

				core.recordImageMemoryBarrier(cmdStream, resolvedColorBuffer);
			}
			else {
				core.resolveMSAAImage(cmdStream, colorBuffer, resolvedColorBuffer);
			}
		}

		bloomFlares.execWholePipeline(cmdStream, resolvedColorBuffer, windowWidth, windowHeight, 
			glm::normalize(cameraManager.getActiveCamera().getFront()));

		core.prepareImageForStorage(cmdStream, swapchainInput);
		core.prepareImageForSampling(cmdStream, resolvedColorBuffer);

		auto timeSinceStart = std::chrono::duration_cast<std::chrono::microseconds>(end - appStartTime);
		float timeF         = static_cast<float>(timeSinceStart.count()) * 0.01;

		core.recordComputeDispatchToCmdStream(
			cmdStream, 
			tonemappingPipeline, 
			fulsscreenDispatchCount,
			{ vkcv::DescriptorSetUsage(0, core.getDescriptorSet(tonemappingDescriptorSet).vulkanHandle) },
			vkcv::PushConstantData(&timeF, sizeof(timeF)));

		// present and end
		core.prepareSwapchainImageForPresent(cmdStream);
		core.submitCommandStream(cmdStream);

		// draw UI
		gui.beginGUI();

		if (renderUI) {
			ImGui::Begin("Settings");

			ImGui::Checkbox("MSAA custom resolve", &msaaCustomResolve);

			ImGui::DragFloat2("Light angles", &lightAnglesDegree.x);
			ImGui::ColorEdit3("Sun color", &lightColor.x);
			ImGui::DragFloat("Sun strength", &lightStrength);
			ImGui::DragFloat("Max shadow distance", &maxShadowDistance);
			maxShadowDistance = std::max(maxShadowDistance, 1.f);

			ImGui::ColorEdit3("Sky color", &skySettings.color.x);
			ImGui::DragFloat("Sky strength", &skySettings.strength, 0.1);

			ImGui::Checkbox("Draw voxel visualisation", &renderVoxelVis);
			ImGui::SliderInt("Visualisation mip", &voxelVisualisationMip, 0, 7);
			ImGui::DragFloat("Voxelization extent", &voxelizationExtent, 1.f, 0.f);
			voxelizationExtent = std::max(voxelizationExtent, 1.f);
			voxelVisualisationMip = std::max(voxelVisualisationMip, 0);

			ImGui::ColorEdit3("Scattering color", &scatteringColor.x);
			ImGui::DragFloat("Scattering density", &scatteringDensity, 0.0001);
			ImGui::ColorEdit3("Absorption color", &absorptionColor.x);
			ImGui::DragFloat("Absorption density", &absorptionDensity, 0.0001);
			ImGui::DragFloat("Volumetric ambient", &volumetricAmbient, 0.002);

			if (ImGui::Button("Reload forward pass")) {

				vkcv::ShaderProgram newForwardProgram;
				compiler.compile(vkcv::ShaderStage::VERTEX, std::filesystem::path("resources/shaders/shader.vert"),
					[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
					newForwardProgram.addShader(shaderStage, path);
				});
				compiler.compile(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("resources/shaders/shader.frag"),
					[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
					newForwardProgram.addShader(shaderStage, path);
				});
				forwardPipelineConfig.m_ShaderProgram = newForwardProgram;
				vkcv::PipelineHandle newPipeline = core.createGraphicsPipeline(forwardPipelineConfig);

				if (newPipeline) {
					forwardPipeline = newPipeline;
				}
			}
			if (ImGui::Button("Reload tonemapping")) {

				vkcv::ShaderProgram newProgram;
				compiler.compile(vkcv::ShaderStage::COMPUTE, std::filesystem::path("resources/shaders/tonemapping.comp"),
					[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
					newProgram.addShader(shaderStage, path);
				});
				vkcv::PipelineHandle newPipeline = core.createComputePipeline(
					newProgram,
					{ core.getDescriptorSet(tonemappingDescriptorSet).layout });

				if (newPipeline) {
					tonemappingPipeline = newPipeline;
				}
			}
			ImGui::End();
		}

		gui.endGUI();

		core.endFrame();
	}
	
	return 0;
}