/*
 * 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/Render/RenderResource.hpp"
#include "Math/Math.hpp"
#include "Math/Pose.hpp"
#include "Drawable.hpp"
#include "UpdateFrequency.hpp"
#include "Shader/DescriptorInputDescription.hpp"
#include <vector>
#include <memory>

namespace OpenVulkano::Scene
{
	class Scene;

	class Node : public RenderResourceHolder<Node>, public ICloseable
	{
		friend Scene;

	public:
		static constexpr size_t SIZE = sizeof(Math::Matrix4f);
		static constexpr DescriptorSetLayoutBinding DESCRIPTOR_SET_LAYOUT_BINDING = { 0, DescriptorSetLayoutBinding::Type::TYPE_UNIFORM_BUFFER_DYNAMIC, 1, ShaderProgramType::ALL_GRAPHICS };

		Math::Matrix4f localMat, worldMat;
		Node* parent = nullptr;
		Scene* scene = nullptr;
		std::vector<Node*> children;
		std::vector<Drawable*> drawables;
		UpdateFrequency matrixUpdateFrequency = UpdateFrequency::Never;
		bool enabled = true;

	public:
		Node();
		
		Node(const Math::Matrix4f& pose);

		~Node() noexcept override;

		void Init();

		void Close() override;

		void AddChild(Node* node);

		void AddChildIfParentless(Node* node);

		inline void AddChild(Drawable* drawable) { AddDrawable(drawable); }

		void RemoveChild(Node* node);

		void RemoveChildNodes();

		inline void RemoveChild(Drawable* drawable) { RemoveDrawable(drawable); }

		void AddDrawable(Drawable* drawable);

		void RemoveDrawable(Drawable* drawable);

		void AddDrawableIfParentless(Drawable* drawable);

		void SetMatrix(const Math::Matrix4f& mat);

		[[nodiscard]] Math::Matrix3f GetRotationMatrix() const { return static_cast<const Math::Matrix3f>(localMat); }

		[[nodiscard]] const Math::Matrix4f& GetMatrix() const { return localMat; }

		[[nodiscard]] const Math::Matrix4f& GetWorldMatrix() const { return worldMat; }

		[[nodiscard]] Math::PoseF GetPose() { return Math::PoseF(GetMatrix()); }

		[[nodiscard]] Math::PoseF GetWorldPose() { return Math::PoseF(GetWorldMatrix()); }

		[[nodiscard]] bool IsEnabled() const { return enabled; }

		void Enable() { enabled = true; }

		void Disable() { enabled = false; }
		
		void SetEnabled(bool enable) { enabled = enable; }

		[[nodiscard]] Node* GetParent() const { return parent; }

		[[nodiscard]] Scene* GetScene() const { return scene; }

		[[nodiscard]] bool IsRoot() const { return scene && parent == this; }

		[[nodiscard]] UpdateFrequency GetUpdateFrequency() const { return matrixUpdateFrequency; }

		void SetUpdateFrequency(UpdateFrequency frequency)
		{
			if (!children.empty()) throw std::runtime_error("The update must not be changed for nodes with children.");
			this->matrixUpdateFrequency = frequency;
		}

		float Distance(const Math::Vector4f& pos) const
		{
			return Math::Utils::distance(worldMat[3], pos);
		}

		float Distance2(const Math::Vector4f& pos) const
		{
			return Math::Utils::distance2(worldMat[3], pos);
		}

	protected:
		virtual void UpdateWorldMatrix(const Math::Matrix4f& parentWorldMat);

	private:
		void SetParent(Node* parent);

		void SetScene(Scene* scene);
	};
}
