using System;
using Godot;

namespace Movementtests.player_controller.Scripts;

public partial class HealthSystem : Node3D
{
	new enum Rotation
	{
		NoRotation = 0,
		CameraRotationTriggered = 1,
		RotatingOnZAxis = 2,
		ReturningBack = 3,
	}
	
	[ExportGroup("Health Metrics")]
	[ExportSubgroup("Amounts")]
	[Export(PropertyHint.Range, "0,100,0.1,or_greater")]
	public float MaxHealth     { get; set; }     = 100.0f;
	[Export(PropertyHint.Range, "0,100,0.1,or_greater")]
	public float CurrentHealth { get; set; }     = 100.0f;
	[Export(PropertyHint.Range, "0,100,0.1,or_greater")]
	public float MinimalDamageUnit { get; set; } = 25.0f;
	[Export(PropertyHint.Range, "-100,0,0.1,or_smaller")]
	public float ThresholdVelYForDamage { get; set; } = -15.0f;
	[ExportSubgroup("Regeneration")]
	[Export(PropertyHint.Range, "0,10,0.01,suffix:s,or_greater")]
	public float SecondsBeforeRegeneration { get; set; } = 5.5f;
	[Export(PropertyHint.Range, "0,10,0.01,or_greater")]
	public float RegenerationSpeed         { get; set; } = 10.0f;

	[ExportGroup("Damage Camera Effects")]
	[ExportSubgroup("Camera Shake")]
	[Export(PropertyHint.Range, "0,100,0.1,or_greater")]
	public float RotationSpeed  { get; set; } = 9.0f;
	[Export(PropertyHint.Range, "0,180,0.1,degrees")]
	public float RotationDegree { get; set; } = 14.0f;
	[ExportSubgroup("Visual Distortion")]
	// Screen darkness controls how dark the screen will be, where 0.0 - natural
	// color of the screen(unaltered) and 1.0 - black screen
	[Export(PropertyHint.Range, "0.0,1.0,0.01")]
	public float ScreenDarknessMin  { get; set; } = 0.0f;
	[Export(PropertyHint.Range, "0.0,1.0,0.01")]
	public float ScreenDarknessMax  { get; set; } = 0.3f;
	[Export(PropertyHint.Range, "0.0,1.0,0.01")]
	public float DistortionSpeedMin { get; set; } = 0.0f;
	[Export(PropertyHint.Range, "0.0,1.0,0.01")]
	public float DistortionSpeedMax { get; set; } = 0.6f;
	[Export(PropertyHint.Range, "0.0,1.0,0.01")]
	public float DistortionSizeMin  { get; set; } = 0.0f;
	[Export(PropertyHint.Range, "0.0,1.0,0.01")]
	public float DistortionSizeMax  { get; set; } = 1.0f;
	[ExportSubgroup("Vignetting")]
	[Export(PropertyHint.Range, "0.0,1.0,0.01")]
	public float ActiveZoneMultiplierMin     { get; set; } = 0.45f;
	[Export(PropertyHint.Range, "0.0,1.0,0.01")]
	public float ActiveZoneMultiplierMax     { get; set; } = 0.475f;
	[Export(PropertyHint.Range, "0.0,1.0,0.01,or_greater")]
	public float MultiplierDeltaForAnimation { get; set; } = 0.066f;
	[Export(PropertyHint.Range, "0.0,1.0,0.01")]
	public float Softness                    { get; set; } = 1.0f;
	[Export(PropertyHint.Range, "0.0,10,0.01")]
	public float SpeedMin { get; set; } = 2.95f;
	[Export(PropertyHint.Range, "0.0,10,0.01")]
	public float SpeedMax { get; set; } = 4.0f;

	// Death / GameOver
	[ExportGroup("Death")]
	[ExportSubgroup("Before Fade Out")]
	[Export(PropertyHint.Range, "0,1,0.01,or_less,or_greater")]
	public float BlurLimitValueToStartFadeOut { get; set; } = 0.3f;
	[Export(PropertyHint.Range, "0,1,0.01,or_greater")]
	public float BlurValueToStartFadeOut      { get; set; } = 3.376f;
	[ExportSubgroup("Speeds")]
	[Export(PropertyHint.Range, "0.1,20.0,0.1,or_greater")]
	public float CameraDropSpeedOnDeath { get; set; } = 18.0f;
	[Export(PropertyHint.Range, "0.01,5.0,0.01,or_greater")]
	public float FadeOutSpeed           { get; set; } = 0.11f;

	[Export(PropertyHint.Range, "0.1,10,0.01,or_greater")]
	public float BlurLimitSpeedOnDeath { get; set; } = 0.9f;
	[Export(PropertyHint.Range, "0.1,10,0.01,or_greater")]
	public float BlurSpeedOnDeath      { get; set; } = 1.5f;

	[ExportSubgroup("Target values")]
	[Export(PropertyHint.Range, "0,5.0,0.01,suffix:m,or_greater")]
	public float CameraHeightOnDeath { get; set; } = 0.68f;
	[Export(PropertyHint.Range, "0,5.0,0.01,suffix:m,or_greater")]
	public float FadeOutTargetValue  { get; set; } = 4.0f;

	// TODO: add setter: BlurLimitValueToStartFadeOut should always be less than BlurLimitTargetValue
	// (control it in editor)
	[Export(PropertyHint.Range, "0,10,0.01,or_greater")]
	public float BlurLimitTargetValue { get; set; } = 0.5f;

	// TODO: add setter: BlurValueToStartFadeOut should always be less than BlurTargetValue
	// (control it in editor)
	[Export(PropertyHint.Range, "0,10,0.01,or_greater")]
	public float BlurTargetValue { get; set; } = 7.0f;

	[ExportSubgroup("Other")]
	[Export(PropertyHint.Range, "0.0,4.0,0.01,suffix:s,or_greater")]
	public float ScreenDarknessToReloadScene { get; set; } = 1.74f;


	// Required to hide Vignette effect
	private float _currentHealthInPrevFrame;

	private float _currentVelocityYInAir;

	private CharacterBody3D _characterBody3D;

	private Camera3D _camera;
	private float _cameraInitialRotationZ;
	private float _targetRotationZAxis;
	private Rotation _cameraRotation = Rotation.NoRotation;
	private float _progressOnCamRotation;

	private Vector2 _uvOffset = Vector2.Zero;
	private float _offsetResetThreshold = 5.0f;

	private ShaderMaterial _distortionMaterial;

	private float _timeAccumulator;

	private float _currentSpeed;

	private const float InitialMultiplierMidVal = 0.6f;
	private const float MultiplierMidValToHideVignette = 0.8f;
	private float _currentMultiplierMidValue;

	private ShaderMaterial _vignetteMaterial;

	private bool _deathAnimationPlayed;
	private float _screenDarknessOnDeath;
	private float _currentBlurLimit;
	private float _currentBlur;
	private float _currentScreenDarkness;
	
	private bool _dead;
	private Node3D _head;
	private ShaderMaterial _blurMaterial;

	public struct HealthSystemInitParams
	{
		public CharacterBody3D Parent;
		public Camera3D Camera;
		public Node3D Head;
		public ColorRect VignetteRect;
		public ColorRect DistortionRect;
		public ColorRect BlurRect;
	}
	
	public void Init(HealthSystemInitParams initParams)
	{
		_currentHealthInPrevFrame = CurrentHealth;
		_currentMultiplierMidValue = InitialMultiplierMidVal;
		
		_currentSpeed = SpeedMin;

		_characterBody3D = initParams.Parent;
		_camera = initParams.Camera;
		
		_head = initParams.Head;

		_vignetteMaterial = initParams.VignetteRect.Material as ShaderMaterial;
		_distortionMaterial = initParams.DistortionRect.Material as ShaderMaterial;
		_blurMaterial = initParams.BlurRect.Material as ShaderMaterial;
		
		// Resetting shaders' parameters
		
		_vignetteMaterial.SetShaderParameter(Constants.VIGNETTE_SHADER_MULTIPLIER, 1.0f);	
		_vignetteMaterial.SetShaderParameter(Constants.VIGNETTE_SHADER_SOFTNESS, 1.0f);	
		
		_distortionMaterial.SetShaderParameter(Constants.DISTORTION_SHADER_SCREEN_DARKNESS, 0.0f);
		_distortionMaterial.SetShaderParameter(Constants.DISTORTION_SHADER_DARKNESS_PROGRESSION, 0.0f);
		
		_distortionMaterial.SetShaderParameter(
			Constants.DISTORTION_SHADER_UV_OFFSET, new Vector2(0.0f, 0.0f));

		_distortionMaterial.SetShaderParameter(Constants.DISTORTION_SHADER_SIZE, 0.0);
		
		_blurMaterial.SetShaderParameter(Constants.BLUR_SHADER_LIMIT, 0.0f);
		_blurMaterial.SetShaderParameter(Constants.BLUR_SHADER_BLUR, 0.0f);
	}
	
	public override void _Process(double delta)
	{
		float deltaConverted = (float)delta;

		HandleDeath(deltaConverted);
		
		HandleVignetteShader(deltaConverted);
		HandleDistortionShader(deltaConverted);
		
		HandleCameraRotationOnHit(deltaConverted);
		HandleDamageOnFall();

		HandleHealthRegeneration(deltaConverted);
	}
	
	public void TakeDamage(float amount)
	{
		if (_dead)
		{
			return;
		}
		
		if (_cameraRotation == Rotation.NoRotation)
		{
			_cameraRotation = Rotation.CameraRotationTriggered;
		}

		CurrentHealth -= amount;
		CurrentHealth = Mathf.Clamp(CurrentHealth, 0, MaxHealth);
		
		if (CurrentHealth == 0)
		{
			_dead = true;
			return;
		}

		_lastHitTime = DateTime.UtcNow;
	}

	public float GetCurrentHealth() { return CurrentHealth; }
	
#if DEBUG
	public override void _UnhandledInput(InputEvent @event)
	{
		if (@event is InputEventKey eventKey)
			
			if (eventKey.Pressed && eventKey.Keycode == Key.H)
				TakeDamage(MinimalDamageUnit);
	}
#endif

	public bool IsDead() { return _dead; }

	private void HandleDeath(float delta)
	{
		if (!_dead) { return; }

		if (!_deathAnimationPlayed)
		{
			_deathAnimationPlayed = true;
		}
		
		Vector3 newPosition = _head.Position;
		newPosition.Y = Mathf.Lerp(newPosition.Y, CameraHeightOnDeath, CameraDropSpeedOnDeath * delta);
		
		if (newPosition.Y < CameraHeightOnDeath) { newPosition.Y = CameraHeightOnDeath; }
		
		_head.Position = newPosition;
		
		_currentBlurLimit = Mathf.Lerp(
			_currentBlurLimit, BlurLimitTargetValue, BlurLimitSpeedOnDeath * delta);
		
		_blurMaterial.SetShaderParameter(Constants.BLUR_SHADER_LIMIT, _currentBlurLimit);
		
		_currentBlur = Mathf.Lerp(_currentBlur, BlurTargetValue, BlurSpeedOnDeath * delta);
		_blurMaterial.SetShaderParameter(Constants.BLUR_SHADER_BLUR, _currentBlur);	
		
		if (_currentBlurLimit >= BlurLimitValueToStartFadeOut && _currentBlur >= BlurValueToStartFadeOut)
		{
			float currentScreenDarknessVariant = (float)_distortionMaterial.GetShaderParameter(
				Constants.DISTORTION_SHADER_SCREEN_DARKNESS);
		
			_screenDarknessOnDeath = Mathf.Lerp(
				currentScreenDarknessVariant, FadeOutTargetValue, FadeOutSpeed * delta);
			
			_distortionMaterial.SetShaderParameter(
				Constants.DISTORTION_SHADER_SCREEN_DARKNESS, _screenDarknessOnDeath);
			
			if (_screenDarknessOnDeath >= ScreenDarknessToReloadScene)
			{
				GD.Print("reload");
				// Reload the current scene
				GetTree().ReloadCurrentScene();
			}
		}
	}

	private void HandleVignetteShader(float delta)
	{
		if (Mathf.IsEqualApprox(CurrentHealth, MaxHealth))
		{
			_currentHealthInPrevFrame = CurrentHealth;
			_currentMultiplierMidValue = InitialMultiplierMidVal;
			_timeAccumulator = 0;
			return;
		}
		
		float healthNormalized = CurrentHealth / MaxHealth;
		float healthReverted = 1.0f - healthNormalized;
		
		float newAnimationSpeed = Mathf.Lerp(SpeedMin, SpeedMax, healthReverted);
		_currentSpeed = Mathf.Lerp(_currentSpeed, newAnimationSpeed, delta);
		
		float completeSinCycle = Mathf.Tau / _currentSpeed;

		_timeAccumulator = Mathf.Wrap(_timeAccumulator + delta, 0.0f, completeSinCycle);

		float rawAnimationWeight = Mathf.Sin(_timeAccumulator * _currentSpeed);

		float animationWeight = Mathf.Abs(rawAnimationWeight);

		float difference = _currentHealthInPrevFrame - CurrentHealth;

		float newMultiplierMidValue;	
		
		if (difference < 0)
		{
			newMultiplierMidValue = Mathf.Lerp(
				MultiplierMidValToHideVignette, ActiveZoneMultiplierMin, healthReverted);
		} else
		{
			newMultiplierMidValue = Mathf.Lerp(
				ActiveZoneMultiplierMax, ActiveZoneMultiplierMin, healthReverted);
		}
		
		_currentMultiplierMidValue = Mathf.Lerp(
			_currentMultiplierMidValue, newMultiplierMidValue,  delta);
			
		float multiplier = Mathf.Lerp(
			_currentMultiplierMidValue - MultiplierDeltaForAnimation,
			_currentMultiplierMidValue + MultiplierDeltaForAnimation,
			animationWeight * animationWeight
		);
		
		_vignetteMaterial.SetShaderParameter(Constants.VIGNETTE_SHADER_MULTIPLIER, multiplier);
		_vignetteMaterial.SetShaderParameter(Constants.VIGNETTE_SHADER_SOFTNESS, Softness);

		_currentHealthInPrevFrame = CurrentHealth;
	}

	private void HandleDistortionShader(float delta)
	{
		if (Mathf.IsEqualApprox(CurrentHealth, MaxHealth))
		{
			return;
		}

		float healthNormalized = CurrentHealth / MaxHealth;
		float healthReverted = 1 - healthNormalized;
		
		_distortionMaterial.SetShaderParameter(
			Constants.DISTORTION_SHADER_DARKNESS_PROGRESSION, healthReverted);

		if (!_dead)
		{
			float screenDarkness = Mathf.Remap(
				healthReverted, 0, 1, ScreenDarknessMin, ScreenDarknessMax);
		
			_distortionMaterial.SetShaderParameter(
				Constants.DISTORTION_SHADER_SCREEN_DARKNESS, screenDarkness);
		}
		
		float distortionSpeed =  Mathf.Remap(
			healthReverted, 0.0f, 1.0f, DistortionSpeedMin, DistortionSpeedMax);

		float offsetVal = delta * distortionSpeed;
		
		_uvOffset += new Vector2(offsetVal, offsetVal);
		
		_distortionMaterial.SetShaderParameter(
			Constants.DISTORTION_SHADER_UV_OFFSET, _uvOffset);
		
		if (_uvOffset.X > _offsetResetThreshold) { _uvOffset.X = 0.0f; _uvOffset.Y = 0.0f; }
		
		float distortionSize = Mathf.Remap(
			healthReverted, 0.0f, 1.0f, DistortionSizeMin, DistortionSizeMax);
		
		_distortionMaterial.SetShaderParameter(
			Constants.DISTORTION_SHADER_SIZE, distortionSize);
	}

	private void RotateCameraOnZAxis(float delta, float targetAngleInRadians, Rotation rotationStateToSetOnFinish)
	{
		_progressOnCamRotation += delta * RotationSpeed;
		_progressOnCamRotation = Mathf.Clamp(_progressOnCamRotation, 0f, 1f);
			
		float lerpedAngleZ = Mathf.LerpAngle(
			_camera.Rotation.Z,targetAngleInRadians, _progressOnCamRotation);
			
		_camera.Rotation = new Vector3(_camera.Rotation.X, _camera.Rotation.Y, lerpedAngleZ);
		
		float difference = Mathf.Abs(targetAngleInRadians - _camera.Rotation.Z);
		
		if (difference < Constants.ACCEPTABLE_TOLERANCE)
		{
			_cameraRotation = rotationStateToSetOnFinish;
			_progressOnCamRotation = 0;
		} 
	}
	
	private void HandleCameraRotationOnHit(float delta)
	{
		if (_cameraRotation == Rotation.NoRotation || _dead) return;

		if (_cameraRotation == Rotation.CameraRotationTriggered)
		{
			if (GD.Randi() % 2 == 0)
			{
				_targetRotationZAxis = Mathf.DegToRad(RotationDegree * -1);
			}
			else
			{
				_targetRotationZAxis = Mathf.DegToRad(RotationDegree);
			}

			_cameraRotation = Rotation.RotatingOnZAxis;
		}

		if (_cameraRotation == Rotation.RotatingOnZAxis)
		{
			RotateCameraOnZAxis(delta, _targetRotationZAxis, Rotation.ReturningBack);
		}

		if (_cameraRotation == Rotation.ReturningBack)
		{
			RotateCameraOnZAxis(delta, 0, Rotation.NoRotation);
		}
	}

	private void HandleDamageOnFall()
	{
		if (_dead) { return;}
		
		if (!_characterBody3D.IsOnFloor())
		{
			_currentVelocityYInAir = _characterBody3D.Velocity.Y;
		}
		else
		{
			if (_currentVelocityYInAir < ThresholdVelYForDamage && ThresholdVelYForDamage > 0)
			{
				float hit = Mathf.Remap(_currentVelocityYInAir,
					ThresholdVelYForDamage, ThresholdVelYForDamage - 9.0f, 
					MinimalDamageUnit, MaxHealth);
				
				GD.Print("Hit damage: ", hit);
				
				TakeDamage(hit);
			}

			_currentVelocityYInAir = 0.0f;
		}
	}

	private DateTime? _lastHitTime;

	private void HandleHealthRegeneration(float delta)
	{
		if (_lastHitTime == null || _dead) return;
		
		DateTime lastHitTimeConverted = (DateTime)_lastHitTime;
		
		double differenceInSeconds = (DateTime.UtcNow - lastHitTimeConverted).TotalSeconds;
		float differenceInSecondsConverted = (float)differenceInSeconds;
		
		if (differenceInSecondsConverted < SecondsBeforeRegeneration)
		{
			return;
		}
		
		if (Mathf.IsEqualApprox(CurrentHealth, MaxHealth))
		{
			CurrentHealth = MaxHealth;
			_lastHitTime = null;
			return;
		}

		CurrentHealth += delta * RegenerationSpeed;
		CurrentHealth = Mathf.Clamp(CurrentHealth, 0, MaxHealth);
	}
}
