/*
 * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
 */

#pragma once

#ifndef OX_BARE_METAL
#include <functional>
#endif

#include <ox/event/signal.hpp>
#include <ox/model/typenamecatcher.hpp>
#include <ox/std/hashmap.hpp>
#include <ox/std/utility.hpp>

namespace keel {

class AssetManager;

template<typename T>
class AssetRef;

#ifndef OX_BARE_METAL
template<typename T>
class AssetContainer {

	friend AssetManager;
	friend AssetRef<T>;

	protected:
		ox::Signal<ox::Error()> updated;

	private:
		T m_obj;
		mutable int m_references = 0;

	public:
		template<class... Args>
		explicit constexpr AssetContainer(Args&&... args): m_obj(ox::forward<Args>(args)...) {
		}

		AssetContainer(AssetContainer const&) = delete;
		AssetContainer(AssetContainer&&) = delete;
		AssetContainer &operator=(AssetContainer const&) = delete;
		AssetContainer &operator=(AssetContainer&&) = delete;

		[[nodiscard]]
		constexpr T *get() noexcept {
			return &m_obj;
		}

		[[nodiscard]]
		constexpr T const*get() const noexcept {
			 return &m_obj;
		}

		constexpr void set(T &&val) {
			m_obj = std::move(val);
		}

		constexpr void set(T const&val) {
			m_obj = val;
		}

	protected:
		constexpr void incRefs() const noexcept {
			oxAssert(m_references < ox::MaxValue<decltype(m_references)>, "reference count exceeds maximum");
			++m_references;
		}

		constexpr void decRefs() const noexcept {
			--m_references;
			oxAssert(m_references >= 0, "negative references");
		}

		[[nodiscard]]
		constexpr int references() const noexcept {
			return m_references;
		}

};

template<typename T>
class AssetRef: public ox::SignalHandler {
	private:
		AssetContainer<T> const*m_ctr = nullptr;

	public:
		ox::Signal<ox::Error()> updated;

		explicit constexpr AssetRef(AssetContainer<T> const*c = nullptr) noexcept;

		constexpr AssetRef(AssetRef const&h) noexcept;

		constexpr AssetRef(AssetRef &&h) noexcept;

		~AssetRef() noexcept override {
			if (m_ctr) {
				m_ctr->decRefs();
			}
		}

		[[nodiscard]]
		constexpr T const*get() const noexcept {
			if (m_ctr) {
				return m_ctr->get();
			}
			return nullptr;
		}

		constexpr T const&operator*() const noexcept {
			return *m_ctr->get();
		}

		constexpr T const*operator->() const noexcept {
			return m_ctr->get();
		}

		AssetRef &operator=(AssetRef const&h) noexcept {
			if (this == &h) {
				return *this;
			}
			if (m_ctr) {
				m_ctr->decRefs();
				std::ignore = m_ctr->updated.disconnectObject(this);
			}
			m_ctr = h.m_ctr;
			if (m_ctr) {
				m_ctr->updated.connect(&updated, &ox::Signal<ox::Error()>::emitCheckError);
				m_ctr->incRefs();
			}
			return *this;
		}

		AssetRef &operator=(AssetRef &&h) noexcept {
			if (this == &h) {
				return *this;
			}
			if (m_ctr) {
				m_ctr->decRefs();
				std::ignore = m_ctr->updated.disconnectObject(this);
			}
			m_ctr = h.m_ctr;
			if (m_ctr) {
				std::ignore = m_ctr->updated.disconnectObject(&h);
				m_ctr->updated.connect(this, &AssetRef::emitUpdated);
			}
			h.m_ctr = nullptr;
			return *this;
		}

		explicit constexpr operator bool() const noexcept {
			return m_ctr;
		}

	private:
		constexpr ox::Error emitUpdated() const noexcept {
			updated.emit();
			return {};
		}
};

template<typename T>
constexpr AssetRef<T>::AssetRef(AssetContainer<T> const*c) noexcept: m_ctr(c) {
	if (m_ctr) {
		m_ctr->updated.connect(this, &AssetRef::emitUpdated);
		m_ctr->incRefs();
	}
}

template<typename T>
constexpr AssetRef<T>::AssetRef(AssetRef const&h) noexcept: m_ctr(h.m_ctr) {
	if (m_ctr) {
		m_ctr->updated.connect(this, &AssetRef::emitUpdated);
		m_ctr->incRefs();
	}
}

template<typename T>
constexpr AssetRef<T>::AssetRef(AssetRef &&h) noexcept: m_ctr(h.m_ctr) {
	if (m_ctr) {
		m_ctr->updated.connect(this, &AssetRef::emitUpdated);
	}
	h.m_ctr = nullptr;
}

class Context;

class AssetManager {
	private:
		class AssetTypeManagerBase: public ox::SignalHandler {
			public:
				~AssetTypeManagerBase() override = default;

				virtual ox::Error updateAssetId(ox::StringViewCR oldId, ox::StringViewCR newId) noexcept = 0;

				virtual void gc() noexcept = 0;
		};

		template<typename T>
		class AssetTypeManager: public AssetTypeManagerBase {
			public:
				using Loader = std::function<ox::Result<T>(ox::StringViewCR assetId)>;
			private:
				Loader m_loader{};
				ox::HashMap<ox::String, ox::UPtr<AssetContainer<T>>> m_cache;

			public:
				explicit AssetTypeManager(Loader &&loader) noexcept: m_loader(std::move(loader)) {}

				ox::Result<AssetRef<T>> getAsset(ox::StringViewCR assetId) const noexcept {
					OX_REQUIRE(out, m_cache.at(assetId));
					if (!out || !*out) {
						return ox::Error(1, "asset is null");
					}
					return AssetRef<T>(out->get());
				}

				ox::Result<AssetRef<T>> loadAsset(ox::StringViewCR assetId) noexcept {
					auto &p = m_cache[assetId];
					OX_REQUIRE_M(obj, m_loader(assetId));
					if (!p) {
						p = ox::make_unique<AssetContainer<T>>(std::move(obj));
					} else {
						p->set(std::move(obj));
						p->updated.emit();
					}
					return AssetRef<T>(p.get());
				}

				ox::Error reloadAsset(ox::StringViewCR assetId) noexcept {
					auto &p = m_cache[assetId];
					OX_REQUIRE_M(obj, m_loader(assetId));
					if (!p) {
						p = ox::make_unique<AssetContainer<T>>(std::move(obj));
					} else {
						p->set(std::move(obj));
						p->updated.emit();
					}
					return {};
				}

				ox::Error updateAssetId(ox::StringViewCR oldId, ox::StringViewCR newId) noexcept final {
					if (!m_cache.contains(oldId)) {
						return {};
					}
					auto &o = m_cache[oldId];
					auto &n = m_cache[newId];
					n = std::move(o);
					m_cache.erase(oldId);
					return {};
				}

				void gc() noexcept final {
					for (size_t i = 0; i < m_cache.keys().size();) {
						auto const &ack = m_cache.keys()[i];
						auto &ac = m_cache[ack];
						if (!ac->references()) {
							m_cache.erase(ack);
							continue;
						}
						++i;
					}
				}

		};

		ox::HashMap<ox::String, ox::UPtr<AssetTypeManagerBase>> m_assetTypeManagers;
		ox::HashMap<ox::String, ox::UPtr<ox::Signal<ox::Error(ox::StringViewCR assetId)>>> m_fileUpdated;

		template<typename T>
		ox::Result<AssetTypeManager<T>*> getTypeManager() noexcept {
			constexpr auto &typeId = ox::ModelTypeId_v<T>;
			static_assert(typeId != "", "Types must have TypeName to use AssetManager");
			auto &am = m_assetTypeManagers[typeId];
			auto const out = dynamic_cast<AssetTypeManager<T>*>(am.get());
			if (!out) {
				return ox::Error(1, "no AssetTypeManager for type");
			}
			return out;
		}

	public:
		template<typename T>
		void initTypeManager(auto const&makeLoader, Context &ctx) noexcept {
			constexpr auto &typeId = ox::ModelTypeId_v<T>;
			static_assert(typeId != "", "Types must have TypeName to use AssetManager");
			auto &am = m_assetTypeManagers[typeId];
			if (!am) {
				am = ox::make_unique<AssetTypeManager<T>>(makeLoader(ctx));
			}
		}

		template<typename T>
		ox::Result<AssetRef<T>> getAsset(ox::StringViewCR assetId) noexcept {
			OX_REQUIRE(m, getTypeManager<T>());
			return m->getAsset(assetId);
		}

		ox::Error reloadAsset(ox::StringViewCR assetId) noexcept {
			if (m_fileUpdated.contains(assetId)) {
				m_fileUpdated[assetId]->emit(assetId);
			}
			return {};
		}

		template<typename T>
		ox::Result<AssetRef<T>> loadAsset(ox::StringViewCR assetId) noexcept {
			OX_REQUIRE(m, getTypeManager<T>());
			OX_REQUIRE(out, m->loadAsset(assetId));
			if (!m_fileUpdated.contains(assetId)) [[unlikely]] {
				m_fileUpdated[assetId] = ox::make_unique<ox::Signal<ox::Error(ox::StringViewCR assetId)>>();
			}
			m_fileUpdated[assetId]->connect(m, &AssetTypeManager<T>::reloadAsset);
			return out;
		}

		ox::Error updateAssetId(ox::StringViewCR oldId, ox::StringViewCR newId) noexcept {
			gc();
			if (m_fileUpdated.contains(newId)) {
				return ox::Error{1, "new asset ID already has an entry"};
			}
			if (!m_fileUpdated.contains(oldId)) {
				return {};
			}
			auto &o = m_fileUpdated[oldId];
			auto &n = m_fileUpdated[newId];
			n = std::move(o);
			m_fileUpdated.erase(oldId);
			for (auto &k : m_assetTypeManagers.keys()) {
				auto const &tm = m_assetTypeManagers[k];
				std::ignore = tm->updateAssetId(oldId, newId);
			}
			return {};
		}

		void gc() noexcept {
			for (auto const&amk : m_assetTypeManagers.keys()) {
				auto &am = m_assetTypeManagers[amk];
				am->gc();
			}
			for (auto const&k : m_fileUpdated.keys()) {
				auto &s = m_fileUpdated[k];
				if (s->connectionCnt() == 0) {
					m_fileUpdated.erase(k);
				}
			}
		}
};
#else
template<typename T>
class AssetRef {
	private:
		T const *m_obj = nullptr;

	public:
		constexpr AssetRef() noexcept = default;

		explicit constexpr AssetRef(T const *obj) noexcept: m_obj(obj) {
		}

		constexpr T const *get() const noexcept {
			return m_obj;
		}

		constexpr T const &operator*() const & noexcept {
			return *m_obj;
		}

		constexpr T const *operator->() const noexcept {
			return m_obj;
		}

		explicit constexpr operator bool() const noexcept {
			return m_obj;
		}
};
#endif

}
