#pragma once

#include <assert.h>
#include "state/Types.h"
#include "state/board/Board.h"
#include "state/Cards/Manager.h"
#include "state/detail/PlayerDataStructureMaintainer.h"
#include "state/detail/InvokeCallback.h"

namespace state {
	template <CardZone ChangingCardZone, CardType ChangingCardType>
	class ZoneChanger
	{
	public:
		ZoneChanger(State & state, board::Board & board, Cards::Manager & cards_mgr,CardRef card_ref)
			: state_(state), board_(board), cards_mgr_(cards_mgr), card_ref_(card_ref)
		{
			assert(cards_mgr_.Get(card_ref_).GetZone() == ChangingCardZone);
			assert(cards_mgr_.Get(card_ref_).GetCardType() == ChangingCardType);
		}

		template <CardZone ChangeToZone,
			typename = std::enable_if_t<detail::PlayerDataStructureMaintainer<ChangingCardType, ChangeToZone>::SpecifyAddPosition == false>>
			void ChangeTo(PlayerIdentifier player_identifier)
		{
			constexpr bool zone_changed = (ChangingCardZone != ChangeToZone);

			detail::PlayerDataStructureMaintainer<ChangingCardType, ChangingCardZone>::Remove(board_, cards_mgr_, card_ref_);
			if (zone_changed) detail::InvokeCallback<ChangingCardType, ChangingCardZone>::Removed(state_, card_ref_);
			cards_mgr_.GetMutable(card_ref_).SetZone()(player_identifier, ChangeToZone);
			detail::PlayerDataStructureMaintainer<ChangingCardType, ChangeToZone>::Add(board_, cards_mgr_, card_ref_);
			if (zone_changed) detail::InvokeCallback<ChangingCardType, ChangeToZone>::Added(state_, card_ref_);
		}

		template <CardZone ChangeToZone,
			typename = std::enable_if_t<detail::PlayerDataStructureMaintainer<ChangingCardType, ChangeToZone>::SpecifyAddPosition == true>>
			void ChangeTo(PlayerIdentifier player_identifier, int pos)
		{
			constexpr bool zone_changed = (ChangingCardZone != ChangeToZone);

			detail::PlayerDataStructureMaintainer<ChangingCardType, ChangingCardZone>::Remove(board_, cards_mgr_, card_ref_);
			if (zone_changed) detail::InvokeCallback<ChangingCardType, ChangingCardZone>::Removed(state_, card_ref_);
			cards_mgr_.GetMutable(card_ref_).SetZone()(player_identifier, ChangeToZone);
			detail::PlayerDataStructureMaintainer<ChangingCardType, ChangeToZone>::Add(board_, cards_mgr_, card_ref_, pos);
			if (zone_changed) detail::InvokeCallback<ChangingCardType, ChangeToZone>::Added(state_, card_ref_);
		}

		void ReplaceBy(CardRef new_ref)
		{
			assert(cards_mgr_.Get(new_ref).GetZone() == kCardZoneNewlyCreated
				|| cards_mgr_.Get(new_ref).GetZone() == kCardZoneSetASide
			);
			assert(cards_mgr_.Get(card_ref_).GetCardType() == cards_mgr_.Get(new_ref).GetCardType());

			detail::PlayerDataStructureMaintainer<ChangingCardType, ChangingCardZone>::ReplaceBy(board_, cards_mgr_, card_ref_, new_ref);

			cards_mgr_.GetMutable(new_ref).SetZone()(cards_mgr_.Get(card_ref_).GetPlayerIdentifier(), cards_mgr_.Get(card_ref_).GetZone());
			cards_mgr_.GetMutable(new_ref).SetZonePos()(cards_mgr_.Get(card_ref_).GetZonePosition());
			cards_mgr_.GetMutable(card_ref_).SetZone().Zone(kCardZoneSetASide);

			detail::InvokeCallback<ChangingCardType, ChangingCardZone>::Removed(state_, card_ref_);
			detail::InvokeCallback<ChangingCardType, ChangingCardZone>::Added(state_, new_ref);
		}

	private:
		State & state_;
		board::Board& board_;
		Cards::Manager& cards_mgr_;
		CardRef card_ref_;
	};

	template <CardType ChangingCardType>
	class ZoneChangerWithUnknownZone
	{
	public:
		ZoneChangerWithUnknownZone(State & state, board::Board & board, Cards::Manager & cards_mgr,CardRef card_ref)
			: state_(state), board_(board), cards_mgr_(cards_mgr), card_ref_(card_ref)
		{
			assert(cards_mgr.Get(card_ref_).GetCardType() == ChangingCardType);
		}

		template <CardZone ChangeToZone,
			typename = std::enable_if_t<detail::PlayerDataStructureMaintainer<ChangingCardType, ChangeToZone>::SpecifyAddPosition == false>>
			void ChangeTo(PlayerIdentifier player_identifier)
		{
			switch (cards_mgr_.Get(card_ref_).GetZone())
			{
			case kCardZoneGraveyard:
				ZoneChanger<kCardZoneGraveyard, ChangingCardType>(state_, board_, cards_mgr_, card_ref_)
					.template ChangeTo<ChangeToZone>(player_identifier);
				return;
			case kCardZoneHand:
				ZoneChanger<kCardZoneHand, ChangingCardType>(state_, board_, cards_mgr_, card_ref_)
					.template ChangeTo<ChangeToZone>(player_identifier);
				return;
			case kCardZonePlay:
				ZoneChanger<kCardZonePlay, ChangingCardType>(state_, board_, cards_mgr_, card_ref_)
					.template ChangeTo<ChangeToZone>(player_identifier);
				return;
			case kCardZoneSetASide:
				ZoneChanger<kCardZoneSetASide, ChangingCardType>(state_, board_, cards_mgr_, card_ref_)
					.template ChangeTo<ChangeToZone>(player_identifier);
				return;
			case kCardZoneRemoved:
				ZoneChanger<kCardZoneRemoved, ChangingCardType>(state_, board_, cards_mgr_, card_ref_)
					.template ChangeTo<ChangeToZone>(player_identifier);
				return;
			case kCardZoneInvalid:
				ZoneChanger<kCardZoneInvalid, ChangingCardType>(state_, board_, cards_mgr_, card_ref_)
					.template ChangeTo<ChangeToZone>(player_identifier);
				return;
			case kCardZoneNewlyCreated:
				ZoneChanger<kCardZoneNewlyCreated, ChangingCardType>(state_, board_, cards_mgr_, card_ref_)
					.template ChangeTo<ChangeToZone>(player_identifier);
				return;
			default:
				throw std::runtime_error("Unknown card zone");
			}
		}

		template <CardZone ChangeToZone,
			typename = std::enable_if_t<detail::PlayerDataStructureMaintainer<ChangingCardType, ChangeToZone>::SpecifyAddPosition == true>>
			void ChangeTo(PlayerIdentifier player_identifier, int pos)
		{
			switch (cards_mgr_.Get(card_ref_).GetZone())
			{
			case kCardZoneGraveyard:
				ZoneChanger<kCardZoneGraveyard, ChangingCardType>(state_, board_, cards_mgr_, card_ref_)
					.template ChangeTo<ChangeToZone>(player_identifier, pos);
				return;
			case kCardZoneHand:
				ZoneChanger<kCardZoneHand, ChangingCardType>(state_, board_, cards_mgr_, card_ref_)
					.template ChangeTo<ChangeToZone>(player_identifier, pos);
				return;
			case kCardZonePlay:
				ZoneChanger<kCardZonePlay, ChangingCardType>(state_, board_, cards_mgr_, card_ref_)
					.template ChangeTo<ChangeToZone>(player_identifier, pos);
				return;
			case kCardZoneSetASide:
				ZoneChanger<kCardZoneSetASide, ChangingCardType>(state_, board_, cards_mgr_, card_ref_)
					.template ChangeTo<ChangeToZone>(player_identifier, pos);
				return;
			case kCardZoneRemoved:
				ZoneChanger<kCardZoneRemoved, ChangingCardType>(state_, board_, cards_mgr_, card_ref_)
					.template ChangeTo<ChangeToZone>(player_identifier, pos);
				return;
			case kCardZoneInvalid:
				ZoneChanger<kCardZoneInvalid, ChangingCardType>(state_, board_, cards_mgr_, card_ref_)
					.template ChangeTo<ChangeToZone>(player_identifier, pos);
				return;
			case kCardZoneNewlyCreated:
				ZoneChanger<kCardZoneNewlyCreated, ChangingCardType>(state_, board_, cards_mgr_, card_ref_)
					.template ChangeTo<ChangeToZone>(player_identifier, pos);
				return;
			default:
				throw std::runtime_error("Unknown card zone");
			}
		}

	private:
		State & state_;
		board::Board& board_;
		Cards::Manager& cards_mgr_;
		CardRef card_ref_;
	};

	namespace detail {
		template <CardZone ChangingCardZone, CardZone ChangeToZone>
		struct ZoneChangerWithUnknownType {
			static void ChangeTo(State & state_, board::Board & board_, Cards::Manager & cards_mgr_, CardRef card_ref_,
				PlayerIdentifier player_identifier)
			{
				switch (cards_mgr_.Get(card_ref_).GetCardType())
				{
				case kCardTypeHero:
					ZoneChanger<ChangingCardZone, kCardTypeHero>(state_, board_, cards_mgr_, card_ref_)
						.template ChangeTo<ChangeToZone>(player_identifier);
					return;
				case kCardTypeHeroPower:
					ZoneChanger<ChangingCardZone, kCardTypeHeroPower>(state_, board_, cards_mgr_, card_ref_)\
						.template ChangeTo<ChangeToZone>(player_identifier);
					return;
				case kCardTypeMinion:
					ZoneChanger<ChangingCardZone, kCardTypeMinion>(state_, board_, cards_mgr_, card_ref_)
						.template ChangeTo<ChangeToZone>(player_identifier);
					return;
				case kCardTypeSpell:
					ZoneChanger<ChangingCardZone, kCardTypeSpell>(state_, board_, cards_mgr_, card_ref_)
						.template ChangeTo<ChangeToZone>(player_identifier);
					return;
				case kCardTypeWeapon:
					ZoneChanger<ChangingCardZone, kCardTypeWeapon>(state_, board_, cards_mgr_, card_ref_)
						.template ChangeTo<ChangeToZone>(player_identifier);
					return;
				case kCardTypeEnchantment:
					ZoneChanger<ChangingCardZone, kCardTypeEnchantment>(state_, board_, cards_mgr_, card_ref_)
						.template ChangeTo<ChangeToZone>(player_identifier);
					return;
				default:
					throw std::runtime_error("unknown card type");
				}
			}

			static void ChangeTo(State & state_, board::Board & board_, Cards::Manager & cards_mgr_, CardRef card_ref_,
				PlayerIdentifier player_identifier, int pos)
			{
				switch (cards_mgr_.Get(card_ref_).GetCardType())
				{
				case kCardTypeHero:
					ZoneChanger<ChangingCardZone, kCardTypeHero>(state_, board_, cards_mgr_, card_ref_)
						.template ChangeTo<ChangeToZone>(player_identifier, pos);
					return;
				case kCardTypeHeroPower:
					ZoneChanger<ChangingCardZone, kCardTypeHeroPower>(state_, board_, cards_mgr_, card_ref_)
						.template ChangeTo<ChangeToZone>(player_identifier, pos);
					return;
				case kCardTypeMinion:
					ZoneChanger<ChangingCardZone, kCardTypeMinion>(state_, board_, cards_mgr_, card_ref_)
						.template ChangeTo<ChangeToZone>(player_identifier, pos);
					return;
				case kCardTypeSpell:
					ZoneChanger<ChangingCardZone, kCardTypeSpell>(state_, board_, cards_mgr_, card_ref_)
						.template ChangeTo<ChangeToZone>(player_identifier, pos);
					return;
				case kCardTypeWeapon:
					ZoneChanger<ChangingCardZone, kCardTypeWeapon>(state_, board_, cards_mgr_, card_ref_)
						.template ChangeTo<ChangeToZone>(player_identifier, pos);
					return;
				case kCardTypeEnchantment:
					ZoneChanger<ChangingCardZone, kCardTypeEnchantment>(state_, board_, cards_mgr_, card_ref_)
						.template ChangeTo<ChangeToZone>(player_identifier, pos);
					return;
				default:
					throw std::runtime_error("unknown card type");
				}
			}
		};

		template <CardZone ChangingCardZone>
		struct ZoneChangerWithUnknownType<ChangingCardZone, kCardZonePlay> {
			static void ChangeTo(State & state_, board::Board & board_, Cards::Manager & cards_mgr_, CardRef card_ref_,
				PlayerIdentifier player_identifier)
			{
				static constexpr CardZone ChangeToZone = kCardZonePlay;
				switch (cards_mgr_.Get(card_ref_).GetCardType())
				{
				case kCardTypeHero:
					ZoneChanger<ChangingCardZone, kCardTypeHero>(state_, board_, cards_mgr_, card_ref_)
						.template ChangeTo<ChangeToZone>(player_identifier);
					return;
				case kCardTypeHeroPower:
					ZoneChanger<ChangingCardZone, kCardTypeHeroPower>(state_, board_, cards_mgr_, card_ref_)
						.template ChangeTo<ChangeToZone>(player_identifier);
					return;
				case kCardTypeSpell:
					ZoneChanger<ChangingCardZone, kCardTypeSpell>(state_, board_, cards_mgr_, card_ref_)
						.template ChangeTo<ChangeToZone>(player_identifier);
					return;
				case kCardTypeWeapon:
					ZoneChanger<ChangingCardZone, kCardTypeWeapon>(state_, board_, cards_mgr_, card_ref_)
						.template ChangeTo<ChangeToZone>(player_identifier);
					return;
				case kCardTypeEnchantment:
					ZoneChanger<ChangingCardZone, kCardTypeEnchantment>(state_, board_, cards_mgr_, card_ref_)
						.template ChangeTo<ChangeToZone>(player_identifier);
					return;
				case kCardTypeMinion:
					assert(false); // should specify position
				default:
					throw std::runtime_error("unknown card type");
				}
			}

			static void ChangeTo(State & state_, board::Board & board_, Cards::Manager & cards_mgr_, CardRef card_ref_,
				PlayerIdentifier player_identifier, int pos)
			{
				static constexpr CardZone ChangeToZone = kCardZonePlay;
				switch (cards_mgr_.Get(card_ref_).GetCardType())
				{
				case kCardTypeMinion:
					return ZoneChanger<ChangingCardZone, kCardTypeMinion>(state_, board_, cards_mgr_, card_ref_)
						.template ChangeTo<ChangeToZone>(player_identifier, pos);
				case kCardTypeHero:
					assert(false); // should not specify position
				case kCardTypeHeroPower:
					assert(false); // should not specify position
				case kCardTypeSpell:
					assert(false); // should not specify position
				case kCardTypeWeapon:
					assert(false); // should not specify position
				case kCardTypeEnchantment:
					assert(false); // should not specify position
				default:
					throw std::runtime_error("unknown card type");
				}
			}
		};
	}

	template <CardZone ChangingCardZone>
	class ZoneChangerWithUnknownType
	{
	public:
		ZoneChangerWithUnknownType(State & state, board::Board & board, Cards::Manager & cards_mgr,CardRef card_ref)
			: state_(state), board_(board), cards_mgr_(cards_mgr), card_ref_(card_ref)
		{
			assert(cards_mgr.Get(card_ref_).GetZone() == ChangingCardZone);
		}

		template <CardZone ChangeToZone>
		void ChangeTo(PlayerIdentifier player_identifier)
		{
			return detail::ZoneChangerWithUnknownType<ChangingCardZone, ChangeToZone>::ChangeTo(
				state_, board_, cards_mgr_, card_ref_, player_identifier);
		}

		template <CardZone ChangeToZone>
		void ChangeTo(PlayerIdentifier player_identifier, int pos)
		{
			return detail::ZoneChangerWithUnknownType<ChangingCardZone, ChangeToZone>::ChangeTo(
				state_, board_, cards_mgr_, card_ref_, player_identifier, pos);
		}

	private:
		State & state_;
		board::Board& board_;
		Cards::Manager& cards_mgr_;
		CardRef card_ref_;
	};

	class ZoneChangerWithUnknownZoneUnknownType
	{
	public:
		ZoneChangerWithUnknownZoneUnknownType(State & state, board::Board & board, Cards::Manager& cards_mgr,CardRef card_ref)
			: state_(state), board_(board), cards_mgr_(cards_mgr), card_ref_(card_ref)
		{}

		template <CardZone ChangeToZone>
		void ChangeTo(PlayerIdentifier player_identifier)
		{
			switch (cards_mgr_.Get(card_ref_).GetCardType())
			{
			case kCardTypeMinion:
				return ZoneChangerWithUnknownZone<kCardTypeMinion>(state_, board_, cards_mgr_, card_ref_)
					.template ChangeTo<ChangeToZone>(player_identifier);
			case kCardTypeHeroPower:
				return ZoneChangerWithUnknownZone<kCardTypeHeroPower>(state_, board_, cards_mgr_, card_ref_)
					.template ChangeTo<ChangeToZone>(player_identifier);
			case kCardTypeSpell:
				return ZoneChangerWithUnknownZone<kCardTypeSpell>(state_, board_, cards_mgr_, card_ref_)
					.template ChangeTo<ChangeToZone>(player_identifier);
			case kCardTypeWeapon:
				return ZoneChangerWithUnknownZone<kCardTypeWeapon>(state_, board_, cards_mgr_, card_ref_)
					.template ChangeTo<ChangeToZone>(player_identifier);
			default:
				throw std::runtime_error("unknown card type");
			}
		}

		template <CardZone ChangeToZone>
		void ChangeTo(PlayerIdentifier player_identifier, int pos)
		{
			switch (cards_mgr_.Get(card_ref_).GetCardType())
			{
			case kCardTypeHero:
				return ZoneChangerWithUnknownZone<kCardTypeHero>(state_, board_, cards_mgr_, card_ref_)
					.template ChangeTo<ChangeToZone>(player_identifier, pos);
			case kCardTypeMinion:
				return ZoneChangerWithUnknownZone<kCardTypeMinion>(state_, board_, cards_mgr_, card_ref_)
					.template ChangeTo<ChangeToZone>(player_identifier, pos);
			case kCardTypeHeroPower:
				return ZoneChangerWithUnknownZone<kCardTypeHeroPower>(state_, board_, cards_mgr_, card_ref_)
					.template ChangeTo<ChangeToZone>(player_identifier, pos);
			case kCardTypeSpell:
				return ZoneChangerWithUnknownZone<kCardTypeSpell>(state_, board_, cards_mgr_, card_ref_)
					.template ChangeTo<ChangeToZone>(player_identifier, pos);
			case kCardTypeWeapon:
				return ZoneChangerWithUnknownZone<kCardTypeWeapon>(state_, board_, cards_mgr_, card_ref_)
					.template ChangeTo<ChangeToZone>(player_identifier, pos);
			default:
				throw std::runtime_error("unknown card type");
			}
		}

	private:
		State & state_;
		board::Board& board_;
		Cards::Manager& cards_mgr_;
		CardRef card_ref_;
	};
}