////////////////////////////////////////////////////////////////////////////////
// CombatVehicle.cpp
// Copyright Bjoern Ganster
// Developed since 2021
////////////////////////////////////////////////////////////////////////////////

#include "CombatVehicle.h"
#include "GroundVehicleAI.h"
#include "PlayerGroundVehicle.h"
#include "VTOLAI.h"

#include <iostream>

#define FILE_NUMBER FILE_NO_COMBAT_VEHICLE_CPP

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

#define CombatVehicleMaxSections 5

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

CombatVehicle::CombatVehicle(CombatVehicleTypes subType)
: Vehicle("CombatVehicle", CombatVehicleID, nullptr, 6, "FALRT"),
  mSubType(subType),
  mTurretTurn(0.0),
  mTurretTilt(0.0),
  mTurretTurnLimit(0.0),
  mTurretTiltLimit(0.0)
{
	SET_SCOPE;
	mSections.resize(CombatVehicleMaxSections);

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

	RETURN;
}

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

CombatVehicle::~CombatVehicle()
{
}

////////////////////////////////////////////////////////////////////////////////
// Load hovercraft from XML

void CombatVehicle::readSpecialAttributesFromRoot(XMLTreeConstIterator& iter)
{
	SET_SCOPE;
	mMetersPerSecond = iter.getDoubleAttributeByName("metersPerSecond", mMetersPerSecond);
	mTurretTurnLimit = iter.getDoubleAttributeByName("turretTurnLimit", 90.0);
	mTurretTiltLimit = iter.getDoubleAttributeByName("turretTiltLimit", 30.0);
	engineRating = (int)iter.getIntAttributeValueByName("engine", 100);
	RETURN;
}

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

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

	if (nodeName == "turret") {
		placePart(iter, geometries, (int) CombatVehicleSections::turret, mechDir);
	} else if (nodeName == "windshield") {
		placePart(iter, geometries, (int) CombatVehicleSections::windshield, mechDir);
	} else if (nodeName == "Cockpit") {
		placePart(iter, geometries, (int) CombatVehicleSections::cockpit, mechDir);
	} else if (nodeName == "critical-layout") {
		// Defines where to place weapons in 3D
		criticalLayouts.resize(CombatVehicleMaxSections);
		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 <= CombatVehicleMaxSections) {
				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;
}

////////////////////////////////////////////////////////////////////////////////
// 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 CombatVehicle::getSectionByName(const BGCPString& str)
{
	SET_SCOPE;
	BGCPString strlc = str.lowerStringASCII();
	if (strlc == "front") {
		RETURN (int) CombatVehicleSections::body;
	} else if (strlc == "leftside") {
		RETURN (int) CombatVehicleSections::body;
	} else if (strlc == "rightside") {
		RETURN (int) CombatVehicleSections::body;
	} else if (strlc == "rear") {
		RETURN (int) CombatVehicleSections::body;
	} else if (strlc == "turret") {
		RETURN (int) CombatVehicleSections::turret;
	} else if (strlc == "windshield") {
		RETURN (int) CombatVehicleSections::windshield;
	} else if (strlc == "rotor") {
		RETURN (int) CombatVehicleSections::rotor;
	} else {
		RETURN -1;
	}
}

// Show or hide the vehicle's windshield (cockpit view may require it to be hidden)
void CombatVehicle::windshieldVisible(bool newVal)
{
	RenderableObject* child = getObject((int)CombatVehicleSections::windshield);
	if (child != nullptr) {
		if (newVal) {
			child->clearFlag(RenderableObject::Flag_invisible);
		} else {
			child->setFlag(RenderableObject::Flag_invisible);
		}
	}
}

// Set direction where the vehicle is aiming, by rotating the turret
void CombatVehicle::setAim(double turn, double tilt)
{
	mTurretTurn = putInRange(-mTurretTurnLimit, turn, mTurretTurnLimit);
	mTurretTilt = putInRange(-mTurretTiltLimit, tilt, mTurretTiltLimit);
}

// Set current animation matrices
void CombatVehicle::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);
	}

	Matrix turretMatrix = M;
	turretMatrix = turretMatrix * TranslationMatrix(m_joint[(int)CombatVehicleSections::turret]);
	turretMatrix = turretMatrix * RotationMatrix(M_PI * mTurretTurn / 180.0, 0, 1, 0);
	turretMatrix = turretMatrix * RotationMatrix(M_PI * mTurretTilt / 180.0, 1, 0, 0);
	turretMatrix = turretMatrix * TranslationMatrix(-m_joint[(int)CombatVehicleSections::turret]);
	setMatrixIfNN(getObject((int)CombatVehicleSections::turret), turretMatrix);

	RETURN;
}

// Move aim towards given angles by at most delta
void CombatVehicle::adjustAim(double desiredTorsoTwist, double desiredTorsoTilt, double delta)
{
	// Only one solution is possible, left or right. For Mechs like the Urbanmech, the torso won't turn beyond a certain limit,
	// so we need to find the right solution.
	SET_SCOPE;
	double torsoTurnDelta = 0.0;

	// desiredTorsoTwist is given as the desired direction against north, including walking direction.
	// Change the coordinate system for currentTorsoDir to be relative to the walking direction instead
	desiredTorsoTwist = fixAngle(-180.0, desiredTorsoTwist, 180.0);
	if (mTurretTurn - 1.0 > desiredTorsoTwist) {
		torsoTurnDelta = -1.0; // turn right
	} else if (mTurretTurn + 1.0 < desiredTorsoTwist) {
		torsoTurnDelta = 1.0; // turn left
	}
	if (mTurretTurn - 5.0 > desiredTorsoTwist) {
		torsoTurnDelta = -5.0; // turn right
	} else if (mTurretTurn + 5.0 < desiredTorsoTwist) {
		torsoTurnDelta = 5.0; // turn left
	}

	torsoTurnDelta = putInRange(-1.0, torsoTurnDelta, 1.0);
	//mVehicle->setAim(mTorsoTwist+torsoTurnDelta, 0.0);
	mTurretTurn = putInRange(-mTurretTurnLimit, mTurretTurn + torsoTurnDelta, mTurretTurnLimit);

	// Choose torso tilt
	// The mech wants to walk behind the enemy mech, but wants to aim right at it, so the angles differ
	if (mTurretTilt < desiredTorsoTilt
	&&  mTurretTilt + 1.0 < mTurretTiltLimit)
	{
		mTurretTilt += 1.0;
	}
	if (mTurretTilt > desiredTorsoTilt
	&&  mTurretTilt - 1.0 > -mTurretTiltLimit)
	{
		mTurretTilt -= 1.0;
	}

	RETURN;
}

// Get tilt and turn for current aim direction
double CombatVehicle::getAimTilt() const
{
	return mTurretTilt;
}

double CombatVehicle::getAimTurn() const
{
	return mTurretTurn;
}

// Get texture used for the cockpit
Mesh* CombatVehicle::getCockpitMesh()
{
	SET_SCOPE;
	Mesh* cockpitMesh = dynamic_cast<Mesh*>(getObject((int)CombatVehicleSections::cockpit));
	if (cockpitMesh == nullptr) {
		cockpitMesh = this;
	}
	RETURN cockpitMesh;
}

// Check for a collision with a polygon and handle it
void CombatVehicle::checkCollision(const MeshPolygon* poly, Point3D& movement) 
{
	// If only the left or right wheels touch ground, rotate the vehicle around its length axis
	// If only the front or rear vehicles touch ground, rotate the vehicle around its transverse axis (Querachse)
	// todo: Stop climbing if the polygon is too steep

	if (poly == nullptr || poly->getVertexCount() < 3) {
		return;
	}

	SET_SCOPE;
	BoundingBox polyBB = poly->getTransformedBB();

	// Simply check the corner points of the bounding box against the polygon ...
	bool touchesThisPoly = false;
	double moveUp = 0.0;
	BoundingBox vehicleBB = getTransformedBB();
	vehicleBB.applyMovement(movement);
	if (vehicleBB.valid()
	&& polyBB.intersects(vehicleBB)) 
	{
		for (size_t i = 0; i < getObjectCount(); ++i) {
			RenderableObject* bodyPart = getObject(i);
			if (bodyPart != nullptr) {
				BoundingBox bodyPartBB = bodyPart->getTransformedBB();
				bodyPartBB.applyMovement(movement);
				if (bodyPartBB.valid()
				&& polyBB.intersects(bodyPartBB)) 
				{
					// Find the polygon(s) that cause the collision
					Mesh* bodyPartMesh = dynamic_cast<Mesh*>(bodyPart);
					for (size_t j = 0; j < bodyPartMesh->getPolygonCount(); ++j) {
						MeshPolygon* bodyPartPolygon = bodyPartMesh->getPolygon(j);
						if (poly->collides(bodyPartPolygon, movement)) {
							for (size_t k = 0; k < bodyPartPolygon->getVertexCount(); ++k) {
								int l = bodyPartPolygon->getVertexIndex(k);
								Point3D movedP = bodyPartMesh->getTransformedVertex(l) + movement;
								double moveUpHere = calcMoveUp(poly, movedP);
								if (moveUpHere > moveUp) {
									moveUp = moveUpHere;
									touchesThisPoly = true;
									mHasGroundContact = true;
								}
							}
						}
					}
				}
			}
		}
	}

	if (touchesThisPoly) {
		movement -= moveUp * poly->getTransformedNormal();
	}

	RETURN;
}

// Based on the vehicle, connect the vehicle to a suitable AI
AbstractVehicleAI* CombatVehicle::createAI(bool isPlayer, int environment)
{
	SET_SCOPE;
	if (mSubType == CombatVehicleTypes::submarine || mSubType == CombatVehicleTypes::vtol) {
		mAI = new VTOLAI("VTOLAI", this);
		RETURN mAI;
	} else {
		if (isPlayer) {
			mAI = new PlayerGroundVehicle(this);
		} else {
			mAI = new GroundVehicleAI("GroundVehicleAI", this);
		}
		RETURN mAI;
	}
}

bool CombatVehicle::reticleMovesVertically() const
{
	return true;
}

// Calculate heat for next frame
void CombatVehicle::calcNextFrameHeat(DWORD now, double deltaT)
{
	// Conventional vehicles are not allowed to produce heat
}

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

// Calculate movement for next frame
void CombatVehicle::calcNextFrameMovement(double deltaT, RenderableObject* scene, DWORD now, double gravity)
{
	SET_SCOPE;

	if (isDestroyed()) {
		if (hasGroundContact()) {
			mMobility = 0.0;
			mSpeed = 0.0;
		} else {
			mAcceleration = Point3D(0, -deltaT * gravity, 0);
		}
		m_turnDelta = 0.0;
	}

	// Update position
	bool movementChanged = false;
	Point3D oldPos = getPosition();
	Point3D thisFrameMovement = deltaT * mMovement;

	// Now check the new position against the scene and update the position, until all collisions have been resolved
	setHasGroundContact(false);
	int loops = 0;
	do {
		SET_OP;
		setMatrices(getSpeed(), now);
		Matrix m;
		cacheTransformedVertices(m);
		BoundingBox vehicleBB = getTransformedBB();
		vehicleBB.applyMovement(thisFrameMovement);

		// Vhicles must not fall through the ground at zero no matter what
		if (vehicleBB.min().y < mLift) {
			SET_OP;
			oldPos.y += mLift - vehicleBB.min().y;
			setHasGroundContact(true);
			movementChanged = true;
			thisFrameMovement.y = 0.0;
			mMovement.y = 0.0;
		} else {
			SET_OP;
			movementChanged = false;
			std::vector<RenderableObject*> colliders;
			findColliders(vehicleBB, scene, this, colliders);
			for (size_t i = 0; i < colliders.size(); ++i) {
				RenderableObject* collider = colliders[i];
				if (collider != nullptr) {
					Vehicle* otherVehicle = ::getVehicle(collider);
					if (otherVehicle != nullptr) {
						resolveObjectCollision(this, otherVehicle);
					} else {
						resolveGroundCollision(this, collider, vehicleBB, thisFrameMovement, movementChanged);
					}
				}
			}
		}
		++loops;
	} while (movementChanged && loops < MAX_COLLISION_RESOLVE_LOOPS);

	if (loops > MAX_COLLISION_RESOLVE_LOOPS) {
		COUT << "Failed to resolve collisions for";
		MVMKFN.printLn();
		mMovement = Point3D(0, 0, 0);
	}

	// Now we can update the position
	SET_OP;
	setPosition(oldPos + thisFrameMovement);
	setMatrices(getSpeed(), now);

	// todo: this code assumes we are dealing with a ground-based vehicle ...
	// but DropShips, AeroTechs, VTOLs may accelerate while in the air ...
	// todo: put this code into a decorator that can be connected to either Vehicle or VehicleAI as needed?
	SET_OP;
	if (hasGroundContact()) {
		if (abs(m_speedDelta) > 0.1) {
			setSpeed(getSpeed() + 40.0 * m_speedDelta * deltaT);
		}

		mMovement = getSpeed() * p_front; // mobility is already factored into m_speed
		mMovement.y = 0.0;
		mAcceleration.y = 0.0;
	} else {
		// todo: when in the air, the mech may turn but needs to keep its direction of movement
		mAcceleration.y -= gravity * deltaT;
		mMovement += deltaT * mAcceleration;
	}

	// todo: things get quirky here if we don't allow this also when the vhicle does not touch ground ...
	SET_OP;
	mHeading += m_turnDelta * deltaT;
	double degrees = M_PI * mHeading / 180.0;
	Matrix rot = RotationMatrix(degrees, 0.0, 1.0, 0.0);
	p_front = rot.Transform(Point3D(0, 0, 1));
	p_left = rot.Transform(Point3D(-1, 0, 0));

	SET_OP;
	int jjsActive = getActiveJumpJets();
	int jjs = getInteger(Vehicle::Info_JumpJets);
	if (jjsActive > 0) {
		if (mJumpJetFuel > 0.0) {
			double fuelForThisFrame = jjUseRate * jjsActive * deltaT;
			mJumpJetFuel -= fuelForThisFrame;
			mMovement += deltaT * mAcceleration;
		} else {
			deactivateJumpJets();
		}
	} else {
		mJumpJetFuel = std::min<double>(mJumpJetFuel + 0.5 * deltaT, jjs);
	}

	RETURN;
}

// Query damage to armor
// todo: simplified damage model? Would need CombatVehicle::receiveDamage?
double CombatVehicle::getArmorDamage(int section, int armorType) const
{
	/*SET_SCOPE;
	if (section >= 0 && section < (int) mArmorDamageFront.size()) {
		RETURN mArmorDamageFront[section];
	} else {
		RETURN 0.0;
	}*/
	return mDamageReceived;
}

// Query armor
// todo: simplified damage model? Would need CombatVehicle::receiveDamage?
DWORD CombatVehicle::getArmor(int section, int ArmorType) const
{
	/*SET_SCOPE;
	if (section < mFrontArmor.size()) {
		RETURN mFrontArmor[section];
	} else {
		RETURN 0;
	}*/
	return (DWORD)mHealth;
}
