/*
 * Copyright 2016 - 2022 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 nostalgia::core {

class AssetManager;

template<typename T>
class AssetRef;

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&) = delete;
		AssetContainer(AssetContainer&&) = delete;
		AssetContainer& operator=(AssetContainer&) = delete;
		AssetContainer& operator=(AssetContainer&&) = delete;

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

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

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

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

	protected:
		constexpr void incRefs() const noexcept {
			++m_references;
		}

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

		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(const AssetRef &h) noexcept;

		constexpr AssetRef(AssetRef &&h) noexcept;

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

		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();
		}

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

		AssetRef &operator=(const 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(&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(const AssetRef &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::UniquePtr<AssetContainer<T>>> m_cache;

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

				ox::Result<AssetRef<T>> setAsset(const ox::String &path, const T &obj) noexcept {
					auto &p = m_cache[path];
					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(const ox::String &path, T &&obj) noexcept {
					auto &p = m_cache[path];
					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::UniquePtr<AssetTypeManagerBase>> m_assetTypeManagers;

		template<typename T>
		AssetTypeManager<T> *getTypeManager() noexcept {
			constexpr auto typeName = ox::requireModelTypeName<T>();
			static_assert(ox_strcmp(typeName, "") != 0, "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(const ox::String &path) noexcept {
			auto m = getTypeManager<T>();
			return m->getAsset(path);
		}

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

		void gc() noexcept {
			for (const auto &amk : m_assetTypeManagers.keys()) {
				auto &am = m_assetTypeManagers[amk];
				am->gc();
			}
		}
};

}