/* Copyright (c) 2019-2025, Arm Limited and Contributors
 * Copyright (c) 2024-2025, NVIDIA CORPORATION. All rights reserved.
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * 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.
 */

#pragma once

#include "buffer_pool.h"
#include "rendering/hpp_pipeline_state.h"
#include "rendering/hpp_render_target.h"
#include "rendering/pipeline_state.h"
#include "rendering/render_frame.h"
#include "scene_graph/components/light.h"
#include "scene_graph/node.h"

namespace vkb
{
class RenderContext;
class RenderTarget;
class ShaderSource;

namespace core
{
template <vkb::BindingType bindingType>
class CommandBuffer;
}

namespace rendering
{
class HPPRenderContext;

struct alignas(16) Light
{
	glm::vec4 position;         // position.w represents type of light
	glm::vec4 color;            // color.w represents light intensity
	glm::vec4 direction;        // direction.w represents range
	glm::vec2 info;             // (only used for spot lights) info.x represents light inner cone angle, info.y represents light outer cone angle
};

template <vkb::BindingType bindingType>
struct LightingState
{
	std::vector<Light>            directional_lights;
	std::vector<Light>            point_lights;
	std::vector<Light>            spot_lights;
	BufferAllocation<bindingType> light_buffer;
};

using LightingStateC   = LightingState<vkb::BindingType::C>;
using LightingStateCpp = LightingState<vkb::BindingType::Cpp>;

/**
 * @brief Calculates the vulkan style projection matrix
 * @param proj The projection matrix
 * @return The vulkan style projection matrix
 */
glm::mat4 vulkan_style_projection(const glm::mat4 &proj);

inline const std::vector<std::string> light_type_definitions = {
    "DIRECTIONAL_LIGHT " + std::to_string(static_cast<float>(sg::LightType::Directional)),
    "POINT_LIGHT " + std::to_string(static_cast<float>(sg::LightType::Point)),
    "SPOT_LIGHT " + std::to_string(static_cast<float>(sg::LightType::Spot))};

/**
 * @brief This class defines an interface for subpasses
 *        where they need to implement the draw function.
 *        It is used to construct a RenderPipeline
 */
template <vkb::BindingType bindingType>
class Subpass
{
	using ResolveModeFlagBitsType = typename std::conditional<bindingType == vkb::BindingType::Cpp, vk::ResolveModeFlagBits, VkResolveModeFlagBits>::type;
	using SampleCountflagBitsType = typename std::conditional<bindingType == vkb::BindingType::Cpp, vk::SampleCountFlagBits, VkSampleCountFlagBits>::type;

	using DepthStencilStateType =
	    typename std::conditional<bindingType == vkb::BindingType::Cpp, vkb::rendering::HPPDepthStencilState, vkb::DepthStencilState>::type;
	using RenderContextType = typename std::conditional<bindingType == vkb::BindingType::Cpp, vkb::rendering::HPPRenderContext, vkb::RenderContext>::type;
	using RenderTargetType  = typename std::conditional<bindingType == vkb::BindingType::Cpp, vkb::rendering::HPPRenderTarget, vkb::RenderTarget>::type;

  public:
	Subpass(RenderContextType &render_context, ShaderSource &&vertex_shader, ShaderSource &&fragment_shader);

	Subpass(const Subpass &)            = delete;
	Subpass(Subpass &&)                 = default;
	virtual ~Subpass()                  = default;
	Subpass &operator=(const Subpass &) = delete;
	Subpass &operator=(Subpass &&)      = delete;

	/**
	 * @brief Draw virtual function
	 * @param command_buffer Command buffer to use to record draw commands
	 */
	virtual void draw(vkb::core::CommandBuffer<bindingType> &command_buffer) = 0;

	/**
	 * @brief Prepares the shaders and shader variants for a subpass
	 */
	virtual void prepare() = 0;

	/**
	 * @brief Prepares the lighting state to have its lights
	 *
	 * @tparam A light structure that has 'directional_lights', 'point_lights' and 'spot_light' array fields defined.
	 * @param scene_lights All of the light components from the scene graph
	 * @param max_lights_per_type The maximum amount of lights allowed for any given type of light.
	 */
	template <typename T>
	void allocate_lights(const std::vector<sg::Light *> &scene_lights,
	                     size_t                          max_lights_per_type);

	const std::vector<uint32_t>                               &get_color_resolve_attachments() const;
	const std::string                                         &get_debug_name() const;
	const uint32_t                                            &get_depth_stencil_resolve_attachment() const;
	ResolveModeFlagBitsType                                    get_depth_stencil_resolve_mode() const;
	DepthStencilStateType                                     &get_depth_stencil_state();
	const bool                                                &get_disable_depth_stencil_attachment() const;
	const ShaderSource                                        &get_fragment_shader() const;
	const std::vector<uint32_t>                               &get_input_attachments() const;
	LightingState<bindingType>                                &get_lighting_state();
	const std::vector<uint32_t>                               &get_output_attachments() const;
	RenderContextType                                         &get_render_context();
	std::unordered_map<std::string, ShaderResourceMode> const &get_resource_mode_map() const;
	SampleCountflagBitsType                                    get_sample_count() const;
	const ShaderSource                                        &get_vertex_shader() const;
	void                                                       set_color_resolve_attachments(std::vector<uint32_t> const &color_resolve);
	void                                                       set_debug_name(const std::string &name);
	void                                                       set_disable_depth_stencil_attachment(bool disable_depth_stencil);
	void                                                       set_depth_stencil_resolve_attachment(uint32_t depth_stencil_resolve);
	void                                                       set_depth_stencil_resolve_mode(ResolveModeFlagBitsType mode);
	void                                                       set_input_attachments(std::vector<uint32_t> const &input);
	void                                                       set_output_attachments(std::vector<uint32_t> const &output);
	void                                                       set_sample_count(SampleCountflagBitsType sample_count);

	/**
	 * @brief Updates the render target attachments with the ones stored in this subpass
	 *        This function is called by the RenderPipeline before beginning the render
	 *        pass and before proceeding with a new subpass.
	 */
	void update_render_target_attachments(RenderTargetType &render_target);

  private:
	/// Default to no color resolve attachments
	std::vector<uint32_t> color_resolve_attachments = {};

	std::string debug_name{};

	/**
	 * @brief When creating the renderpass, if not None, the resolve
	 *        of the multisampled depth attachment will be enabled,
	 *        with this mode, to depth_stencil_resolve_attachment
	 */
	vk::ResolveModeFlagBits depth_stencil_resolve_mode{vk::ResolveModeFlagBits::eNone};

	vkb::rendering::HPPDepthStencilState depth_stencil_state{};

	/**
	 * @brief When creating the renderpass, pDepthStencilAttachment will
	 *        be set to nullptr, which disables depth testing
	 */
	bool disable_depth_stencil_attachment{false};

	/// Default to no depth stencil resolve attachment
	uint32_t depth_stencil_resolve_attachment{VK_ATTACHMENT_UNUSED};

	/// The structure containing all the requested render-ready lights for the scene
	LightingStateCpp lighting_state{};

	ShaderSource fragment_shader;

	/// Default to no input attachments
	std::vector<uint32_t> input_attachments = {};

	/// Default to swapchain output attachment
	std::vector<uint32_t> output_attachments = {0};

	vkb::rendering::HPPRenderContext &render_context;

	// A map of shader resource names and the mode of constant data
	std::unordered_map<std::string, ShaderResourceMode> resource_mode_map;

	vk::SampleCountFlagBits sample_count{vk::SampleCountFlagBits::e1};
	ShaderSource            vertex_shader;
};

using SubpassC   = Subpass<vkb::BindingType::C>;
using SubpassCpp = Subpass<vkb::BindingType::Cpp>;
}        // namespace rendering
}        // namespace vkb

#include "rendering/hpp_render_context.h"

namespace vkb
{
namespace rendering
{
inline glm::mat4 vulkan_style_projection(const glm::mat4 &proj)
{
	// Flip Y in clipspace. X = -1, Y = -1 is topLeft in Vulkan.
	glm::mat4 mat = proj;
	mat[1][1] *= -1;

	return mat;
}

template <vkb::BindingType bindingType>
inline Subpass<bindingType>::Subpass(RenderContextType &render_context, ShaderSource &&vertex_source, ShaderSource &&fragment_source) :
    render_context{reinterpret_cast<vkb::rendering::HPPRenderContext &>(render_context)},
    vertex_shader{std::move(vertex_source)},
    fragment_shader{std::move(fragment_source)}
{
}

template <vkb::BindingType bindingType>
inline const std::vector<uint32_t> &Subpass<bindingType>::get_input_attachments() const
{
	return input_attachments;
}

template <vkb::BindingType bindingType>
inline LightingState<bindingType> &Subpass<bindingType>::get_lighting_state()
{
	if constexpr (bindingType == vkb::BindingType::Cpp)
	{
		return lighting_state;
	}
	else
	{
		return reinterpret_cast<LightingState<bindingType> &>(lighting_state);
	}
}

template <vkb::BindingType bindingType>
inline const std::vector<uint32_t> &Subpass<bindingType>::get_output_attachments() const
{
	return output_attachments;
}

template <vkb::BindingType bindingType>
inline typename vkb::rendering::Subpass<bindingType>::RenderContextType &Subpass<bindingType>::get_render_context()
{
	if constexpr (bindingType == vkb::BindingType::Cpp)
	{
		return render_context;
	}
	else
	{
		return reinterpret_cast<vkb::RenderContext &>(render_context);
	}
}

template <vkb::BindingType bindingType>
std::unordered_map<std::string, ShaderResourceMode> const &Subpass<bindingType>::get_resource_mode_map() const
{
	return resource_mode_map;
}

template <vkb::BindingType bindingType>
inline typename Subpass<bindingType>::SampleCountflagBitsType Subpass<bindingType>::get_sample_count() const
{
	if constexpr (bindingType == vkb::BindingType::Cpp)
	{
		return sample_count;
	}
	else
	{
		return static_cast<VkSampleCountFlagBits>(sample_count);
	}
}

template <vkb::BindingType bindingType>
inline const ShaderSource &Subpass<bindingType>::get_vertex_shader() const
{
	return vertex_shader;
}

template <vkb::BindingType bindingType>
template <typename T>
void Subpass<bindingType>::allocate_lights(const std::vector<sg::Light *> &scene_lights,
                                           size_t                          max_lights_per_type)
{
	lighting_state.directional_lights.clear();
	lighting_state.point_lights.clear();
	lighting_state.spot_lights.clear();

	for (auto &scene_light : scene_lights)
	{
		const auto &properties = scene_light->get_properties();
		auto       &transform  = scene_light->get_node()->get_transform();

		Light light{{transform.get_translation(), static_cast<float>(scene_light->get_light_type())},
		            {properties.color, properties.intensity},
		            {transform.get_rotation() * properties.direction, properties.range},
		            {properties.inner_cone_angle, properties.outer_cone_angle}};

		switch (scene_light->get_light_type())
		{
			case sg::LightType::Directional:
			{
				if (lighting_state.directional_lights.size() < max_lights_per_type)
				{
					lighting_state.directional_lights.push_back(light);
				}
				else
				{
					LOGE("Subpass::allocate_lights: exceeding max_lights_per_type of {} for directional lights", max_lights_per_type);
				}
				break;
			}
			case sg::LightType::Point:
			{
				if (lighting_state.point_lights.size() < max_lights_per_type)
				{
					lighting_state.point_lights.push_back(light);
				}
				else
				{
					LOGE("Subpass::allocate_lights: exceeding max_lights_per_type of {} for point lights", max_lights_per_type);
				}
				break;
			}
			case sg::LightType::Spot:
			{
				if (lighting_state.spot_lights.size() < max_lights_per_type)
				{
					lighting_state.spot_lights.push_back(light);
				}
				else
				{
					LOGE("Subpass::allocate_lights: exceeding max_lights_per_type of {} for spot lights", max_lights_per_type);
				}
				break;
			}
			default:
				LOGE("Subpass::allocate_lights: encountered unknown light type {}", to_string(scene_light->get_light_type()));
				break;
		}
	}

	T light_info;

	std::copy(lighting_state.directional_lights.begin(), lighting_state.directional_lights.end(), light_info.directional_lights);
	std::copy(lighting_state.point_lights.begin(), lighting_state.point_lights.end(), light_info.point_lights);
	std::copy(lighting_state.spot_lights.begin(), lighting_state.spot_lights.end(), light_info.spot_lights);

	auto &render_frame          = render_context.get_active_frame();
	lighting_state.light_buffer = render_frame.allocate_buffer(vk::BufferUsageFlagBits::eUniformBuffer, sizeof(T));
	lighting_state.light_buffer.update(light_info);
}

template <vkb::BindingType bindingType>
inline const std::vector<uint32_t> &Subpass<bindingType>::get_color_resolve_attachments() const
{
	return color_resolve_attachments;
}

template <vkb::BindingType bindingType>
inline const std::string &Subpass<bindingType>::get_debug_name() const
{
	return debug_name;
}

template <vkb::BindingType bindingType>
inline const uint32_t &Subpass<bindingType>::get_depth_stencil_resolve_attachment() const
{
	return depth_stencil_resolve_attachment;
}

template <vkb::BindingType bindingType>
inline typename Subpass<bindingType>::ResolveModeFlagBitsType Subpass<bindingType>::get_depth_stencil_resolve_mode() const
{
	if constexpr (bindingType == vkb::BindingType::Cpp)
	{
		return depth_stencil_resolve_mode;
	}
	else
	{
		return static_cast<VkResolveModeFlagBits>(depth_stencil_resolve_mode);
	}
}

template <vkb::BindingType bindingType>
inline typename Subpass<bindingType>::DepthStencilStateType &Subpass<bindingType>::get_depth_stencil_state()
{
	if constexpr (bindingType == vkb::BindingType::Cpp)
	{
		return depth_stencil_state;
	}
	else
	{
		return reinterpret_cast<vkb::DepthStencilState &>(depth_stencil_state);
	}
}

template <vkb::BindingType bindingType>
inline const bool &Subpass<bindingType>::get_disable_depth_stencil_attachment() const
{
	return disable_depth_stencil_attachment;
}

template <vkb::BindingType bindingType>
inline const ShaderSource &Subpass<bindingType>::get_fragment_shader() const
{
	return fragment_shader;
}

template <vkb::BindingType bindingType>
inline void Subpass<bindingType>::set_color_resolve_attachments(std::vector<uint32_t> const &color_resolve)
{
	color_resolve_attachments = color_resolve;
}

template <vkb::BindingType bindingType>
inline void Subpass<bindingType>::set_depth_stencil_resolve_attachment(uint32_t depth_stencil_resolve)
{
	depth_stencil_resolve_attachment = depth_stencil_resolve;
}

template <vkb::BindingType bindingType>
inline void Subpass<bindingType>::set_debug_name(const std::string &name)
{
	debug_name = name;
}

template <vkb::BindingType bindingType>
inline void Subpass<bindingType>::set_disable_depth_stencil_attachment(bool disable_depth_stencil)
{
	disable_depth_stencil_attachment = disable_depth_stencil;
}

template <vkb::BindingType bindingType>
inline void Subpass<bindingType>::set_depth_stencil_resolve_mode(ResolveModeFlagBitsType mode)
{
	if constexpr (bindingType == vkb::BindingType::Cpp)
	{
		depth_stencil_resolve_mode = mode;
	}
	else
	{
		depth_stencil_resolve_mode = static_cast<vk::ResolveModeFlagBits>(mode);
	}
}

template <vkb::BindingType bindingType>
inline void Subpass<bindingType>::set_input_attachments(std::vector<uint32_t> const &input)
{
	input_attachments = input;
}

template <vkb::BindingType bindingType>
inline void Subpass<bindingType>::set_output_attachments(std::vector<uint32_t> const &output)
{
	output_attachments = output;
}

template <vkb::BindingType bindingType>
inline void Subpass<bindingType>::set_sample_count(SampleCountflagBitsType sample_count_)
{
	if constexpr (bindingType == vkb::BindingType::Cpp)
	{
		sample_count = sample_count_;
	}
	else
	{
		sample_count = static_cast<vk::SampleCountFlagBits>(sample_count_);
	}
}

template <vkb::BindingType bindingType>
inline void Subpass<bindingType>::update_render_target_attachments(RenderTargetType &render_target)
{
	render_target.set_input_attachments(input_attachments);
	render_target.set_output_attachments(output_attachments);
}
}        // namespace rendering
}        // namespace vkb
