using Celeste.Mod.LakeSideCode.Entities;
using Celeste.Mod.LakeSideCode.FishDefs;
using Celeste.Mod.LakeSideCode.Triggers;
using Microsoft.Xna.Framework;
using Monocle;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace Celeste.Mod.LakeSideCode.Components {
	/// <summary>
	/// This component handles the Fishing player state,
	/// and holds most of the core logic for anything fishing-related.
	/// </summary>
	internal class Fishing : Component {

		// public const string WindupAnimation = "LakeSideCode_Windup";
		// public const string CastAnimation = "LakeSideCode_Cast";
		// public const string PostCastAnimation = "LakeSideCode_PostCast";
		// public const string FishingIdleAnimation = "LakeSideCode_FishingIdle";
		// public const string StruggleAnimation = "LakeSideCode_Struggle";
		// public const string ReelInAnimation = "LakeSideCode_ReelIn";
		// public const string PutRodAwayAnimation = "LakeSideCode_PutRodAway";

		public const string CastAnimation = "LakeSideCode_Cast";
        public const string FishingIdleAnimation = "LakeSideCode_FishingIdle";
        public const string IdleStruggleAnimation = "LakeSideCode_IdleStruggle";
        public const string NoBiteAnimation = "LakeSideCode_NoBite";
        public const string PostCastAnimation = "LakeSideCode_PostCast";
        public const string PutRodAwayAnimation = "LakeSideCode_PutRodAway";
        public const string ReelInAnimation = "LakeSideCode_ReelIn";
        public const string WindupAnimation = "LakeSideCode_Windup";
        public const string PreStruggleAnimation = "LakeSideCode_PreStruggle";
        public const string ReelInWaitAnimation = "LakeSideCode_ReelInWait";

		private static readonly float StoppingForce = 400f;

		/// <summary>
		/// This method is called by the statemachine when the player enters fishing state
		/// </summary>
		/// <param name="player"></param>
		internal static void StaticStateBegin(Player player) {
			Fishing fsh = player.Get<Fishing>();
			player.Sprite.Visible = player.Hair.Visible = false;
			fsh.sprite.Visible = true;
			fsh.sprite.FlipX = player.Facing == Facings.Left;
			fsh.sprite.Play(WindupAnimation);
			fsh.lure?.RemoveSelf();
			fsh.lure = null;
			fsh.cancelRequested = false;
		}

		/// <summary>
		/// This method is called by the statemachine every frame the player is in fishing state
		/// </summary>
		/// <param name="player"></param>
		public static int StaticStateUpdate(Player player) {
			Fishing fsh = player.Get<Fishing>();
			if (!player.onGround) return ExitStateTo(player, Player.StNormal, fsh);
			if (Input.Dash.Pressed && !fsh.windingUp) fsh.cancelRequested = true;
			if (fsh.lure != null) fsh.lure.LineBeginPoint = player.Position + GetPoleTipPosition(fsh.sprite);
			player.Speed = Calc.Approach(player.Speed, Vector2.Zero, Engine.DeltaTime * StoppingForce);
			return fsh.StateID;
		}

		/// <summary>
		/// This coroutine is run by the statemachine when the player enters fishing state
		/// </summary>
		/// <param name="player"></param>
		public static IEnumerator StaticStateCorou(Player player) {
			Fishing fsh = player.Get<Fishing>();
			Level level = player.SceneAs<Level>();
			bool facingLeft = player.Facing == Facings.Left;

			const float MinWindupTime = 0.95f;
			const float MaxWindupTime = 2f;
			const float MinAngle = 0.2f;
			const float MaxAngle = 0.82f;
			const float MinCastStrength = 120f;
			const float MaxCastStrength = 200f;

			float timer = 0f;
			float angle = facingLeft ? MinAngle - MathF.PI : -MinAngle;
			float castStrength = MinCastStrength;
			float targetAngle = facingLeft ? MaxAngle - MathF.PI : -MaxAngle;
			player.Scene.Add(fsh.lure = new Lure(player.TopCenter, Calc.AngleToVector(angle, castStrength)));
			fsh.lure.Visible = false;
			yield return null;  // Wait for the engine to actually get it into the scene before rendering anything about it
			fsh.windingUp = true;
			Logger.Log(LogLevel.Info, "fishing", "WINDING");
			yield return 0.1f;
			while (timer < MinWindupTime || Input.Dash.Check) {
				timer += Engine.DeltaTime;
				if (Input.Dash.Check) {
					angle = Calc.Approach(angle, targetAngle, (MaxAngle - MinAngle) * Engine.DeltaTime / MaxWindupTime);
					castStrength = Calc.Approach(castStrength, MaxCastStrength, (MaxCastStrength - MinCastStrength) * Engine.DeltaTime / MaxWindupTime);
				}
				fsh.lure.Velocity = Calc.AngleToVector(angle, castStrength);
				yield return null;
			}
			fsh.windingUp = false;
			fsh.sprite.Play(CastAnimation);
			Audio.Play("event:/Lakeside_RodSwish");

			Logger.Log(LogLevel.Info, "fishing", "CAST ANIMATION");

			timer = 0f;
			while (timer < 0.2f) {
				// if (fsh.cancelRequested) {
				// 	yield return ReelInCoroutine(player, fsh, null, level, facingLeft);
				// 	yield break;
				// }
				timer += Engine.DeltaTime;
				yield return null;
			}
			yield return 0.2f;

			// Cast
			fsh.lure.Visible = true;
			if (fsh.lure.CollideCheck<Solid>()) {
				player.Scene.Add(new MiniTextbox("LAKESIDE_FISHING_NOT_ENOUGH_SPACE"));
				yield return ReelInCoroutine(player, fsh, null, level, facingLeft);
				yield break;
			}

			// Wait for the lure to land in water
			while (!fsh.lure.ReadyForCatchSequence) {
				if (fsh.cancelRequested) {
					yield return ReelInCoroutine(player, fsh, null, level, facingLeft);
					yield break;
				}
				yield return 0.05f;
			}
			// Decide what's going to happen
			FishingTrigger trigger = fsh.lure.TriggerCheck();
			FishType fishType = trigger?.ChooseFish(fsh.lure.InWater, level.Session) ?? FishType.Nothing;
			if (fishType == FishType.Nothing) {
				Logger.Log(LogLevel.Info, "fishing", "NOTHING");

				timer = 0f;
				while (timer < 1f) {
					if (fsh.cancelRequested) {
						yield return ReelInCoroutine(player, fsh, null, level, facingLeft);
						yield break;
					}
					timer += Engine.DeltaTime;
					yield return null;
				}
				// nothing seems to be biting...
				if(fsh.lure.InWater) {
					player.Scene.Add(new MiniTextbox("LAKESIDE_FISHING_NO_BITE"));
				}
				fsh.sprite.Play(NoBiteAnimation);
				yield return 0.5f;
				yield return ReelInCoroutine(player, fsh, null, level, facingLeft);
				yield break;
			}
			// Nibbles...
			timer = 0f;
			while (timer < 1f) {
				if (fsh.cancelRequested) {
					yield return ReelInCoroutine(player, fsh, null, level, facingLeft);
					yield break;
				}
				timer += Engine.DeltaTime;
				yield return null;
			}
			fsh.lure.Nibble();
			timer = 0f;
			while (timer < 1f) {
				if (fsh.cancelRequested) {
					yield return ReelInCoroutine(player, fsh, null, level, facingLeft);
					yield break;
				}
				timer += Engine.DeltaTime;
				yield return null;
			}
			// and then the fish hits the lure!
			fsh.lure.Hit();
			// Struggle
			yield return StruggleRoutine(player, fsh, fishType, level);
			// Reel it in
			Fish fish = new(fsh.lure.Position, fishType);
			fish.FollowLure(fsh.lure, new(0, 8));
			player.Scene.Add(fish);
			yield return ReelInCoroutine(player, fsh, fish, level, facingLeft);
		}

		private static IEnumerator StruggleRoutine(Player player, Fishing fsh, FishType fishType, Level level) {
			if (fsh.hud == null) yield break;

			level.CanRetry = false;

			fsh.lure.CollideFirst<Water>().TopSurface.DoRipple(fsh.lure.Position, 1f);
			fsh.lure.MoveV(3);

			Logger.Log(LogLevel.Info, "fishing", "STARTING TO STRUGGLE");

			fsh.sprite.Play(PreStruggleAnimation);
			Audio.Play("event:/Lakeside_RodFullyCharged");

			yield return 0.6f;

			fsh.hud.struggling = true;
			fsh.hud.struggleProgress = 0f;
			fsh.hud.strugglePos = 0.5f;
			fsh.hud.struggleMovement = 0f;
			fsh.hud.struggleFishType = fishType;

			const float MaxMoveSpeed = 1.5f;
			const float MoveAccelTime = 0.5f;
			const float TargetThreshold = 0.15f;

			FishingHUD hud = fsh.hud;

			fsh.lure.WaterHitX = fsh.lure.Position.X;

			while (hud.struggleProgress < 1f) {
				// Update struggle player position
				hud.struggleMovement = Calc.Approach(hud.struggleMovement, MaxMoveSpeed * Input.MoveX.Value, Engine.DeltaTime / MoveAccelTime);
				hud.strugglePos += hud.struggleMovement * Engine.DeltaTime;
				if (hud.strugglePos < 0f && hud.struggleMovement <= 0f) {
					hud.strugglePos = 0f;
					hud.struggleMovement *= -0.7f;
				}
				else if (hud.strugglePos > 1f && hud.struggleMovement >= 0f) {
					hud.strugglePos = 1f;
					hud.struggleMovement *= -0.7f;
				}
				// update fish position
				hud.struggleFishPos = FishStruggleMovement(hud.struggleFishMovementControl);
				// Determine whether the player is accurate
				bool onTarget = MathF.Abs(hud.strugglePos - hud.struggleFishPos) < TargetThreshold;
				hud.struggleLeniency = TargetThreshold;
				hud.struggleFishMovementControl += onTarget ? Engine.DeltaTime : Engine.DeltaTime * 0.6f;
				hud.struggleProgress += onTarget ? Engine.DeltaTime * 0.3f : Engine.DeltaTime * 0.05f;

				float oldPos = fsh.lure.Position.X;

				fsh.lure.Position.X = MathHelper.Lerp(fsh.lure.WaterHitX, player.Position.X, hud.struggleProgress * 0.5f);

				if(fsh.lure.CollideCheck<Solid>()) {
					fsh.lure.Position.X = oldPos;
				}

				if(fsh.catchAudio != null) {
					fsh.catchAudio.setPitch(1f + 0.8f * (float)hud.struggleProgress);
				}

				if(onTarget && fsh.catchAudio == null) {
					fsh.catchAudio = Audio.Loop("event:/Lakeside_RodReel");
				}

				if(!onTarget && fsh.catchAudio != null) {
					fsh.catchAudio.stop(FMOD.Studio.STOP_MODE.IMMEDIATE);
					fsh.catchAudio = null;
				}

				yield return null;
			}

			if(fsh.catchAudio != null) {
				fsh.catchAudio.stop(FMOD.Studio.STOP_MODE.IMMEDIATE);
				fsh.catchAudio = null;
			}

			Logger.Log(LogLevel.Info, "fishing", "AFTER STRUGGLE LOOP");

			yield return 0.4f;
			hud.struggling = false;
		}

		private static float FishStruggleMovement(float t) {
			return 0.5f
				+ 0.1f * MathF.Sin(t * 2.5f + 123f)
				+ 0.2f * MathF.Sin(t * 1.8f + 456f)
				+ 0.2f * MathF.Sin(t * 0.8f + 789f);
		}

		private static IEnumerator ReelInCoroutine(Player player, Fishing fsh, Fish fish, Level level, bool facingLeft) {
			fsh.lure.ReelIn(player.TopCenter);
			fsh.sprite.Play(ReelInAnimation);
			if(fish != null) {
				Audio.Play("event:/Lakeside_RodCatch");
			}
			do {
				yield return null;
			} while (!fsh.lure.FinishedReeling);
			fish?.Collect(level);
			fsh.sprite.Play(PutRodAwayAnimation);
			yield return 0.5f;
			player.StateMachine.State = ExitStateTo(player, Player.StNormal, fsh);
			player.Facing = facingLeft ? Facings.Left : Facings.Right;
			if(LakeSideCodeModule.Session.FishDiscoveryFade >= 1) {
				level.CanRetry = true;
			}
		}

		private static readonly Vector2[] PoleTipPos_Cast = [
			new(-16, -20), new(-16, -20), new(-16, -20), new(-6, -22),
			new(  4, -21), new( 10, -20), new( 17, -14), new(17, -14),
		];
		private static readonly Vector2[] PoleTipPos_PostCast = [
			new(14, -14), new(13, -14), new(12, -16), new(11, -16),
			new(11, -15), new(11, -15), new(11, -15), new(11, -15),
			new(11, -15), new(10, -15), new( 8, -16), new( 8, -16),
			new( 8, -16), new( 8, -16), new(10, -15), new(11, -15), new(11, -15),
		];
		private static readonly Vector2[] PoleTipPos_Idle = [
			new(11, -14), new(11, -14), new(11, -14), new(11, -13),
			new(11, -13), new(11, -13), new(11, -13), new(11, -13), new(11, -13)
        ];
        private static readonly Vector2[] PoleTipPos_ReelIn = [
            new(5, -19), new(-3, -22), new(-6, -22), new(-6, -22),
            new(-7, -23), new(-7, -23), new(-7, -23), new(-7, -23)
        ];
		private static readonly Vector2[] PoleTipPos_PreStruggle = [
			new(14, -10), new(15, -10), new(15, -9), new(15, -9), new(15, -9),
            new(15, -9), new(15, -9), new(15, -8), new(14, -8), new(14, -8)
		];
		private static readonly Vector2[] PoleTipPos_IdleStruggle = [
			new(14, -8), new(14, -8), new(14, -7), new(14, -7),
			new(14, -8), new(14, -8), new(14, -8)
        ];
    

        private static Vector2 GetPoleTipPosition(Sprite sprite) {
            Vector2[] arr = sprite.CurrentAnimationID switch
            {
                CastAnimation => PoleTipPos_Cast,
                FishingIdleAnimation => PoleTipPos_Idle,
                IdleStruggleAnimation => PoleTipPos_IdleStruggle,
                PostCastAnimation => PoleTipPos_PostCast,
                ReelInAnimation => PoleTipPos_ReelIn,
                PreStruggleAnimation => PoleTipPos_PreStruggle,
                ReelInWaitAnimation => [new(-7, -23)],
                _ => PoleTipPos_Idle
            };
            Vector2 ret = arr[Utils.Mod(sprite.CurrentAnimationFrame, arr.Length)];
			if (sprite.FlipX) ret.X = -ret.X;

			return ret;
		}

		private static int ExitStateTo(Player player, int nextState, Fishing fsh) {
			player.Sprite.Visible = player.Hair.Visible = true;
			fsh.sprite.Visible = false;
			fsh.lure?.RemoveSelf();
			fsh.lure = null;
			Input.Dash.ConsumePress();
			return nextState;
		}




		private FishingHUD hud;
		public readonly int StateID;
		private readonly Sprite sprite;
		private Lure lure;
		private bool windingUp = false;
		private bool cancelRequested = false;
		private FMOD.Studio.EventInstance catchAudio = null;

		public Fishing(int stateID) : base (true, true) {
			StateID = stateID;
			sprite = GFX.SpriteBank.Create("LakeSideCode_Fishing");
			sprite.Visible = false;
		}

		/// <summary>
		/// Call this method to make the player enter the fishing state
		/// </summary>
		/// <returns>true if valid state transition, otherwise false</returns>
		public bool EnterState() {
			// Make sure we're allowed to do this
			if (!LakeSideCodeModule.Session.HasFishingRod) return false;
			if (Entity is not Player pl) return false;
			if (pl.Ducking) return false;
			if (pl.Holding != null) return false;
			// Enter the state
			pl.StateMachine.State = StateID;
			return true;
		}

		public override void Added(Entity entity) {
			base.Added(entity);
			entity.Add(sprite);
			TryAddHUD(entity);
		}

		public override void Removed(Entity entity) {
			base.Removed(entity);
			sprite.RemoveSelf();
			TryRemoveHUD();
		}

		public override void EntityAdded(Scene scene) {
			base.EntityAdded(scene);
			TryAddHUD();
		}

		public override void EntityRemoved(Scene scene) {
			base.EntityRemoved(scene);
			TryRemoveHUD();
		}

		/// <summary>
		/// This Update is called every frame regardless of player state
		/// </summary>
		public override void Update() {
			if (Entity is not Player p) return;
			if (p.StateMachine.State != Player.StNormal) return;
			if (!p.onGround) return;
			if (!p.CanUnDuck) return;
			if (Input.Dash.Pressed) {
				if (TalkComponent.PlayerOver == null || !Input.Talk.Pressed) {
					// Let talk components take priority if the keybind is on the same key
					if (EnterState()) {
						Input.Dash.ConsumePress();
					}
				}
			}
			else if (Input.Grab.Pressed && Input.MoveX.Value == 0 && LakeSideCodeModule.Session.Inventory != FishType.Nothing)
			{
				Input.Grab.ConsumePress();
				if (TalkComponent.PlayerOver == null || !Input.Talk.Pressed) {
					Fish fish = new(p.TopCenter, LakeSideCodeModule.Session.Inventory) {
						CanPickUp = true,
						NewCatch = false,
					};
					LakeSideCodeModule.Session.EmptyInventory();
					Scene.Add(fish);
					Logger.Log(LogLevel.Info, "F", "z");
				}
			}
        }

		public override void Render() {
			base.Render();

			if (windingUp && lure != null) {
				Vector2 pos = lure.Position;
				Vector2 vel = lure.Velocity;
				for (int i = 0; i < 250 && !lure.GetPositionAfterPhysicsUpdate(ref pos, ref vel); i++) {
					if ((i & 0x03) == 0) Draw.Point(pos, Color.LightGray);
				}
			}

		}

		private void TryAddHUD(Entity entity = null) {
			entity ??= Entity;
			if (entity.Scene != null) {
				hud ??= [];
				entity.Scene.Add(hud);
			}
		}

		private void TryRemoveHUD() {
			hud?.RemoveSelf();
		}

	}
}
