// Aesir Interactive GmbH, (c) 2019

#pragma once

#include "CoreMinimal.h"
#include "Templates/Casts.h"


/*
namespace TIsUBaseInterfacePrivate
{
	template<class InterfaceClass>
	struct TIsUBaseInterfaceImpl;
	template<>
	struct TIsUBaseInterfaceImpl<bool> { enum { Value = false };};
	template<>
	struct TIsUBaseInterfaceImpl<UClass*> { enum { Value = true };};
}
template<class InterfaceClass>
struct TIsUBaseInterface { enum { Value = TIsUBaseInterfacePrivate::TIsUBaseInterfaceImpl<decltype(&InterfaceClass::StaticClass, true)>::Value}; };
*/

struct CHasStaticUClass
{
	template<class ObjectClass>
	auto Requires() -> decltype(
		ObjectClass::StaticClass()
	);
};

struct CHasInterfaceUClass
{
	template<class InterfaceClass>
	auto Requires() -> decltype(
		InterfaceClass::UClassType::StaticClass()
	);
};


template<class InterfaceClass>
struct TIsUInterface
{
	enum
	{
		Value = !TModels<CHasStaticUClass, InterfaceClass>::Value && TModels<CHasInterfaceUClass, InterfaceClass>::Value
	};
};

/**
 * General purpose service locator that is available from the world settings
 */
class LYRAGAME_API FServiceLocator
{
public:
	DECLARE_EVENT_OneParam(FServiceLocator, FServiceRegisteredEvent, FServiceLocator&);
	
	/**
	 *	@return the registered service cast to ServiceClass or nullptr if the service is not registered or the cast failed
	 */
	template <class ServiceClass>
	bool IsServiceProvided() const
	{
		const UClass* ServiceUClass = GetServiceUClass<ServiceClass>();
		return Services.Contains(ServiceUClass);
	}

	/**
	 *	@return the registered service cast to ServiceClass or nullptr if the service is not registered or the cast failed
	 */
	template <class ServiceType>
	typename TEnableIf<!TIsIInterface<ServiceType>::Value, ServiceType*>::Type
	GetService() const
	{
		const UClass* ServiceUClass = GetServiceUClass<ServiceType>();
		UObject* const* ServiceInstance = Services.Find(ServiceUClass);
		if (!ServiceInstance)
			return nullptr;

		return Cast<ServiceType>(*ServiceInstance);
	}

	/**
	 *	@return the registered service as TScriptInterface or nullptr if the service is not registered or the cast failed
	 */
	template<class ServiceInterface>
	typename TEnableIf<TIsIInterface<ServiceInterface>::Value, TScriptInterface<ServiceInterface>>::Type
	GetService() const
	{
		const UClass* ServiceUClass = GetServiceUClass<ServiceInterface>();
		UObject* const* ServiceInstance = Services.Find(ServiceUClass);
		if (!ServiceInstance)
			return nullptr;

		return TScriptInterface<ServiceInterface>(&**ServiceInstance);
	}
	
	/**
	 *	@return the registered service cast to ServiceClass. Crashes if the service is not available.
	 */
	template <class ServiceClass>
	typename TEnableIf<!TIsIInterface<ServiceClass>::Value, ServiceClass*>::Type
	GetServiceChecked() const
	{
		const UClass* ServiceUClass = GetServiceUClass<ServiceClass>();

		UObject *const* ServiceObject = Services.Find(ServiceUClass);
		checkf(ServiceObject, TEXT("No object was found for service %s"), *ServiceUClass->GetName());
		UObject *const ServiceCast = Cast<ServiceClass>(*ServiceObject);
		checkf(IsValid(ServiceCast), TEXT("Object %s is not of type %s"), *((*ServiceObject)->GetName()), *ServiceUClass->GetName());
		return Cast<ServiceClass>(*ServiceObject);
	}

	/**
	 *	@return the registered service cast to ServiceClass. Crashes if the service is not available.
	 */
	template<class ServiceInterface>
	typename TEnableIf<TIsIInterface<ServiceInterface>::Value, TScriptInterface<ServiceInterface>>::Type
	GetServiceChecked() const
	{
		const UClass* ServiceUClass = GetServiceUClass<ServiceInterface>();

		UObject *const* ServiceObject = Services.Find(ServiceUClass);
		checkf(ServiceObject, TEXT("No object was found for service %s"), *ServiceUClass->GetName());
		checkf(Cast<ServiceInterface>(ServiceObject), TEXT("Object %s does not implement %s"), *(*ServiceObject)->GetName(), *ServiceUClass->GetName());
		return TScriptInterface<ServiceInterface>(*ServiceObject);
	}

	
	
	/**
	 *	Registers the given object interface with the service locator. 
	 *	Will refuse to work if the type is not actually derived from the interface.
	 *	Note: you should always explicitly provide the template list to make sure you register the service as the intended type
	 *	@return void
	 */
	template <class ServiceInterface, class ChildType>
	typename TEnableIf<TIsDerivedFrom<ChildType, ServiceInterface>::Value, void>::Type 
	ProvideService(ChildType* ServiceInstance)
	{
		const UClass* ServiceUClass = GetServiceUClass<ServiceInterface>();
		Services.Emplace(ServiceUClass, ServiceInstance);
		NotifyServiceProvided(ServiceUClass);
	}

	/**
	 *	Unregisters the given object with the service locator
	 *	Note: you should always explicitly provide the template list to make sure you unregister the service as the intended type
	 */
	template<class ServiceClass>
	void WithdrawService(ServiceClass* ServiceInstance)
	{
		const UClass* ServiceClassObject = GetServiceUClass<ServiceClass>();
		ensureMsgf(GetService<ServiceClass>() == ServiceInstance, TEXT("The service you are trying to unregister is not the one that has been provided!"));
		Services.Remove(ServiceClassObject);
	}

	/**	Waits for the service class to be registered or calls the callback immediately if the service is already registerd */
	template<class ServiceClass>
	void WaitForService(FServiceRegisteredEvent::FDelegate Callback)
	{
		const UClass* ServiceUClass = GetServiceUClass<ServiceClass>();
		ServiceRegisteredCallbacks.FindOrAdd(ServiceUClass).Add(Callback);
	}

	template<class ServiceType>
	struct ServiceTypeMapper
	{
		using MappedType = decltype(DeclVal<FServiceLocator>().GetService<ServiceType>());
	};

	template<typename...ServiceTypes>
	auto GetServices() -> decltype(auto)
	{
		return MakeTuple<typename ServiceTypeMapper<ServiceTypes>::MappedType...>(GetService<ServiceTypes>()...);
	}

private:
	void NotifyServiceProvided(const UClass* ServiceUClass)
	{
		FServiceRegisteredEvent& ServiceRegisteredEvent = ServiceRegisteredCallbacks.FindOrAdd(ServiceUClass);
		ServiceRegisteredEvent.Broadcast(*this);
		ServiceRegisteredEvent.Clear();
	}

	/** This statically gets the UClass from any given IInterface-Type */
	template <typename ServiceInterface>
	static typename TEnableIf<TIsUInterface<ServiceInterface>::Value,const UClass*>::Type
	GetServiceUClass()
	{
		return ServiceInterface::UClassType::StaticClass();
	}

	/** This statically gets the UClass from any given UObject-Type */
	template <typename ServiceClass>
	static typename TEnableIf<TModels<CHasStaticUClass, ServiceClass>::Value,const UClass*>::Type
	GetServiceUClass()
	{
		return ServiceClass::StaticClass();
	}

	TMap<const UClass*, FServiceRegisteredEvent> ServiceRegisteredCallbacks;

	TMap<const UClass*, UObject*> Services;
};