Weapons/MeleeWeaponAttack.cs
using System.Reflection.Emit;

namespace Opium;

public struct WindupSetup
{
	[Property] public float WindupTime { get; set; }

	public override string ToString()
	{
		return $"Windup (time: {WindupTime})";
	}
}

public struct ArcSetup
{
	public ArcSetup()
	{
	}

	public override string ToString()
	{
		return $"Arc (enabled: {HasArc}) (degrees: {Arc}), (radius: {ArcRadius}), (trace: {ArcTraceRadius})";
	}

	/// <summary>
	/// Does this state have an arc?
	/// </summary>
	[Property] public bool HasArc { get; set; }

	/// <summary>
	/// The arc (in degrees) for the melee weapon strike
	/// </summary>
	[Property] public float Arc { get; set; } = 45f;

	/// <summary>
	/// The radius of the arc.
	/// </summary>
	[Property] public float ArcRadius { get; set; } = 10f;

	[Property] public bool InvertArc { get; set; } = false;

	[Property] public float MinimumTimeForHit { get; set; } = 0.25f;

	/// <summary>
	/// The trace radius for the arc.
	/// </summary>
	[Property] public float ArcTraceRadius { get; set; } = 1f;

	[Property, Sandbox.Range( -1, 1 )] public float ArcUpAmount { get; set; } = 0.0f;
}

public enum AttackState
{
	Inactive,
	Windup,
	ReadyToAttack,
	Swinging,
	/// <summary>
	/// A heavy attack swing, which differs to a regular swing. This is called when you've wound up all the way to ReadyToAttack and are still holding the attack button down.
	/// </summary>
	SwingingHeavyAttack,
	Hit
}

public struct StateInfo
{
	/// <summary>
	/// The state.
	/// </summary>
	[KeyProperty, Category( "General" ), Title( "State Name" )] public AttackState State { get; set; }

	[Property, Category( "Animation" )] public string AnimationState { get; set; }
	[Property, Category( "Animation" )] public string VariationState { get; set; }

	[Property, Category( "Camera Effects" )] public string OnEnterCameraEffect { get; set; }

	/// <summary>
	/// How quickly can we move while in this state?
	/// </summary>
	[Property, Category( "General" )] public float MovementSpeed { get; set; }
	[Property, Category( "General" )] public float FieldOfViewOffset { get; set; }

	[Property, Category( "Stamina" )] public float OnEnterStaminaDrain { get; set; }
	[Property, Category( "Stamina" )] public float StaminaDrainAmount { get; set; }
	[Property, Category( "Stamina" )] public float StaminaDrainDelay { get; set; }

	/// <summary>
	/// If defined as non-zero, we will transition to a specified state after a set time.
	/// </summary>
	[Property, Category( "State Transitions" )] public float StateLength { get; set; }

	[Property, Category( "State Transitions" )] public AttackState TransitionState { get; set; }

	/// <summary>
	/// Any attack state can have an arc.
	/// </summary>
	[Property, Category( "Setup" )] public ArcSetup Arc { get; set; }

	/// <summary>
	/// An action that is called every update while we're in this state.
	/// </summary>
	[Property, Category( "Actions" )] public Action<MeleeWeaponAttack> OnUpdate { get; set; }

	/// <summary>
	/// An action that is called when we enter this state.
	/// </summary>
	[Property, Category( "Actions" )] public Action<MeleeWeaponAttack, AttackState> OnEnter { get; set; }

	/// <summary>
	/// An action that is called when we exit this state.
	/// </summary>
	[Property, Category( "Actions" )] public Action<MeleeWeaponAttack, AttackState> OnExit { get; set; }

	/// <summary>
	/// Da sound
	/// </summary>
	[Property, Category( "Sounds" )] public string OnEnterVoice { get; set; }
}

public partial class MeleeWeaponAttack : Component
{
	/// <summary>
	/// Accessor for the weapon.
	/// </summary>
	public MeleeWeapon Weapon => Components.Get<MeleeWeapon>( FindMode.InParent );

	/// <summary>
	/// The attack resource we'll be using.
	/// </summary>
	[Property] public MeleeWeaponAttackResource Resource { get; set; }

	// Forwarding properties for Resource.
	public string AttackTypeName => Resource.AttackTypeName;
	public AttackState ActivationState => Resource.ActivationState;
	public List<StateInfo> StateInfo => Resource.States;

	public bool IsActive => State != AttackState.Inactive;

	public TimeSince TimeSinceState { get; protected set; }
	public TimeUntil TimeUntilNextState { get; protected set; }
	public AttackState NextState { get; protected set; }
	public bool HasStateTransition { get; protected set; }

	protected void OnStateChanged( AttackState before, AttackState after )
	{
		foreach ( var controller in Components.GetAll<MeleeStateController>() )
		{
			StateObjects = new();

			if ( controller.IsValid() )
			{
				controller.OnStateChanged( before, after );
			}

		}
	}

	/// <summary>
	/// Get a named camera effect from the gameobject we're on
	/// </summary>
	/// <typeparam name="T"></typeparam>
	/// <param name="name"></param>
	/// <returns></returns>
	protected T GetNamedCameraEffect<T>( string name ) where T : CameraEffect
	{
		if ( name is null || string.IsNullOrEmpty( name ) ) return null;

		var foundGo = Weapon.GameObject.GetAllObjects( false ).FirstOrDefault( x => x.Name.ToLowerInvariant().Equals( name.ToLowerInvariant() ) );
		if ( !foundGo.IsValid() ) return null;

		var effect = foundGo.Components.Get<T>( FindMode.EverythingInSelfAndDescendants );
		if ( effect is not null ) return effect;
		return null;
	}

	private AttackState state;
	[Property, ReadOnly, Category( "Data" )] public AttackState State
	{
		get => state;
		set
		{
			// No re-entrant state.
			if ( state == value )
			{
				//Log.Trace( $"Tried to re-enter state {value}" );
				return;
			}

			var oldState = state;

			state = value;
			TimeSinceState = 0;

			var stateInfo = GetStateInfo( value );
			stateInfo.OnEnter?.Invoke( this, oldState );

			OnStateChanged( oldState, state );

			if ( Weapon.IsValid() && Weapon.Actor.IsValid() )
			{
				if ( stateInfo.OnEnterVoice is not null )
				{
					Weapon.Actor.PlayVoice( stateInfo.OnEnterVoice );
				}

				if ( Weapon.Actor is Opium.PlayerController player && player.IsValid() )
				{
					var cameraEffect = GetNamedCameraEffect<CameraEffect>( stateInfo.OnEnterCameraEffect );
					if ( cameraEffect is not null )
					{
						cameraEffect.Player = player;
						cameraEffect.Enabled = true;
					}

					if ( stateInfo.OnEnterStaminaDrain != 0f )
					{
						player.GetMechanic<StaminaMechanic>()?.RemoveStamina( stateInfo.OnEnterStaminaDrain, 2.0f );
					}
				}
			}


			//Log.Trace( $"State set to: {value}" );

			// Define next state info
			NextState = stateInfo.TransitionState;
			HasStateTransition = stateInfo.StateLength > 0;
			TimeUntilNextState = stateInfo.StateLength;

			var oldStateInfo = GetStateInfo( oldState );
			oldStateInfo.OnExit?.Invoke( this, state );
		}
	}

	/// <summary>
	/// Grab state info.
	/// </summary>
	/// <param name="state"></param>
	/// <returns></returns>
	public StateInfo GetStateInfo( AttackState state )
	{
		return StateInfo.FirstOrDefault( x => x.State == state );
	}

	public StateInfo CurrentStateInfo => GetStateInfo( State );

	/// <summary>
	/// An accessor to get the current state via animation.
	/// </summary>
	public string CurrentAnimatonState
	{
		get
		{
			var stateName = CurrentStateInfo.AnimationState;
			if ( !string.IsNullOrEmpty( stateName) )
			{
				return stateName;
			}

			return "default";
		}
	}

	/// <summary>
	/// A simple enough method that handles transitioning to states if our current state has a transition state.
	/// </summary>
	void UpdateStateTransition()
	{
		if ( HasStateTransition )
		{
			if ( TimeUntilNextState <= 0 )
			{
				State = NextState;
			}
		}
	}

	/// <summary>
	/// A state controller.
	/// </summary>
	[Property, Category( "Setup" )] public MeleeStateController StateController { get; set; }

	public bool IsPerformingArc { get; set; }

	bool ShouldUseHitState( SceneTraceResult tr )
	{
		if ( tr.GameObject.Components.Get<Actor>( FindMode.EverythingInSelfAndAncestors ) is not null ) return false;
		if ( tr.GameObject.Tags.Has( "interact" ) ) return false;
		if ( tr.GameObject.Tags.Has( "carry" ) ) return false;

		return true;
	}

	private HashSet<GameObject> StateObjects { get; set; } = new();

	[ConVar( "op_dev_arc" )] public static bool IsDebugging { get; set; } = false;

	void UpdateArc()
	{
		var stateInfo = GetStateInfo( State );
		var arc = stateInfo.Arc;
		var delta = TimeUntilNextState.Fraction;

		// Does this state even have an arc?
		if ( arc.HasArc )
		{
			var arcOrigin = Weapon.Actor.CameraObject.Components.Get<WeaponArcOrigin>( FindMode.EverythingInSelfAndDescendants );
			var cameraRotation = Weapon.Actor.CameraObject.Transform.Rotation;

			var origin = Weapon.Actor.CameraObject.Transform.Position;

			var arcRadius = arc.ArcRadius;
			var startAngle = arc.InvertArc ? -arcRadius : arcRadius;
			var endAngle = arc.InvertArc ? arcRadius : -arcRadius;

			var t = startAngle.DegreeToRadian().LerpTo( endAngle.DegreeToRadian(), delta );
			var x = MathF.Cos( t ) * arcRadius;
			var y = MathF.Sin( t ) * arcRadius;
			var z = MathF.Tan( t ) * arc.ArcUpAmount * arcRadius;

			var currentSweepPosition = origin + (cameraRotation.Forward * x) + (cameraRotation.Right * y) + (cameraRotation.Up * z);
			var didHit = Weapon.TestHit( currentSweepPosition, out var tr );
			Gizmo.Draw.Color = delta < arc.MinimumTimeForHit ? Color.Red : Color.White;

			if ( IsDebugging )
			{
				Gizmo.Draw.LineSphere( origin, arcRadius );
				Gizmo.Draw.LineSphere( currentSweepPosition, 8 );
				Gizmo.Draw.Line( origin, currentSweepPosition );
			}

			if ( didHit)
			{
				var isHitter = ShouldUseHitState( tr );
				if ( isHitter && delta < arc.MinimumTimeForHit ) return;

				if ( !StateObjects.Contains( tr.GameObject ) )
				{
					Weapon.DoMeleeHit( tr );
				}

				StateObjects.Add( tr.GameObject );

				if ( isHitter )
				{
					State = AttackState.Hit;
				}
				else
				{
					var cameraEffect = GetNamedCameraEffect<CameraEffect>( "Hit Effect" );
					if ( cameraEffect is not null )
					{
						cameraEffect.Player = Weapon.Actor as Opium.PlayerController;
						cameraEffect.Enabled = true;
					}
				}
			}
		}
	}

	void UpdateStamina()
	{
		var stateInfo = GetStateInfo( State );

		// No drain, no game
		if ( stateInfo.StaminaDrainAmount == 0 )
		{
			return;
		}

		if ( TimeSinceState >= stateInfo.StaminaDrainDelay )
		{
			Weapon.Actor.GetMechanic<StaminaMechanic>()?.RemoveStamina( stateInfo.StaminaDrainAmount );
		}
	}

	protected override void OnUpdate()
	{
		var stateInfo = GetStateInfo( State );
		// Run our state-based action.
		stateInfo.OnUpdate?.Invoke( this );

		UpdateStateTransition();
		UpdateArc();
		UpdateStamina();

		if ( StateController != null )
		{
			StateController.Update( this );
		}

		if (TimeSinceState >= stateInfo.StateLength )
		{
			hasAttacked = false;
		}
	}

	public bool hasAttacked = false;

	public void OnAttackReleased( bool doSound = true )
	{
		if ( hasAttacked ) return;

		if ( Resource.OnAttackVoice is not null && doSound )
		{
			Weapon?.Actor?.PlayVoice( Resource.OnAttackVoice );
		}

		hasAttacked = true;
	}

	public void Activate()
	{
		// can't activate while already active, might be bullshit
		if ( IsActive ) return;

		State = ActivationState;
	}
}