////////////////////////////////////////////////////////////////////////////////
// AeroTech.cpp
// Implementation of a vehicle class for Aerospace fighters
// Copyright Bjoern Ganster
// Developed since 2021
////////////////////////////////////////////////////////////////////////////////

#include "AeroTech.h"
#include "FighterPlayer.h"
#include "FighterAI.h"
#include "FighterAI-space.h"
#include "MechSim.h"

#define FILE_NUMBER FILE_NO_AEROTECH_CPP

////////////////////////////////////////////////////////////////////////////////

// Parts may be rendered with different settings
#define AeroTechMaxParts 7

ENUM_CLASS AeroTechParts {
	nose, 
	left_wing,
	right_wing,
	aft,
	cockpit,
	windshield,
	body
};

// Sections are logical units described by the game rules
ENUM_CLASS AeroTechSections {
	Nose,
	Aft,
	LeftWing,
	RightWing
};

////////////////////////////////////////////////////////////////////////////////
// Constructor

AeroTech::AeroTech(Aerotech_Subtype aSubType)
: Vehicle("AeroTech", AeroTechID, nullptr, AeroTechMaxParts, "NALR"),
  mLastRoll(0.0),
  mCurrRoll(0.0),
  mNextRoll(0.0),
  mLastPitch(0.0),
  mCurrPitch(0.0),
  mNextPitch(0.0),
  mDamagedAvionics(0),
  mThrust(1.0),
  mStructuralIntegrityDamage(0.0),
  mGearDestroyed(false),
  mCurrentHeat(0.0),
  mDestroyedFighterMovement(0.0, 0.0, 0.0),
  mSafeThrust(0),
  mSubType(aSubType)
{
	SET_SCOPE;
	mSections.resize(AeroTechMaxSections);

	for (int i = 0; i < AeroTechMaxSections; i++) {
		mSections[i].sectionName = tr(i + 53);
		mSectionDamage[i] = 0;
	}

	RETURN;
}

////////////////////////////////////////////////////////////////////////////////
// Destructor

AeroTech::~AeroTech()
{
}

////////////////////////////////////////////////////////////////////////////////
// Read tags and attributes that are specific to an AeroTech from XML

void AeroTech::readSpecialAttributesFromRoot(XMLTreeConstIterator& iter)
{
	SET_SCOPE;
	mSpeed = 0.2 * mMetersPerSecond;
	mStructuralIntegrity = iter.getDoubleAttributeByName("si", 1);
	mOriginalStructuralIntegrity = mStructuralIntegrity;

	BGCPString engineStr;
	if (iter.getAttributeValueByName("engine", engineStr)) {
		if (engineStr.endsWith("XL")) {
			EngineType = ENGINE_TYPE_XL;
		} else {
			EngineType = ENGINE_TYPE_STD;
		}
		engineRating = (int)engineStr.toInt();

		// Aerospace fighters get two extra points of safe thrust, but they must use 2 points of thrust
		// to offset gravity ...
		mSafeThrust = 2 + engineRating / tonnage;
		mMetersPerSecond = 45 * (mSafeThrust-2); // max thrust = 1.5 * safe thrust, 1 thrust points gives about 300 meters in 10 s
	} else {
		mMetersPerSecond = iter.getDoubleAttributeByName("metersPerSecond");
		engineRating = 1;
	}

	BGCPString hsType;
	if (iter.getAttributeValueByName("hsType", hsType)) {
		if (hsType.equalsNoCase("single")) {
			HeatSinkType = IS_Heat_Sink;
		} else if (hsType.equalsNoCase("double")) {
			if (techType == Tech_Type_Clan) {
				HeatSinkType = Clan_Double_Heat_Sink;
			} else {
				HeatSinkType = IS_Double_Heat_Sink;
			}
		}
	} else {
		HeatSinkType = IS_Heat_Sink;
	}

	RETURN;
}

void AeroTech::readSpecialAttributes(XMLTreeConstIterator& iter, const std::vector<RenderableObject*>& geometries, const PathChar* mechDir)
{
	SET_SCOPE;
	BGCPString nodeName;
	iter.getNodeName(nodeName);

	if (nodeName.equalsNoCase("windshield")) {
		placePart(iter, geometries, (int) AeroTechParts::windshield, mechDir);
	} else if (nodeName.equalsNoCase("nose")) {
		placePart(iter, geometries, (int) AeroTechParts::nose, mechDir);
	} else if (nodeName.equalsNoCase("aft")) {
		placePart(iter, geometries, (int) AeroTechParts::aft, mechDir);
	} else if (nodeName.equalsNoCase("leftwing")) {
		placePart(iter, geometries, (int) AeroTechParts::left_wing, mechDir);
	} else if (nodeName.equalsNoCase("rightwing")) {
		placePart(iter, geometries, (int) AeroTechParts::right_wing, mechDir);
	} else if (nodeName.equalsNoCase("cockpit")) {
		placePart(iter, geometries, (int) AeroTechParts::cockpit, mechDir);
	} else if (nodeName.equalsNoCase("critical-layout")) {
		// Defines where to place weapons in 3D
		criticalLayouts.resize(AeroTechMaxSections);
		for (size_t k = 0; k < iter.getChildCount(); k++) {
			iter.goToChild(k);
			BGCPString sectionName;
			iter.getNodeName(sectionName);
			int location = getSectionByName(sectionName);
			if (location >= 0 && location <= AeroTechMaxSections) {
				criticalLayouts[location].start.x = iter.getDoubleAttributeByName("sx");
				criticalLayouts[location].start.y = iter.getDoubleAttributeByName("sy");
				criticalLayouts[location].start.z = iter.getDoubleAttributeByName("sz");
				criticalLayouts[location].direction.x = iter.getDoubleAttributeByName("dx");
				criticalLayouts[location].direction.y = iter.getDoubleAttributeByName("dy");
				criticalLayouts[location].direction.z = iter.getDoubleAttributeByName("dz");
			}
			iter.goToParent();
		}
	}
	RETURN;
}

// Compute additional values after loading a config (like heat sinks)
void AeroTech::postLoadConfig()
{
	// While Mechs distinguish between heat sinks that have criticals and heat sinks that have mass,
	// let's keep things simple for Aerotechs: 10 heat sinks are integral to the engine and don't take 
	// mass or criticals, while all others do (aerotechs don't even a normal critical hits table under normal BT rules)
	SET_SCOPE;
	int hsCrits = 0;
	switch(HeatSinkType) {
		case IS_Heat_Sink:
			hsCrits = countCriticals(IS_Heat_Sink);
			heatSinks = 10 + hsCrits;
			RETURN;
		case IS_Double_Heat_Sink:
			hsCrits = countCriticals(IS_Double_Heat_Sink);
			heatSinks = 10 + hsCrits/3;
			RETURN;
		default:
		case Clan_Double_Heat_Sink:
			hsCrits = countCriticals(Clan_Double_Heat_Sink);
			heatSinks = 10 + hsCrits/2;
			RETURN;
	}
}

////////////////////////////////////////////////////////////////////////////////
// Help function for parsing that translates a section name to a number
// We cannot simply compare with the section names, because these may contain spaces
// and may be translated

int AeroTech::getSectionByName(const BGCPString& str)
{
	SET_SCOPE;
	BGCPString strlc = str.lowerStringASCII();
	if (strlc == "nose") {
		RETURN 0;
	} else if (strlc == "leftwing") {
		RETURN 1;
	} else if (strlc == "rightwing") {
		RETURN 2;
	} else if (strlc == "aft") {
		RETURN 3;
	}

	RETURN -1;
}

////////////////////////////////////////////////////////////////////////////////
// Show or hide the vehicle's windshiled (cockpit view may require it to be hidden)

void AeroTech::windshieldVisible(bool newVal)
{
	SET_SCOPE;
	RenderableObject* child = getObject((int) AeroTechParts::windshield);
	if (child != nullptr) {
		if (newVal) {
			child->clearFlag(RenderableObject::Flag_invisible);
		} else {
			child->setFlag(RenderableObject::Flag_invisible);
		}
	}
	RETURN;
}

////////////////////////////////////////////////////////////////////////////////
// Check for a collision with a polygon and handle it

void AeroTech::checkCollision(const MeshPolygon* poly, Point3D& movement) 
{
	// Aerotechs may touch ground only while starting or landing with the gears
	// Everything else is crashing with damage

	setStatus(VehicleStatus::destroyed);
}

////////////////////////////////////////////////////////////////////////////////
// Get texture used for the cockpit (used for rendering the instruments)

Mesh* AeroTech::getCockpitMesh()
{
	SET_SCOPE;
	Mesh* cockpitMesh = dynamic_cast<Mesh*>(getObject((int) AeroTechParts::cockpit));
	if (cockpitMesh == nullptr) {
		cockpitMesh = this;
	}
	RETURN cockpitMesh;
}

////////////////////////////////////////////////////////////////////////////////
// Based on the vehicle type, connect the vehicle to a suitable AI

AbstractVehicleAI* AeroTech::createAI(bool isPlayer, int environment)
{
	// Mark all criticals as undamaged
	SET_SCOPE;
	mCriticalDamaged.resize(AeroTechMaxSections);
	for (int i = 0; i < AeroTechMaxSections; i++) {
		for (int j = 0; j < getCriticalCount(i); j++) {
			mCriticalDamaged[i].push_back(false);
		}
	}
	mFuel = 80 * countWeaponInAllSections(WeaponType::IS1_Fuel);

	// Construct AI or player
	if (isPlayer) {
		mAI = new FighterPlayer(this);
	} else {
		if (environment == space) {
			mAI = new FighterAISpace(this);
		} else {
			mAI = new FighterAI(this);
		}
	}

	RETURN mAI;
}

// Set current animation matrices
void AeroTech::setMatrices(double /*speed*/, DWORD /*missionTime*/)
{
	SET_SCOPE;
	Point3D pos = getPosition();
	TranslationMatrix t1 (pos.x, pos.y, pos.z);
	Matrix M = t1 * Matrix(p_left, p_up, p_front);

	for (size_t i = 0; i < getObjectCount(); ++i) {
		RenderableObject* ro = getObject(i);
		ro->setMatrix(M);
	}

	RETURN;
}

// After loading a config, use this function to set min/max speeds using vehicle-type-specific rules
void AeroTech::setMinMaxSpeed()
{
	mMinSpeed = 0.0;
	mMaxSpeed = mMetersPerSecond;
}

// Check for for critical damage, e.g. if a weapon is destroyed by a critical hit
void AeroTech::checkCriticalDamage(int section, int criticalToDestroy)
{
	if (section >= AeroTechMaxSections 
	|| criticalToDestroy >= (int) mCriticalDamaged[section].size()) 
	{
		return;
	}

	SET_SCOPE;
	mCriticalDamaged[section][criticalToDestroy] = true;
	WeaponType weaponType = getCriticals(section, criticalToDestroy);

	switch(weaponType) {
		case WeaponType::CE_Engine:
			// Count engine hits. The third engine hit disables the Mech until it can be repaired.
			// todo: reduce thrust according when the engine is hit.
			++mEngineHits;
			RETURN;
		case WeaponType::CE_Life_Support:
			// todo: Any hit disables the system completely, resulting in damage to the pilot in case of heat or hostile environments ???
			//mLifeSupportDestroyed = true;
			RETURN;
		case WeaponType::CE_Cockpit:
			// A hit to the cockpit kills the pilot and disable this aircraft
			setStatus(VehicleStatus::destroyed);
			RETURN;
		case WeaponType::CE_Sensors:
			// todo ...
			RETURN;
		case WeaponType::IS_Heat_Sink:
		case WeaponType::IS_Double_Heat_Sink:
		case WeaponType::Clan_Double_Heat_Sink:
			// Count damaged heat sink criticals
			// (this is a simplification compared to BT rules, where each critical hit can disable a heat sink completely)
			++mHeatSinkCriticalHits;
			RETURN;
		case WeaponType::IS1_Avionics:
			++mDamagedAvionics;
			RETURN;
		case WeaponType::IS1_Gear:
			mGearDestroyed = true;
			RETURN;
		case WeaponType::IS1_Fuel:
			mFuel -= irand(0, 20);
			RETURN;
		default:
			// A weapon was destroyed, mark it as unusable
			MountedWeapon* weapon = getMountedWeapon(section, criticalToDestroy);
			if (weapon != nullptr) {
				weapon->destroyed = true;
				// todo: check for ammo explosion. Fireworks!
				// todo: check for exploded Gauss rifles ...
				// todo: show destroyed weapons in grey in the HUD, do not allow AI or players to fire destroyed weapons
			}
			RETURN;
	}
}

// Function to notify the GameObject that it is taking damage
// See AeroTech, second edition, p.17 to 21 for details
// Critical hits are still missing
void AeroTech::receiveDamage(RenderableObject* limb, double damage, GameObject* sender, const Point3D& pointOfImpact)
{
	if (sender == this) {
		return; // ignore self-inflicted damage ...
	}

	// Temporarily roll a location randomly, may be corrected below
	SET_SCOPE;
	int hitSection = irand<int>(0, 4);

	for (int i = 0; i < 3; ++i) {
		if (limb == getObject(i)) {
			hitSection = i;
		}
	}

	int criticalHits = 0;
	if (damage > 0.1 * mArmor[hitSection]) {
		// Section armor threshold exceeded
		criticalHits = 2;
	}

	auto sectionCriticals = getCriticalCount(hitSection);
	size_t sectionDamagedCriticals = std::count(mCriticalDamaged[hitSection].begin(), mCriticalDamaged[hitSection].end(), true);
	if (mSectionDamage[hitSection] <= mArmor[hitSection]) {
		// Apply damage to armor
		SET_OP;
		mSectionDamage[hitSection] += damage;
	} else {
		// Damage exceeds remaining armor
		SET_OP;
		damage -= mArmor[hitSection] - mSectionDamage[hitSection];
		mSectionDamage[hitSection] = mArmor[hitSection];
		mStructuralIntegrityDamage += 0.5*damage;

		if (mStructuralIntegrityDamage > mStructuralIntegrity) {
			setStatus(VehicleStatus::destroyed);
		} else {
			double criticalsDamagedPercent = sectionDamagedCriticals / sectionCriticals;
			double siLost = mStructuralIntegrityDamage / mOriginalStructuralIntegrity;
			if (siLost > criticalsDamagedPercent) {
				criticalHits = std::max(criticalHits, 1);
			}
		}
	}

	if (criticalHits > 0 && (int) sectionDamagedCriticals < sectionCriticals) {
		if (((int)sectionDamagedCriticals) + criticalHits > sectionCriticals) {
			// Mark all equipment in that section as destroyed
			for (int i = 0; i < sectionCriticals; ++i) {
				if (!mCriticalDamaged[hitSection][i]) {
					checkCriticalDamage(hitSection, i);
				}
			}
		} else {
			// Roll critical hit locations
			for (int i = 0; i < criticalHits; ++i) {
				int criticalToDestroy;
				do {
					criticalToDestroy = irand(0, sectionCriticals);
				} while (mCriticalDamaged[hitSection][criticalToDestroy] == true);
				checkCriticalDamage(hitSection, criticalToDestroy);
			}
		}
	}
	
	RETURN;
}

// Return the number of the part corresponding to a section
// used by updateLoadoutGeometry() to find the mesh corresponding to the mech section
int AeroTech::section2part(int section, bool allowLowerArms)
{
	return 0;
}

// Jet cockpits do not have a reticle that moves horizontally
bool AeroTech::reticleMovesHorizontally() const
{
	return false;
}

// Get/set armor values
DWORD AeroTech::getArmor(int section, int /*ArmorType*/) const
{
	SET_SCOPE;
	if (section < AeroTechMaxSections) {
		RETURN mArmor[section];
	} else {
		RETURN 0;
	}
}

bool AeroTech::setArmor(int section, DWORD newVal, int /*ArmorType*/)
{
	SET_SCOPE;
	if (section < AeroTechMaxSections) {
		mArmor[section] = newVal;
		RETURN true;
	} else {
		RETURN false;
	}
}

// Querying this for an aircraft will give its structural integrity instead
DWORD AeroTech::getInternalStructure(int section) const
{
	return (DWORD) mStructuralIntegrity;
}

// Query damage to armor and internal structure
double AeroTech::getArmorDamage(int section, int armorType) const
{
	return mSectionDamage[section];
}

// Calculate heat for next frame
void AeroTech::calcNextFrameHeat(DWORD now, double deltaT)
{
	SET_SCOPE;
	double heatDelta = 0.0;

	// Movement does not cause heat for aerospace fighters

	// Add heat from engine hits
	heatDelta += deltaT * 0.5 * getEngineHits();

	// Add heat from fired weapons
	size_t weaponsCount = getWeaponListEntryCount();
	for (size_t i = 0; i < weaponsCount; ++i) {
		double lastFired = getLastFireTime(i);
		const MWEquipment* baseWeapon = getBaseWeapon(i);
		if (lastFired > 0 && lastFired + 1000.0 * baseWeapon->fireDuration  > now) {
			heatDelta += deltaT * baseWeapon->Heat / baseWeapon->fireDuration;
		}
	}

	// Subtract cooling from heat sinks
	// todo: factor in mHeatSinkCriticalHits
	int effectiveHeatSinks = getEffectiveHeatSinks();
	heatDelta -= 0.1 * effectiveHeatSinks * deltaT; // One heat sink gets rid of 1 point of heat per 10 seconds, so 0.1 points per second

	// todo: double effectiveness of heat sinks that are below water (max. of 6 additional heat points)
	
	mCurrentHeat = std::max(mCurrentHeat + heatDelta, 0.0);

	// todo: On high heat, the jet may shut down or ammo may explode
	// At certain heat levels, the jet may shut down or suffer random movement
	// todo: MechWarrior may take damage if heat reaches or exceeds 15.0}

	RETURN;
}

// Get current heat value for vehicle
double AeroTech::getHeat() const NOEXCEPT
{
	return mCurrentHeat;
}

// Set thrust while considering minimum thrust required by the environment
void AeroTech::setThrust(double newVal, int env)
{
	SET_SCOPE;
	if (env == space) {
		mThrust = newVal;
	} else {
		mThrust = 0.1 + 0.9 * newVal;
	}
	RETURN;
}

// Calculate movement for next frame
void AeroTech::calcNextFrameMovement(double deltaT, RenderableObject* scene, DWORD now, double gravity)
{
	MechSim* mechSim = getMechSim();
	if (mechSim == nullptr) {
		return;
	}

	SET_SCOPE;
	auto env = mechSim->getEnvironment();

	// Sometimes numbers get out of hand - in that case, reset them to stable values 
	// (observed with SPA1 and SPA2)
	double p_front_len = p_front.length();
	double p_left_len = p_left.length();
	double p_up_len = p_up.length();
	if (p_front_len < 0.9 || p_front_len > 1.1
	||  p_left_len  < 0.9 || p_left_len  > 1.1
	||  p_up_len    < 0.9 || p_up_len    > 1.1)
	{
		p_front = Point3D( 0, 0, 1);
		p_left  = Point3D(-1, 0, 0);
		p_up    = Point3D( 0, 1, 0);
		mCurrRoll = 0;
		mCurrPitch = 0;
	}

	if (mPosition.length() > 100000) {
		mPosition = Point3D(0, 500, 500);
	}

	if (isDestroyed()) {
		if (mPosition.y <= 0.0) {
			// Destroyed aerospace fighter has hit the ground
			mPosition.y = 0.0;
			mSpeed = 0.0;
		} else {
			// Destroyed aerospace fighters cannot accelerate. Instead, they lose speed and they fall until they hit the ground.
			mCurrPitch = 1.0;
			mCurrRoll = 0.0;
			mDestroyedFighterMovement += Point3D(0, -deltaT * gravity, 0);
			mPosition += deltaT * mDestroyedFighterMovement;
		}
		setMatrices(getSpeed(), now);
		RETURN;
	}

	// todo: check if this aerospace fighter crashed into an obstacle or other vehicle
	SET_OP;
	if (mPosition.y < 0.0 && env != space) {
		int damagedLimbNum = irand(0, AeroTechMaxSections);
		RenderableObject* damagedLimb = getObject(damagedLimbNum);
		receiveDamage(damagedLimb, irand(0, 10), nullptr, Point3D());
	}

	SET_OP;
	double pitchRate = getPitchRate(env);
	double rollRate  = getRollRate();
	if (mCurrRoll != 0.0) {
		Matrix M = RotationMatrix(M_PI * mCurrRoll * rollRate * deltaT / 180.0, p_front.x, p_front.y, p_front.z);
		p_left = M.Transform(p_left);
		p_up = M.Transform(p_up);
	}

	if (mCurrPitch != 0.0) {
		Matrix M = RotationMatrix(M_PI * mCurrPitch * pitchRate * deltaT / 180.0, p_left.x, p_left.y, p_left.z);
		p_front = M.Transform(p_front);
		p_up = M.Transform(p_up);
	}

	// Accelerate or decelerate based on thrust changes
	mMobility = 1.0 - 0.2 * getEngineHits();
	double maxDeltaSpeed = 2 * mSafeThrust * deltaT * mMobility; // Vandal has safe thrust of 21, 2 * 21 yields 4.2 g
	if (env == normal) {
		double desiredSpeed = mMobility * mThrust * getMaxSpeed();
		double deltaSpeed = desiredSpeed - mSpeed;
		if (deltaSpeed > maxDeltaSpeed) {
			deltaSpeed = maxDeltaSpeed;
		}
		if (deltaSpeed < -maxDeltaSpeed) {
			deltaSpeed = -maxDeltaSpeed;
		}
		mSpeed += deltaSpeed;
		mMovementVector = mSpeed * p_front;
	} else {
		// In space, no one will hear you scream
		mMovementVector += mThrust * maxDeltaSpeed * p_front;
		mSpeed = mMovementVector.length();
	}

	SET_OP;
	Point3D position = getPosition() + deltaT * mMovementVector;
	setPosition(position);
	setMatrices(getSpeed(), now);
	RETURN;
}

double AeroTech::getPitchRate(int env) const
{
	SET_SCOPE;
	double avionicsHealth = 1.0 - 0.1 * mDamagedAvionics;

	if (env == space) {
		RETURN avionicsHealth * 180.0;
	} else {
		// In atmospheric flight, the pitch rate depends on speed
		double currSpeed = getSpeed();
		if (currSpeed < 100) {
			RETURN 50 - 30 * currSpeed / 100;
		} else {
			RETURN avionicsHealth * 20.0;
		}
	}
}

double AeroTech::getRollRate() const
{
	SET_SCOPE;
	double avionicsHealth = 1.0 - 0.1 * mDamagedAvionics;
	RETURN avionicsHealth * 180.0;
}

// Set colors for wireframe rendering according to section and damage
void AeroTech::setWireframeColors()
{
	SET_SCOPE;
	for (size_t section = 0; section < AeroTechMaxSections; ++section) {
		RenderableObject* child = getObject(section);
		if (child != nullptr) {
			Color c(1, 0, 0);
			if (vehicleStatus != VehicleStatus::destroyed) {
				ColorBase sectionDamage = (ColorBase)(mSectionDamage[section] / mArmor[section]);
				c = Color(sectionDamage, 0, 1.0f - sectionDamage);
			}
			child->setRecursiveWireframeColor(c);
		}
	}
	RETURN;
}

void AeroTech::setStatus(VehicleStatus newStatus)
{
	SET_SCOPE;
	if (vehicleStatus != newStatus 
	&&  newStatus == VehicleStatus::destroyed) 
	{
		mDestroyedFighterMovement = getSpeed() * p_front;
	}
	Vehicle::setStatus(newStatus);
	RETURN;
}

// Get max armor allowed for a section
int AeroTech::getMaxArmor(int section)
{
	return 100;
}
