Actors/Actor.cs
using Opium.AI;

namespace Opium;

/// <summary>
/// An actor. This could be an AI actor, or a player.
/// </summary>
public partial class Actor : Component, Component.IDamageable, IKillable
{
	/// <summary>
	/// The voice list for this actor.
	/// </summary>
	[Property] public VoiceListResource VoiceList { get; set; }

	/// <summary>
	/// The relationship setup of this Actor
	/// </summary>
	[RequireComponent] public Relationship Relationship { get; set; }

	/// <summary>
	/// How much health does this actor have?
	/// </summary>
	[Property] public float Health { get; set; } = 100f;

	/// <summary>
	/// Is this actor alive?
	/// </summary>
	[Property] public bool IsAlive { get; set; } = true;

	/// <summary>
	/// The actor's current weapon (if any).
	/// </summary>
	public virtual BaseWeapon ActiveWeapon { get; }

	/// <summary>
	/// The GameObject for this actor's Camera (if any)
	/// This can also be known as the actor's eyes. So where they're looking from.
	/// </summary>
	public virtual GameObject CameraObject { get; }

	/// <summary>
	/// The last damage info this actor took.
	/// </summary>
	public Sandbox.DamageInfo LastDamage { get; set; }

	/// <summary>
	/// When did this actor take damage last?
	/// </summary>
	public TimeSince TimeSinceLastDamage { get; set; } = 100f;

	/// <summary>
	/// Input layer for actors.
	/// </summary>
	public ActorInput Input { get; set; } = new();

	/// <summary>
	/// A list of damage sounds.
	/// </summary>
	[Property, Category( "Audio" )] public List<SoundEvent> DamageSounds { get; set; } = new();

	/// <summary>
	/// Get a random damage sound.
	/// </summary>
	/// <returns></returns>
	public SoundEvent GetDamageSound() => Game.Random.FromList( DamageSounds );

	/// <summary>
	/// A list of death sounds.
	/// </summary>
	[Property, Category( "Audio" )] public List<SoundEvent> DeathSounds { get; set; } = new();

	/// <summary>
	/// Get a random death sound.
	/// </summary>
	/// <returns></returns>
	public SoundEvent GetDeathSound() => Game.Random.FromList( DeathSounds );

	/// <summary>
	/// Called every mechanics update.
	/// </summary>
	protected virtual void OnMechanicsUpdate()
	{
	}

	/// <summary>
	/// Should we actually inflict damage?
	/// </summary>
	/// <param name="damage"></param>
	/// <returns></returns>
	public virtual bool ShouldDamage( in Sandbox.DamageInfo damage )
	{
		// Awful demo stuff
		if ( this is Agent agent )
		{
			if ( agent.StateMachine.CurrentState is BlockingState )
			{
				// Don't damage AI players in block state.
				if ( agent.StateMachine.TimeInState > 0.3f ) return false;
			}
		}

		// Blocking
		if ( ActiveWeapon is MeleeWeapon melee && ( melee.BlockAttack?.IsActive ?? false ) )
		{
			// Let the weapon know that we got hit
			melee.BlockAttack.State = AttackState.Hit;

			// Can instantly react
			melee.TimeSinceShoot = 1f;

			// Trigger event to the attacker's actor if we have one
			damage.Attacker.Components.Get<Actor>( FindMode.EverythingInSelfAndAncestors )?.TriggerEvent( "block_react", this );

			var didBreak = Components.Get<Actor>( FindMode.EnabledInSelfAndChildren )?.GetMechanic<PostureMechanic>()
				?.Compensate( damage ) ?? false;

			// Only block if we didn't break posture from this attack
			if ( !didBreak && melee.BlockDamageFactor <= 0 )
				return false;
		}

		return true;
	}

	/// <summary>
	/// Called when the actor is damaged.
	/// </summary>
	public virtual void OnDamage( in Sandbox.DamageInfo damage )
	{
		if ( !ShouldDamage( damage ) )
		{
			// Make sure damgeinfo knows
			damage.Damage = 0;
			return;
		}

		TimeSinceLastDamage = 0;
		LastDamage = damage;

		Health -= damage.Damage;

		TriggerEvent( "damage", damage );

		TimeSinceLastDamage = 0;

		// Weapons can do stuff?
		ActiveWeapon?.OnDamage( damage );

		if ( Health <= 0 )
		{
			OnKilled( damage );

			if ( GetDeathSound() is { } sound )
			{
				PlayVoice( sound );
			}
		}
		else
		{
			if ( GetDamageSound() is { } sound )
			{
				PlayVoice( sound );
			}
		}
	}

	/// <summary>
	/// Called when the actor is killed.
	/// </summary>
	/// <param name="damageInfo"></param>
	public virtual void OnKilled( in Sandbox.DamageInfo damageInfo )
	{
		IsAlive = false;
	}

	/// <summary>
	/// Called every Scene update
	/// </summary>
	protected override void OnUpdate()
	{
		// Poll for input
		Input?.Update();

		UpdateVoice();
		OnMechanicsUpdate();
	}

	/// <summary>
	/// Tries to set up a viewmodel for a specific weapon. Will not do anything for AI actors, only players.
	/// </summary>
	/// <returns></returns>
	public virtual bool SetupViewModel( BaseWeapon weapon, bool isActive )
	{
		return false;
	}

	/// <summary>
	/// Can this actor fire a weapon?
	/// This is for stuff that's over-arching, like stamina.
	/// </summary>
	/// <returns></returns>
	public virtual bool CanShoot( BaseWeapon weapon )
	{
		return true;
	}

	public virtual bool CanAim( BaseWeapon weapon )
	{
		return true;
	}

	/// <summary>
	/// Tries to drop a weapon if the actor owns it.
	/// </summary>
	/// <param name="weapon"></param>
	/// <returns></returns>
	public virtual WeaponPickup DropWeapon( BaseWeapon weapon )
	{
		return null;
	}

	public interface IEventListener
	{
		public void OnEvent( Actor actor, string eventName, params object[] obj );
	}

	/// <summary>
	/// Triggers an event on this actor. This is pushed down to mechanics, or any state machines.
	/// </summary>
	/// <param name="eventName"></param>
	/// <param name="obj"></param>
	public void TriggerEvent( string eventName, params object[] obj )
	{
		foreach ( var eventListener in Scene.GetAllComponents<IEventListener>() )
		{
			eventListener.OnEvent( this, eventName, obj );
		}

		foreach ( var mechanic in Mechanics )
		{
			mechanic.OnEvent( eventName, obj );
		}

		OnEvent( eventName, obj );
	}

	/// <summary>
	/// Called when an event is triggered by <see cref="TriggerEvent(string, object[])"/>
	/// </summary>
	/// <param name="eventName"></param>
	/// <param name="obj"></param>
	public virtual void OnEvent( string eventName, params object[] obj )
	{
	}

	// TODO: move this
	protected readonly float DefaultDetectionRange = 600f;

	/// <summary>
	/// How detected are we in relation to a specific actor?
	/// Maybe this belongs in its own component. Idk.
	/// </summary>
	/// <param name="actor"></param>
	/// <returns></returns>
	public virtual float GetDetectionFactor( Actor actor )
	{
		return DefaultDetectionRange - Transform.Position.Distance( actor.Transform.Position );
	}

	protected override void OnEnabled()
	{
		Tags.Add( "actor" );
	}
}