#include "stdafx.h"
#include "Behaviour.h"
#include "BehaviourTree.h"
#include <IExamInterface.h>
#include "Thinker.h"

#define TO_RAD(i) i * (M_PI / 180)

static int randNumRange(int minRange, int maxRange) {
	std::random_device rd;
	std::mt19937 seed(rd());
	std::uniform_int_distribution<> range(minRange, maxRange);

	return range(seed);
}

namespace BT_Action
{
	BT::State SetTimer(Blackboard* blackboardPtr, const std::string& timerName, bool doOnce) {
		bool didOnce{};
		blackboardPtr->GetData("Timer" + timerName + "DoOnce", didOnce);

		if (doOnce && didOnce)
			return BT::State::Success;

		if (doOnce)
			blackboardPtr->ChangeData("Timer" + timerName + "DoOnce", true);

		blackboardPtr->ChangeData("Timer" + timerName, std::chrono::steady_clock::now());

		return BT::State::Success;
	}

	BT::State UnlockTimer(Blackboard* blackboardPtr, const std::string& timerName) {
		blackboardPtr->ChangeData("TimerLock" + timerName, false);

		return BT::State::Success;
	}

	BT::State LockTimer(Blackboard* blackboardPtr, const std::string& timerName) {
		blackboardPtr->ChangeData("TimerLock" + timerName, true);

		return BT::State::Success;
	}

	BT::State GoToTarget(Blackboard* blackboardPtr) {
		IExamInterface* interfacePtr{};
		Elite::Vector2 target{};
		SteeringPlugin_Output steering{};

		std::chrono::steady_clock::time_point timer{};
		float maxTime{};
		bool doOnce{};

		blackboardPtr->GetData("Interface", interfacePtr);
		blackboardPtr->GetData("Target", target);
		blackboardPtr->GetData("Steering", steering);

		blackboardPtr->GetData("FailSafe", timer);
		blackboardPtr->GetData("MaxFailSafe", maxTime);
		blackboardPtr->GetData("FailSafeDoOnce", doOnce);

		if (!doOnce) {
			blackboardPtr->ChangeData("FailSafe", std::chrono::steady_clock::now());
			blackboardPtr->ChangeData("FailSafeDoOnce", true);
		}

		const auto agentInfo = interfacePtr->Agent_GetInfo();

		const auto nextTargetPos = interfacePtr->NavMesh_GetClosestPathPoint(target);

		steering.LinearVelocity = nextTargetPos - agentInfo.Position;
		steering.LinearVelocity.Normalize();
		steering.LinearVelocity *= agentInfo.MaxLinearSpeed * 100;

		const std::chrono::steady_clock::time_point currentTime{ std::chrono::steady_clock::now() };
		const std::chrono::duration<float> elapsedSec{ currentTime - timer };

		if (elapsedSec.count() > maxTime) {
			blackboardPtr->ChangeData("FailSafeDoOnce", false);
			return BT::State::Success;
		}

		if (Distance(target, agentInfo.Position) < 2.f) {
			blackboardPtr->ChangeData("FailSafeDoOnce", false);

			return BT::State::Success;
		}

		blackboardPtr->ChangeData("Steering", steering);

		return BT::State::Running;
	}

	BT::State EnableSpin(Blackboard* blackboardPtr) {
		IExamInterface* interfacePtr{};
		SteeringPlugin_Output steering{};

		blackboardPtr->GetData("Interface", interfacePtr);
		blackboardPtr->GetData("Steering", steering);

		steering.AutoOrient = true;
		steering.AngularVelocity = interfacePtr->Agent_GetInfo().MaxAngularSpeed;

		blackboardPtr->ChangeData("Spin", true);
		blackboardPtr->ChangeData("Steering", steering);

		return BT::State::Success;
	}

	BT::State DisableSpin(Blackboard* blackboardPtr) {
		blackboardPtr->ChangeData("Spin", false);

		return BT::State::Success;
	}


	BT::State FindClosestEdge(Blackboard* blackboardPtr, int degree) {
		IExamInterface* interfacePtr{};
		blackboardPtr->GetData("Interface", interfacePtr);

		const Elite::Vector2 playerPos{ interfacePtr->Agent_GetInfo().Position };

		constexpr float offset{ 3.f };
		const Elite::Vector2 center{ interfacePtr->GetPurgeZonesInFOV()[0].Center };
		const float radius{ interfacePtr->GetPurgeZonesInFOV()[0].Radius + offset };

		float closestTarget{ FLT_MAX };
		Elite::Vector2 finalTarget{};

		constexpr int circleDegrees{ 360 };

		if (degree > circleDegrees) {
			degree = circleDegrees;
		}
		for (int i = 0; i <= circleDegrees; i += degree) {
			const Elite::Vector2 pointOnCircle{ center.x + radius * std::cosf(TO_RAD(i)), center.y + radius * std::sinf(TO_RAD(i)) };

			const float targetDistance{ playerPos.DistanceSquared(pointOnCircle) };

			if (closestTarget > targetDistance) {
				closestTarget = targetDistance;
				finalTarget = pointOnCircle;
			}
		}

		blackboardPtr->ChangeData("Target", finalTarget);

		return BT::State::Success;
	}

	BT::State SetZombieTarget(Blackboard* blackboardPtr) {
		IExamInterface* interfacePtr{};
		blackboardPtr->GetData("Interface", interfacePtr);

		if (interfacePtr->GetEnemiesInFOV().capacity() == 0) {
			return BT::State::Failure;
		}

		EnemyInfo zombieInfo{};
		float closestZombie{ FLT_MAX };

		for (const auto zombie : interfacePtr->GetEnemiesInFOV()) {
			const float distance{ interfacePtr->Agent_GetInfo().Position.DistanceSquared(zombie.Location) };

			if (closestZombie < distance)
				continue;

			closestZombie = distance;
			zombieInfo = zombie;
		}

		blackboardPtr->ChangeData("TargetZombie", zombieInfo);

		return BT::State::Success;
	}

	BT::State AvoidingZombie(Blackboard* blackboardPtr) {
		IExamInterface* interfacePtr{};
		SteeringPlugin_Output steering{};

		blackboardPtr->GetData("Interface", interfacePtr);
		blackboardPtr->GetData("Steering", steering);

		Elite::Vector2 evadeDirection{};

		for (auto zombie : interfacePtr->GetEnemiesInFOV()) {
			Elite::Vector2 currentPos{ interfacePtr->Agent_GetInfo().Position };
			Elite::Vector2 targetPos{ zombie.Location };
			Elite::Vector2 goingAwayVec{ currentPos - targetPos };
			float distance = goingAwayVec.MagnitudeSquared();

			evadeDirection += goingAwayVec / distance;
		}

		steering.LinearVelocity = evadeDirection.GetNormalized() * interfacePtr->Agent_GetInfo().MaxLinearSpeed;

		blackboardPtr->ChangeData("Steering", steering);

		return BT::State::Success;
	}

	BT::State RotateToZombie(Blackboard* blackboardPtr) {
		IExamInterface* interfacePtr{};
		EnemyInfo zombieInfo{};
		SteeringPlugin_Output steering{};

		blackboardPtr->GetData("Interface", interfacePtr);
		blackboardPtr->GetData("TargetZombie", zombieInfo);
		blackboardPtr->GetData("Steering", steering);

		const float maxAngularVelocity{ interfacePtr->Agent_GetInfo().MaxAngularSpeed };
		const float targetAngle{ VectorToOrientation((zombieInfo.Location - interfacePtr->Agent_GetInfo().Position).GetNormalized()) };
		const float angleDiff{ targetAngle - interfacePtr->Agent_GetInfo().Orientation };

		steering.AngularVelocity = angleDiff * maxAngularVelocity;

		blackboardPtr->ChangeData("Steering", steering);
		blackboardPtr->ChangeData("angleDiff", angleDiff);

		return BT::State::Success;
	}

	BT::State ReadyToShoot(Blackboard* blackboardPtr, float minAngleDiff) {
		float angleDiff{};

		blackboardPtr->GetData("angleDiff", angleDiff);

		return (std::abs(angleDiff) < minAngleDiff) ? BT::State::Success : BT::State::Failure;
	}

	BT::State Shoot(Blackboard* blackboardPtr, eItemType type) {
		IExamInterface* interfacePtr{};
		Thinker* thinkerPtr{};

		blackboardPtr->GetData("Interface", interfacePtr);
		blackboardPtr->GetData("Brain", thinkerPtr);

		const auto item = thinkerPtr->FindLeastValueItem(type);

		if (item->ItemInfo.Value <= 0)
			return BT::State::Failure;

		if (interfacePtr->Inventory_UseItem(item->invIndex)) {
			--item->ItemInfo.Value;
			return BT::State::Success;
		}

		return BT::State::Failure;
	}

	BT::State SetItemAsTarget(Blackboard* blackboardPtr) {
		IExamInterface* interfacePtr{};
		blackboardPtr->GetData("Interface", interfacePtr);

		if (interfacePtr->GetItemsInFOV().capacity() == 0) {
			return BT::State::Failure;
		}

		ItemInfo targetItem{};
		float closestItem{ FLT_MAX };

		for (const auto item : interfacePtr->GetItemsInFOV()) {
			const float distance{ interfacePtr->Agent_GetInfo().Position.DistanceSquared(item.Location) };

			if (closestItem < distance)
				continue;

			closestItem = distance;
			targetItem = item;
		}

		blackboardPtr->ChangeData("TargetItem", targetItem);
		blackboardPtr->ChangeData("Target", targetItem.Location);

		return BT::State::Success;
	}

	BT::State DestroyItemOnFloor(Blackboard* blackboardPtr) {
		IExamInterface* interfacePtr{};
		ItemInfo targetItem{};

		blackboardPtr->GetData("Interface", interfacePtr);
		blackboardPtr->GetData("TargetItem", targetItem);

		if (interfacePtr->DestroyItem(targetItem))
			return BT::State::Success;

		return BT::State::Failure;
	}

	BT::State PickUpItem(Blackboard* blackboardPtr) {
		IExamInterface* interfacePtr{};
		Thinker* thinkerPtr{};
		ItemInfo targetItem{};
		int freeSlot{};

		blackboardPtr->GetData("Interface", interfacePtr);
		blackboardPtr->GetData("Brain", thinkerPtr);
		blackboardPtr->GetData("TargetItem", targetItem);
		blackboardPtr->GetData("NextFreeSlot", freeSlot);

		if (interfacePtr->GrabItem(targetItem)) {
			interfacePtr->Inventory_AddItem(freeSlot, targetItem);

			blackboardPtr->ChangeData("NextFreeSlot", thinkerPtr->AddItemToMemory(targetItem));

			return BT::State::Success;
		}

		return BT::State::Failure;
	}

	BT::State SwapItem(Blackboard* blackboardPtr) {
		IExamInterface* interfacePtr{};
		Thinker* thinkerPtr{};
		ItemInfo targetItem{};

		blackboardPtr->GetData("Interface", interfacePtr);
		blackboardPtr->GetData("Brain", thinkerPtr);
		blackboardPtr->GetData("TargetItem", targetItem);

		if (interfacePtr->GrabItem(targetItem)) {
			const int slot = thinkerPtr->FindEmptyValue(targetItem);
			interfacePtr->Inventory_RemoveItem(slot);
			interfacePtr->Inventory_AddItem(slot, targetItem);

			return BT::State::Success;
		}

		return BT::State::Failure;
	}

	BT::State CheckItem(Blackboard* blackboardPtr) {
		IExamInterface* interfacePtr{};
		Thinker* thinkerPtr{};
		ItemInfo targetItem{};

		blackboardPtr->GetData("Interface", interfacePtr);
		blackboardPtr->GetData("Brain", thinkerPtr);
		blackboardPtr->GetData("TargetItem", targetItem);

		const int slotIndex{ thinkerPtr->CheckItem(targetItem) };

		if (slotIndex == interfacePtr->Inventory_GetCapacity() - 1) {
			if (targetItem.Type == eItemType::SHOTGUN || targetItem.Type == eItemType::PISTOL) {
				interfacePtr->DestroyItem(targetItem);
				return BT::State::Success;
			}

			if (interfacePtr->GrabItem(targetItem)) {
				interfacePtr->Inventory_AddItem(slotIndex, targetItem);
				interfacePtr->Inventory_UseItem(slotIndex);
				interfacePtr->Inventory_RemoveItem(slotIndex);

				return BT::State::Success;
			}
		}
		else {
			if (!(targetItem.Type == eItemType::SHOTGUN || targetItem.Type == eItemType::PISTOL)) {
				interfacePtr->Inventory_UseItem(slotIndex);
			}

			if (interfacePtr->GrabItem(targetItem)) {
				interfacePtr->Inventory_RemoveItem(slotIndex);
				interfacePtr->Inventory_AddItem(slotIndex, targetItem);
				return BT::State::Success;
			}
		}

		return BT::State::Failure;
	}

	BT::State UseItem(Blackboard* blackboardPtr, eItemType type) {
		IExamInterface* interfacePtr{};
		Thinker* thinkerPtr{};

		blackboardPtr->GetData("Interface", interfacePtr);
		blackboardPtr->GetData("Brain", thinkerPtr);

		const auto item = thinkerPtr->FindLeastValueItem(type);

		if (item->ItemInfo.Value <= 0)
			return BT::State::Failure;

		if (interfacePtr->Inventory_UseItem(item->invIndex)) {
			item->ItemInfo.Value = 0;
			return BT::State::Success;
		}

		return BT::State::Failure;
	}

	BT::State TryFindHouse(Blackboard* blackboardPtr, float searchRadius, int degree) {
		IExamInterface* interfacePtr{};
		Thinker* thinkerPtr{};

		blackboardPtr->GetData("Interface", interfacePtr);
		blackboardPtr->GetData("Brain", thinkerPtr);

		blackboardPtr->ChangeData("TargetHouse", HouseInfo{});

		const Elite::Vector2 playerPos{ interfacePtr->Agent_GetInfo().Position };

		float closestTarget{ FLT_MAX };
		Elite::Vector2 finalTarget{};

		constexpr int circleDegrees{ 360 };

		if (degree > circleDegrees) {
			degree = circleDegrees;
		}

		for (int i = 0; i <= circleDegrees; i += degree) {
			const Elite::Vector2 pointOnCircle{ playerPos.x + searchRadius * std::cosf(TO_RAD(i)), playerPos.y + searchRadius * std::sinf(TO_RAD(i)) };
			const Elite::Vector2 target = interfacePtr->NavMesh_GetClosestPathPoint(pointOnCircle);


			if (target == Elite::Vector2(400, 400)) {
				std::cout << pointOnCircle << std::endl;
			}

			if (pointOnCircle != target) {
				constexpr float houseOffset{ 5.f };
				if (thinkerPtr->CheckIfTargetIsExplored(target, houseOffset))
					continue;

				const float targetDistance{ playerPos.DistanceSquared(target) };

				if (closestTarget > targetDistance) {
					closestTarget = targetDistance;
					finalTarget = target;
				}
			}
		}

		if (finalTarget == Elite::Vector2{}) {
			return BT::State::Failure;
		}

		blackboardPtr->ChangeData("Target", finalTarget);

		return BT::State::Success;
	}

	BT::State FindRandomLocation(Blackboard* blackboardPtr, float randomRadius) {
		IExamInterface* interfacePtr{};
		blackboardPtr->GetData("Interface", interfacePtr);

		const Elite::Vector2 playerPos{ interfacePtr->Agent_GetInfo().Position };
		Elite::Vector2 target = Elite::Vector2(playerPos.x + randNumRange(-randomRadius, randomRadius),
			playerPos.y + randNumRange(-randomRadius, randomRadius));

		blackboardPtr->ChangeData("Target", target);

		return BT::State::Success;
	}

	BT::State GetHouseAsTarget(Blackboard* blackboardPtr, float maxTravelDistance) {
		IExamInterface* interfacePtr{};
		Thinker* thinkerPtr{};

		blackboardPtr->GetData("Interface", interfacePtr);
		blackboardPtr->GetData("Brain", thinkerPtr);

		const HouseInfo targetHouse{ thinkerPtr->CheckHouseValidTarget(interfacePtr->Agent_GetInfo().Position, maxTravelDistance) };

		if (targetHouse.Size.x <= 0) {
			return BT::State::Failure;
		}

		blackboardPtr->ChangeData("Target", targetHouse.Center);
		blackboardPtr->ChangeData("TargetHouse", targetHouse);

		return BT::State::Success;
	}

	BT::State CheckHouses(Blackboard* blackboardPtr) {
		Thinker* thinkerPtr{};
		IExamInterface* interfacePtr{};

		blackboardPtr->GetData("Brain", thinkerPtr);
		blackboardPtr->GetData("Interface", interfacePtr);

		if (thinkerPtr->CheckHousesForMemory(interfacePtr->GetHousesInFOV())) {
			return BT::State::Success;
		}

		return BT::State::Failure;
	}

	BT::State SetExpireDate(Blackboard* blackboardPtr) {
		Thinker* thinkerPtr{};
		HouseInfo targetHouse{};

		blackboardPtr->GetData("Brain", thinkerPtr);
		blackboardPtr->GetData("TargetHouse", targetHouse);

		thinkerPtr->SetTargetHouseExpireDate(targetHouse);

		return BT::State::Success;
	}

	BT::State GetInsideTarget(Blackboard* blackboardPtr, float offset) {
		HouseInfo targetHouse{};
		blackboardPtr->GetData("TargetHouse", targetHouse);

		const Elite::Vector2 houseCenter{ targetHouse.Center };
		const Elite::Vector2 houseSize{ targetHouse.Size };

		const Elite::Vector2 targetLocation(randNumRange(int(houseCenter.x - houseSize.x / 2 + offset), int(houseCenter.x + houseSize.x / 2 - offset)),
			randNumRange(int(houseCenter.y - houseSize.y / 2 + offset), int(houseCenter.y + houseSize.y / 2 - offset)));

		blackboardPtr->ChangeData("Target", targetLocation);

		return BT::State::Success;
	}
}

namespace BT_Condition
{
	bool CheckTimerLock(Blackboard* blackboardPtr, const std::string& timerName) {
		bool lock{};
		blackboardPtr->GetData(timerName + "TimerLock", lock);

		return !lock;
	}

	bool CheckTimer(Blackboard* blackboardPtr, const std::string& timerName, bool doOnce) {
		std::chrono::steady_clock::time_point timer{};
		float maxTime{};

		blackboardPtr->GetData("Timer" + timerName, timer);
		blackboardPtr->GetData("MaxTime" + timerName, maxTime);

		const std::chrono::steady_clock::time_point currentTime{ std::chrono::steady_clock::now() };
		const std::chrono::duration<float> elapsedSec{ currentTime - timer };

		if (elapsedSec.count() > maxTime) {
			if (doOnce)
				blackboardPtr->ChangeData("Timer" + timerName + "DoOnce", false);

			return true;
		}

		return false;
	}

	bool SeeZombie(Blackboard* blackboardPtr) {
		IExamInterface* interfacePtr{};
		blackboardPtr->GetData("Interface", interfacePtr);

		return interfacePtr->GetEnemiesInFOV().capacity() > 0;
	}

	bool HasWeapon(Blackboard* blackboardPtr) {
		return ItemInInv(blackboardPtr, eItemType::SHOTGUN) || ItemInInv(blackboardPtr, eItemType::PISTOL);
	}

	bool InRange(Blackboard* blackboardPtr, float maxRange) {
		IExamInterface* interfacePtr{};
		EnemyInfo zombieInfo{};

		blackboardPtr->GetData("Interface", interfacePtr);
		blackboardPtr->GetData("TargetZombie", zombieInfo);

		const float dist2{ (zombieInfo.Location - interfacePtr->Agent_GetInfo().Position).MagnitudeSquared() };

		return dist2 <= maxRange * maxRange;
	}

	bool SeePurgeZone(Blackboard* blackboardPtr) {
		IExamInterface* interfacePtr{};
		blackboardPtr->GetData("Interface", interfacePtr);

		return interfacePtr->GetPurgeZonesInFOV().capacity() > 0;
	}

	bool ItemInInv(Blackboard* blackboardPtr, eItemType type) {
		Thinker* thinkerPtr{};
		blackboardPtr->GetData("Brain", thinkerPtr);

		return thinkerPtr->IsItemInInv(type);
	}

	bool HpUnderThreshold(Blackboard* blackboardPtr, float threshold) {
		IExamInterface* interfacePtr{};
		blackboardPtr->GetData("Interface", interfacePtr);

		return interfacePtr->Agent_GetInfo().Health <= threshold;
	}

	bool CheckMinNeededEnergy(Blackboard* blackboardPtr) {
		IExamInterface* interfacePtr{};
		Thinker* thinkerPtr{};

		blackboardPtr->GetData("Interface", interfacePtr);
		blackboardPtr->GetData("Brain", thinkerPtr);

		const auto item = thinkerPtr->FindLeastValueItem(eItemType::FOOD);

		return interfacePtr->Agent_GetInfo().Energy <= 10.f - item->ItemInfo.Value;
	}

	bool SeeItem(Blackboard* blackboardPtr) {
		IExamInterface* interfacePtr{};
		blackboardPtr->GetData("Interface", interfacePtr);

		return interfacePtr->GetItemsInFOV().capacity() > 0;
	}

	bool IsTypeOfItem(Blackboard* blackboardPtr, eItemType type) {
		ItemInfo targetItem{};
		blackboardPtr->GetData("TargetItem", targetItem);

		return targetItem.Type == type;
	}

	bool InvIsNotFull(Blackboard* blackboardPtr) {
		Thinker* thinkerPtr{};
		blackboardPtr->GetData("Brain", thinkerPtr);
		return thinkerPtr->IsInvNotFull();
	}

	bool EmptyValue(Blackboard* blackboardPtr) {
		Thinker* thinkerPtr{};
		blackboardPtr->GetData("Brain", thinkerPtr);

		if (thinkerPtr->EmptyValue()) {
			std::cout << "empty item\n";
		}

		return thinkerPtr->EmptyValue();
	}

	bool InvIsFull(Blackboard* blackboardPtr) {
		return !InvIsNotFull(blackboardPtr);
	}

	bool InsideTargetHouse(Blackboard* blackboardPtr) {
		IExamInterface* interfacePtr{};
		Thinker* thinkerPtr{};
		HouseInfo targetHouse{};

		blackboardPtr->GetData("Interface", interfacePtr);
		blackboardPtr->GetData("Brain", thinkerPtr);
		blackboardPtr->GetData("TargetHouse", targetHouse);

		if (interfacePtr->Agent_GetInfo().IsInHouse) {
			return thinkerPtr->CheckIfTargetIsInside(targetHouse, interfacePtr->Agent_GetInfo().Position);
		}
		return false;
	}

	bool SeeHouse(Blackboard* blackboardPtr) {
		IExamInterface* interfacePtr{};
		blackboardPtr->GetData("Interface", interfacePtr);

		return interfacePtr->GetHousesInFOV().capacity() > 0;
	}

	bool NewHouse(Blackboard* blackboardPtr) {
		Thinker* thinkerPtr{};
		blackboardPtr->GetData("Brain", thinkerPtr);

		return thinkerPtr->NewHouseToExplore();
	}

	bool ReExploreHouse(Blackboard* blackboardPtr) {
		Thinker* thinkerPtr{};
		blackboardPtr->GetData("Brain", thinkerPtr);

		return thinkerPtr->HouseToReExplore();
	}
}
