////////////////////////////////////////////////////////////////////////////////
// FighterAI.cpp
// AI class that controls a conventional or aerospace fighter
// Written by Björn Ganster since 2021
////////////////////////////////////////////////////////////////////////////////

#include "FighterAI.h"
#include "MechSim.h"

#include <iostream>

#define FILE_NUMBER FILE_NO_FIGHTER_AI_CPP	

////////////////////////////////////////////////////////////////////////////////
// Construct fighter AI

FighterAI::FighterAI(AeroTech* aerotech)
: AbstractVehicleAI("FighterAI", FighterAI_ID, aerotech),
  mAeroTech(aerotech),
  mTriggerRange(500),
  mEvadeRange(100),
  mSaveRange(1000),
  mEvadedObject(nullptr),
  mLastFired(0),
  mCurrentGoal(0),
  mLastMoveFrame(0),
  mAimError(0.1),
  mPiloting(4)
{
}

void FighterAI::setAim(double dx, double dy)
{
	// ignored so far, aerospace fighters do not have turrets (yet)
	// according to Wolf74, aerospace fighters could have the weapons mounted in turrets, like certain vehicles/mech torso
}

bool FighterAI::weaponActive(int i, bool groupFire) const
{
	return false;
}

void FighterAI::planMovement(DWORD now)
{
	if (mAeroTech == nullptr
	||  mAeroTech->isDestroyed()
	||  mLastMoveFrame + mMsBetweenMoveFrames >= now) 
	{
		return;
	}

	SET_SCOPE;
	if (mPiloting > 6) {
		// At piloting 7 or above, remain still ... could also mean to blindly follow the flight plan
		mAeroTech->mCurrRoll = 0.0;
		mAeroTech->mCurrPitch = 0.0;
		mAeroTech->mLastRoll = 0.0;
		mAeroTech->mLastPitch = 0.0;
		mAeroTech->mNextRoll = 0.0;
		mAeroTech->mNextPitch = 0.0;
		RETURN;
	}

	MechSim* mechSim = getMechSim();
	if (mechSim == nullptr) {
		RETURN;
	}

	// Debug code ...
	/*if (mechSim->getMechSimDebug()) {
		COUT << "DEBUG NOW" << std::endl;
	}*/

	SET_OP;
	int env = mechSim->getEnvironment();
	double interpolationFactor = ((double)(now-mLastMoveFrame)) / mMsBetweenMoveFrames;
	mAeroTech->mCurrRoll  = mAeroTech->mLastRoll  + interpolationFactor * (mAeroTech->mNextRoll  - mAeroTech->mLastRoll);
	mAeroTech->mCurrPitch = mAeroTech->mLastPitch + interpolationFactor * (mAeroTech->mNextPitch - mAeroTech->mLastPitch);
	mAeroTech->mLastRoll  = mAeroTech->mNextRoll;
	mAeroTech->mLastPitch = mAeroTech->mNextPitch;
	
	SET_OP;
	mLastMoveFrame = now;
	mEvadedObject = nullptr;
	Point3D thisPos = mVehicle->getPosition();
	double minDistSoFar = 0;

	// Check for objects to evade or to target ...
	if (mechSim != nullptr) {
		SET_OP;
		RenderableObject* scene = mechSim->getScene();
		if (scene != nullptr) {
			for (size_t i = 0; i < scene->getObjectCount(); ++i) {
				RenderableObject* child = scene->getObject(i);
				if (child != nullptr) {
					GameObject* go = dynamic_cast<GameObject*>(child);
					if (go != nullptr) {
						// Check for an object to evade
						double dist = (go->getPosition() - thisPos).length();
						if (dist < mEvadeRange && dist < minDistSoFar) {
							minDistSoFar = dist;
							mEvadedObject = go;
						}

						// Check if an enemy has entered triggerRange
						if (mTarget == nullptr) {
							Vehicle* vehicle = dynamic_cast<Vehicle*>(go);
							if (vehicle != nullptr && vehicle->getFaction() != mVehicle->getFaction() && dist < mTriggerRange) {
								mTarget = vehicle;
							}
						}
					}
				}
			}
		}
	}

	// Check flight altitude. If we are low and heading down, we need to fly up unconditionally
	SET_OP;
	double minAltitude = 300.0;
	if (env == space) {
		mAeroTech->setThrust(0.1, env);
	} else {
		if (thisPos.y < minAltitude && mVehicle->p_front.y <= 0.0) {
			SET_OP;
			Point3D desiredPos = thisPos + 200 * mVehicle->p_front;
			desiredPos.y = 2 * minAltitude;
			flyTowards(desiredPos, env);
			mAeroTech->setThrust(0.1, env);
			RETURN;
		} else {
			mAeroTech->setThrust(0.8, env);
		}
	}

	// If the AI is currently evading/escaping the enemy, move away from the enemy at max. thrust until safeDistance is reached
	if (mEvadedObject != nullptr) {
		SET_OP;
		Point3D desiredPos = - mEvadedObject->getPosition();
		if (desiredPos.y < 2 * minAltitude) {
			desiredPos.y = 2 * minAltitude;
		}
		flyTowards(desiredPos, env);
		RETURN;
	}

	if (mTarget != nullptr) {
		if (!mTarget->isDestroyed()) {
			fight(now, env);
		} else {
			mTarget = nullptr;
		}
	} else if (mCurrentGoal >= mGoals.size()) {
		SET_OP;
		mVehicle->setStatus(VehicleStatus::shutDown);
	} else {
		SET_OP;
		auto goalType = mGoals[mCurrentGoal].type;
		switch(goalType) {
			default:
			case SubObjectiveType::sleep:
				// Sleep? Not really, it rather means to circle around mWaitPosition
				// todo: set mWaitPosition somehow ...
				checkWakeUp(env);
				break;
			case SubObjectiveType::attack:
				doAttack();
				break;
			case SubObjectiveType::defend:
				doDefend(env);
				break;
			case SubObjectiveType::inspect:
				doInspect();
				break;
			case SubObjectiveType::visit:
				doVisit(env);
				break;
		}
	}

	RETURN;
}

// Notify AI of an event, like a destroyed object
void FighterAI::updateObjectStatus(const BGCPString& id, SubObjectiveType newStatus)
{
	SET_SCOPE;
	if (mCurrentGoal >= mGoals.size()
	||  mVehicle == nullptr
	||  mVehicle->isDestroyed()) {
		RETURN;
	}

	SET_OP;
	auto& goal = mGoals[mCurrentGoal];
	if (goal.type == SubObjectiveType::sleep
	&&  goal.trigger == id) 
	{
		++mCurrentGoal;
		if (mVehicle->getStatus() == shutDown) {
			mVehicle->setStatus(engineRunning);
		}
	}
	RETURN;
}

// Set range where AI triggers
void FighterAI::setTriggerRange(double newVal)
{
	mTriggerRange = newVal;
}

// Add single goal to attack (possibly after sleep goal)
void FighterAI::addAttackGoal(const BGCPString& target)
{
	SET_SCOPE;
	AIGoal newGoal;
	newGoal.type = SubObjectiveType::attack;
	newGoal.target = target;
	mGoals.push_back(newGoal);
	RETURN;
}

// Add goals for AI
void FighterAI::addGoals(const std::vector<AIGoal>& goals, bool clearExisting)
{
	SET_SCOPE;
	if (clearExisting) {
		mGoals.clear();
	}
	
	SET_OP;
	for (size_t i = 0; i < goals.size(); ++i) {
		mGoals.emplace_back(goals[i]);
	}
	RETURN;
}

// Fly towards given position. Returns the difference of the current flying direction to desiredPos as an angle in degrees.
double FighterAI::flyTowards(const Point3D& desiredPos, int env)
{
	// Roll left or right according to the first polar coordinate
	SET_SCOPE;
	Point3D thisPos = mVehicle->getPosition();
	double coord1 = calcPolarCoord(thisPos, mVehicle->p_front, mVehicle->p_up, desiredPos);

	// New code to calculate roll: check how much we need to roll in order to reach a stated goal
	double aiFrameDurationInSeconds = mMsBetweenMoveFrames / 1000.0;
	double maxRollPerAIFrame = mAeroTech->getRollRate() * aiFrameDurationInSeconds;
	double step = 1.0;
	if (coord1 < 2 * maxRollPerAIFrame)
		step = 0.5;
	mAeroTech->mNextRoll = coord1 / maxRollPerAIFrame;
	if (mAimError > 0.01) {
		mAeroTech->mNextRoll = putInRange(-1.0, mAeroTech->mNextRoll, 1.0);
		mAeroTech->mNextRoll += frand(-mAimError, mAimError);
	}
	mAeroTech->mNextRoll = step * putInRange(-1.0, mAeroTech->mNextRoll, 1.0);

	// Pitch down/up according to the second polar coordinate
	SET_OP;
	double coord2 = calcPolarCoord(thisPos, mVehicle->p_left, mVehicle->p_front, desiredPos);
	double pitchPerAIFrame = mAeroTech->getPitchRate(env) * aiFrameDurationInSeconds;
	mAeroTech->mNextPitch = coord2 / pitchPerAIFrame;
	if (coord2 < pitchPerAIFrame) 
		step = 0.5;
	else
		step = 1.0;
	if (mAimError > 0.01) {
		mAeroTech->mNextPitch = putInRange(-1.0, mAeroTech->mNextPitch, 0.2);
		mAeroTech->mNextPitch += frand(-mAimError, mAimError);
	}
	mAeroTech->mNextPitch = step * putInRange(-1.0, mAeroTech->mNextPitch, 0.2);

	// todo: calc return value as the difference between vehicle's p_front and (desiredPos-mVehicle->getPosition())
	RETURN coord2;
}

// AI modes
void FighterAI::fight(DWORD now, int env)
{
	if (mVehicle == nullptr || mTarget == nullptr) {
		return;
	}

	SET_SCOPE;
	Point3D thisPos = mVehicle->getPosition();

	// Aim for the enemy
	Point3D targetPos = mTarget->getPosition();
	double aimQuality = flyTowards(targetPos, env);

	if (fabs(aimQuality) < 5.0) {
		// If the enemy is outside the range of the current weapon, check the next weapon. Otherwise, fire.
		bool readyToFire = mVehicle->canFireWeapon(mVehicle->mActiveWeapon, now);
		if (!readyToFire) {
			mVehicle->cycleWeapon(false);
			mLastFired = now; // to delay AI shooting
		} else if (mLastFired + 1000.0 * mTriggerSpeed < now) {
			double weaponRange = mVehicle->getActiveWeaponRange();
			double dist = (targetPos - thisPos).length();
			if (dist < weaponRange) {
				mVehicle->fireWeapons(false, now);
				mLastFired = now;
			}
		}
	}

	RETURN;
}

void FighterAI::checkWakeUp(int env)
{
	if (mVehicle == nullptr) {
		return;
	}

	SET_SCOPE;
	if (mVehicle->getStatus() == VehicleStatus::shutDown) {
		// Check if an enemy is within wakeup/attack range
		MechSim* mechSim = getMechSim();
		RenderableObject* scene = mechSim->getScene();
		if (scene != nullptr) {
			for (size_t i = 0; i < scene->getObjectCount(); ++i) {
				RenderableObject* object = scene->getObject(i);
				Vehicle* vehicle = dynamic_cast<Vehicle*>(object);
				if (vehicle != nullptr && vehicle != mVehicle) {
					BGCPString vehicleFaction = vehicle->getFaction();
					double dist = (vehicle->getPosition()-mVehicle->getPosition()).length();
					if (dist < mTriggerRange
					&&  !vehicleFaction.empty()
					&&  vehicleFaction != mVehicle->getFaction()
					&&  vehicle->getStatus() != VehicleStatus::destroyed)
					{
						setTarget(vehicle);
						return;
					}
				}
			}

			// Check if a sleep timer has run out
			// todo: this only works if the first goal is a sleep goal, because the sleep time is counted from mission start here
			// To solve this properly, we would have to keep track of when an AI started on the current goal
			SET_OP;
			if (mCurrentGoal < mGoals.size()) {
				const auto& goal = mGoals[mCurrentGoal];
				if (goal.type == SubObjectiveType::sleep && (int)mechSim->missionTime() > 1000 * goal.duration)
				{
					++mCurrentGoal;
				}
			}
		}
	}

	flyTowards(mWaitPosition, env);
	RETURN;
}

void FighterAI::doAttack()
{
	SET_SCOPE;
	if (mCurrentGoal >= mGoals.size()) {
		RETURN;
	}

	BGCPString targetID = mGoals[mCurrentGoal].target;
	MechSim* mechSim = getMechSim();
	RenderableObject* scene = mechSim->getScene();
	GameObject* gameObject = findGameObject(scene, targetID);

	if (gameObject != nullptr) {
		if (gameObject->isDestroyed()) {
			++mCurrentGoal;
		} else {
			setTarget(gameObject);
		}
	}

	RETURN;
}

void FighterAI::doDefend(int env)
{
	// todo (if ever needed): attack any enemy that comes too close to the target to defend ...
	SET_SCOPE;
	if (mCurrentGoal < mGoals.size() && mVehicle != nullptr) {
		const AIGoal& goal = mGoals[mCurrentGoal];
		flyTowards(goal.coords, env);
	}
	RETURN;
}

void FighterAI::doInspect()
{
	// todo - not actually possible for an aerospace fighter?
}

void FighterAI::doVisit(int env)
{
	SET_SCOPE;
	if (mCurrentGoal < mGoals.size() && mVehicle != nullptr) {
		const AIGoal& goal = mGoals[mCurrentGoal];
		Point3D vehiclePos = mVehicle->getPosition();
		double dist = (goal.coords - vehiclePos).length();

		if (dist > 100.0) {
			flyTowards(goal.coords, env);
		} else {
			++mCurrentGoal;
			MechSim* mechSim = getMechSim();
			mechSim->updateObjectStatus(goal.trigger, SubObjectiveType::event, false);
		}
	}
	RETURN;
}

// Set skills (will be reflected by other variables)
// As long as an aerospace fighter aims simply by pointing its nose, piloting and gunnery are the same for aerospace fighters
void FighterAI::setPiloting(int newVal)
{
	// The lower, the better for the AI (0: ace pilot, 6: untalented beginner)
	SET_SCOPE;
	//newVal = putInRange(0, newVal, 6);
	if (newVal < 0) {
		newVal = 0;
	}
	mPiloting = newVal;
	mMsBetweenMoveFrames = 200 + 50 * newVal; // shouldn't be less than 50
	mAimError = 0.02 * newVal;
	RETURN;
}
