/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

#pragma once

#include "Base/ICloseable.hpp"
#include "Base/Utils.hpp"
#include "Base/Render/RenderResource.hpp"
#include "VertexInputDescription.hpp"
#include "ShaderProgramType.hpp"
#include "DescriptorInputDescription.hpp"
#include "PushConstant.hpp"
#include <string>
#include <stdexcept>

namespace OpenVulkano::Scene
{
	enum class CullMode : uint32_t
	{
		NONE = 0,
		FRONT,
		BACK,
		FRONT_AND_BACK
	};

	enum class Topology : uint32_t
	{
		POINT_LIST = 0,
		LINE_LIST,
		LINE_STRIPE,
		TRIANGLE_LIST,
		TRIANGLE_STRIP,
		TRIANGLE_FAN,
		LINE_LIST_WITH_ADJACENCY,
		LINE_STRIP_WITH_ADJACENCY,
		TRIANGLE_LIST_WITH_ADJACENCY,
		TRIANGLE_STRIP_WITH_ADJACENCY,
		PATCH_LIST
	};

	enum class CompareOp : uint32_t
	{
		NEVER = 0,
		LESS = 1,
		EQUAL = 2,
		LESS_OR_EQUAL = 3,
		GREATER = 4,
		NOT_EQUAL = 5,
		GREATER_OR_EQUAL = 6,
		ALWAYS = 7
	};

	struct ShaderProgram
	{
		ShaderProgramType type;
		std::string name;

		ShaderProgram(ShaderProgramType type, const std::string& name) : type(type), name(name) {}

		ShaderProgram(const ShaderProgram& program) = default;
		ShaderProgram& operator=(const ShaderProgram& program) = default;

		ShaderProgram(ShaderProgram&& program) noexcept = default;
		ShaderProgram& operator=(ShaderProgram&& program) noexcept = default;

		[[nodiscard]] std::string GetShaderNameOpenGL() const
		{
			std::string oglName;
			std::string_view ext = type.GetExtensionOpenGL();
			oglName.reserve(name.size() + ext.size());
			return oglName.append(name).append(ext);
		}

		[[nodiscard]] std::string GetShaderNameVulkan() const
		{
			return GetShaderNameOpenGL() + ".spv";
		}
	};



	class Shader final : public RenderResourceHolder<Shader>, public ICloseable
	{
	public:
		std::vector<ShaderProgram> shaderPrograms{};
		std::vector<VertexInputDescription> vertexInputDescriptions{};
		std::vector<std::vector<DescriptorSetLayoutBinding>> descriptorSets;
		std::vector<PushConstantRange> pushConstantRanges;
		Topology topology = Topology::TRIANGLE_LIST;
		CullMode cullMode = CullMode::BACK;
		CompareOp depthCompareOp = CompareOp::LESS;
		bool alphaBlend = false; // TODO allow fine control over blending
		bool depthTest = true;
		bool depthWrite = true;
		bool dynamicViewport = true; // If disabled the swapchains fullscreen viewport will always be used, regardless of framebuffer or viewport

		Shader() = default;
		~Shader() override { Shader::Close(); }

		Shader& AddShaderProgram(const ShaderProgram& shaderProgram)
		{
			CheckShaderInitState();
			shaderPrograms.push_back(shaderProgram);
			return *this;
		}

		Shader& AddShaderProgram(ShaderProgram&& shaderProgram)
		{
			CheckShaderInitState();
			shaderPrograms.push_back(std::move(shaderProgram));
			return *this;
		}

		template<typename ...ARGS>
		Shader& AddShaderProgram(ARGS&&... args)
		{
			CheckShaderInitState();
			shaderPrograms.emplace_back(std::forward<ARGS>(args)...);
			return *this;
		}

#pragma clang diagnostic push
#pragma ide diagnostic ignored "LoopDoesntUseConditionVariableInspection"
		Shader& AddVertexInputDescription(const VertexInputDescription& inputDescription, int bindingId = -1)
		{
			CheckShaderInitState();
			if (bindingId < 0) bindingId = static_cast<int>(vertexInputDescriptions.size());
			while (bindingId > static_cast<int>(vertexInputDescriptions.size()))
			{
				vertexInputDescriptions.emplace_back(0, 0);
			}
			if (bindingId == static_cast<int>(vertexInputDescriptions.size()))
			{
				vertexInputDescriptions.emplace_back(bindingId, inputDescription);
			}
			else
			{
				vertexInputDescriptions[bindingId] = inputDescription;
			}
			return *this;
		}

		int AddDescriptorSetLayoutBinding(const DescriptorSetLayoutBinding& binding, int setId = -1)
		{
			CheckShaderInitState();
			if (setId < 0) setId = static_cast<int>(descriptorSets.size() + 2);
			if (setId < 2) throw std::runtime_error("Cant bind set id 0 or 1! They are used for node and camera!");
			setId -= 2;
			while (setId >= static_cast<int>(descriptorSets.size()))
			{
				descriptorSets.emplace_back();
			}
			descriptorSets[setId].push_back(binding);
			return setId + 2;
		}
#pragma clang diagnostic pop

		void AddPushConstantRange(const PushConstantRange& pushConstantRange)
		{
			pushConstantRanges.push_back(pushConstantRange);
		}

		void Close() override
		{
			if (HasRenderResource())
				GetRenderResource().Release();
		}

	private:
		void CheckShaderInitState() const
		{
			if (HasRenderResource()) throw std::runtime_error("Shader already initialized!");
		}
	};
}
