/*
 * BinaryDeserializer.h, part of VCMI engine
 *
 * Authors: listed in file AUTHORS in main folder
 *
 * License: GNU General Public License v2.0 or later
 * Full text of license available in license.txt file, in main folder
 *
 */
#pragma once

#include "CSerializer.h"
#include "CTypeList.h"
#include "ESerializationVersion.h"
#include "../mapObjects/CGHeroInstance.h"

VCMI_LIB_NAMESPACE_BEGIN

class DLL_LINKAGE CLoaderBase
{
protected:
	IBinaryReader * reader;
public:
	CLoaderBase(IBinaryReader * r): reader(r){};

	inline void read(void * data, unsigned size, bool reverseEndianness)
	{
		auto bytePtr = reinterpret_cast<std::byte*>(data);

		reader->read(bytePtr, size);
		if(reverseEndianness)
			std::reverse(bytePtr, bytePtr + size);
	};
};

/// Main class for deserialization of classes from binary form
/// Effectively revesed version of BinarySerializer
class DLL_LINKAGE BinaryDeserializer : public CLoaderBase
{
	template<typename Ser,typename T>
	struct LoadIfStackInstance
	{
		static bool invoke(Ser &s, T &data)
		{
			return false;
		}
	};

	template<typename Ser>
	struct LoadIfStackInstance<Ser, CStackInstance *>
	{
		static bool invoke(Ser &s, CStackInstance* &data)
		{
			CArmedInstance *armedObj;
			SlotID slot;
			s.load(armedObj);
			s.load(slot);
			if(slot != SlotID::COMMANDER_SLOT_PLACEHOLDER)
			{
				assert(armedObj->hasStackAtSlot(slot));
				data = armedObj->stacks[slot];
			}
			else
			{
				auto * hero = dynamic_cast<CGHeroInstance *>(armedObj);
				assert(hero);
				assert(hero->commander);
				data = hero->commander;
			}
			return true;
		}
	};

	template <typename T, typename Enable = void>
	struct ClassObjectCreator
	{
		static T *invoke(IGameCallback *cb)
		{
			static_assert(!std::is_base_of_v<GameCallbackHolder, T>, "Cannot call new upon map objects!");
			static_assert(!std::is_abstract_v<T>, "Cannot call new upon abstract classes!");
			return new T();
		}
	};

	template<typename T>
	struct ClassObjectCreator<T, typename std::enable_if_t<std::is_abstract_v<T>>>
	{
		static T *invoke(IGameCallback *cb)
		{
			throw std::runtime_error("Something went really wrong during deserialization. Attempted creating an object of an abstract class " + std::string(typeid(T).name()));
		}
	};

	template<typename T>
	struct ClassObjectCreator<T, typename std::enable_if_t<std::is_base_of_v<GameCallbackHolder, T> && !std::is_abstract_v<T>>>
	{
		static T *invoke(IGameCallback *cb)
		{
			static_assert(!std::is_abstract_v<T>, "Cannot call new upon abstract classes!");
			return new T(cb);
		}
	};

	STRONG_INLINE ui32 readAndCheckLength()
	{
		ui32 length;
		load(length);
		//NOTE: also used for h3m's embedded in campaigns, so it may be quite large in some cases (e.g. XXL maps with multiple objects)
		if(length > 1000000)
		{
			logGlobal->warn("Warning: very big length: %d", length);
			reader->reportState(logGlobal);
		};
		return length;
	}

	template <typename Type> class CPointerLoader;

	class IPointerLoader
	{
	public:
		virtual void * loadPtr(CLoaderBase &ar, IGameCallback * cb, ui32 pid) const =0; //data is pointer to the ACTUAL POINTER
		virtual ~IPointerLoader() = default;

		template<typename Type> static IPointerLoader *getApplier(const Type * t = nullptr)
		{
			return new CPointerLoader<Type>();
		}
	};

	template <typename Type>
	class CPointerLoader : public IPointerLoader
	{
	public:
		void * loadPtr(CLoaderBase &ar, IGameCallback * cb, ui32 pid) const override //data is pointer to the ACTUAL POINTER
		{
			auto & s = static_cast<BinaryDeserializer &>(ar);

			//create new object under pointer
			Type * ptr = ClassObjectCreator<Type>::invoke(cb); //does new npT or throws for abstract classes
			s.ptrAllocated(ptr, pid);

			ptr->serialize(s);

			return static_cast<void*>(ptr);
		}
	};

	CApplier<IPointerLoader> applier;

	int write(const void * data, unsigned size);

public:
	using Version = ESerializationVersion;

	bool reverseEndianness; //if source has different endianness than us, we reverse bytes
	Version version;

	std::map<ui32, void*> loadedPointers;
	std::map<const void*, std::shared_ptr<void>> loadedSharedPointers;
	IGameCallback * cb = nullptr;
	bool smartPointerSerialization;
	bool saving;

	BinaryDeserializer(IBinaryReader * r);

	template<class T>
	BinaryDeserializer & operator&(T & t)
	{
		this->load(t);
		return * this;
	}

	template < class T, typename std::enable_if_t < std::is_fundamental_v<T> && !std::is_same_v<T, bool>, int  > = 0 >
	void load(T &data)
	{
		this->read(static_cast<void *>(&data), sizeof(data), reverseEndianness);
	}

	template < typename T, typename std::enable_if_t < is_serializeable<BinaryDeserializer, T>::value, int  > = 0 >
	void load(T &data)
	{
		////that const cast is evil because it allows to implicitly overwrite const objects when deserializing
		typedef typename std::remove_const_t<T> nonConstT;
		auto & hlp = const_cast<nonConstT &>(data);
		hlp.serialize(*this);
	}
	template < typename T, typename std::enable_if_t < std::is_array_v<T>, int  > = 0 >
	void load(T &data)
	{
		ui32 size = std::size(data);
		for(ui32 i = 0; i < size; i++)
			load(data[i]);
	}

	template < typename T, typename std::enable_if_t < std::is_enum_v<T>, int  > = 0 >
	void load(T &data)
	{
		si32 read;
		load( read );
		data = static_cast<T>(read);
	}

	template < typename T, typename std::enable_if_t < std::is_same_v<T, bool>, int > = 0 >
	void load(T &data)
	{
		ui8 read;
		load( read );
		data = static_cast<bool>(read);
	}

	template <typename T, typename std::enable_if_t < !std::is_same_v<T, bool >, int  > = 0>
	void load(std::vector<T> &data)
	{
		ui32 length = readAndCheckLength();
		data.resize(length);
		for(ui32 i=0;i<length;i++)
			load( data[i]);
	}

	template <typename T, typename std::enable_if_t < !std::is_same_v<T, bool >, int  > = 0>
	void load(std::deque<T> & data)
	{
		ui32 length = readAndCheckLength();
		data.resize(length);
		for(ui32 i = 0; i < length; i++)
			load(data[i]);
	}

	template < typename T, typename std::enable_if_t < std::is_pointer_v<T>, int  > = 0 >
	void load(T &data)
	{
		bool isNull;
		load( isNull );
		if(isNull)
		{
			data = nullptr;
			return;
		}

		loadPointerImpl(data);
	}

	template < typename T, typename std::enable_if_t < std::is_base_of_v<Entity, std::remove_pointer_t<T>>, int  > = 0 >
	void loadPointerImpl(T &data)
	{
		using DataType = std::remove_pointer_t<T>;

		typename DataType::IdentifierType index;
		load(index);

		auto * constEntity = index.toEntity(VLC);
		auto * constData = dynamic_cast<const DataType *>(constEntity);
		data = const_cast<DataType *>(constData);
	}

	template < typename T, typename std::enable_if_t < !std::is_base_of_v<Entity, std::remove_pointer_t<T>>, int  > = 0 >
	void loadPointerImpl(T &data)
	{
		if(reader->smartVectorMembersSerialization)
		{
			typedef typename std::remove_const_t<typename std::remove_pointer_t<T>> TObjectType; //eg: const CGHeroInstance * => CGHeroInstance
			typedef typename VectorizedTypeFor<TObjectType>::type VType;									 //eg: CGHeroInstance -> CGobjectInstance
			typedef typename VectorizedIDType<TObjectType>::type IDType;
			if(const auto *info = reader->getVectorizedTypeInfo<VType, IDType>())
			{
				IDType id;
				load(id);
				if(id != IDType(-1))
				{
					data = static_cast<T>(reader->getVectorItemFromId<VType, IDType>(*info, id));
					return;
				}
			}
		}

		if(reader->sendStackInstanceByIds)
		{
			bool gotLoaded = LoadIfStackInstance<BinaryDeserializer,T>::invoke(* this, data);
			if(gotLoaded)
				return;
		}

		ui32 pid = 0xffffffff; //pointer id (or maybe rather pointee id)
		if(smartPointerSerialization)
		{
			load( pid ); //get the id
			auto i = loadedPointers.find(pid); //lookup

			if(i != loadedPointers.end())
			{
				// We already got this pointer
				// Cast it in case we are loading it to a non-first base pointer
				data = static_cast<T>(i->second);
				return;
			}
		}
		//get type id
		ui16 tid;
		load( tid );

		if(!tid)
		{
			typedef typename std::remove_pointer_t<T> npT;
			typedef typename std::remove_const_t<npT> ncpT;
			data = ClassObjectCreator<ncpT>::invoke(cb);
			ptrAllocated(data, pid);
			load(*data);
		}
		else
		{
			auto * app = applier.getApplier(tid);
			if(app == nullptr)
			{
				logGlobal->error("load %d %d - no loader exists", tid, pid);
				data = nullptr;
				return;
			}
			data = static_cast<T>(app->loadPtr(*this, cb, pid));
		}
	}

	template <typename T>
	void ptrAllocated(const T *ptr, ui32 pid)
	{
		if(smartPointerSerialization && pid != 0xffffffff)
			loadedPointers[pid] = (void*)ptr; //add loaded pointer to our lookup map; cast is to avoid errors with const T* pt
	}

	template<typename Base, typename Derived> void registerType(const Base * b = nullptr, const Derived * d = nullptr)
	{
		applier.registerType(b, d);
	}

	template <typename T>
	void load(std::shared_ptr<T> &data)
	{
		typedef typename std::remove_const_t<T> NonConstT;
		NonConstT *internalPtr;
		load(internalPtr);

		void * internalPtrDerived = static_cast<void*>(internalPtr);

		if(internalPtr)
		{
			auto itr = loadedSharedPointers.find(internalPtrDerived);
			if(itr != loadedSharedPointers.end())
			{
				// This pointers is already loaded. The "data" needs to be pointed to it,
				// so their shared state is actually shared.
				data = std::static_pointer_cast<T>(itr->second);
			}
			else
			{
				auto hlp = std::shared_ptr<NonConstT>(internalPtr);
				data = hlp;
				loadedSharedPointers[internalPtrDerived] = std::static_pointer_cast<void>(hlp);
			}
		}
		else
			data.reset();
	}

	void load(std::monostate & data)
	{
		// no-op
	}

	template <typename T>
	void load(std::shared_ptr<const T> & data)
	{
		std::shared_ptr<T> nonConstData;

		load(nonConstData);

		data = nonConstData;
	}

	template <typename T>
	void load(std::unique_ptr<T> &data)
	{
		T *internalPtr;
		load( internalPtr );
		data.reset(internalPtr);
	}
	template <typename T, size_t N>
	void load(std::array<T, N> &data)
	{
		for(ui32 i = 0; i < N; i++)
			load( data[i] );
	}
	template <typename T>
	void load(std::set<T> &data)
	{
		ui32 length = readAndCheckLength();
		data.clear();
		T ins;
		for(ui32 i=0;i<length;i++)
		{
			load( ins );
			data.insert(ins);
		}
	}
	template <typename T, typename U>
	void load(std::unordered_set<T, U> &data)
	{
		ui32 length = readAndCheckLength();
		data.clear();
		T ins;
		for(ui32 i=0;i<length;i++)
		{
			load(ins);
			data.insert(ins);
		}
	}
	template <typename T>
	void load(std::list<T> &data)
	{
		ui32 length = readAndCheckLength();
		data.clear();
		T ins;
		for(ui32 i=0;i<length;i++)
		{
			load(ins);
			data.push_back(ins);
		}
	}
	template <typename T1, typename T2>
	void load(std::pair<T1,T2> &data)
	{
		load(data.first);
		load(data.second);
	}

	template <typename T1, typename T2>
	void load(std::map<T1,T2> &data)
	{
		ui32 length = readAndCheckLength();
		data.clear();
		T1 key;
		for(ui32 i=0;i<length;i++)
		{
			load(key);
			load(data[key]);
		}
	}
	void load(std::string &data)
	{
		ui32 length = readAndCheckLength();
		data.resize(length);
		this->read(static_cast<void *>(data.data()), length, false);
	}

	template<typename... TN>
	void load(std::variant<TN...> & data)
	{
		si32 which;
		load( which );
		assert(which < sizeof...(TN));

		// Create array of variants that contains all default-constructed alternatives
		const std::variant<TN...> table[] = { TN{ }... };
		// use appropriate alternative for result
		data = table[which];
		// perform actual load via std::visit dispatch
		std::visit([&](auto& o) { load(o); }, data);
	}

	template<typename T>
	void load(std::optional<T> & data)
	{
		ui8 present;
		load( present );
		if(present)
		{
			//TODO: replace with emplace once we start request Boost 1.56+, see PR360
			T t;
			load(t);
			data = std::make_optional(std::move(t));
		}
		else
		{
			data = std::optional<T>();
		}
	}

	template <typename T>
	void load(boost::multi_array<T, 3> & data)
	{
		ui32 length = readAndCheckLength();
		ui32 x;
		ui32 y;
		ui32 z;
		load(x);
		load(y);
		load(z);
		data.resize(boost::extents[x][y][z]);
		assert(length == data.num_elements()); //x*y*z should be equal to number of elements
		for(ui32 i = 0; i < length; i++)
			load(data.data()[i]);
	}
	template <std::size_t T>
	void load(std::bitset<T> &data)
	{
		static_assert(T <= 64);
		if constexpr (T <= 16)
		{
			uint16_t read;
			load(read);
			data = read;
		}
		else if constexpr (T <= 32)
		{
			uint32_t read;
			load(read);
			data = read;
		}
		else if constexpr (T <= 64)
		{
			uint64_t read;
			load(read);
			data = read;
		}
	}
};

VCMI_LIB_NAMESPACE_END