/*
 * 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 <concepts>
#include <utility>
#include <type_traits>

namespace OpenVulkano
{
	namespace Vulkan
	{
		class MetalBackedTexture;
	}

	class RenderResourcePtr;

	class IRenderResourceHelper
	{
		friend class RenderResourcePtr;

	protected:
		void UpdateRenderResource(RenderResourcePtr* resource);

		void ResetRenderResource(RenderResourcePtr* resource);

		virtual void DoRelease() = 0;

		virtual void Release() = 0;
	};

	template <class T>
	concept Renderable = std::is_convertible_v<T, RenderResourcePtr&>;

	class RenderResourcePtr final
	{
		friend class IRenderResourceHelper;
		friend class Vulkan::MetalBackedTexture;

		IRenderResourceHelper* renderObject = nullptr;

	public:
		RenderResourcePtr() = default;
		RenderResourcePtr(const RenderResourcePtr& ignored) noexcept { /* Do not copy, copy will be created by renderer */ }
		RenderResourcePtr(RenderResourcePtr&& move) noexcept : renderObject(move.renderObject) { move.renderObject = nullptr; }

		~RenderResourcePtr()
		{
			Release();
		}

		void Release()
		{
			if (!renderObject) return;
			renderObject->DoRelease();
			renderObject = nullptr;
		}

		template<class T>
		T* As() { return static_cast<T*>(renderObject); }

		template<class T>
		operator T() { return static_cast<T>(renderObject); }

		operator bool() const { return renderObject; }

		RenderResourcePtr& operator =(RenderResourcePtr&& move) noexcept
		{
			if (renderObject) renderObject->Release();
			renderObject = move.renderObject;
			move.renderObject = nullptr;
			return *this;
		}
	};

	inline void IRenderResourceHelper::UpdateRenderResource(RenderResourcePtr* resource)
	{
		if (resource)
			resource->renderObject = this;
	}

	inline void IRenderResourceHelper::ResetRenderResource(RenderResourcePtr* resource)
	{
		if (resource)
			resource->renderObject = nullptr;
	}

	template<Renderable API_INDEPENDENT_CLASS>
	class IRenderResource : public IRenderResourceHelper
	{
		API_INDEPENDENT_CLASS* m_owner;

	protected:
		IRenderResource(API_INDEPENDENT_CLASS* owner) : m_owner(owner)
		{
			if (m_owner) UpdateRenderResource(GetOwnerResource());
		}

		IRenderResource(const IRenderResource<API_INDEPENDENT_CLASS>& copy) = delete;

		IRenderResource(IRenderResource<API_INDEPENDENT_CLASS>&& move) noexcept
			: m_owner(move.m_owner)
		{
			if (m_owner)
			{
				UpdateRenderResource(GetOwnerResource());
			}
			move.m_owner = nullptr;
		}

		RenderResourcePtr* GetOwnerResource() { if (!m_owner) return nullptr; else return &static_cast<RenderResourcePtr&>(*m_owner); }

		void DoRelease() final override
		{
			m_owner = nullptr;
			Release();
		}

	public:
		virtual ~IRenderResource()
		{
			if (m_owner) ResetRenderResource(GetOwnerResource());
		}

		void UpdateAddress(API_INDEPENDENT_CLASS* owner)
		{
			m_owner = owner;
		}

		operator API_INDEPENDENT_CLASS*() const { return m_owner; }

		API_INDEPENDENT_CLASS* GetOwner() const { return m_owner; }
	};

	/**
	 * This is a convenience class to create a Renderable class.
	 * You can also just create a RenderResource instance a conversion operator inside your own class to make it Renderable; and you probably want to delete your move constructor in that case.
	 */
	template <class T>
	class RenderResourceHolder
	{
		RenderResourcePtr renderResource;

	protected:
		RenderResourceHolder() = default;
		RenderResourceHolder(const RenderResourceHolder& ignored) noexcept {}
		RenderResourceHolder(RenderResourceHolder&& move) noexcept
				: renderResource(std::move(move.renderResource))
		{
			if (IRenderResource<T>* renderRes = renderResource)
				renderRes->UpdateAddress(static_cast<T*>(this));
		}

		~RenderResourceHolder() = default;

	public:
		operator RenderResourcePtr&() { return renderResource; }

		RenderResourcePtr& GetRenderResource() const { return const_cast<RenderResourceHolder*>(this)->renderResource; }

		template<Renderable RT>
		operator RT() const { return renderResource; }

		bool HasRenderResource() const { return renderResource; }

		RenderResourceHolder& operator =(const RenderResourceHolder& copy) noexcept
		{
			renderResource.Release();
			return *this;
		}

		RenderResourceHolder& operator =(RenderResourceHolder&& move) noexcept
		{
			renderResource = std::move(move.renderResource);
			if (IRenderResource<T>* renderRes = renderResource)
				renderRes->UpdateAddress(static_cast<T*>(this));
			return *this;
		}

		void Swap(RenderResourceHolder& other) noexcept
		{
			RenderResourceHolder tmp(std::move(*this));
			*this = std::move(other);
			other = std::move(tmp);
		}
	};
}
