//=============================================================================
// KFTargetingWeaponComponent
//=============================================================================
// Actor that encapsulates the management and replication of targeting weapon info.
// Enables any weapon to be targeting.
//=============================================================================
// Killing Floor 2
// Copyright (C) 2019 Tripwire Interactive LLC
//=============================================================================
class KFTargetingWeaponComponent extends Actor
	abstract;

/** Fire mode 0 is the default weapon firing. */
const DEFAULT_FIREMODE			= 0;
/** Fire mode 1 is the alternate weapon firing. */
const ALTFIRE_FIREMODE			= 1;

enum ETargetingMode
{
	ETargetingMode_Zed,
	ETargetingMode_Player,
};

const TARGETINGMODE_FLAGS_ZED = 1;
const TARGETINGMODE_FLAGS_PLAYER = 2;
const TARGETINGMODE_FLAGS_ALL = 3;

var byte TargetingModeFlags[2];

var KFWeapon KFW;

/*********************************************************************************************
 * @name Weapon lock on support
 **********************************************************************************************/

/** How far out should we be considering actors for a lock */
var float LockRange[2];

/** How long does the player need to target an actor to lock on to it*/
var float LockAcquireTime[2];

/** How long does the player need to target a large zed to lock on to it */
var float LockAcquireTime_Large[2];

/** How long does the player need to target a boss to lock on to it*/
var float LockAcquireTime_Boss[2];

/** How long does the player need to target a versus zed to lock on to it */
var float LockAcquireTime_Versus[2];

/** Once locked, how long can the player go without painting the object before they lose the lock */
var float LockTolerance[2];

/** What "target" is this weapon locked on to */
var KFPawn LockedTarget[2];

/** What hitzone is this weapon locked on to */
var int LockedHitZone[2];

/** What "target" is current pending to be locked on to */
var KFPawn PendingLockedTarget[2];

/** What hitzone is this weapon trying to lock on to */
var int PendingHitZone[2];

/** angle for locking for lock targets */
var float LockAim[2];

/** The frequency with which we play the Lock Targeting sound */
var float LockTargetingSoundInterval[2];

/** Sound Effects to play when Locking */
var AkBaseSoundObject LockAcquiredSoundFirstPerson;
var AkBaseSoundObject LockLostSoundFirstPerson;
var AkBaseSoundObject LockTargetingSoundFirstPerson;

/** How much time is left before pending lock-on can be acquired */
var float PendingLockAcquireTimeLeft[2];
/** How much time is left before pending lock-on is lost */
var float PendingLockTimeout[2];
/** How much time is left before lock-on is lost */
var float LockedOnTimeout;
var float LockedOnTimeoutTimer[2];

/** Location on an enemy we're currently locked onto */
var vector LockedAimLocation[2];

var array<name> HumanTargetableBoneNames;

/** Vulnerable zones on an enemy we're currently locked onto */
var array<vector> TargetVulnerableLocations_Zed;
var array<vector> TargetVulnerableLocations_Player;

/** The frequency with which we replicate the targeting location to the server. Only matters for showing other players where we shot */
var float TargetLocationReplicationInterval;
var float TargetLocationReplicationTimer[2];

var bool bTargetingUpdated;

simulated event PostBeginPlay()
{
	super.PostBeginPlay();
	SetTickIsDisabled(true);
}

simulated function Init(KFWeapon InKFW)
{
	KFW = InKFW;
	Instigator = InKFW.Instigator;
}

simulated event Tick(float DeltaTime)
{
	super.Tick(DeltaTime);

	if (Instigator != none && Instigator.Controller != none && Instigator.IsLocallyControlled())
    {
        CheckTargetLock(DeltaTime);
    }
}

/**
* This function checks to see if we are locked on a target
*/
simulated function CheckTargetLock(float DeltaTime)
{
	local bool bAllowLockOn;
	local byte Flags;

	bTargetingUpdated = false;

	if ((Instigator == None) ||
		(Instigator.Controller == None) ||
		(KFW != Instigator.Weapon) ||
		KFPlayerController(Instigator.Controller) == none)
	{
		return;
	}

	TargetVulnerableLocations_Zed.Length = 0;
	TargetVulnerableLocations_Player.Length = 0;

	Flags = TargetingModeFlags[KFW.bUseAltFireMode ? ALTFIRE_FIREMODE : DEFAULT_FIREMODE];

	if (!AllowTargetLockOn(ETargetingMode_Zed))
	{
		AdjustLockTarget(ETargetingMode_Zed, None);
		PendingLockedTarget[ETargetingMode_Zed] = None;
		LockedAimLocation[ETargetingMode_Zed]=vect(0,0,0);
	}
	else
	{
		bAllowLockOn = true;
		if ((Flags & TARGETINGMODE_FLAGS_ZED) != 0)
		{
			UpdateTargetLock(ETargetingMode_Zed, DeltaTime);
		}
	}

	if (!AllowTargetLockOn(ETargetingMode_Player))
	{
		AdjustLockTarget(ETargetingMode_Player, None);
		PendingLockedTarget[ETargetingMode_Player] = None;
		LockedAimLocation[ETargetingMode_Player]=vect(0,0,0);
	}
	else
	{
		bAllowLockOn = true;
		if ((Flags & TARGETINGMODE_FLAGS_PLAYER) != 0)
		{
			UpdateTargetLock(ETargetingMode_Player, DeltaTime);
		}
	}

	if (!bAllowLockOn)
	{
		ClearTimer(nameof(PlayTargetingBeepTimer));
		return;
	}

	bTargetingUpdated = true;
}

/** returns true if lock-on is possible */
simulated function bool AllowTargetLockOn(byte TargetingMode)
{
	return !Instigator.bNoWeaponFiring;
}

simulated function AdjustLockTarget(byte TargetingMode, KFPawn NewLockTarget)
{
	if (LockedTarget[TargetingMode] == NewLockTarget)
	{
		// no need to update
		return;
	}

	if (NewLockTarget == None)
	{
		// Clear the lock
		if (LockedTarget[TargetingMode] != none)
		{
			LockedTarget[TargetingMode] = None;

			if (TargetingMode == ETargetingMode_Zed)
			{
				TargetVulnerableLocations_Zed.Length = 0;
			}
			else
			{
				TargetVulnerableLocations_Player.Length = 0;
			}

			LockedAimLocation[TargetingMode]=vect(0,0,0);
			ServerSetTargetingLocation(TargetingMode, LockedAimLocation[TargetingMode]);

			if (Instigator != None && Instigator.IsHumanControlled())
    		{
    			KFW.PlaySoundBase(LockLostSoundFirstPerson, true);
    		}
		}
	}
	else
	{
		// Set the lock
		LockedTarget[TargetingMode] = NewLockTarget;

		if (Instigator != None && Instigator.IsHumanControlled())
		{
			KFW.PlaySoundBase(LockAcquiredSoundFirstPerson, true);
		}
	}
}

reliable server private function ServerSetTargetingLocation(byte TargetingMode, vector NewTargetingLocation)
{
	LockedAimLocation[TargetingMode] = NewTargetingLocation;
}

simulated function PlayTargetingBeepTimer()
{
	if (Instigator != None && Instigator.IsHumanControlled() )
	{
		KFW.PlaySoundBase(LockTargetingSoundFirstPerson, true);
	}
}

simulated function UpdateTargetLock(byte TargetingMode, float DeltaTime)
{
	local Actor BestTarget;

	BestTarget = DetermineBestTarget(TargetingMode);
	UpdateLockTargets(TargetingMode, DeltaTime, BestTarget);
	SetupAimLock(TargetingMode, DeltaTime);
}

simulated function Actor DetermineBestTarget(byte TargetingMode)
{
    local vector StartTrace;
    local rotator ViewRotation;
    local vector X,Y,Z;
    local Actor BestTarget, HitActor, TA;
	local vector EndTrace, Aim, HitLocation, HitNormal;
	local KFPlayerController KFPC;

    KFPC = KFPlayerController(Instigator.Controller);

	// Get the location and rotation that a shot would take
	StartTrace = Instigator.GetWeaponStartTraceLocation();
    ViewRotation = Instigator.GetViewRotation();

	// Add in the free-aim rotation
	ViewRotation += KFPC.WeaponBufferRotation;

    GetAxes(ViewRotation, X, Y, Z);

	BestTarget = None;

	// Begin by tracing the shot to see if it hits anyone
	Aim = vector(ViewRotation);
	EndTrace = StartTrace + Aim * GetLockRange();
	HitActor = KFW.Trace(HitLocation, HitNormal, EndTrace, StartTrace, true,,, TRACEFLAG_Bullet);

	// Check for a hit
	if ((HitActor == None) || !CanLockOnTo(TargetingMode, HitActor))
	{
		TA = PickTarget(Aim, StartTrace, TargetingMode == ETargetingMode_Player);
		if (TA != None && CanLockOnTo(TargetingMode, TA))
		{
            // Trace to see if we hit a destructible, as the PickTarget code only traces against world geometry
            // @todo: Set up pick target to ignore pawns (as it should), but not trace through destructibles
            HitActor = Trace(HitLocation, HitNormal, TA.Location, StartTrace, true,,, TRACEFLAG_Bullet);

            // Make sure we didn't hit a destructible
            if (KFFracturedMeshActor(HitActor) != none || KFDestructibleActor(HitActor) != none)
            {
                BestTarget = none;
            }
            else
            {
                BestTarget = TA;
        	}
		}
	}
	else	// We hit a valid target
	{
		BestTarget = HitActor;
	}

	return BestTarget;
}

simulated function bool CanLockOnTo(byte TargetingMode, Actor TA)
{
	Local KFPawn PawnTarget;

    PawnTarget = KFPawn(TA);

    // Make sure the pawn is legit, isn't dead, and isn't already at full health
    if ((TA == None) ||
		!TA.bProjTarget ||
		TA.bDeleteMe ||
		(PawnTarget == None) ||
        (TA == Instigator) ||
		(PawnTarget.Health <= 0) ||
        (!KFW.bUseAltFireMode && !KFW.HasAmmo(DEFAULT_FIREMODE)) ||
		(KFW.bUseAltFireMode && !KFW.HasAmmo(ALTFIRE_FIREMODE)))
	{
		return false;
	}

	return ((TargetingMode == ETargetingMode_Zed && !WorldInfo.GRI.OnSameTeam(Instigator,TA)) ||
			(TargetingMode == ETargetingMode_Player && WorldInfo.GRI.OnSameTeam(Instigator,TA)));
}

simulated function Actor PickTarget(vector Aim, vector StartTrace, optional bool bTargetTeammates=false)
{
	local float BestAim, BestDist;

	BestAim = GetLockAim();
	BestDist = 0.0;
	return KFPlayerController(Instigator.Controller).GetPickedAimAtTarget(
		BestAim, BestDist, Aim, StartTrace, GetLockRange(), bTargetTeammates);
}

simulated function UpdateLockTargets(byte TargetingMode, float DeltaTime, Actor BestTarget)
{
	// clear lock if target is destroyed
	if (LockedTarget[TargetingMode] != None)
	{
		if (LockedTarget[TargetingMode].bDeleteMe)
		{
			AdjustLockTarget(TargetingMode, None);
		}
	}

	// If we have a "possible" target, note its time mark
	if (BestTarget != None)
	{
		if (BestTarget == LockedTarget[TargetingMode])
		{
			UpdateTargetLocked(TargetingMode);
		}
		// We have our best target, see if they should become our current target.
		// Check for a new Pending Lock
		else if (PendingLockedTarget[TargetingMode] != BestTarget)
		{
			UpdatePendingLockTarget(TargetingMode, BestTarget);
		}

		AcquireLockTarget(TargetingMode, DeltaTime, BestTarget);
	}
	// If we lost a target, attempt to invalidate the pending target
	else
	{
		TimeoutPendingLockTarget(TargetingMode, DeltaTime);
	}

	TimeoutLockTarget(TargetingMode, DeltaTime, BestTarget);
}

simulated function UpdateTargetLocked(byte TargetingMode)
{
	LockedOnTimeoutTimer[TargetingMode] = GetLockTolerance();
	if (PendingLockedTarget[TargetingMode] != none)
	{
    	ClearTimer(nameof(PlayTargetingBeepTimer));
    	PendingLockedTarget[TargetingMode] = None;
	}
}

simulated function UpdatePendingLockTarget(byte TargetingMode, Actor BestTarget)
{
	local KFPawn_Monster KFPM;

	KFPM = KFPawn_Monster(BestTarget);

	PendingLockedTarget[TargetingMode] = KFPawn(BestTarget);
	PendingLockTimeout[TargetingMode] = GetLockTolerance();
	PendingLockAcquireTimeLeft[TargetingMode] = GetLockAcquireTime();

	if (KFPM != none)
	{
		if (KFPM.IsABoss())
		{
			PendingLockAcquireTimeLeft[TargetingMode] = GetLockAcquireTime_Boss();
		}
		else if (KFPM.bVersusZed)
		{
			PendingLockAcquireTimeLeft[TargetingMode] = GetLockAcquireTime_Versus();
		}
		else if (KFPM.IsLargeZed())
		{
			PendingLockAcquireTimeLeft[TargetingMode] = GetLockAcquireTime_Large();
		}
	}

	SetTimer(LockTargetingSoundInterval[TargetingMode], true, nameof(PlayTargetingBeepTimer));

	// Play the "targeting" beep when we begin attempting to lock onto a target
	// that we haven't locked onto yet
	if (Instigator != None && Instigator.IsHumanControlled())
	{
    	PlaySoundBase(LockTargetingSoundFirstPerson, true);
	}
}

simulated function AcquireLockTarget(byte TargetingMode, float DeltaTime, Actor BestTarget)
{
	// Acquire new target if LockAcquireTime has passed
	if (PendingLockedTarget[TargetingMode] != None)
	{
		PendingLockAcquireTimeLeft[TargetingMode] -= DeltaTime;
		if (PendingLockedTarget[TargetingMode] == BestTarget && PendingLockAcquireTimeLeft[TargetingMode] <= 0)
		{
			AdjustLockTarget(TargetingMode, PendingLockedTarget[TargetingMode]);
			PendingLockedTarget[TargetingMode] = None;
			ClearTimer(nameof(PlayTargetingBeepTimer));
		}
	}
}

simulated function TimeoutPendingLockTarget(byte TargetingMode, float DeltaTime)
{
	if (PendingLockedTarget[TargetingMode] != None)
	{
		PendingLockTimeout[TargetingMode] -= DeltaTime;
		if (PendingLockTimeout[TargetingMode] <= 0 || !CanLockOnTo(TargetingMode, PendingLockedTarget[TargetingMode]))
		{
			PendingLockedTarget[TargetingMode] = None;
			ClearTimer(nameof(PlayTargetingBeepTimer));
		}
	}
}

simulated function TimeoutLockTarget(byte TargetingMode, float DeltaTime, Actor BestTarget)
{
	// If the new target is not the same, attempt to invalidate current locked on target
	if (LockedTarget[TargetingMode] != None && BestTarget != LockedTarget[TargetingMode])
	{
		LockedOnTimeoutTimer[TargetingMode] -= DeltaTime;
		if (LockedOnTimeoutTimer[TargetingMode] <= 0.f || !CanLockOnTo(TargetingMode, LockedTarget[TargetingMode]))
		{
			AdjustLockTarget(TargetingMode, None);
		}
	}
}

simulated function SetupAimLock(byte TargetingMode, float DeltaTime)
{
	local vector Aim, StartTrace, BestZoneLocation;
	local int OldHitZone;

	Aim = vector(Instigator.GetViewRotation());
	//StartTrace = KFW.GetSafeStartTraceLocation();
	StartTrace = Instigator.GetWeaponStartTraceLocation();

	// Set the aiming location if we have a target lock
    if (LockedTarget[TargetingMode] != none)
	{
        OldHitZone = LockedHitZone[TargetingMode];
        LockedHitZone[TargetingMode] =
			AddTargetingZones(TargetingMode, LockedTarget[TargetingMode], StartTrace, Aim, BestZoneLocation);
        if (OldHitZone != LockedHitZone[TargetingMode])
        {
       		if (Instigator != None && Instigator.IsHumanControlled())
			{
				KFW.PlaySoundBase(LockTargetingSoundFirstPerson, true);
			}
        }

        TargetLocationReplicationTimer[TargetingMode] -= DeltaTime;

        if (TargetLocationReplicationTimer[TargetingMode] <= 0 || IsZero(LockedAimLocation[TargetingMode]))
        {
            TargetLocationReplicationTimer[TargetingMode] = TargetLocationReplicationInterval;
            ServerSetTargetingLocation(TargetingMode, LockedAimLocation[TargetingMode]);
        }

        // Set the location where the shot will go
        LockedAimLocation[TargetingMode] = BestZoneLocation;
	}
	else
	{
        LockedHitZone[TargetingMode] = -1;
	}

	// Set the pending target location
    if (PendingLockedTarget[TargetingMode] != none)
	{
        PendingHitZone[TargetingMode] =
			AddTargetingZones(TargetingMode, PendingLockedTarget[TargetingMode], StartTrace, Aim, BestZoneLocation);
	}
	else
	{
		PendingHitZone[TargetingMode] = -1;
	}
}

simulated function int AddTargetingZones(
	byte TargetingMode, KFPawn KFPTarget, vector StartTrace, vector Aim, out vector BestZoneLocation)
{
	local KFPawn_Monster KFPM;
	local KFPawn_Human KFPH;

	if (TargetingMode == ETargetingMode_Zed)
	{
		KFPM = KFPawn_Monster(KFPTarget);
		if (KFPM != none)
		{
			return GetZedVulnerableLocations(KFPM, Aim, StartTrace, BestZoneLocation);
		}
	}
	else
	{
		KFPH = KFPawn_Human(KFPTarget);
		if (KFPH != none)
		{
			return GetHumanVulnerableLocations(KFPH, Aim, StartTrace, BestZoneLocation);
		}
	}

	return -1;
}

simulated function int GetZedVulnerableLocations(
	KFPawn_Monster Zed, vector Aim, vector StartTrace, out vector BestZoneLocation)
{
	local vector ZoneLocation;
	local int BestZoneIndex;
	local float BestZoneDot;
	local vector DirToZone;
	local float DotToZone;
	local int i;

	// Get all the vulnerable hit zones
	for (i = 0; i < Zed.WeakSpotSocketNames.Length; i++)
	{
		Zed.Mesh.GetSocketWorldLocationAndRotation(Zed.WeakSpotSocketNames[i], ZoneLocation);

		if (!IsZero(ZoneLocation))
		{
			TargetVulnerableLocations_Zed.AddItem(ZoneLocation);

			// Figure out which target zone we are closest to
			DirToZone = ZoneLocation - StartTrace;
			DotToZone = Normal(Aim) dot Normal(DirToZone);

			if (DotToZone > BestZoneDot)
			{
				BestZoneIndex = TargetVulnerableLocations_Zed.Length - 1;
				BestZoneDot = DotToZone;

				// Set the location for the zone we are closest targeting
				BestZoneLocation = ZoneLocation;
			}
		}
	}

	return BestZoneIndex;
}

simulated function int GetHumanVulnerableLocations(
	KFPawn_Human Human, vector Aim, vector StartTrace, out vector BestZoneLocation)
{
	local vector ZoneLocation;
	local int BestZoneIndex;
	local float BestZoneDot;
	local vector DirToZone;
	local float DotToZone;
	local int i;

	// Get all the vulnerable hit zones
	for (i = 0; i < HumanTargetableBoneNames.Length; i++)
	{
		ZoneLocation = Human.Mesh.GetBoneLocation(HumanTargetableBoneNames[i]);
		if(!IsZero(ZoneLocation))
		{
			TargetVulnerableLocations_Player.AddItem(ZoneLocation);

			// Figure out which target zone we are closest to
			DirToZone = ZoneLocation - StartTrace;
			DotToZone = Normal(Aim) dot Normal(DirToZone);

			if( DotToZone > BestZoneDot )
			{
				BestZoneIndex = TargetVulnerableLocations_Player.Length - 1;
				BestZoneDot = DotToZone;

				// Set the location for the zone we are closest targeting
				BestZoneLocation = ZoneLocation;
			}
		}
	}

	return BestZoneIndex;
}

/*********************************************************************************************
 * @name KFWeapon hooks
 **********************************************************************************************/

simulated function OnWeaponAttachedTo()
{
	SetTickIsDisabled(false);
}

simulated function OnWeaponDetached()
{
	SetTickIsDisabled(true);
}

/*********************************************************************************************
 * @name Getters
 **********************************************************************************************/

simulated function float GetLockRange()
{
	return LockRange[KFW.bUseAltFireMode ? ALTFIRE_FIREMODE : DEFAULT_FIREMODE];
}

simulated function float GetLockAim()
{
	return LockAim[KFW.bUseAltFireMode ? ALTFIRE_FIREMODE : DEFAULT_FIREMODE];
}

simulated function float GetLockAcquireTime()
{
	return LockAcquireTime[KFW.bUseAltFireMode ? ALTFIRE_FIREMODE : DEFAULT_FIREMODE];
}

simulated function float GetLockAcquireTime_Large()
{
	return LockAcquireTime_Large[KFW.bUseAltFireMode ? ALTFIRE_FIREMODE : DEFAULT_FIREMODE];
}

simulated function float GetLockAcquireTime_Boss()
{
	return LockAcquireTime_Boss[KFW.bUseAltFireMode ? ALTFIRE_FIREMODE : DEFAULT_FIREMODE];
}

simulated function float GetLockAcquireTime_Versus()
{
	return LockAcquireTime_Versus[KFW.bUseAltFireMode ? ALTFIRE_FIREMODE : DEFAULT_FIREMODE];
}

simulated function float GetLockTolerance()
{
	return LockTolerance[KFW.bUseAltFireMode ? ALTFIRE_FIREMODE : DEFAULT_FIREMODE];
}

defaultproperties
{
	RemoteRole=ROLE_SimulatedProxy
	bOnlyRelevantToOwner=true
	bHidden=true
	bCollideActors=false
	bProjTarget=false
	bStatic=false
	bNoDelete=false
}
