using System;
using System.Linq;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Rendering;
using static AffectingForcesManager;

public class NimbleZoneDetection : MonoBehaviour
{
	public AffectingForcesManager forcesManager;

	public GameObject renderedZoneObject;


	public enum Gravities
	{
		DownGravity, NoGravity, InwardsGravity, OutwardsGravity
	}

	[SerializeField]
	private static float gravityFactor = 30f;
	[SerializeField]
	private Gravities outsideGravityFunction = Gravities.NoGravity;
	[SerializeField]
	private Zone zone = Zone.NimbleZone;

	// Ripple properties
	[SerializeField]
	private float rippleFrequency = 3f;
	[SerializeField]
	private float rippleDensity = 30f;
	[SerializeField]
	private float rippleAmplitude = 0.1f;
	[SerializeField]
	private float rippleRadius = 1f;
	[SerializeField]
	private float rippleDuration = 1f;
	[SerializeField]
	private float impactVelocityModifier = 1f;
	[SerializeField]
	[Range(0, 1)]
	private float minImpact = 0.2f;
	[SerializeField]
	private float maxVelocity = 45f;
	private int maxRippleAmount = 5;
	private int playerCount = 2;
	private int spawnedCount = 0;
	private MeshRenderer meshRenderer;
	private Material material;

	void Awake()
	{
		meshRenderer = renderedZoneObject.GetComponent<MeshRenderer>();
		material = meshRenderer.material;
		ResetRippleShaderProperties();
	}

	private Func<Vector3, Vector3>[] gravityFunctions =
		{ DownGravity, NoGravity, InwardsGravity, OutwardsGravity };

	private static Func<Vector3, Vector3> NoGravity =
		new Func<Vector3, Vector3>(position => new Vector3());
	private static Func<Vector3, Vector3> DownGravity =
		new Func<Vector3, Vector3>(position => Vector3.down * gravityFactor);

	private static Func<Vector3, Vector3> InwardsGravity =
		new Func<Vector3, Vector3>
			(position => new Vector3(position.x, position.y, 0).normalized * -gravityFactor);

	private static Func<Vector3, Vector3> OutwardsGravity = new Func<Vector3, Vector3>
	(position => new Vector3(position.x, position.y, 0).normalized * gravityFactor);

	public Func<Vector3, Vector3> GetGravityFunction(Gravities gravity)
	{
		return gravityFunctions[(int)gravity];
	}

	private void OnTriggerEnter(Collider collider)
	{
		int instanceID = collider.gameObject.GetInstanceID();
		if (spawnedCount < playerCount)
		{
			++spawnedCount;
		}
		else if (collider.tag == "Spike")
		{
			SpawnRipple(collider, false);
			return;
		}
		if (collider.tag == "Character" || collider.tag == "Player")
		{
			forcesManager.SetGravityForInstance(instanceID, NoGravity);
			forcesManager.SetZoneForInstance(instanceID, zone);
		}
	}

	private void OnTriggerExit(Collider collider)
	{
		int instanceID = collider.gameObject.GetInstanceID();
		if (collider.tag == "Spike")
		{
			SpawnRipple(collider, true);
			return;
		}
		if (collider.tag == "Character" || collider.tag == "Player")
		{
			forcesManager.SetGravityForInstance(instanceID, GetGravityFunction(outsideGravityFunction));
			forcesManager.SetZoneForInstance(instanceID, Zone.OutsideZone);
		}
	}

	private void Update()
	{
		material.SetFloat("_ShaderTime", Time.timeSinceLevelLoad);
	}

	private float ImpactVelocityEffect(float x, float velocity)
	{
		return math.max(math.smoothstep(0, maxVelocity, velocity), minImpact)
			* impactVelocityModifier * x;
	}

	private void SpawnRipple(Collider collider, bool isOutwardsRipple)
	{
		Rigidbody body = collider.attachedRigidbody;
		GameObject gameObject = collider.gameObject;

		float velocity = body.velocity.magnitude;
		Vector3 position = (gameObject.transform.position - transform.position).normalized;

		Vector4[] rippleOrigins = material.GetVectorArray("_RippleOrigins");

		float currentTime = Time.timeSinceLevelLoad;
		float[] startedTimes = material.GetFloatArray("_RippleStartTimes");
		float[] startedRippleDurations = material.GetFloatArray("_RippleDurations");

		float[] rippleAmplitudes = material.GetFloatArray("_RippleAmplitudes");
		float[] rippleFrequencies = material.GetFloatArray("_RippleFrequencies");
		float[] rippleDensities = material.GetFloatArray("_RippleDensities");
		float[] rippleRadii = material.GetFloatArray("_RippleRadii");

		for (int i = 0; i < startedTimes.Length; ++i)
		{
			if (startedTimes[i] + startedRippleDurations[i] < currentTime)
			{
				rippleOrigins[i] = new Vector4(position.x, position.y, position.z, 0);
				material.SetVectorArray("_RippleOrigins", rippleOrigins);
				startedTimes[i] = currentTime;
				material.SetFloatArray("_RippleStartTimes", startedTimes);

				float amplitude = isOutwardsRipple ? rippleAmplitude : -rippleAmplitude;

				rippleAmplitudes[i] = ImpactVelocityEffect(amplitude, velocity);
				rippleFrequencies[i] = ImpactVelocityEffect(rippleFrequency, velocity);
				rippleDensities[i] = ImpactVelocityEffect(rippleDensity, velocity);
				startedRippleDurations[i] = ImpactVelocityEffect(rippleDuration, velocity);
				rippleRadii[i] = ImpactVelocityEffect(rippleRadius, velocity);
				material.SetFloatArray("_RippleAmplitudes", rippleAmplitudes);
				material.SetFloatArray("_RippleFrequencies", rippleFrequencies);
				material.SetFloatArray("_RippleDensities", rippleDensities);
				material.SetFloatArray("_RippleAmplitudes", rippleAmplitudes);
				material.SetFloatArray("_RippleDurations", startedRippleDurations);
				material.SetFloatArray("_RippleRadii", rippleRadii);
				break;
			}

		}
	}

	private void ResetRippleShaderProperties()
	{
		Vector4[] rippleOrigins = new Vector4[maxRippleAmount];
		float[] startedTimes = new float[maxRippleAmount];
		float[] rippleAmplitudes = new float[maxRippleAmount];
		float[] rippleFrequencies = new float[maxRippleAmount];
		float[] rippleDensities = new float[maxRippleAmount];
		float[] rippleDurations = new float[maxRippleAmount];
		float[] rippleRadii = new float[maxRippleAmount];

		// Initialize Ripple Shader Properties
		material.SetVectorArray("_RippleOrigins", rippleOrigins);
		material.SetFloatArray("_RippleStartTimes", startedTimes);
		material.SetFloatArray("_RippleAmplitudes", rippleAmplitudes);
		material.SetFloatArray("_RippleFrequencies", rippleFrequencies);
		material.SetFloatArray("_RippleDensities", rippleDensities);
		material.SetFloatArray("_RippleDurations", rippleDurations);
		material.SetFloatArray("_RippleRadii", rippleRadii);
	}
}
