//========= Copyright Valve Corporation, All rights reserved. ============//
// tf_bot_engineer_move_to_build.cpp
// Engineer moving into position to build
// Michael Booth, February 2009

#include "cbase.h"
#include "nav_mesh/tf_nav_mesh.h"
#include "tf_player.h"
#include "tf_gamerules.h"
#include "tf_obj_sentrygun.h"
#include "tf_weapon_builder.h"
#include "team_train_watcher.h"
#include "bot/tf_bot.h"
#include "bot/behavior/engineer/tf_bot_engineer_build.h"
#include "bot/behavior/engineer/tf_bot_engineer_move_to_build.h"
#include "bot/behavior/engineer/tf_bot_engineer_building.h"
#include "bot/map_entities/tf_bot_hint_sentrygun.h"
#include "bot/behavior/tf_bot_get_ammo.h"
#include "bot/behavior/tf_bot_retreat_to_cover.h"
#include "bot/behavior/engineer/tf_bot_engineer_build_teleport_exit.h"
#include "trigger_area_capture.h"

#include "raid/tf_raid_logic.h"


extern ConVar tf_bot_path_lookahead_range;

ConVar tf_bot_debug_sentry_placement( "tf_bot_debug_sentry_placement", "0", FCVAR_CHEAT );
ConVar tf_bot_max_teleport_exit_travel_to_point( "tf_bot_max_teleport_exit_travel_to_point", "2500", FCVAR_CHEAT, "In an offensive engineer bot's tele exit is farther from the point than this, destroy it" );
ConVar tf_bot_min_teleport_travel( "tf_bot_min_teleport_travel", "3000", FCVAR_CHEAT, "Minimum travel distance between teleporter entrance and exit before engineer bot will build one" );

//--------------------------------------------------------------------------------------------------------
static Vector s_pointCentroid;

int CompareRangeToPoint( CTFNavArea * const *area1, CTFNavArea * const *area2 )
{
	float d1 = ( (*area1)->GetCenter() - s_pointCentroid ).LengthSqr();
	float d2 = ( (*area2)->GetCenter() - s_pointCentroid ).LengthSqr();

	// reversed so farthest is sorted first in the vector
	if ( d1 < d2 )
		return 1;

	if ( d1 > d2 )
		return -1;

	return 0;
}


//---------------------------------------------------------------------------------------------
void CTFBotEngineerMoveToBuild::CollectBuildAreas( CTFBot *me )
{
	// if we have a predesignated build area, we're done
	if ( me->GetHomeArea() )
		return;

	m_sentryAreaVector.RemoveAll();

	CUtlVector< CTFNavArea * > pointAreaVector;
	Vector pointCentroid = vec3_origin;
	float pointEnemyIncursion = 0.0f;
	int i;

	int myTeam = me->GetTeamNumber();
	int enemyTeam = ( myTeam == TF_TEAM_BLUE ) ? TF_TEAM_RED : TF_TEAM_BLUE;

	CCaptureZone *zone = me->GetFlagCaptureZone();
	if ( zone )
	{
		// NOTE: Not strictly the right thing - should defend location of our team's flag
		CTFNavArea *zoneArea = (CTFNavArea *)TheTFNavMesh()->GetNearestNavArea( zone->WorldSpaceCenter(), false, 500.0f, true );
		if ( zoneArea )
		{
			pointAreaVector.AddToTail( zoneArea );
			pointCentroid += zoneArea->GetCenter();
			pointEnemyIncursion += zoneArea->GetIncursionDistance( enemyTeam );
		}
	}
	else if ( TFGameRules()->GetGameType() == TF_GAMETYPE_ESCORT )
	{
		CTeamTrainWatcher *trainWatcher;

		if ( myTeam == TF_TEAM_BLUE )
		{
			trainWatcher = TFGameRules()->GetPayloadToPush( me->GetTeamNumber() );
		}
		else
		{
			trainWatcher = TFGameRules()->GetPayloadToBlock( me->GetTeamNumber() );
		}

		if ( trainWatcher )
		{
			Vector checkpointPos = trainWatcher->GetNextCheckpointPosition();

			CTFNavArea *checkpointArea = (CTFNavArea *)TheTFNavMesh()->GetNearestNavArea( checkpointPos, false, 500.0f, true );
			if ( checkpointArea )
			{
				pointAreaVector.AddToTail( checkpointArea );
				pointCentroid += checkpointArea->GetCenter();
				pointEnemyIncursion += checkpointArea->GetIncursionDistance( enemyTeam );
			}
		}
	}
	else
	{
		// collect all areas overlapping the point
		CTeamControlPoint *ctrlPoint = me->GetMyControlPoint();
		if ( !ctrlPoint )
			return;

		const CUtlVector< CTFNavArea * > *ctrlPointAreaVector = TheTFNavMesh()->GetControlPointAreas( ctrlPoint->GetPointIndex() );

		if ( ctrlPointAreaVector )
		{
			for( i=0; i<ctrlPointAreaVector->Count(); ++i )
			{
				CTFNavArea *area = ctrlPointAreaVector->Element(i);

				pointAreaVector.AddToTail( area );
				pointCentroid += area->GetCenter();
				pointEnemyIncursion += area->GetIncursionDistance( enemyTeam );
			}
		}
	}

	if ( pointAreaVector.Count() == 0 )
		return;

	pointCentroid /= pointAreaVector.Count();
	pointEnemyIncursion /= pointAreaVector.Count();


	// collect all areas that can see the point
	CUtlVector< CTFNavArea * > exposedAreaVector;
	for( i=0; i<pointAreaVector.Count(); ++i )
	{
		CTFAreaCollector collect;
		pointAreaVector[i]->ForAllPotentiallyVisibleAreas( collect );

		for( int j=0; j<collect.m_vector.Count(); ++j )
		{
			CTFNavArea *visibleArea = collect.m_vector[j];


			if ( visibleArea->GetIncursionDistance( myTeam ) < 0 || visibleArea->GetIncursionDistance( enemyTeam ) < 0 )
				continue;

			if ( TFGameRules()->IsInKothMode() )
			{
				// ignore areas the enemy can reach first
				if ( visibleArea->GetIncursionDistance( myTeam ) >= visibleArea->GetIncursionDistance( enemyTeam ) )
					continue;
			}

// incursion flow is badly behaved at cap #1, stage #2 in dustbowl
// 			else
// 			{
// 				if ( pointEnemyIncursion > visibleArea->GetIncursionDistance( enemyTeam ) )
// 					continue;
// 			}

			if ( TFGameRules()->GetGameType() == TF_GAMETYPE_CP )
			{
				// don't build directly on the point
				if ( visibleArea->HasAttributeTF( TF_NAV_CONTROL_POINT ) )
					continue;

				// ignore areas below the point
				const float tooFarBelow = 150.0f;
				if ( visibleArea->GetCenter().z < pointCentroid.z - tooFarBelow )
					continue;

				// ignore areas too far from the point for the sentry gun to reach
				const float tolerance = 1.1f;
				if ( ( visibleArea->GetCenter() - pointCentroid ).IsLengthGreaterThan( SENTRY_MAX_RANGE * tolerance ) )
					continue;
			}

			// ignore areas that don't have clear line of FIRE (not sight)
			const float sentryEyeHeight = 60.0f;
			const float pointFlagHeight = 70.0f; // 100.0f;
			if ( !me->IsLineOfFireClear( visibleArea->GetCenter() + Vector( 0, 0, sentryEyeHeight ), pointCentroid + Vector( 0, 0, pointFlagHeight ) ) )
				continue;

			if ( !exposedAreaVector.HasElement( visibleArea ) )
				exposedAreaVector.AddToTail( visibleArea );
		}
	}

	// keep the farthest away areas
	const float keepRatio = 1.0f; // 0.5f;
	s_pointCentroid = pointCentroid;
	exposedAreaVector.Sort( CompareRangeToPoint );

	for( i=0; i<exposedAreaVector.Count() * keepRatio; ++i )
	{
		CTFNavArea *usableArea = exposedAreaVector[i];

		m_sentryAreaVector.AddToTail( usableArea );
	}

	// calculate total surface area
	m_totalSurfaceArea = 0.0f;
	FOR_EACH_VEC( m_sentryAreaVector, it )
	{
		CTFNavArea *area = m_sentryAreaVector[ it ];

		m_totalSurfaceArea += area->GetSizeX() * area->GetSizeY();

		if ( tf_bot_debug_sentry_placement.GetBool() )
		{
			TheNavMesh->AddToSelectedSet( area );
		}
	}
}


//---------------------------------------------------------------------------------------------
/**
 * Doesn't recompute the potential areas, just reselected from the list
 */
void CTFBotEngineerMoveToBuild::SelectBuildLocation( CTFBot *me )
{
	m_path.Invalidate();

	m_sentryBuildHint = NULL;
	m_sentryBuildLocation = vec3_origin;


	// if we have a build spot, use it
	if ( me->GetHomeArea() )
	{
		m_sentryBuildLocation = me->GetHomeArea()->GetCenter();
		return;
	}

	// if we have a set of specific build locations, pick one of them
	CUtlVector< CTFBotHintSentrygun * > sentryHintVector;

	CTFBotHintSentrygun *sentryHint;
	for( sentryHint = static_cast< CTFBotHintSentrygun * >( gEntList.FindEntityByClassname( NULL, "bot_hint_sentrygun" ) );
		 sentryHint;
		 sentryHint = static_cast< CTFBotHintSentrygun * >( gEntList.FindEntityByClassname( sentryHint, "bot_hint_sentrygun" ) ) )
	{
		// clear the previous owner if it is us
		if ( sentryHint->GetPlayerOwner() == me )
		{
			sentryHint->SetPlayerOwner( NULL );
		}
		if ( sentryHint->IsAvailableForSelection( me ) )
		{
			sentryHintVector.AddToTail( sentryHint );
		}
	}

	if ( sentryHintVector.Count() > 0 )
	{
		int which = RandomInt( 0, sentryHintVector.Count()-1 );

		m_sentryBuildHint = sentryHintVector[ which ];
		m_sentryBuildHint->SetPlayerOwner( me );
		m_sentryBuildLocation = m_sentryBuildHint->GetAbsOrigin();

		return;
	}


	// collect nav area candidates
	CollectBuildAreas( me );

	// choose based on surface area to avoid biasing finely subdivided areas of the mesh
	float which = RandomFloat( 0.0f, m_totalSurfaceArea - 1.0f );
	float soFar = 0.0f;
	FOR_EACH_VEC( m_sentryAreaVector, sit )
	{
		CTFNavArea *area = m_sentryAreaVector[ sit ];

		soFar += area->GetSizeX() * area->GetSizeY();

		if ( which < soFar )
		{
			m_sentryBuildLocation = area->GetRandomPoint();
			return;
		}
	}

	if ( !HushAsserts() )
	{
		Assert( !"Failed to find a build location" );
	}
	m_sentryBuildLocation = me->GetAbsOrigin();
}


//---------------------------------------------------------------------------------------------
ActionResult< CTFBot >	CTFBotEngineerMoveToBuild::OnStart( CTFBot *me, Action< CTFBot > *priorAction )
{
	m_path.SetMinLookAheadDistance( me->GetDesiredPathLookAheadRange() );

#ifdef TF_RAID_MODE
	if ( TFGameRules()->IsRaidMode() )
	{
		if ( me->GetHomeArea() && TFGameRules()->GetRaidLogic() )
		{
			// try to pick a new area
			CTFNavArea *sentryArea = TFGameRules()->GetRaidLogic()->SelectRaidSentryArea();
			if ( sentryArea )
			{
				me->SetHomeArea( sentryArea );
			}
		}
	}
#endif // TF_RAID_MODE

	SelectBuildLocation( me );

	return Continue();
}


//---------------------------------------------------------------------------------------------
ActionResult< CTFBot >	CTFBotEngineerMoveToBuild::Update( CTFBot *me, float interval )
{
	if ( me->WasPointJustLost() )
	{
		if ( m_fallBackTimer.HasStarted() )
		{
			if ( m_fallBackTimer.IsElapsed() )
			{
				SelectBuildLocation( me );
				m_fallBackTimer.Invalidate();
			}
			else
			{
				// wait a moment while we decide where to build near fallback point
				return Continue();
			}
		}
	}

	CBaseObject	*mySentry = me->GetObjectOfType( OBJ_SENTRYGUN );
	if ( mySentry )
	{
		// we already have a sentry from a previous life - continue what we were doing

		// if we used a sentry hint last time, reuse it
		CTFBotHintSentrygun *sentryHint;
		for( sentryHint = static_cast< CTFBotHintSentrygun * >( gEntList.FindEntityByClassname( NULL, "bot_hint_sentrygun" ) );
			 sentryHint;
			 sentryHint = static_cast< CTFBotHintSentrygun * >( gEntList.FindEntityByClassname( sentryHint, "bot_hint_sentrygun" ) ) )
		{
			if ( sentryHint->GetPlayerOwner() == me )
			{
				return ChangeTo( new CTFBotEngineerBuilding( sentryHint ), "Going back to my existing sentry nest and reusing a sentry hint" );
			}
		}

		return ChangeTo( new CTFBotEngineerBuilding, "Going back to my existing sentry nest" );
	}

	// offensive engineers need to place a forward teleporter
	if ( TFGameRules()->GetGameType() == TF_GAMETYPE_CP && !TFGameRules()->IsInKothMode() && me->GetTeamNumber() == TF_TEAM_BLUE )
	{
		CObjectTeleporter *myTeleportExit = (CObjectTeleporter *)me->GetObjectOfType( OBJ_TELEPORTER, MODE_TELEPORTER_EXIT );
		int myTeam = me->GetTeamNumber();

		if ( myTeleportExit )
		{
			// if exit is too far from the point, destroy it and try again
			CTeamControlPoint *point = me->GetMyControlPoint();
			if ( point )
			{
				CTFNavArea *pointArea = TheTFNavMesh()->GetControlPointCenterArea( point->GetPointIndex() );

				myTeleportExit->UpdateLastKnownArea();
				CTFNavArea *exitArea = (CTFNavArea *)myTeleportExit->GetLastKnownArea();

				if ( pointArea && exitArea )
				{
					float travelToPoint = fabs( exitArea->GetIncursionDistance( myTeam ) - pointArea->GetIncursionDistance( myTeam ) );

					if ( travelToPoint > tf_bot_max_teleport_exit_travel_to_point.GetFloat() )
					{
						// too far, destroy it
						myTeleportExit->DestroyObject();
						myTeleportExit = NULL;
					}
				}
			}
		}
		else
		{
			CObjectTeleporter *myTeleportEntrance = (CObjectTeleporter *)me->GetObjectOfType( OBJ_TELEPORTER, MODE_TELEPORTER_ENTRANCE );
			CTFNavArea *myArea = me->GetLastKnownArea();

			bool shouldBuildExit = true;

			// if we have a teleporter entrance, don't place the exit too close to it
			if ( myTeleportEntrance && myArea )
			{
				myTeleportEntrance->UpdateLastKnownArea();
				CTFNavArea *enterArea = (CTFNavArea *)myTeleportEntrance->GetLastKnownArea();

				if ( enterArea )
				{
					float travelBetween = fabs( enterArea->GetIncursionDistance( myTeam ) - myArea->GetIncursionDistance( myTeam ) );

					if ( travelBetween < tf_bot_min_teleport_travel.GetFloat() )
					{
						shouldBuildExit = false;
					}
				}
			}

			if ( shouldBuildExit )
			{
				// no exit yet - need to place one
				// when we see the enemy, retreat to cover and build the exit there
				if ( me->GetVisionInterface()->GetPrimaryKnownThreat( true ) )
				{
					if ( !me->m_Shared.InCond( TF_COND_INVULNERABLE ) && ShouldRetreat( me ) != ANSWER_NO )
					{
						Action< CTFBot > *nextActionWhenInCover = new CTFBotEngineerBuildTeleportExit;
						return SuspendFor( new CTFBotRetreatToCover( nextActionWhenInCover ), "Retreating to a safe place to build my teleporter exit" );
					}
				}
			}
		}
	}

	// move to build position
	if ( m_repathTimer.IsElapsed() )
	{
		m_repathTimer.Start( RandomFloat( 1.0f, 2.0f ) );

		CTFBotPathCost cost( me, SAFEST_ROUTE );
		m_path.Compute( me, m_sentryBuildLocation, cost );
	}

	Vector forward;
	me->EyeVectors( &forward );
	forward.z = 0.0f;
	forward.NormalizeInPlace();

	Vector myBlueprintPosition = me->GetAbsOrigin() + 50.0f * forward;

	const float closeToHome = 25.0f;
	Vector toBuild = m_sentryBuildLocation - myBlueprintPosition;
	Vector toMe = m_sentryBuildLocation - me->GetAbsOrigin();

	if ( me->GetLocomotionInterface()->IsOnGround() )
	{
		// we need to wait until we're on the ground since the Build action assumes our position OnStart is where we are going to build
		if ( toMe.AsVector2D().IsLengthLessThan( closeToHome ) || toBuild.AsVector2D().IsLengthLessThan( closeToHome ) )
		{
			if ( m_sentryBuildHint != NULL )
			{
				return ChangeTo( new CTFBotEngineerBuilding( m_sentryBuildHint ), "Reached my precise build location" );
			}

			return ChangeTo( new CTFBotEngineerBuilding, "Reached my build location" );
		}

		m_path.Update( me );
	}

	return Continue();
}


//---------------------------------------------------------------------------------------------
EventDesiredResult< CTFBot > CTFBotEngineerMoveToBuild::OnStuck( CTFBot *me )
{
//	SelectBuildLocation( me );
	return TryContinue();
}


//---------------------------------------------------------------------------------------------
EventDesiredResult< CTFBot > CTFBotEngineerMoveToBuild::OnMoveToSuccess( CTFBot *me, const Path *path )
{
	return TryContinue();
}


//---------------------------------------------------------------------------------------------
EventDesiredResult< CTFBot > CTFBotEngineerMoveToBuild::OnMoveToFailure( CTFBot *me, const Path *path, MoveToFailureType reason )
{
	SelectBuildLocation( me );

	return TryContinue();
}


//---------------------------------------------------------------------------------------------
EventDesiredResult< CTFBot > CTFBotEngineerMoveToBuild::OnTerritoryLost( CTFBot *me, int territoryID )
{
	// we have to wait a moment until contested point changes to select a new build spot
	m_fallBackTimer.Start( 0.2f );

	return TryContinue();
}
