Units/Targeter.cs
namespace PlanetMeat;

[Group( "Planet Meat - Units" ), Title( "Targeter" ), Icon( "visibility" )]
public sealed class Targeter : Component
{
	public enum TargetingStrategies { Nearest, Healthiest }

	[Property]
	public TargetingStrategies TargetingStrategy
	{
		get;
		set
		{
			field = value;
			TargetingComparer = null;
		}
	} = TargetingStrategies.Nearest;

	[Property] public bool CanTargetEnemies { get; set; } = true;
	[Property] public bool CanTargetFriends { get; set; } = false;
	[Property] public bool CanTargetLiving { get; set; } = true;
	[Property] public bool CanTargetDead { get; set; } = false;

	public int NumTargets
	{
		get;
		set
		{
			if ( _nextTargets is not null && _nextTargets.Capacity < value + 1 )
				_nextTargets.Capacity = value + 1;
			field = value;
		}
	} = 1;

	public Targetable SoleFocus { get; set; }

	private List<Targetable> _nextTargets;
	public List<Targetable> NextTargets
	{
		get
		{
			if ( _nextTargets is null )
				_nextTargets = new List<Targetable>( NumTargets + 1 );
			else
				_nextTargets.Clear();

			if ( SoleFocus is null )
			{
				if ( TargetingComparer is IComparer<Targetable> comparer )
				{
					foreach ( var t in TargetsInRange )
					{
						var index = _nextTargets.BinarySearch( t, comparer );
						if ( index < 0 ) index = ~index;

						if ( index < NumTargets )
						{
							_nextTargets.Insert( index, t );
							if ( _nextTargets.Count > NumTargets )
								_nextTargets.RemoveAt( NumTargets );
						}
					}
				}
			}
			else if ( IsValidTarget( SoleFocus ) )
			{
				_nextTargets.Add( SoleFocus );
			}

			return _nextTargets;
		}
	}
	public IEnumerable<Targetable> TargetsInRange => Scene
		.FindInPhysics( new Sphere( WorldPosition, Range ) )
		.Select( g => g.Components.Get<Targetable>() )
		.Where( t => t is not null && CanTarget( t ) );

	public ITargeterStatus Status => field ??= this.FindNearestUpwards<ITargeterStatus>();

	public float Range => AggregateRange.Total;
	private float RangeSq
	{
		get
		{
			if ( float.IsNaN( field ) )
			{
				var r = Range;
				field = r > 0 ? r * r : -1;
			}
			return field;
		}
		set;
	} = float.NaN;

	[Property] public int BaseRange { get; set; } = 2048;
	[Property] public string RangeImprovement { get; set; }
	private ValueAggregator<int> AggregateRange
	{
		get
		{
			if ( field is null )
			{
				field = ValueAggregator<int>.FromImprovement( RangeImprovement, BaseRange );
				DirtyRange();
			}
			return field;
		}
	}

	[Property] public float Arc { get; set; } = -1.0f;

	private float DistSqTo( Targetable t ) => t.WorldPosition.DistanceSquared( WorldPosition );
	private bool InRange( Targetable t ) => RangeSq < 0 || DistSqTo( t ) <= RangeSq;
	private bool InArc( Targetable t ) => Arc < -0.99f || (t.WorldPosition - WorldPosition).Normal.Dot( WorldRotation.Forward ) > 0.5f;
	private bool CanSee( Targetable t ) => InRange( t ) && InArc( t );
	public bool IsValidTarget( Targetable t ) => t is not null && CanTarget( t ) && CanSee( t );

	private delegate I TargetCompare<in T, out I>( T leader, T challenger );
	private IComparer<Targetable> TargetingComparer
	{
		get
		{
			return field ??= Comparer<Targetable>.Create( TargetingStrategy switch
			{
				TargetingStrategies.Healthiest => CompareHealthiest,
				_ => CompareNearest
			} );
		}
		set;
	} = null;

	private int CompareNearest( Targetable a, Targetable b )
	{
		if ( a is null )
			return 1;
		if ( b is null )
			return -1;

		return (int)(DistSqTo( a ) - DistSqTo( b ));
	}

	private int CompareHealthiest( Targetable a, Targetable b )
	{
		if ( a is null )
			return 1;
		if ( b is null )
			return -1;

		var compareCurrent = b.CurrentHp.CompareTo( a.CurrentHp );
		if ( compareCurrent != 0 )
			return compareCurrent;

		var compareMax = b.MaxHp.CompareTo( a.MaxHp );
		if ( compareMax != 0 )
			return compareMax;

		return (int)(DistSqTo( a ) - DistSqTo( b ));
	}

	public void DirtyRange()
	{
		RangeSq = float.NaN;
	}

	public void ContributeRange( string source, int amount )
	{
		AggregateRange.SetContribution( source, amount );
		DirtyRange();
	}

	public void ContributeRange( Component comp, int amount )
	{
		AggregateRange.SetContribution( comp, amount );
		DirtyRange();
	}

	public float GetRangeWithout( Component contributor )
	{
		return AggregateRange.Without( contributor );
	}

	public bool CanTarget( Targetable t )
	{
		return t.IsValid
			&& (t.Killed ? CanTargetDead : CanTargetLiving)
			&& (Status.IsFriendlyTo( t ) ? CanTargetFriends : CanTargetEnemies);
	}

	protected override void DrawGizmos()
	{
		base.DrawGizmos();
		Gizmo.Draw.Color = Color.Orange;
		Gizmo.Draw.LineSphere( Vector3.Zero, BaseRange );
	}
}