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

#pragma once

#include <ox/event/signal.hpp>
#include <ox/fs/fs.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 const T *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 {
			++m_references;
		}

		constexpr void decRefs() const noexcept {
			--m_references;
		}

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

};

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

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

		explicit constexpr AssetRef(const AssetContainer<T> *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 const T *get() const noexcept {
			if (m_ctr) {
				return m_ctr->get();
			}
			return nullptr;
		}

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

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

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

		AssetRef &operator=(AssetRef &&h) noexcept {
			if (this == &h) {
				return *this;
			}
			if (m_ctr) {
				m_ctr->decRefs();
				oxIgnoreError(m_ctr->updated.disconnectObject(this));
			}
			m_ctr = h.m_ctr;
			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 OxError(0);
		}
};

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

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;
	m_ctr->updated.connect(this, &AssetRef::emitUpdated);
	h.m_ctr = nullptr;
}

class AssetManager {
	private:
		class AssetTypeManagerBase {
			public:
				virtual ~AssetTypeManagerBase() = default;

				virtual void gc() noexcept = 0;
		};

		template<typename T>
		class AssetTypeManager: public AssetTypeManagerBase {
			private:
				ox::HashMap<ox::String, ox::UPtr<AssetContainer<T>>> m_cache;

			public:
				ox::Result<AssetRef<T>> getAsset(ox::StringView const&assetId) const noexcept {
					auto out = m_cache.at(assetId);
					oxReturnError(out);
					return AssetRef<T>(out.value->get());
				}

				ox::Result<AssetRef<T>> setAsset(ox::StringView const&assetId, T const&obj) noexcept {
					auto &p = m_cache[assetId];
					if (!p) {
						p = ox::make_unique<AssetContainer<T>>(obj);
					} else {
						p->set(obj);
						p->updated.emit();
					}
					return AssetRef<T>(p.get());
				}

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

				void gc() noexcept final {
					for (const auto &ack : m_cache.keys()) {
						auto &ac = m_cache[ack];
						if (!ac->references()) {
							m_cache.erase(ack);
						}
					}
				}
		};

		ox::HashMap<ox::String, ox::UPtr<AssetTypeManagerBase>> m_assetTypeManagers;

		template<typename T>
		AssetTypeManager<T> *getTypeManager() noexcept {
			constexpr auto typeName = ox::requireModelTypeName<T>();
			static_assert(ox::StringView(typeName) != "", "Types must have TypeName to use AssetManager");
			auto &am = m_assetTypeManagers[typeName];
			if (!am) {
				am = ox::make_unique<AssetTypeManager<T>>();
			}
			return dynamic_cast<AssetTypeManager<T>*>(am.get());
		}

	public:
		template<typename T>
		ox::Result<AssetRef<T>> getAsset(ox::CRStringView assetId) noexcept {
			auto m = getTypeManager<T>();
			return m->getAsset(assetId);
		}

		template<typename T>
		ox::Result<AssetRef<T>> setAsset(ox::CRStringView assetId, const T &obj) noexcept {
			auto m = getTypeManager<T>();
			return m->setAsset(assetId, obj);
		}

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

	public:
		constexpr AssetRef() noexcept = default;

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

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

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

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

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

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

}
