Animals/CatchableFish.cs
using System;
using Clover.Carriable;
using Clover.Components;
using Clover.Data;
using Clover.Objects;

namespace Clover.Animals;

[Category( "Clover/Animals" )]
public class CatchableFish : Component, IFootstepEvent
{
	public enum FishState
	{
		Idle = 0,
		Swimming = 1,
		FoundBobber = 2,
		TryingToEat = 3,
		Fighting = 4
	}

	[RequireComponent] public WorldLayerObject WorldLayerObject { get; set; }

	[Property] public SkinnedModelRenderer Renderer { get; set; }

	[Property] public FishData Data { get; set; }

	[Property] public SoundEvent SplashSound { get; set; }
	[Property] public SoundEvent NibbleSound { get; set; }
	[Property] public SoundEvent ChompSound { get; set; }
	[Property] public SoundEvent CatchSound { get; set; }

	private const float _bobberMaxDistance = 40f;
	private const float _bobberDiscoverAngle = 45f;

	private Vector3 _velocity = Vector3.Zero;
	private const float _maxSwimSpeed = 30f;
	private const float _maxSwimSpeedPanic = 40f;
	private const float _swimAcceleration = 1.5f;
	private const float _swimDeceleration = 0.5f;

	// private const float _catchMsecWindow = 1500f;

	private TimeUntil _nextSplashSound = 0;

	public FishState State { get; set; } = FishState.Idle;

	public FishingBobber Bobber { get; set; }

	public Rotation WishedRotation { get; set; }


	public RangedFloat NibbleTime = new(0.5f, 2f);

	public float CurrentNibbleTime;

	public TimeUntil NextNibble;

	private TimeSince _lastPanic = 999;

	public bool IsNibbling => CurrentNibbleTime > NextNibble.Passed;

	public float Stamina;

	public void SetState( FishState state )
	{
		// Log.Info( $"Fish state changed to {state}." );
		State = state;
		/*_lastAction = Time.Now;
		_actionDuration = 0;
		_panicMaxIdles = 0;
		_swimTarget = Vector3.Zero;
		_swimProgress = 0;
		_nibbles = 0;
		_isNibbleDeep = false;*/
	}

	protected override void OnFixedUpdate()
	{
		// don't simulate fish if there are no players in the world
		if ( WorldLayerObject.World == null || !WorldLayerObject.World.PlayersInWorld.Any() )
		{
			// Log.Info( "No players in the world." );
			return;
		}

		switch ( State )
		{
			case FishState.Idle:
				Idle();
				break;
			case FishState.Swimming:
				Swim();
				break;
			case FishState.FoundBobber:
				FoundBobber();
				break;
			// case FishState.TryingToEat:
			// 	TryToEat( delta );
			// 	break;
			case FishState.Fighting:
				Fight();
				break;
		}

		// Animate();

		WorldRotation = Rotation.Lerp( WorldRotation, WishedRotation, Time.Delta * 2f );
	}

	private void Fight()
	{
		if ( _nextSplashSound )
		{
			// GetNode<AudioStreamPlayer3D>( "Splash" ).Play();
			GameObject.PlaySound( SplashSound );
			_nextSplashSound = (TimeUntil)Math.Clamp( Random.Shared.Float( 0.2f, 0.5f ) + (Stamina / 50f), 0.1, 10f );

			Bobber.Rod.SplashParticle.Clone( WorldPosition, Rotation.FromPitch( -90f ) );
		}

		if ( !Bobber.IsValid() )
		{
			Log.Warning( "Bobber is not valid." );
			SetState( FishState.Idle );
			return;
		}

		var fleePosition = Bobber.WorldPosition + Bobber.WorldRotation.Forward * 32f;
		// fleePosition += Bobber.WorldRotation.Right * Random.Shared.Float( -32f, 32f );
		fleePosition += Bobber.WorldRotation.Right * ((Sandbox.Utility.Noise.Perlin( Time.Now * 20f ) * 64f) - 32f);

		// Gizmo.Draw.Arrow( fleePosition + Vector3.Up * 16f, fleePosition + Vector3.Down * 16f );
		// Gizmo.Draw.LineSphere( fleePosition, 8f );

		if ( !FishingRod.CheckForWater( fleePosition + Vector3.Up * 8f ) )
		{
			return;
		}

		var distance = WorldPosition.Distance( Bobber.Rod.WorldPosition );

		if ( distance > Bobber.Rod.LineLength )
		{
			fleePosition = Bobber.WorldPosition;
			Bobber.Rod.LineStrength -= Time.Delta * 0.3f;
		}

		var swimSpeed = _maxSwimSpeedPanic;

		// TODO: properly adjust this based on distance and stamina
		if ( distance < 96f )
		{
			swimSpeed = _maxSwimSpeedPanic * (distance / 40f);
		}

		if ( Stamina <= 0 )
		{
			swimSpeed *= 0.1f;
		}

		Bobber.WorldPosition += (fleePosition - Bobber.WorldPosition).Normal * swimSpeed * Time.Delta;

		WorldPosition = Bobber.WorldPosition;

		Stamina -= Time.Delta * 0.1f;

		Bobber.Rod.LineStrength -= Time.Delta * 0.1f;

		// Gizmo.Draw.Text( Stamina.ToString(), new Transform( WorldPosition + Vector3.Up * 32f ) );

		/*if ( Stamina <= 0 )
		{
			CatchFish();
		}*/

		// Log.Info( "Fighting the fish." );
	}

	private void Animate()
	{
		/*if ( State == FishState.Swimming )
		{
			AnimationPlayer.Play( "swimming" );
			// AnimationPlayer.SpeedScale =
		}
		else
		{
			AnimationPlayer.Play( "idle" );
		}*/

		// Renderer.Set( "swimming", State == FishState.Swimming );
	}

	public void TryHook()
	{
		// if last nibble is within the catch window, catch the fish
		if ( IsNibbling )
		{
			if ( Random.Shared.Float() <= 0.7f )
			{
				Bobber.OnFight();
				HookFish();
			}
			else
			{
				Scare( Bobber.WorldPosition );
			}
		}
		else
		{
			Log.Info( "Nibble was not deep enough." );
			Scare( Bobber.WorldPosition );
		}
	}

	private void Scare( Vector3 source = default )
	{
		if ( _lastPanic < 5f ) return;
		Log.Info( "Scared the fish." );
		// Gizmo.Draw.Line( WorldPosition, source );
		// Gizmo.Draw.LineSphere( source, 8f );
		// Gizmo.Draw.LineSphere( WorldPosition, 8f );
		_lastPanic = 0;
		// _swimProgress = 0;
		_lastAction = Time.Now;
		_actionDuration = 0;
		FindSwimAwayFromTarget( source );
		SetState( FishState.Swimming );
		GameObject.PlaySound( SplashSound );
	}

	private void FoundBobber()
	{
		if ( !ActionDone ) return;

		if ( !Bobber.IsValid() )
		{
			Log.Warning( "Bobber is not valid." );
			SetState( FishState.Idle );
			Bobber = null;
			return;
		}

		/*if ( _isNibbleDeep )
		{
			FailCatch();
			return;
		}*/

		var bobberPosition = Bobber.WorldPosition.WithZ( 0 );
		var fishPosition = WorldPosition.WithZ( 0 );

		// rotate towards the bobber
		var moveDirection = (bobberPosition - fishPosition).Normal;

		var newRotation = Rotation.LookAt( (bobberPosition - fishPosition), Vector3.Up );

		WishedRotation = newRotation;

		var distance = fishPosition.Distance( bobberPosition );

		if ( distance > 3f )
		{
			var speed = _maxSwimSpeed * distance / 8f;

			// move towards the bobber
			WorldPosition += moveDirection * speed * Time.Delta;
			return;
		}

		if ( !NextNibble ) return;

		CurrentNibbleTime = NibbleTime.GetValue();

		NextNibble = Random.Shared.Float( 3f, 8f );

		Sound.Play( NibbleSound, WorldPosition );
		Bobber.OnNibble();
	}

	private void FailCatch()
	{
		Log.Info( "Failed to catch the fish." );
		Bobber.Rod.FishGotAway();
		DestroyGameObject();
		// QueueFree();
	}

	private void HookFish()
	{
		Log.Info( "Hooked the fish." );
		SetState( FishState.Fighting );

		switch ( Data?.Size )
		{
			case FishData.FishSize.Tiny:
				// _stamina = GD.RandRange( 2, 7 );
				Stamina = Random.Shared.Float( 2, 7 );
				break;
			case FishData.FishSize.Small:
				// _stamina = GD.RandRange( 5, 10 );
				Stamina = Random.Shared.Float( 5, 10 );
				break;
			case FishData.FishSize.Medium:
				// _stamina = GD.RandRange( 8, 15 );
				Stamina = Random.Shared.Float( 8, 15 );
				break;
			case FishData.FishSize.Large:
				// _stamina = GD.RandRange( 10, 20 );
				Stamina = Random.Shared.Float( 10, 20 );
				break;
			default:
				Stamina = 10;
				break;
		}

		_lastAction = Time.Now;
		// _actionDuration = GD.RandRange( 4000, 8000 );
		_actionDuration = Random.Shared.Float( 4, 8 );
	}

	private void CatchFish()
	{
		// SetState( FishState.Caught );
		// GetNode<AudioStreamPlayer3D>( "Catch" ).Play();
		GameObject.PlaySound( CatchSound );
		Bobber.Rod.CatchFish( this );
		// SetState( FishState.Caught );
	}

	private float _swimRandomRadius = 128f;

	private Vector3 _swimTarget;
	private const int _swimTargetTries = 10;
	// private float _swimProgress;

	private void Swim()
	{
		if ( !ActionDone ) return;

		// find a new swim target
		if ( _swimTarget == Vector3.Zero )
		{
			var randomTries = 0;

			do
			{
				GetNewSwimTarget();
				randomTries++;
			} while ( _swimTarget == Vector3.Zero && randomTries < _swimTargetTries );

			if ( _swimTarget == Vector3.Zero )
			{
				Log.Warning( "Failed to find a swim target." );
				SetState( FishState.Idle );
				return;
			}

			_lastAction = Time.Now;
			// _swimStartPos = WorldPosition;
			// _swimProgress = 0;

			Log.Trace( $"New swim target: {_swimTarget}." );
		}

		var swimSpeed = _maxSwimSpeed;
		if ( _lastPanic < 5f )
		{
			// swimSpeed = 15f;
		}

		// move towards the target smoothly
		// var moveDirection = (_swimTarget - WorldPosition).Normal;
		// var swimDistance = _swimTarget.Distance( _swimStartPos );

		// _swimProgress += Time.Delta * (swimSpeed / swimDistance);

		// Vector3 preA = _swimStartPos;
		// Vector3 postB = _swimTarget;

		// Gizmo.Draw.LineSphere( preA, 8f );
		// Gizmo.Draw.LineSphere( postB, 8f );
		// Gizmo.Draw.Arrow( preA + Vector3.Up * 8f, postB + Vector3.Up * 8f );

		var distance = WorldPosition.Distance( _swimTarget );

		var acceleration = _swimAcceleration;

		if ( distance < 16f )
		{
			acceleration = _swimDeceleration;
		}

		var swimDirection = (_swimTarget - WorldPosition).Normal;

		_velocity += swimDirection * acceleration;

		if ( distance < 8f )
		{
			_velocity = _velocity.ClampLength( 8f );
		}

		_velocity = _velocity.ClampLength( swimSpeed );

		var newRotation = Rotation.LookAt( _velocity.Normal, Vector3.Up );

		WorldPosition += _velocity * Time.Delta;
		WishedRotation = newRotation;

		// check if the fish has reached the target


		/**/

		if ( distance < 4f )
		{
			Log.Trace( "Reached swim target." );
			_swimTarget = Vector3.Zero;
			SetState( FishState.Idle );
			return;
		}

		CheckForBobber();
	}

	private void FindSwimAwayFromTarget( Vector3 source )
	{
		var currentPos = WorldPosition;
		var direction = (currentPos - source).Normal.WithZ( 0 );

		var basePoint = currentPos + direction * 128f;

		for ( var i = 0; i < 10; i++ )
		{
			var randomPoint = basePoint + new Vector3(
				Random.Shared.Float( -_swimRandomRadius, _swimRandomRadius ),
				Random.Shared.Float( -_swimRandomRadius, _swimRandomRadius ),
				0 );

			var traceWater = Scene.Trace.Ray( randomPoint + Vector3.Up * 16f, randomPoint + Vector3.Down * 32f )
				.WithTag( "water" )
				.Run();

			// Gizmo.Draw.Line( randomPoint + Vector3.Up * 16f, randomPoint + Vector3.Down * 32f );

			if ( !traceWater.Hit )
			{
				// Log.Warning( $"No water found at {randomPoint}." );
				// this will just try again
				continue;
			}

			var traceTerrain = Scene.Trace.Ray( randomPoint + Vector3.Up * 16f, randomPoint + Vector3.Down * 32f )
				.WithTag( "terrain" )
				.Run();

			if ( traceTerrain.Hit )
			{
				// Log.Warning( $"Terrain found at {randomPoint}." );
				// this will just try again
				continue;
			}

			var trace = Scene.Trace.Ray( currentPos, randomPoint )
				.WithTag( "terrain" )
				.Run();

			if ( trace.Hit )
			{
				// Log.Warning( $"Terrain found between {currentPos} and {randomPoint}." );
				// this will just try again
				continue;
			}

			Log.Info( $"Found a flee swim target: {randomPoint}." );
			// Gizmo.Draw.LineSphere( randomPoint, 32f );
			_swimTarget = randomPoint;
			return;
		}

		Log.Warning( "Failed to find a flee swim target." );
	}

	private void GetNewSwimTarget()
	{
		var randomPoint = WorldPosition + new Vector3(
			Random.Shared.Float( -_swimRandomRadius, _swimRandomRadius ),
			Random.Shared.Float( -_swimRandomRadius, _swimRandomRadius ),
			0 );

		var traceWater = Scene.Trace.Ray( randomPoint + Vector3.Up * 16f, randomPoint + Vector3.Down * 32f )
			.WithTag( "water" )
			.Run();

		// Gizmo.Draw.Line( randomPoint + Vector3.Up * 16f, randomPoint + Vector3.Down * 32f );

		if ( !traceWater.Hit )
		{
			// Log.Warning( $"No water found at {randomPoint}." );
			// this will just try again
			return;
		}

		var traceTerrain = Scene.Trace.Ray( randomPoint + Vector3.Up * 16f, randomPoint + Vector3.Down * 32f )
			.WithTag( "terrain" )
			.Run();

		if ( traceTerrain.Hit )
		{
			// Log.Warning( $"Terrain found at {randomPoint}." );
			// this will just try again
			return;
		}

		var trace = Scene.Trace.Ray( WorldPosition, randomPoint )
			.WithTag( "terrain" )
			.Run();

		if ( trace.Hit )
		{
			// Log.Warning( $"Terrain found between {WorldPosition} and {randomPoint}." );
			// this will just try again
			return;
		}

		_swimTarget = randomPoint;
	}

	private float _lastAction;
	private float _actionDuration;
	private float _panicMaxIdles;

	private bool ActionDone => Time.Now - _lastAction > _actionDuration;

	public float Weight { get; internal set; }


	private void Idle()
	{
		CheckForBobber();

		if ( !ActionDone ) return;

		// randomly start swimming, 20% chance
		if ( Random.Shared.Float() <= 0.2f || _panicMaxIdles > 5 )
		{
			Log.Trace( "Starting to swim after being idle." );
			SetState( FishState.Swimming );
			_panicMaxIdles = 0;
			return;
		}

		// else, stay idle
		// _actionDuration = (float)GD.RandRange( 1000, 5000 );
		_actionDuration = Random.Shared.Float( 1, 5 );
		_lastAction = Time.Now;
		_panicMaxIdles++;

		Log.Trace( $"Idle for {_actionDuration} msec." );
	}

	private void CheckForBobber()
	{
		if ( Bobber.IsValid() )
		{
			return;
		}

		// var bobber = GetTree().GetNodesInGroup<FishingBobber>( "fishing_bobber" ).FirstOrDefault();
		var bobber = Scene.GetAllComponents<FishingBobber>().FirstOrDefault( x => !x.IsProxy && !x.Fish.IsValid() );

		if ( !bobber.IsValid() )
		{
			return;
		}

		if ( !bobber.IsInWater ) return;

		var bobberPosition = bobber.WorldPosition.WithZ( 0 );
		var fishPosition = WorldPosition.WithZ( 0 );

		// check if the bobber is near the fish
		var distance = fishPosition.Distance( bobberPosition );
		if ( distance > _bobberMaxDistance )
		{
			// Log.Info($"Bobber is too far away ({distance})." );
			return;
		}

		// check if the bobber is within the fish's view
		var direction = (bobberPosition - WorldPosition).Normal;
		var angle = MathX.RadianToDegree( MathF.Acos( direction.Dot( WorldRotation.Forward ) ) );

		/*if ( angle > _bobberDiscoverAngle )
		{
			Log.Info($"Bobber is not in view ({angle})." );
			return;
		} */

		Bobber = bobber;
		Bobber.Fish = this;

		Log.Info( "Found the bobber." );

		SetState( FishState.FoundBobber );
	}

	public void OnLinePull()
	{
		/*if ( State != FishState.Fighting ) return;
		_stamina -= 1;
		Log.Info( $"Pulled the fish, stamina left: {_stamina}." );
		if ( _stamina <= 0 )
		{
			CatchFish();
		}*/

		if ( State == FishState.FoundBobber )
		{
			if ( IsNibbling )
			{
				TryHook();
			}
			else
			{
				Scare( Bobber.WorldPosition );
			}

			return;
		}
	}

	internal void SetSize( FishData.FishSize size )
	{
		var scale = 1f;
		switch ( size )
		{
			case FishData.FishSize.Tiny:
				scale = 0.5f;
				break;
			case FishData.FishSize.Small:
				scale = 0.75f;
				break;
			case FishData.FishSize.Medium:
				scale = 1f;
				break;
			case FishData.FishSize.Large:
				scale = 1.25f;
				break;
		}

		WorldScale = new Vector3( scale, scale, scale );
	}

	/*protected override void OnUpdate()
	{
		base.OnUpdate();

		Gizmo.Draw.LineSphere( WorldPosition, 8f );
		Gizmo.Draw.Arrow( WorldPosition, WorldPosition + WorldRotation.Forward * 32f );

		Gizmo.Draw.Arrow( _swimTarget + Vector3.Up * 64f, _swimTarget );
	}*/

	public void OnFootstepEvent( SceneModel.FootstepEvent e )
	{
		if ( e.Transform.Position.Distance( WorldPosition ) < 128f )
		{
			Log.Info( $"Fish heard a footstep: {e.Volume}." );
			// Scare( e.Transform.Position );
		}
	}
}