/*********************************************************\
 * Copyright (c) 2012-2020 The Unrimp Team
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
 * and associated documentation files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or
 * substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
 * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\*********************************************************/


//[-------------------------------------------------------]
//[ Includes                                              ]
//[-------------------------------------------------------]
#include "Examples/Private/Renderer/Mesh/Mesh.h"
#include "Examples/Private/Framework/Color4.h"

#include <Renderer/Public/Context.h>
#include <Renderer/Public/IRenderer.h>
#include <Renderer/Public/Core/IProfiler.h>
#include <Renderer/Public/Resource/Mesh/MeshResource.h>
#include <Renderer/Public/Resource/Mesh/MeshResourceManager.h>
#include <Renderer/Public/Resource/Texture/TextureResource.h>
#include <Renderer/Public/Resource/Texture/TextureResourceManager.h>
#include <Renderer/Public/Resource/MaterialBlueprint/MaterialBlueprintResourceManager.h>

// Disable warnings in external headers, we can't fix them
PRAGMA_WARNING_PUSH
	PRAGMA_WARNING_DISABLE_MSVC(4201)	// warning C4201: nonstandard extension used: nameless struct/union
	PRAGMA_WARNING_DISABLE_MSVC(4464)	// warning C4464: relative include path contains '..'
	PRAGMA_WARNING_DISABLE_MSVC(4324)	// warning C4324: '<x>': structure was padded due to alignment specifier
	PRAGMA_WARNING_DISABLE_MSVC(4668)	// warning C4668: '_M_HYBRID_X86_ARM64' is not defined as a preprocessor macro, replacing with '0' for '#if/#elif'
	#include <glm/gtc/type_ptr.hpp>
	#include <glm/gtc/matrix_transform.hpp>
PRAGMA_WARNING_POP


//[-------------------------------------------------------]
//[ Public methods                                        ]
//[-------------------------------------------------------]
Mesh::Mesh() :
	mMeshResourceId(Renderer::getInvalid<Renderer::MeshResourceId>()),
	m_argb_nxaTextureResourceId(Renderer::getInvalid<Renderer::TextureResourceId>()),
	m_hr_rg_mb_nyaTextureResourceId(Renderer::getInvalid<Renderer::TextureResourceId>()),
	mEmissiveTextureResourceId(Renderer::getInvalid<Renderer::TextureResourceId>()),
	mObjectSpaceToClipSpaceMatrixUniformHandle(NULL_HANDLE),
	mObjectSpaceToViewSpaceMatrixUniformHandle(NULL_HANDLE),
	mGlobalTimer(0.0f)
{
	// Nothing here
}


//[-------------------------------------------------------]
//[ Public virtual IApplication methods                   ]
//[-------------------------------------------------------]
void Mesh::onInitialization()
{
	// Get and check the renderer instance
	Renderer::IRenderer& renderer = getRendererSafe();
	Rhi::IRhiPtr rhi(getRhi());

	// Don't create initial pipeline state caches after a material blueprint has been loaded since this example isn't using the material blueprint system
	renderer.getMaterialBlueprintResourceManager().setCreateInitialPipelineStateCaches(false);

	{
		{ // Create the root signature
			Rhi::DescriptorRangeBuilder ranges[5];
			ranges[0].initialize(Rhi::ResourceType::UNIFORM_BUFFER, 0, "UniformBlockDynamicVs", Rhi::ShaderVisibility::VERTEX);
			ranges[1].initialize(Rhi::ResourceType::TEXTURE_2D,		0, "_argb_nxa",				Rhi::ShaderVisibility::FRAGMENT);
			ranges[2].initialize(Rhi::ResourceType::TEXTURE_2D,		1, "_hr_rg_mb_nya",			Rhi::ShaderVisibility::FRAGMENT);
			ranges[3].initialize(Rhi::ResourceType::TEXTURE_2D,		2, "EmissiveMap",			Rhi::ShaderVisibility::FRAGMENT);
			ranges[4].initializeSampler(0, Rhi::ShaderVisibility::FRAGMENT);

			Rhi::RootParameterBuilder rootParameters[2];
			rootParameters[0].initializeAsDescriptorTable(4, &ranges[0]);
			rootParameters[1].initializeAsDescriptorTable(1, &ranges[4]);

			// Setup
			Rhi::RootSignatureBuilder rootSignatureBuilder;
			rootSignatureBuilder.initialize(static_cast<uint32_t>(GLM_COUNTOF(rootParameters)), rootParameters, 0, nullptr, Rhi::RootSignatureFlags::ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);

			// Create the instance
			mRootSignature = rhi->createRootSignature(rootSignatureBuilder);
		}

		// Create uniform buffer
		// -> Direct3D 9 does not support uniform buffers
		// -> Direct3D 10, 11 and 12 do not support individual uniforms
		// -> The RHI is just a light weight abstraction layer, so we need to handle the differences
		// -> Allocate enough memory for two 4x4 floating point matrices
		if (0 != rhi->getCapabilities().maximumUniformBufferSize)
		{
			mUniformBuffer = renderer.getBufferManager().createUniformBuffer(2 * 4 * 4 * sizeof(float), nullptr, Rhi::BufferUsage::DYNAMIC_DRAW);
		}

		// Vertex input layout
		static constexpr Rhi::VertexAttribute vertexAttributesLayout[] =
		{
			{ // Attribute 0
				// Data destination
				Rhi::VertexAttributeFormat::FLOAT_3,		// vertexAttributeFormat (Rhi::VertexAttributeFormat)
				"Position",									// name[32] (char)
				"POSITION",									// semanticName[32] (char)
				0,											// semanticIndex (uint32_t)
				// Data source
				0,											// inputSlot (uint32_t)
				0,											// alignedByteOffset (uint32_t)
				sizeof(float) * 5 + sizeof(short) * 4,		// strideInBytes (uint32_t)
				0											// instancesPerElement (uint32_t)
			},
			{ // Attribute 1
				// Data destination
				Rhi::VertexAttributeFormat::FLOAT_2,		// vertexAttributeFormat (Rhi::VertexAttributeFormat)
				"TexCoord",									// name[32] (char)
				"TEXCOORD",									// semanticName[32] (char)
				0,											// semanticIndex (uint32_t)
				// Data source
				0,											// inputSlot (uint32_t)
				sizeof(float) * 3,							// alignedByteOffset (uint32_t)
				sizeof(float) * 5 + sizeof(short) * 4,		// strideInBytes (uint32_t)
				0											// instancesPerElement (uint32_t)
			},
			{ // Attribute 2
				// Data destination
				Rhi::VertexAttributeFormat::SHORT_4,		// vertexAttributeFormat (Rhi::VertexAttributeFormat)
				"QTangent",									// name[32] (char)
				"TEXCOORD",									// semanticName[32] (char)
				1,											// semanticIndex (uint32_t)
				// Data source
				0,											// inputSlot (uint32_t)
				sizeof(float) * 5,							// alignedByteOffset (uint32_t)
				sizeof(float) * 5 + sizeof(short) * 4,		// strideInBytes (uint32_t)
				0											// instancesPerElement (uint32_t)
			}
		};
		const Rhi::VertexAttributes vertexAttributes(static_cast<uint32_t>(GLM_COUNTOF(vertexAttributesLayout)), vertexAttributesLayout);

		{ // Create sampler state and wrap it into a resource group instance
			Rhi::SamplerState samplerStateSettings = Rhi::ISamplerState::getDefaultSamplerState();
			samplerStateSettings.addressU = Rhi::TextureAddressMode::WRAP;
			samplerStateSettings.addressV = Rhi::TextureAddressMode::WRAP;
			Rhi::IResource* samplerStateResource = mSamplerStatePtr = rhi->createSamplerState(samplerStateSettings);
			mSamplerStateGroup = mRootSignature->createResourceGroup(1, 1, &samplerStateResource);
		}

		{ // Create the graphics program
			// Get the shader source code (outsourced to keep an overview)
			const char* vertexShaderSourceCode = nullptr;
			const char* fragmentShaderSourceCode = nullptr;
			#include "Mesh_GLSL_450.h"	// For Vulkan
			#include "Mesh_GLSL_410.h"	// macOS 10.11 only supports OpenGL 4.1 hence it's our OpenGL minimum
			#include "Mesh_GLSL_ES3.h"
			#include "Mesh_HLSL_D3D9.h"
			#include "Mesh_HLSL_D3D10_D3D11_D3D12.h"
			#include "Mesh_Null.h"

			// Create the graphics program
			Rhi::IShaderLanguage& shaderLanguage = rhi->getDefaultShaderLanguage();
			mGraphicsProgram = shaderLanguage.createGraphicsProgram(
				*mRootSignature,
				vertexAttributes,
				shaderLanguage.createVertexShaderFromSourceCode(vertexAttributes, vertexShaderSourceCode),
				shaderLanguage.createFragmentShaderFromSourceCode(fragmentShaderSourceCode));
		}

		// Is there a valid graphics program?
		if (nullptr != mGraphicsProgram)
		{
			// Create the graphics pipeline state object (PSO)
			mGraphicsPipelineState = rhi->createGraphicsPipelineState(Rhi::GraphicsPipelineStateBuilder(mRootSignature, mGraphicsProgram, vertexAttributes, getMainRenderTarget()->getRenderPass()));

			// Optimization: Cached data to not bother the RHI too much
			if (nullptr == mUniformBuffer)
			{
				mObjectSpaceToClipSpaceMatrixUniformHandle = mGraphicsProgram->getUniformHandle("ObjectSpaceToClipSpaceMatrix");
				mObjectSpaceToViewSpaceMatrixUniformHandle = mGraphicsProgram->getUniformHandle("ObjectSpaceToViewSpaceMatrix");
			}
		}

		// Create mesh instance
		renderer.getMeshResourceManager().loadMeshResourceByAssetId(ASSET_ID("Example/Mesh/Imrod/SM_Imrod"), mMeshResourceId, this);

		{ // Load in the albedo, emissive, normal and roughness texture
			Renderer::TextureResourceManager& textureResourceManager = renderer.getTextureResourceManager();
			textureResourceManager.loadTextureResourceByAssetId(ASSET_ID("Example/Mesh/Imrod/T_Imrod_argb_nxa"),     ASSET_ID("Unrimp/Texture/DynamicByCode/Identity_argb_nxa2D"),     m_argb_nxaTextureResourceId, this);
			textureResourceManager.loadTextureResourceByAssetId(ASSET_ID("Example/Mesh/Imrod/T_Imrod_hr_rg_mb_nya"), ASSET_ID("Unrimp/Texture/DynamicByCode/Identity_hr_rg_mb_nya2D"), m_hr_rg_mb_nyaTextureResourceId, this);
			textureResourceManager.loadTextureResourceByAssetId(ASSET_ID("Example/Mesh/Imrod/T_Imrod_e"),            ASSET_ID("Unrimp/Texture/DynamicByCode/IdentityEmissiveMap2D"),   mEmissiveTextureResourceId, this);
		}
	}

	// Since we're always submitting the same commands to the RHI, we can fill the command buffer once during initialization and then reuse it multiple times during runtime
	fillCommandBuffer();
}

void Mesh::onDeinitialization()
{
	{ // Disconnect resource listeners
		const Renderer::IRenderer& renderer = getRendererSafe();
		const Renderer::TextureResourceManager& textureResourceManager = renderer.getTextureResourceManager();
		textureResourceManager.setInvalidResourceId(m_argb_nxaTextureResourceId, *this);
		textureResourceManager.setInvalidResourceId(m_hr_rg_mb_nyaTextureResourceId, *this);
		textureResourceManager.setInvalidResourceId(mEmissiveTextureResourceId, *this);
		renderer.getMeshResourceManager().setInvalidResourceId(mMeshResourceId, *this);
	}

	// Release the used RHI resources
	mObjectSpaceToViewSpaceMatrixUniformHandle = NULL_HANDLE;
	mObjectSpaceToClipSpaceMatrixUniformHandle = NULL_HANDLE;
	mSamplerStateGroup = nullptr;
	mSamplerStatePtr = nullptr;
	mResourceGroup = nullptr;
	mGraphicsProgram = nullptr;
	mGraphicsPipelineState = nullptr;
	mUniformBuffer = nullptr;
	mRootSignature = nullptr;
	mCommandBuffer.clear();
}

void Mesh::onUpdate()
{
	// Stop the stopwatch
	mStopwatch.stop();

	// Update the global timer (FPS independent movement)
	mGlobalTimer += mStopwatch.getMilliseconds() * 0.0005f;

	// Start the stopwatch
	mStopwatch.start();
}

void Mesh::onDraw()
{
	// Get and check the RHI instance
	Rhi::IRhiPtr rhi(getRhi());
	if (nullptr != rhi)
	{
		{ // Set uniform
			// Get the aspect ratio
			float aspectRatio = 4.0f / 3.0f;
			{
				// Get the render target with and height
				const Rhi::IRenderTarget* renderTarget = getMainRenderTarget();
				if (nullptr != renderTarget)
				{
					uint32_t width  = 1;
					uint32_t height = 1;
					renderTarget->getWidthAndHeight(width, height);

					// Get the aspect ratio
					aspectRatio = static_cast<float>(width) / static_cast<float>(height);
				}
			}

			// Calculate the object space to clip space matrix
			const glm::mat4 viewSpaceToClipSpace	= glm::perspective(45.0f, aspectRatio, 100.0f, 0.1f);	// Near and far flipped due to usage of Reversed-Z (see e.g. https://developer.nvidia.com/content/depth-precision-visualized and https://nlguillemot.wordpress.com/2016/12/07/reversed-z-in-opengl/)
			const glm::mat4 viewTranslate			= glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, -7.0f, 25.0f));
			const glm::mat4 worldSpaceToViewSpace	= glm::rotate(viewTranslate, mGlobalTimer, glm::vec3(0.0f, 1.0f, 0.0f));
			const glm::mat4 objectSpaceToWorldSpace	= glm::scale(glm::mat4(1.0f), glm::vec3(0.5f));
				  glm::mat4 objectSpaceToViewSpace	= worldSpaceToViewSpace * objectSpaceToWorldSpace;
			const glm::mat4 objectSpaceToClipSpace	= viewSpaceToClipSpace * objectSpaceToViewSpace;

			// Upload the uniform data
			// -> Two versions: One using an uniform buffer and one setting an individual uniform
			if (nullptr != mUniformBuffer)
			{
				struct UniformBlockDynamicVs final
				{
					float objectSpaceToClipSpaceMatrix[4 * 4];	// Object space to clip space matrix
					float objectSpaceToViewSpaceMatrix[4 * 4];	// Object space to view space matrix
				};
				UniformBlockDynamicVs uniformBlockDynamicVS;
				memcpy(uniformBlockDynamicVS.objectSpaceToClipSpaceMatrix, glm::value_ptr(objectSpaceToClipSpace), sizeof(float) * 4 * 4);

				// TODO(co) float3x3 (currently there are alignment issues when using Direct3D, have a look into possible solutions)
				glm::mat3 objectSpaceToViewSpace3x3 = glm::mat3(objectSpaceToViewSpace);
				objectSpaceToViewSpace = glm::mat4(objectSpaceToViewSpace3x3);
				memcpy(uniformBlockDynamicVS.objectSpaceToViewSpaceMatrix, glm::value_ptr(objectSpaceToViewSpace), sizeof(float) * 4 * 4);

				// Copy data
				Rhi::MappedSubresource mappedSubresource;
				if (rhi->map(*mUniformBuffer, 0, Rhi::MapType::WRITE_DISCARD, 0, mappedSubresource))
				{
					memcpy(mappedSubresource.data, &uniformBlockDynamicVS, sizeof(UniformBlockDynamicVs));
					rhi->unmap(*mUniformBuffer, 0);
				}
			}
			else
			{
				// TODO(co) Not compatible with command buffer: This certainly is going to be removed, we need to implement internal uniform buffer emulation
				// Set uniforms
				mGraphicsProgram->setUniformMatrix4fv(mObjectSpaceToClipSpaceMatrixUniformHandle, glm::value_ptr(objectSpaceToClipSpace));
				mGraphicsProgram->setUniformMatrix3fv(mObjectSpaceToViewSpaceMatrixUniformHandle, glm::value_ptr(glm::mat3(objectSpaceToViewSpace)));
			}
		}

		// Submit command buffer to the RHI implementation, in case the referenced assets have already been loaded and the command buffer has been filled as a consequence
		if (!mCommandBuffer.isEmpty())
		{
			mCommandBuffer.submitToRhi(*rhi);
		}
	}
}

void Mesh::fillCommandBuffer()
{
	const Renderer::IRenderer& renderer = getRendererSafe();
	const Renderer::MeshResource* meshResource = renderer.getMeshResourceManager().tryGetById(mMeshResourceId);
	if (nullptr != meshResource && nullptr != meshResource->getVertexArrayPtr())
	{
		// Due to background texture loading, some textures might not be ready yet resulting in fallback texture usage
		// -> "Mesh::onLoadingStateChange()" will invalidate the resource group as soon as a texture resource finishes loading 
		if (nullptr == mResourceGroup)
		{
			const Renderer::TextureResourceManager& textureResourceManager = renderer.getTextureResourceManager();
			const Renderer::TextureResource* _argb_nxaTextureResource = textureResourceManager.tryGetById(m_argb_nxaTextureResourceId);
			const Renderer::TextureResource* _hr_rg_mb_nyaTextureResource = textureResourceManager.tryGetById(m_hr_rg_mb_nyaTextureResourceId);
			const Renderer::TextureResource* emissiveTextureResource = textureResourceManager.tryGetById(mEmissiveTextureResourceId);
			if (nullptr == _argb_nxaTextureResource || nullptr == _argb_nxaTextureResource->getTexturePtr() ||
				nullptr == _hr_rg_mb_nyaTextureResource || nullptr == _hr_rg_mb_nyaTextureResource->getTexturePtr() ||
				nullptr == emissiveTextureResource || nullptr == emissiveTextureResource->getTexturePtr())
			{
				return;
			}

			// Create resource group
			Rhi::IResource* resources[4] = { mUniformBuffer, _argb_nxaTextureResource->getTexturePtr(), _hr_rg_mb_nyaTextureResource->getTexturePtr(), emissiveTextureResource->getTexturePtr() };
			Rhi::ISamplerState* samplerStates[4] = { nullptr, mSamplerStatePtr, mSamplerStatePtr, mSamplerStatePtr };
			mResourceGroup = mRootSignature->createResourceGroup(0, static_cast<uint32_t>(GLM_COUNTOF(resources)), resources, samplerStates);
		}

		// Get and check the RHI instance
		Rhi::IRhiPtr rhi(getRhi());
		if (nullptr != rhi && nullptr != mGraphicsPipelineState)
		{
			// Combined scoped profiler CPU and GPU sample as well as renderer debug event command
			RENDERER_SCOPED_PROFILER_EVENT(renderer.getContext(), mCommandBuffer, "Mesh")

			// Clear the graphics color buffer of the current render target with gray, do also clear the depth buffer
			Rhi::Command::ClearGraphics::create(mCommandBuffer, Rhi::ClearFlag::COLOR_DEPTH, Color4::GRAY);

			// Set the used graphics root signature
			Rhi::Command::SetGraphicsRootSignature::create(mCommandBuffer, mRootSignature);

			// Set the used graphics pipeline state object (PSO)
			Rhi::Command::SetGraphicsPipelineState::create(mCommandBuffer, mGraphicsPipelineState);

			// Set graphics resource groups
			Rhi::Command::SetGraphicsResourceGroup::create(mCommandBuffer, 0, mResourceGroup);
			Rhi::Command::SetGraphicsResourceGroup::create(mCommandBuffer, 1, mSamplerStateGroup);

			{ // Draw mesh instance
				// Input assembly (IA): Set the used vertex array
				Rhi::Command::SetGraphicsVertexArray::create(mCommandBuffer, meshResource->getVertexArrayPtr());

				// Render the specified geometric primitive, based on indexing into an array of vertices
				Rhi::Command::DrawIndexedGraphics::create(mCommandBuffer, meshResource->getNumberOfIndices());
			}
		}
	}
}
