////////////////////////////////////////////////////////////////////////////////
// VTOL.cpp
// Implementation of a Vehicle class for VTOLs
// Copyright Bjoern Ganster
// Developed since 2021
////////////////////////////////////////////////////////////////////////////////

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

#include <iostream>

#define FILE_NUMBER FILE_NO_COMBAT_VEHICLE_CPP

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

#define VTOLMaxSections 5

ENUM_CLASS VTOLSections {
	body,
	cockpit,
	turret,
	windshield,
	rotor
};

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

VTOL::VTOL(CombatVehicleTypes subType)
: Vehicle("VTOL", VTOL_ID, nullptr, 6, "FALR"),
  mTurretTurn(0.0), 
  mTurretTilt(0.0), 
  mTurretTurnLimit(0.0), 
  mTurretTiltLimit(0.0)
{
	SET_SCOPE;
	mSections.resize(VTOLMaxSections);

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

	switch (subType) {
		case CombatVehicleTypes::submarine:
			mMaxTurnPerSecond = 20.0;
			break;
		case CombatVehicleTypes::vtol:
			mMaxTurnPerSecond = 30.0;
			break;
	}

	RETURN;
}

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

VTOL::~VTOL()
{
}

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

void VTOL::readSpecialAttributesFromRoot(XMLTreeConstIterator& iter)
{
	SET_SCOPE;
	mMetersPerSecond = iter.getDoubleAttributeByName("metersPerSecond", mMetersPerSecond);
	mTurretTurnLimit = iter.getDoubleAttributeByName("turretTurnLimit", 90.0);
	mTurretTiltLimit = iter.getDoubleAttributeByName("turretTiltLimit", 30.0);
	RETURN;
}

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

void VTOL::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) VTOLSections::turret, mechDir);
	} else if (nodeName == "windshield") {
		placePart(iter, geometries, (int) VTOLSections::windshield, mechDir);
	} else if (nodeName == "Cockpit") {
		placePart(iter, geometries, (int) VTOLSections::cockpit, mechDir);
	} else if (nodeName == "critical-layout") {
		// Defines where to place weapons in 3D
		criticalLayouts.resize(VTOLMaxSections);
		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 <= VTOLMaxSections) {
				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 VTOL::getSectionByName(const BGCPString& str)
{
	SET_SCOPE;
	BGCPString strlc = str.lowerStringASCII();
	if (strlc == "front") {
		RETURN (int) VTOLSections::body;
	} else if (strlc == "leftside") {
		RETURN (int) VTOLSections::body;
	} else if (strlc == "rightside") {
		RETURN (int) VTOLSections::body;
	} else if (strlc == "rear") {
		RETURN (int) VTOLSections::body;
	} else if (strlc == "turret") {
		RETURN (int) VTOLSections::turret;
	} else if (strlc == "windshield") {
		RETURN (int) VTOLSections::windshield;
	} else if (strlc == "rotor") {
		RETURN (int) VTOLSections::rotor;
	} else {
		RETURN -1;
	}
}

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

// Set direction where the vehicle is aiming, by moving the turret (vehicles) or twisting and tilting the torso (mechs)
void VTOL::setAim(double turn, double tilt)
{
	mTurretTurn = putInRange(-mTurretTurnLimit, turn, mTurretTurnLimit);
	mTurretTilt = putInRange(-mTurretTiltLimit, tilt, mTurretTiltLimit);
}

// Set current animation matrices
void VTOL::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[(size_t)VTOLSections::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[(size_t)VTOLSections::turret]);
	setMatrixIfNN(getObject((size_t)VTOLSections::turret), turretMatrix);

	RETURN;
}

// Move aim towards given angles by at most delta
void VTOL::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);
	//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 VTOL::getAimTilt() const
{
	return mTurretTilt;
}

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

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

// Check for a collision with a polygon and handle it
void VTOL::checkCollision(const MeshPolygon* poly, Point3D& movement) 
{
	// todo: any collision likely will destroy the vtol, because most likely the rotors will be involved and they break easily ...
}

// Based on the vehicle, connect the vehicle to a suitable AI
AbstractVehicleAI* VTOL::createAI(bool isPlayer, int environment)
{
	SET_SCOPE;
	if (isPlayer) {
		mAI = new PlayerGroundVehicle(this);
	} else {
		mAI = new VTOLAI("VTOLAI", this);

	}
	RETURN mAI;
}

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

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

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

// Calculate distance from ground 
void VTOL::calcDistFromGround(const Point3D& p, const RenderableObject* obj, double& value)
{
	if (obj == this || obj == nullptr) {
		return;
	}

	SET_SCOPE;
	BoundingBox bb = obj->getBoundingBox();
	double dist = bb.getDistance(p);

	if (dist < mLift) {
		const Mesh* mesh = dynamic_cast<const Mesh*>(obj);
		if (mesh != nullptr) {
			for (size_t i = 0; i < mesh->getPolygonCount(); ++i) {
				const MeshPolygon* poly = mesh->getPolygon(i);
				if (poly != nullptr) {
					double dist = poly->distanceTo(p);
					if (dist < value) {
						value = dist;
					}
				}
			}
		}
	}

	SET_OP;
	for (size_t i = 0; i < obj->getObjectCount(); ++i) {
		const RenderableObject* child = obj->getObject(i);
		if (child != nullptr) {
			calcDistFromGround(p, child, value);
		}
	}

	RETURN;
}

// Calculate movement for next frame
void VTOL::calcNextFrameMovement(double deltaT, RenderableObject* scene, DWORD now, double gravity)
{
	SET_SCOPE;
	MechSim* mechSim = getMechSim();
	Point3D oldPos = getPosition();
	Point3D thisFrameMovement = deltaT * mMovement;
	BoundingBox vehicleBB = getTransformedBB();
	vehicleBB.applyMovement(thisFrameMovement);
	double distFromGround = vehicleBB.min().y;
	calcDistFromGround(oldPos, scene, distFromGround);

	if (isDestroyed()) {
		if (distFromGround <= mLift) {
			SET_OP;
			mMovement = Point3D();
		} else {
			SET_OP;
			mMovement.y -= gravity * deltaT;
		}
		setPosition(oldPos + mMovement * deltaT);
		setMatrices(getSpeed(), now);
		RETURN;
	}

	// Check the new position against the scene and update the position, until all collisions have been resolved
	SET_OP;
	setMatrices(getSpeed(), now);
	Matrix m;
	cacheTransformedVertices(m);

	// Vehicles must not fall through the ground at zero no matter what
	if (distFromGround <= mLift) {
		mAcceleration.y = 10.0;
	} else {
		mAcceleration.y = -0.5 * mMovement.y;
	}

	SET_OP;
	if (abs(m_speedDelta) > 0.1) {
		mSpeed += 40.0 * m_speedDelta * deltaT;
	}

	double speedLimit = getMaxSpeed();
	if (mMovement.y >= speedLimit) {
		mMovement.y = speedLimit;
	}

	// Now we can update the position
	SET_OP;
	mMovement += mAcceleration * deltaT;
	setPosition(oldPos + mMovement * deltaT);
	setMatrices(getSpeed(), now);

	// mobility is already factored into m_speed
	mMovement.x = getSpeed() * p_front.x;
	mMovement.z = getSpeed() * p_front.z;
	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));
	RETURN;
}