/*
 * 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/.
 */

#include "LabelDrawable.hpp"
#include "Scene/TextDrawable.hpp"
#include "Scene/DrawEncoder.hpp"
#include "Scene/Vertex.hpp"
#include "Scene/Shader/Shader.hpp"
#include "Scene/IFontAtlasGenerator.hpp"
#include "Scene/Text/FontAtlasType.hpp"
#include <optional>


namespace OpenVulkano::Scene
{
	namespace
	{
		Shader MakeLabelBgShader(const bool billboard)
		{
			Shader backgroundShader;
			backgroundShader.AddShaderProgram(ShaderProgramType::VERTEX, billboard ? "Shader/labelBillboard" : "Shader/label");
			backgroundShader.AddShaderProgram(ShaderProgramType::FRAGMENT, "Shader/label");
			backgroundShader.AddDescriptorSetLayoutBinding(Texture::DESCRIPTOR_SET_LAYOUT_BINDING, 2);
			backgroundShader.AddDescriptorSetLayoutBinding(UniformBuffer::DESCRIPTOR_SET_LAYOUT_BINDING, 4);
			backgroundShader.topology = Topology::TRIANGLE_STRIP;
			backgroundShader.cullMode = CullMode::NONE;
			return backgroundShader;
		}

		Shader MakeLabelTextShader(const FontAtlasType type, const bool billboard)
		{
			Shader shader = TextDrawable::MakeDefaultShader(type);
			shader.depthCompareOp = CompareOp::LESS_OR_EQUAL;
			shader.EnableDepthBias();
			if (billboard)
			{
				for (auto& program : shader.shaderPrograms)
				{
					if (program.type == ShaderProgramType::VERTEX)
					{
						program.name = "Shader/textBillboard";
						break;
					}
				}
				DescriptorSetLayoutBinding billboardUniformBinding = UniformBuffer::DESCRIPTOR_SET_LAYOUT_BINDING;
				shader.AddDescriptorSetLayoutBinding(billboardUniformBinding, 4);
				shader.depthBiasConstant = 0.01f;
			}
			return shader;
		}

		Shader BACKGROUND_SHADER = MakeLabelBgShader(false);
		Shader BACKGROUND_BILLBOARD_SHADER = MakeLabelBgShader(true);
		std::array TEXT_SHADERS = {
			MakeLabelTextShader(FontAtlasType::SDF, false),
			MakeLabelTextShader(FontAtlasType::SDF, true),
			MakeLabelTextShader(FontAtlasType::MSDF, false),
			MakeLabelTextShader(FontAtlasType::MSDF, true),
			MakeLabelTextShader(FontAtlasType::BITMAP, false),
			MakeLabelTextShader(FontAtlasType::BITMAP, true),
		};

		Shader* GetTextShader(const FontAtlasType type, const bool billboard)
		{
			return &TEXT_SHADERS[static_cast<int>(type) << 1 | billboard];
		}
	}

	LabelDrawable::LabelDrawable(const std::shared_ptr<FontAtlas>& atlasData, const LabelDrawableSettings& settings)
	    : Drawable(DrawEncoder::GetDrawEncoder<LabelDrawable>(), DrawPhase::MAIN), m_atlasData(atlasData)
	    , m_labelBuffer(sizeof(LabelUniformData), &m_labelData, 4)
	{
		if (!atlasData || !*atlasData) throw std::runtime_error("Can't create label drawable. Either glyphs or texture is empty");
		SetLabelSettings(settings);
		SetShader(IsBillboard() ? &BACKGROUND_BILLBOARD_SHADER : &BACKGROUND_SHADER);
	}

	void LabelDrawable::SetLabelSettings(const LabelDrawableSettings& settings)
	{
		m_settings = settings;
		m_labelData.color = settings.backgroundColor;
		m_labelData.hasRoundedCorners = settings.hasRoundedCorners;
		m_labelData.hasArrow = settings.hasArrow;
		m_labelData.cornerRadius = settings.cornerRadius * settings.cornerRadius;
		m_labelData.arrowLength = settings.arrowLength;
		m_labelData.arrowWidth = settings.arrowWidth;
	}

	void LabelDrawable::AddText(const std::string& text, const TextConfig& config)
	{
		if (text.empty()) return;

		TextDrawable& textDrawable = m_texts.emplace_back(m_atlasData, config);
		textDrawable.GetConfig().backgroundColor.a = 0; // do not render glyph's background
		double lineHeight = m_atlasData->GetLineHeight();
		textDrawable.GenerateText(text, m_position);
		textDrawable.SetShader(GetTextShader(m_atlasData->GetAtlasType(), IsBillboard()));
		m_bbox.Grow(textDrawable.GetBoundingBox());
		// update position for next text entry
		m_position.y = m_bbox.GetMin().y - lineHeight;

		Math::Vector2f padding = m_settings.padding * 2;
		if (m_settings.hasArrow) padding.y += m_settings.arrowLength;

		m_labelData.textSize = m_bbox.GetSize() + padding;
		m_labelData.bboxCenter = m_bbox.GetCenter();
		if (m_settings.hasArrow) m_labelData.bboxCenter.y -= m_settings.arrowLength * 0.5f;
	}

	std::optional<RayHit> LabelDrawable::Intersect(const Ray& ray) const 
	{ 
		return ray.IntersectAABB(m_bbox);
	}
}
